diff --git a/FCL/build.gradle b/FCL/build.gradle index 14bacbab..a1591e01 100644 --- a/FCL/build.gradle +++ b/FCL/build.gradle @@ -33,9 +33,10 @@ dependencies { implementation project(path: ':FCLCore') implementation project(path: ':FCLLibrary') implementation project(path: ':FCLauncher') + implementation 'org.nanohttpd:nanohttpd:2.3.1' implementation 'org.apache.commons:commons-compress:1.21' 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 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' diff --git a/FCL/src/main/assets/img/alex.png b/FCL/src/main/assets/img/alex.png new file mode 100644 index 00000000..ffd8e071 Binary files /dev/null and b/FCL/src/main/assets/img/alex.png differ diff --git a/FCL/src/main/assets/img/steve.png b/FCL/src/main/assets/img/steve.png new file mode 100644 index 00000000..90d4fa23 Binary files /dev/null and b/FCL/src/main/assets/img/steve.png differ diff --git a/FCL/src/main/java/com/tungsten/fcl/game/FCLCacheRepository.java b/FCL/src/main/java/com/tungsten/fcl/game/FCLCacheRepository.java new file mode 100644 index 00000000..a378909c --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/FCLCacheRepository.java @@ -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(); +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/FCLGameLauncher.java b/FCL/src/main/java/com/tungsten/fcl/game/FCLGameLauncher.java new file mode 100644 index 00000000..39e7e75b --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/FCLGameLauncher.java @@ -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 getConfigurations() { + Map 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(); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/FCLGameRepository.java b/FCL/src/main/java/com/tungsten/fcl/game/FCLGameRepository.java new file mode 100644 index 00000000..b6651bc0 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/FCLGameRepository.java @@ -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 localVersionSettings = new HashMap<>(); + private final Set beingModpackVersions = new HashSet<>(); + + public final EventManager 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 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 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 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 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; + } + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackInstallTask.java b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackInstallTask.java new file mode 100644 index 00000000..719aebc3 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackInstallTask.java @@ -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 { + private final File zipFile; + private final String name; + private final FCLGameRepository repository; + private final DefaultDependencyManager dependency; + private final Modpack modpack; + private final List> dependencies = new ArrayList<>(1); + private final List> 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 config = null; + try { + if (json.exists()) { + config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { + }.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> getDependencies() { + return dependencies; + } + + @Override + public List> 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 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)); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackManifest.java b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackManifest.java new file mode 100644 index 00000000..0300eaba --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackManifest.java @@ -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; + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackProvider.java b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackProvider.java new file mode 100644 index 00000000..7fbe105e --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/HMCLModpackProvider.java @@ -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); + } + } + +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/LoadingState.java b/FCL/src/main/java/com/tungsten/fcl/game/LoadingState.java new file mode 100644 index 00000000..d95696bd --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/LoadingState.java @@ -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); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/LocalizedRemoteModRepository.java b/FCL/src/main/java/com/tungsten/fcl/game/LocalizedRemoteModRepository.java new file mode 100644 index 00000000..1d708ff3 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/LocalizedRemoteModRepository.java @@ -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 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 mods = modTranslations.searchMod(searchFilter); + List 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 getCategories() throws IOException { + return getBackedRemoteModRepository().getCategories(); + } + + @Override + public Optional 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 getRemoteVersionsById(String id) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionsById(id); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/LogExporter.java b/FCL/src/main/java/com/tungsten/fcl/game/LogExporter.java new file mode 100644 index 00000000..4f8440bb --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/LogExporter.java @@ -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 exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) { + Path runDirectory = gameRepository.getRunDirectory(versionId).toPath(); + Path baseDirectory = gameRepository.getBaseDirectory().toPath(); + List versions = new ArrayList<>(); + + String currentVersionId = versionId; + HashSet 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); + } + }); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackException.java b/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackException.java new file mode 100644 index 00000000..2fb3b1ea --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackException.java @@ -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; + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackInstallTask.java b/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackInstallTask.java new file mode 100644 index 00000000..466f6d3f --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/ManuallyCreatedModpackInstallTask.java @@ -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 { + + 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(); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/ModpackHelper.java b/FCL/src/main/java/com/tungsten/fcl/game/ModpackHelper.java new file mode 100644 index 00000000..c499a0a3 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/ModpackHelper.java @@ -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 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 firstLayer = Files.list(root)) { + for (Path dir : toIterable(firstLayer)) { + if (isMinecraftDirectory(dir)) return dir; + + try (Stream 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>() { + }.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 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 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 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 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 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()); + }); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/OAuthServer.java b/FCL/src/main/java/com/tungsten/fcl/game/OAuthServer.java new file mode 100644 index 00000000..8d772b09 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/OAuthServer.java @@ -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 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 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 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 onGrantDeviceCode = new EventManager<>(); + public final EventManager 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 { + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/game/TexturesLoader.java b/FCL/src/main/java/com/tungsten/fcl/game/TexturesLoader.java new file mode 100644 index 00000000..e4dd8eea --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/game/TexturesLoader.java @@ -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 metadata; + + public LoadedTexture(Bitmap image, Map metadata) { + this.image = requireNonNull(image); + this.metadata = requireNonNull(metadata); + } + + public Bitmap getImage() { + return image; + } + + public Map 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 metadata = texture.getMetadata(); + if (metadata == null) { + metadata = emptyMap(); + } + return new LoadedTexture(img, metadata); + } + // ==== + + // ==== Skins ==== + private final static Map 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 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 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 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 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))); + } + } + // ==== +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/Accounts.java b/FCL/src/main/java/com/tungsten/fcl/setting/Accounts.java new file mode 100644 index 00000000..a0eafe56 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/Accounts.java @@ -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> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR); + + // ==== login type / account factory mapping ==== + private static final Map> type2factory = new HashMap<>(); + private static final Map, 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 accounts = observableArrayList(account -> new Observable[] { account }); + private static final ReadOnlyListWrapper accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts); + + private static final ObjectProperty selectedAccount = new SimpleObjectProperty(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 getAccountStorage(Account account) { + Map 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 getAccounts() { + return accounts; + } + + public static ReadOnlyListProperty accountsProperty() { + return accountsWrapper.getReadOnlyProperty(); + } + + public static Account getSelectedAccount() { + return selectedAccount.get(); + } + + public static void setSelectedAccount(Account selectedAccount) { + Accounts.selectedAccount.set(selectedAccount); + } + + public static ObjectProperty 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 getArtifactInfoImmediately() { + Optional 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, 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(); + } + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/AuthlibInjectorServers.java b/FCL/src/main/java/com/tungsten/fcl/setting/AuthlibInjectorServers.java new file mode 100644 index 00000000..414c2d4c --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/AuthlibInjectorServers.java @@ -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 urls; + + public AuthlibInjectorServers(List urls) { + this.urls = urls; + } + + public List 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; + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/Config.java b/FCL/src/main/java/com/tungsten/fcl/setting/Config.java new file mode 100644 index 00000000..84140a2e --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/Config.java @@ -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 configurations = FXCollections.observableMap(new TreeMap<>()); + + @SerializedName("accounts") + private ObservableList> accountStorages = FXCollections.observableArrayList(); + + @SerializedName("authlibInjectorServers") + private ObservableList 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 getConfigurations() { + return configurations; + } + + public ObservableList> getAccountStorages() { + return accountStorages; + } + + public ObservableList 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); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/ConfigHolder.java b/FCL/src/main/java/com/tungsten/fcl/setting/ConfigHolder.java new file mode 100644 index 00000000..bab62608 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/ConfigHolder.java @@ -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 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 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()); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/DownloadProviders.java b/FCL/src/main/java/com/tungsten/fcl/setting/DownloadProviders.java new file mode 100644 index 00000000..f90fb2cb --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/DownloadProviders.java @@ -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 providersById; + public static final Map 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); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/FCLAccounts.java b/FCL/src/main/java/com/tungsten/fcl/setting/FCLAccounts.java new file mode 100644 index 00000000..2df2ac74 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/FCLAccounts.java @@ -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 account = new SimpleObjectProperty<>(); + + private FCLAccounts() { + } + + public static FCLAccount getAccount() { + return account.get(); + } + + public static ObjectProperty accountProperty() { + return account; + } + + public static void setAccount(FCLAccount account) { + FCLAccounts.account.set(account); + } + + public static Task 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; + } + +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/GlobalConfig.java b/FCL/src/main/java/com/tungsten/fcl/setting/GlobalConfig.java new file mode 100644 index 00000000..18c077a6 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/GlobalConfig.java @@ -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 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, JsonDeserializer { + private static final Set 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 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 entry : obj.entrySet()) { + if (!knownFields.contains(entry.getKey())) { + config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class)); + } + } + + return config; + } + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/Profile.java b/FCL/src/main/java/com/tungsten/fcl/setting/Profile.java new file mode 100644 index 00000000..74b04c11 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/Profile.java @@ -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 gameDir; + + public ObjectProperty gameDirProperty() { + return gameDir; + } + + public File getGameDir() { + return gameDir.get(); + } + + public void setGameDir(File gameDir) { + this.gameDir.set(gameDir); + } + + private final ReadOnlyObjectWrapper global = new ReadOnlyObjectWrapper<>(this, "global"); + + public ReadOnlyObjectProperty 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 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, JsonDeserializer { + @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("")); + } + + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/Profiles.java b/FCL/src/main/java/com/tungsten/fcl/setting/Profiles.java new file mode 100644 index 00000000..ab806a65 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/Profiles.java @@ -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 profiles = observableArrayList(profile -> new Observable[] { profile }); + private static final ReadOnlyListWrapper profilesWrapper = new ReadOnlyListWrapper<>(profiles); + + private static ObjectProperty selectedProfile = new SimpleObjectProperty() { + { + 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 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 listener : versionsListeners) + listener.accept(profile); + } + }); + } + + public static ObservableList getProfiles() { + return profiles; + } + + public static ReadOnlyListProperty profilesProperty() { + return profilesWrapper.getReadOnlyProperty(); + } + + public static Profile getSelectedProfile() { + return selectedProfile.get(); + } + + public static void setSelectedProfile(Profile profile) { + selectedProfile.set(profile); + } + + public static ObjectProperty 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> versionsListeners = new ArrayList<>(4); + + public static void registerVersionsListener(Consumer listener) { + Profile profile = getSelectedProfile(); + if (profile != null && profile.getRepository().isLoaded()) + listener.accept(profile); + versionsListeners.add(listener); + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/Settings.java b/FCL/src/main/java/com/tungsten/fcl/setting/Settings.java new file mode 100644 index 00000000..dfddb6ff --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/Settings.java @@ -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())); + } + +} diff --git a/FCL/src/main/java/com/tungsten/fcl/setting/VersionSetting.java b/FCL/src/main/java/com/tungsten/fcl/setting/VersionSetting.java new file mode 100644 index 00000000..86c41118 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/setting/VersionSetting.java @@ -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. + *

+ * 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 javaProperty = new SimpleObjectProperty<>(this, "java", 0); + + public ObjectProperty 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 minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null); + + public ObjectProperty 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. + *

+ * 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. + *

+ * 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. + *

+ * 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
+ * 1 - .minecraft/versions/<version>/
+ */ + private final ObjectProperty gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", GameDirectoryType.ROOT_FOLDER); + + public ObjectProperty gameDirTypeProperty() { + return gameDirTypeProperty; + } + + public GameDirectoryType getGameDirType() { + return gameDirTypeProperty.get(); + } + + public void setGameDirType(GameDirectoryType gameDirType) { + gameDirTypeProperty.set(gameDirType); + } + + private final ObjectProperty processPriorityProperty = new SimpleObjectProperty<>(this, "processPriority", ProcessPriority.NORMAL); + + public ObjectProperty processPriorityProperty() { + return processPriorityProperty; + } + + public ProcessPriority getProcessPriority() { + return processPriorityProperty.get(); + } + + public void setProcessPriority(ProcessPriority processPriority) { + processPriorityProperty.set(processPriority); + } + + // launcher settings + + public Task 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, JsonDeserializer { + @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); + } + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/util/FXUtils.java b/FCL/src/main/java/com/tungsten/fcl/util/FXUtils.java new file mode 100644 index 00000000..79c4a351 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/util/FXUtils.java @@ -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 void onChangeAndOperate(ObservableValue value, Consumer consumer) { + consumer.accept(value.getValue()); + onChange(value, consumer); + } + + public static void onChange(ObservableValue value, Consumer 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; + } + +} diff --git a/FCL/src/main/java/com/tungsten/fcl/util/ModTranslations.java b/FCL/src/main/java/com/tungsten/fcl/util/ModTranslations.java new file mode 100644 index 00000000..3685f8c5 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/util/ModTranslations.java @@ -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 mcmod.cn + */ +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 mods; + private Map modIdMap; // mod id -> mod + private Map curseForgeMap; // curseforge id -> mod + private List> 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 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> modList = new ArrayList<>(); + for (Pair 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 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 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 getModIds() { + return modIds; + } + + public String getName() { + return name; + } + + public String getSubname() { + return subname; + } + + public String getAbbr() { + return abbr; + } + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/util/ResourceNotFoundError.java b/FCL/src/main/java/com/tungsten/fcl/util/ResourceNotFoundError.java new file mode 100644 index 00000000..b97c245d --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/util/ResourceNotFoundError.java @@ -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; + } +} diff --git a/FCL/src/main/java/com/tungsten/fcl/util/WeakListenerHolder.java b/FCL/src/main/java/com/tungsten/fcl/util/WeakListenerHolder.java new file mode 100644 index 00000000..b9570b88 --- /dev/null +++ b/FCL/src/main/java/com/tungsten/fcl/util/WeakListenerHolder.java @@ -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 refs = new LinkedList<>(); + + public WeakListenerHolder() { + } + + public WeakInvalidationListener weak(InvalidationListener listener) { + refs.add(listener); + return new WeakInvalidationListener(listener); + } + + public WeakChangeListener weak(ChangeListener listener) { + refs.add(listener); + return new WeakChangeListener<>(listener); + } + + public WeakListChangeListener weak(ListChangeListener 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); + } +} \ No newline at end of file diff --git a/FCL/src/main/res/drawable/img_chicken.png b/FCL/src/main/res/drawable/img_chicken.png new file mode 100644 index 00000000..e2e0ce06 Binary files /dev/null and b/FCL/src/main/res/drawable/img_chicken.png differ diff --git a/FCL/src/main/res/drawable/img_command.png b/FCL/src/main/res/drawable/img_command.png new file mode 100644 index 00000000..005799e9 Binary files /dev/null and b/FCL/src/main/res/drawable/img_command.png differ diff --git a/FCL/src/main/res/drawable/img_fabric.png b/FCL/src/main/res/drawable/img_fabric.png new file mode 100644 index 00000000..4ab8370f Binary files /dev/null and b/FCL/src/main/res/drawable/img_fabric.png differ diff --git a/FCL/src/main/res/drawable/img_forge.png b/FCL/src/main/res/drawable/img_forge.png new file mode 100644 index 00000000..c79eec11 Binary files /dev/null and b/FCL/src/main/res/drawable/img_forge.png differ diff --git a/FCL/src/main/res/drawable/img_grass.png b/FCL/src/main/res/drawable/img_grass.png new file mode 100644 index 00000000..2380963a Binary files /dev/null and b/FCL/src/main/res/drawable/img_grass.png differ diff --git a/FCL/src/main/res/drawable/img_quilt.png b/FCL/src/main/res/drawable/img_quilt.png new file mode 100644 index 00000000..330beac9 Binary files /dev/null and b/FCL/src/main/res/drawable/img_quilt.png differ diff --git a/FCL/src/main/res/values-zh/strings.xml b/FCL/src/main/res/values-zh/strings.xml index 7876a86f..bbd3aedd 100644 --- a/FCL/src/main/res/values-zh/strings.xml +++ b/FCL/src/main/res/values-zh/strings.xml @@ -12,4 +12,48 @@ 下载资源 多人联机 全局设置 + + 离线账户 + Mojang 账户 + 外置账户 + 微软账户 + 该账户中无角色。 + 无法连接认证服务器,请检查网络。 + 无效的返回码,该认证服务器可能失效。 + 不正确的密码或被限速,请稍后再试。 + 请尝试重新登录。 + 无效的密码 + 你的账户需要迁移至微软账户,如果已经迁移,请尝试重新登录。 + 无法下载 authlib-injector。 请检查网络,或修改下载源。 + 该角色已被删除。 + 登录了错误的账户。 + 无效的皮肤文件 + 你尚未满 18 岁,需要一位成年将你加入至家庭中。 + Xbox Live 不支持您所在的国家/地区。 + 你的微软账户没有链接到 Xbox 账户,请先创建。 + 登录失败 + 你的账户尚未获取 Minecraft : Java Edition + 请检查并确保年龄设置大于 18 岁。 + Microsoft 账户登录完成 + + 未找到文件 + + 无法获取文件。 + 无法校验文件。 + 缺少 SSL 证书。 + + 下载超时 + 无法下载 + + 检查依赖 + 下载依赖 + 检查 Java + 登录 + 等待游戏启动 + 完成 + + 操作已取消 + + 共有目录 + 私有目录 \ No newline at end of file diff --git a/FCL/src/main/res/values/colors.xml b/FCL/src/main/res/values/colors.xml index 1f0112d1..f8092d83 100644 --- a/FCL/src/main/res/values/colors.xml +++ b/FCL/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - #9EFF4A + #7797CF #FF000000 #FFFFFFFF \ No newline at end of file diff --git a/FCL/src/main/res/values/strings.xml b/FCL/src/main/res/values/strings.xml index 38c5854a..a4eee404 100644 --- a/FCL/src/main/res/values/strings.xml +++ b/FCL/src/main/res/values/strings.xml @@ -1,7 +1,9 @@ Fold Craft Launcher + 1_0_0 + Welcome to Fold Craft Launcher - Cannot get EULA, please check the network. + Cannot get EULA, please check the network_ Agree and Continue Install or update app runtime LWJGL 2 @@ -18,4 +20,48 @@ Download Multiplayer Setting + + Offline Account + Mojang Account + External Account + Microsoft Account + There are no characters linked to this account. + Unable to contact authentication servers, your Internet connection may be down. + Invalid server response, the authentication server may not be working. + Incorrect password or rate limited, please try again later. + Please try to re-login again. + Invalid password + Your account needs to be migrated to a Microsoft account. If you already did, you should re-login to your migrated Microsoft account instead. + Unable to download authlib-injector. Please check your network, or try switching to a different download mirror. + The character has already been deleted. + You have logged in to the wrong account. + Invalid skin file + Since you are not yet 18 years old, an adult must add you to a family in order for you to play Minecraft. + Xbox Live is not available in your current country/region. + Your Microsoft account does not have a linked Xbox account yet. Please create one before continuing. + Failed to log in + Your account does not own the Minecraft Java Edition.\nThe game profile may not have been created. + 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. + Microsoft account authorization is now completed. + + File not found + + Unable to access the file. + Cannot verify the integrity of the downloaded files. + Unable to establish SSL connection due to missing SSL certificates in current Java installation. + + Download timeout + Unable to download + + Resolving dependencies + Downloading dependencies + Checking Java version + Logging in + Waiting for the game to launch + Completing launch + + Operation was cancelled + + Shared Directory + Private Directory \ No newline at end of file diff --git a/FCLCore/build.gradle b/FCLCore/build.gradle index 14aa44c9..1e297f4f 100644 --- a/FCLCore/build.gradle +++ b/FCLCore/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.apache.commons:commons-compress:1.21' implementation 'com.moandjiezana.toml:toml4j:0.7.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 'com.google.android.material:material:1.7.0' testImplementation 'junit:junit:4.13.2' diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/Account.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/Account.java index aa33cffd..3da6c41a 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/Account.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/Account.java @@ -2,11 +2,11 @@ package com.tungsten.fclcore.auth; import com.tungsten.fclcore.auth.yggdrasil.Texture; 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.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 java.util.Map; diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/authlibinjector/AuthlibInjectorServer.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/authlibinjector/AuthlibInjectorServer.java index 9f1cb2c8..0379c189 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/authlibinjector/AuthlibInjectorServer.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/authlibinjector/AuthlibInjectorServer.java @@ -28,8 +28,8 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import com.google.gson.annotations.JsonAdapter; import com.tungsten.fclcore.auth.yggdrasil.YggdrasilService; -import com.tungsten.fclcore.fakefx.InvalidationListener; -import com.tungsten.fclcore.fakefx.Observable; +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; import com.tungsten.fclcore.util.fakefx.ObservableHelper; @JsonAdapter(AuthlibInjectorServer.Deserializer.class) diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/microsoft/MicrosoftAccount.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/microsoft/MicrosoftAccount.java index 84e8117d..d06a564d 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/microsoft/MicrosoftAccount.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/microsoft/MicrosoftAccount.java @@ -18,8 +18,8 @@ import com.tungsten.fclcore.auth.ServerResponseMalformedException; import com.tungsten.fclcore.auth.yggdrasil.Texture; import com.tungsten.fclcore.auth.yggdrasil.TextureType; 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.fakefx.ObjectBinding; public class MicrosoftAccount extends OAuthAccount { diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java index 0823595c..28ff1436 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java @@ -22,11 +22,11 @@ import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorDownloadExceptio 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.fakefx.beans.binding.ObjectBinding; import com.tungsten.fclcore.game.Arguments; import com.tungsten.fclcore.game.LaunchOptions; import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.ToStringBuilder; -import com.tungsten.fclcore.fakefx.ObjectBinding; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; public class OfflineAccount extends Account { diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/yggdrasil/YggdrasilAccount.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/yggdrasil/YggdrasilAccount.java index eb9b4838..6a4c7e57 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/yggdrasil/YggdrasilAccount.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/yggdrasil/YggdrasilAccount.java @@ -16,8 +16,8 @@ import com.tungsten.fclcore.auth.ClassicAccount; import com.tungsten.fclcore.auth.CredentialExpiredException; import com.tungsten.fclcore.auth.NoCharacterException; 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.fakefx.ObjectBinding; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; public class YggdrasilAccount extends ClassicAccount { diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Binding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Binding.java deleted file mode 100644 index 9892ddc1..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Binding.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface Binding extends ObservableValue { - - boolean isValid(); - - void invalidate(); - - void dispose(); - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Bindings.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Bindings.java deleted file mode 100644 index 77ecc2ab..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Bindings.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -import java.util.concurrent.Callable; - -public final class Bindings { - - private Bindings() { - } - - public static ObjectBinding createObjectBinding(final Callable func, final Observable... dependencies) { - return new ObjectBinding() { - { - bind(dependencies); - } - - @Override - protected T computeValue() { - try { - return func.call(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - @Override - public void dispose() { - super.unbind(dependencies); - } - }; - } -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanExpression.java deleted file mode 100644 index cf695768..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanExpression.java +++ /dev/null @@ -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 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; - } - }; - } -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanProperty.java deleted file mode 100644 index cc37c852..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanProperty.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public abstract class BooleanProperty extends ReadOnlyBooleanProperty implements - Property, WritableBooleanValue { - - public BooleanProperty() { - } - - @Override - public void setValue(Boolean v) { - if (v == null) { - set(false); - } else { - set(v.booleanValue()); - } - } - - @Override - public void bindBidirectional(Property other) { - - } - - @Override - public void unbindBidirectional(Property 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(); - } -} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ChangeListener.java deleted file mode 100644 index 9bcdf3d9..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ChangeListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface ChangeListener { - - void changed(ObservableValue observable, T oldValue, T newValue); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/FXPermissions.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/FXPermissions.java new file mode 100644 index 00000000..c911e34c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/FXPermissions.java @@ -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"); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/InvalidationListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/InvalidationListener.java deleted file mode 100644 index 5c9cb68e..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/InvalidationListener.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface InvalidationListener { - - public void invalidated(Observable observable); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectBinding.java deleted file mode 100644 index a973298e..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectBinding.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public abstract class ObjectBinding extends ObjectExpression implements Binding { - - private T value; - private boolean valid = false; - private BindingHelperObserver observer; - private ExpressionHelper 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 listener) { - helper = ExpressionHelper.addListener(helper, this, listener); - } - - @Override - public void removeListener(ChangeListener 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]"; - } -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectExpression.java deleted file mode 100644 index 3da21f76..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObjectExpression.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public abstract class ObjectExpression implements ObservableObjectValue { - - @Override - public T getValue() { - return get(); - } - - public ObjectExpression() { - } -} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Observable.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Observable.java deleted file mode 100644 index 8efcae2d..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Observable.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface Observable { - - void addListener(InvalidationListener listener); - - void removeListener(InvalidationListener listener); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableBooleanValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableBooleanValue.java deleted file mode 100644 index bec00202..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableBooleanValue.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface ObservableBooleanValue extends ObservableValue { - - boolean get(); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableList.java deleted file mode 100644 index c9af8adc..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableList.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -import java.util.Collection; -import java.util.List; - -public interface ObservableList extends List, Observable { - - public boolean addAll(E... elements); - - public boolean setAll(E... elements); - - public boolean setAll(Collection col); - - public boolean removeAll(E... elements); - - public boolean retainAll(E... elements); - - public void remove(int from, int to); -} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableObjectValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableObjectValue.java deleted file mode 100644 index cc11f46f..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableObjectValue.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface ObservableObjectValue extends ObservableValue { - - T get(); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableValue.java deleted file mode 100644 index 9d8d09aa..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ObservableValue.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface ObservableValue extends Observable { - - void addListener(ChangeListener listener); - - void removeListener(ChangeListener listener); - - T getValue(); -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/PlatformUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/PlatformUtil.java new file mode 100644 index 00000000..d31a8196 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/PlatformUtil.java @@ -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) () -> System.getProperty("javafx.platform")); + javafxPlatform = str1; + + loadProperties(); + + @SuppressWarnings("removal") + boolean bool1 = AccessController.doPrivileged((PrivilegedAction) () -> Boolean.getBoolean("com.sun.javafx.isEmbedded")); + embedded = bool1; + + @SuppressWarnings("removal") + String str2 = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("glass.platform", "").toLowerCase(Locale.ROOT)); + embeddedType = str2; + + @SuppressWarnings("removal") + boolean bool2 = AccessController.doPrivileged((PrivilegedAction) () -> Boolean.getBoolean("use.egl")); + useEGL = bool2; + + if (useEGL) { + @SuppressWarnings("removal") + boolean bool3 = AccessController.doPrivileged((PrivilegedAction) () -> 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) () -> 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) () -> { + 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; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Property.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Property.java deleted file mode 100644 index 55d180e6..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/Property.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface Property extends ReadOnlyProperty, WritableValue { - - void bind(ObservableValue observable); - - void unbind(); - - boolean isBound(); - - void bindBidirectional(Property other); - - void unbindBidirectional(Property other); - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyBooleanProperty.java deleted file mode 100644 index 5e4d21e8..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyBooleanProperty.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public abstract class ReadOnlyBooleanProperty extends BooleanExpression - implements ReadOnlyProperty { - - 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(); - } - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyProperty.java deleted file mode 100644 index b33a9134..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ReadOnlyProperty.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface ReadOnlyProperty extends ObservableValue { - - Object getBean(); - - String getName(); - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/SimpleBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/SimpleBooleanProperty.java deleted file mode 100644 index d307e060..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/SimpleBooleanProperty.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/UnmodifiableArrayList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/UnmodifiableArrayList.java new file mode 100644 index 00000000..4d62f697 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/UnmodifiableArrayList.java @@ -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 extends AbstractList 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; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableBooleanValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableBooleanValue.java deleted file mode 100644 index 83bec331..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableBooleanValue.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface WritableBooleanValue extends WritableValue { - - boolean get(); - - void set(boolean value); - - @Override - void setValue(Boolean value); - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableValue.java deleted file mode 100644 index d3cc8307..00000000 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WritableValue.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.tungsten.fclcore.fakefx; - -public interface WritableValue { - - T getValue(); - - void setValue(T value); - -} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/DefaultProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/DefaultProperty.java new file mode 100644 index 00000000..c621d4f1 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/DefaultProperty.java @@ -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(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/IDProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/IDProperty.java new file mode 100644 index 00000000..0f91c7ac --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/IDProperty.java @@ -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(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/InvalidationListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/InvalidationListener.java new file mode 100644 index 00000000..a9f3dcce --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/InvalidationListener.java @@ -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. + *

+ * 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); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/NamedArg.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/NamedArg.java new file mode 100644 index 00000000..31c4667e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/NamedArg.java @@ -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 ""; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/Observable.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/Observable.java new file mode 100644 index 00000000..b96abbeb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/Observable.java @@ -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. + *

+ * Note that the same actual {@code InvalidationListener} instance may be + * safely registered for different {@code Observables}. + *

+ * 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. + *

+ * 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); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakInvalidationListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakInvalidationListener.java new file mode 100644 index 00000000..71e59a2a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakInvalidationListener.java @@ -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. + *

+ * 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. + *

+ * 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 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(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); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WeakListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakListener.java similarity index 87% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WeakListener.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakListener.java index 8892369c..96e18617 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/WeakListener.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/WeakListener.java @@ -1,4 +1,4 @@ -package com.tungsten.fclcore.fakefx; +package com.tungsten.fclcore.fakefx.beans; public interface WeakListener { /** @@ -8,4 +8,4 @@ public interface WeakListener { * @return {@code true} if the linked listener was garbage-collected. */ boolean wasGarbageCollected(); -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Binding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Binding.java new file mode 100644 index 00000000..218ffe5d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Binding.java @@ -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 extends ObservableValue { + + boolean isValid(); + + void invalidate(); + + ObservableList getDependencies(); + + void dispose(); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Bindings.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Bindings.java new file mode 100644 index 00000000..2fe3b2bd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/Bindings.java @@ -0,0 +1,4374 @@ +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.Property; +import com.tungsten.fclcore.fakefx.beans.value.ObservableBooleanValue; +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.beans.value.ObservableObjectValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableStringValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; +import com.tungsten.fclcore.fakefx.binding.BidirectionalContentBinding; +import com.tungsten.fclcore.fakefx.binding.ContentBinding; +import com.tungsten.fclcore.fakefx.binding.DoubleConstant; +import com.tungsten.fclcore.fakefx.binding.FloatConstant; +import com.tungsten.fclcore.fakefx.binding.IntegerConstant; +import com.tungsten.fclcore.fakefx.binding.LongConstant; +import com.tungsten.fclcore.fakefx.binding.ObjectConstant; +import com.tungsten.fclcore.fakefx.binding.SelectBinding; +import com.tungsten.fclcore.fakefx.binding.StringConstant; +import com.tungsten.fclcore.fakefx.binding.StringFormatter; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ImmutableObservableList; +import com.tungsten.fclcore.fakefx.collections.ObservableArray; +import com.tungsten.fclcore.fakefx.collections.ObservableFloatArray; +import com.tungsten.fclcore.fakefx.collections.ObservableIntegerArray; +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.fakefx.util.StringConverter; + +import java.lang.ref.WeakReference; +import java.text.Format; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +public final class Bindings { + + private Bindings() { + } + + // ================================================================================================================= + // Helper functions to create custom bindings + + public static BooleanBinding createBooleanBinding(final Callable func, final Observable... dependencies) { + return new BooleanBinding() { + { + bind(dependencies); + } + + @Override + protected boolean computeValue() { + try { + return func.call(); + } catch (Exception e) { + return false; + } + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static DoubleBinding createDoubleBinding(final Callable func, final Observable... dependencies) { + return new DoubleBinding() { + { + bind(dependencies); + } + + @Override + protected double computeValue() { + try { + return func.call(); + } catch (Exception e) { + return 0.0; + } + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static FloatBinding createFloatBinding(final Callable func, final Observable... dependencies) { + return new FloatBinding() { + { + bind(dependencies); + } + + @Override + protected float computeValue() { + try { + return func.call(); + } catch (Exception e) { + return 0.0f; + } + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static IntegerBinding createIntegerBinding(final Callable func, final Observable... dependencies) { + return new IntegerBinding() { + { + bind(dependencies); + } + + @Override + protected int computeValue() { + try { + return func.call(); + } catch (Exception e) { + return 0; + } + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + /** + * Returns an immutable list of the dependencies of this binding. + */ + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + /** + * Helper function to create a custom {@link LongBinding}. + * + * @param func The function that calculates the value of this binding + * @param dependencies The dependencies of this binding + * @return The generated binding + * @since JavaFX 2.1 + */ + public static LongBinding createLongBinding(final Callable func, final Observable... dependencies) { + return new LongBinding() { + { + bind(dependencies); + } + + @Override + protected long computeValue() { + try { + return func.call(); + } catch (Exception e) { + return 0L; + } + } + + /** + * Calls {@link LongBinding#unbind(Observable...)}. + */ + @Override + public void dispose() { + super.unbind(dependencies); + } + + /** + * Returns an immutable list of the dependencies of this binding. + */ + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + /** + * Helper function to create a custom {@link ObjectBinding}. + * + * @param the type of the bound {@code Object} + * @param func The function that calculates the value of this binding + * @param dependencies The dependencies of this binding + * @return The generated binding + * @since JavaFX 2.1 + */ + public static ObjectBinding createObjectBinding(final Callable func, final Observable... dependencies) { + return new ObjectBinding() { + { + bind(dependencies); + } + + @Override + protected T computeValue() { + try { + return func.call(); + } catch (Exception e) { + return null; + } + } + + /** + * Calls {@link ObjectBinding#unbind(Observable...)}. + */ + @Override + public void dispose() { + super.unbind(dependencies); + } + + /** + * Returns an immutable list of the dependencies of this binding. + */ + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + /** + * Helper function to create a custom {@link StringBinding}. + * + * @param func The function that calculates the value of this binding + * @param dependencies The dependencies of this binding + * @return The generated binding + * @since JavaFX 2.1 + */ + public static StringBinding createStringBinding(final Callable func, final Observable... dependencies) { + return new StringBinding() { + { + bind(dependencies); + } + + @Override + protected String computeValue() { + try { + return func.call(); + } catch (Exception e) { + return ""; + } + } + + /** + * Calls {@link StringBinding#unbind(Observable...)}. + */ + @Override + public void dispose() { + super.unbind(dependencies); + } + + /** + * Returns an immutable list of the dependencies of this binding. + */ + @Override + public ObservableList getDependencies() { + return ((dependencies == null) || (dependencies.length == 0))? + FXCollections.emptyObservableList() + : (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + + // ================================================================================================================= + // Select Bindings + + public static ObjectBinding select(ObservableValue root, String... steps) { + return new SelectBinding.AsObject(root, steps); + } + + public static DoubleBinding selectDouble(ObservableValue root, String... steps) { + return new SelectBinding.AsDouble(root, steps); + } + + public static FloatBinding selectFloat(ObservableValue root, String... steps) { + return new SelectBinding.AsFloat(root, steps); + } + + public static IntegerBinding selectInteger(ObservableValue root, String... steps) { + return new SelectBinding.AsInteger(root, steps); + } + + public static LongBinding selectLong(ObservableValue root, String... steps) { + return new SelectBinding.AsLong(root, steps); + } + + public static BooleanBinding selectBoolean(ObservableValue root, String... steps) { + return new SelectBinding.AsBoolean(root, steps); + } + + public static StringBinding selectString(ObservableValue root, String... steps) { + return new SelectBinding.AsString(root, steps); + } + + public static ObjectBinding select(Object root, String... steps) { + return new SelectBinding.AsObject(root, steps); + } + + public static DoubleBinding selectDouble(Object root, String... steps) { + return new SelectBinding.AsDouble(root, steps); + } + + public static FloatBinding selectFloat(Object root, String... steps) { + return new SelectBinding.AsFloat(root, steps); + } + + public static IntegerBinding selectInteger(Object root, String... steps) { + return new SelectBinding.AsInteger(root, steps); + } + + public static LongBinding selectLong(Object root, String... steps) { + return new SelectBinding.AsLong(root, steps); + } + + public static BooleanBinding selectBoolean(Object root, String... steps) { + return new SelectBinding.AsBoolean(root, steps); + } + + public static StringBinding selectString(Object root, String... steps) { + return new SelectBinding.AsString(root, steps); + } + + public static When when(final ObservableBooleanValue condition) { + return new When(condition); + } + + // ================================================================================================================= + // Bidirectional Bindings + + public static void bindBidirectional(Property property1, Property property2) { + BidirectionalBinding.bind(property1, property2); + } + + public static void unbindBidirectional(Property property1, Property property2) { + BidirectionalBinding.unbind(property1, property2); + } + + public static void unbindBidirectional(Object property1, Object property2) { + BidirectionalBinding.unbind(property1, property2); + } + + public static void bindBidirectional(Property stringProperty, Property otherProperty, Format format) { + BidirectionalBinding.bind(stringProperty, otherProperty, format); + } + + public static void bindBidirectional(Property stringProperty, Property otherProperty, StringConverter converter) { + BidirectionalBinding.bind(stringProperty, otherProperty, converter); + } + + public static void bindContentBidirectional(ObservableList list1, ObservableList list2) { + BidirectionalContentBinding.bind(list1, list2); + } + + public static void bindContentBidirectional(ObservableSet set1, ObservableSet set2) { + BidirectionalContentBinding.bind(set1, set2); + } + + public static void bindContentBidirectional(ObservableMap map1, ObservableMap map2) { + BidirectionalContentBinding.bind(map1, map2); + } + + public static void unbindContentBidirectional(Object obj1, Object obj2) { + BidirectionalContentBinding.unbind(obj1, obj2); + } + + public static void bindContent(List list1, ObservableList list2) { + ContentBinding.bind(list1, list2); + } + + /** + * Generates a content binding between an {@link ObservableSet} and a {@link Set}. + *

+ * A content binding ensures that the {@code Set} contains the same elements as the {@code ObservableSet}. + * If the content of the {@code ObservableSet} changes, the {@code Set} will be updated automatically. + *

+ * Once a {@code Set} is bound to an {@code ObservableSet}, the {@code Set} must not be changed directly + * anymore. Doing so would lead to unexpected results. + *

+ * A content-binding can be removed with {@link #unbindContent(Object, Object)}. + * + * @param + * the type of the {@code Set} elements + * @param set1 + * the {@code Set} + * @param set2 + * the {@code ObservableSet} + * @throws NullPointerException + * if one of the sets is {@code null} + * @throws IllegalArgumentException + * if {@code set1} == {@code set2} + * @since JavaFX 2.1 + */ + public static void bindContent(Set set1, ObservableSet set2) { + ContentBinding.bind(set1, set2); + } + + /** + * Generates a content binding between an {@link ObservableMap} and a {@link Map}. + *

+ * A content binding ensures that the {@code Map} contains the same elements as the {@code ObservableMap}. + * If the content of the {@code ObservableMap} changes, the {@code Map} will be updated automatically. + *

+ * Once a {@code Map} is bound to an {@code ObservableMap}, the {@code Map} must not be changed directly + * anymore. Doing so would lead to unexpected results. + *

+ * A content-binding can be removed with {@link #unbindContent(Object, Object)}. + * + * @param + * the type of the key elements of the {@code Map} + * @param + * the type of the value elements of the {@code Map} + * @param map1 + * the {@code Map} + * @param map2 + * the {@code ObservableMap} + * @throws NullPointerException + * if one of the maps is {@code null} + * @throws IllegalArgumentException + * if {@code map1} == {@code map2} + * @since JavaFX 2.1 + */ + public static void bindContent(Map map1, ObservableMap map2) { + ContentBinding.bind(map1, map2); + } + + /** + * Remove a content binding. + * + * @param obj1 + * the first {@code Object} + * @param obj2 + * the second {@code Object} + * @throws NullPointerException + * if one of the {@code Objects} is {@code null} + * @throws IllegalArgumentException + * if {@code obj1} == {@code obj2} + * @since JavaFX 2.1 + */ + public static void unbindContent(Object obj1, Object obj2) { + ContentBinding.unbind(obj1, obj2); + } + + + + // ================================================================================================================= + // Negation + + public static NumberBinding negate(final ObservableNumberValue value) { + if (value == null) { + throw new NullPointerException("Operand cannot be null."); + } + + if (value instanceof ObservableDoubleValue) { + return new DoubleBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected double computeValue() { + return -value.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } else if (value instanceof ObservableFloatValue) { + return new FloatBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected float computeValue() { + return -value.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } else if (value instanceof ObservableLongValue) { + return new LongBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected long computeValue() { + return -value.longValue(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected int computeValue() { + return -value.intValue(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + } + + // ================================================================================================================= + // Sum + + private static NumberBinding add(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return op1.doubleValue() + op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return op1.floatValue() + op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return op1.longValue() + op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return op1.intValue() + op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding add(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return Bindings.add(op1, op2, op1, op2); + } + + public static DoubleBinding add(final ObservableNumberValue op1, double op2) { + return (DoubleBinding) Bindings.add(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding add(double op1, final ObservableNumberValue op2) { + return (DoubleBinding) Bindings.add(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding add(final ObservableNumberValue op1, float op2) { + return Bindings.add(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding add(float op1, final ObservableNumberValue op2) { + return Bindings.add(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding add(final ObservableNumberValue op1, long op2) { + return Bindings.add(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding add(long op1, final ObservableNumberValue op2) { + return Bindings.add(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding add(final ObservableNumberValue op1, int op2) { + return Bindings.add(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding add(int op1, final ObservableNumberValue op2) { + return Bindings.add(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Diff + + private static NumberBinding subtract(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return op1.doubleValue() - op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return op1.floatValue() - op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return op1.longValue() - op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return op1.intValue() - op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding subtract(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return Bindings.subtract(op1, op2, op1, op2); + } + + public static DoubleBinding subtract(final ObservableNumberValue op1, double op2) { + return (DoubleBinding) Bindings.subtract(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding subtract(double op1, final ObservableNumberValue op2) { + return (DoubleBinding) Bindings.subtract(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding subtract(final ObservableNumberValue op1, float op2) { + return Bindings.subtract(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding subtract(float op1, final ObservableNumberValue op2) { + return Bindings.subtract(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding subtract(final ObservableNumberValue op1, long op2) { + return Bindings.subtract(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding subtract(long op1, final ObservableNumberValue op2) { + return Bindings.subtract(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding subtract(final ObservableNumberValue op1, int op2) { + return Bindings.subtract(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding subtract(int op1, final ObservableNumberValue op2) { + return Bindings.subtract(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Multiply + + private static NumberBinding multiply(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return op1.doubleValue() * op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return op1.floatValue() * op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return op1.longValue() * op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return op1.intValue() * op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding multiply(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return Bindings.multiply(op1, op2, op1, op2); + } + + public static DoubleBinding multiply(final ObservableNumberValue op1, double op2) { + return (DoubleBinding) Bindings.multiply(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding multiply(double op1, final ObservableNumberValue op2) { + return (DoubleBinding) Bindings.multiply(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding multiply(final ObservableNumberValue op1, float op2) { + return Bindings.multiply(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding multiply(float op1, final ObservableNumberValue op2) { + return Bindings.multiply(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding multiply(final ObservableNumberValue op1, long op2) { + return Bindings.multiply(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding multiply(long op1, final ObservableNumberValue op2) { + return Bindings.multiply(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding multiply(final ObservableNumberValue op1, int op2) { + return Bindings.multiply(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding multiply(int op1, final ObservableNumberValue op2) { + return Bindings.multiply(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Divide + + private static NumberBinding divide(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return op1.doubleValue() / op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return op1.floatValue() / op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return op1.longValue() / op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return op1.intValue() / op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding divide(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return Bindings.divide(op1, op2, op1, op2); + } + + public static DoubleBinding divide(final ObservableNumberValue op1, double op2) { + return (DoubleBinding) Bindings.divide(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding divide(double op1, final ObservableNumberValue op2) { + return (DoubleBinding) Bindings.divide(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding divide(final ObservableNumberValue op1, float op2) { + return Bindings.divide(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding divide(float op1, final ObservableNumberValue op2) { + return Bindings.divide(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding divide(final ObservableNumberValue op1, long op2) { + return Bindings.divide(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding divide(long op1, final ObservableNumberValue op2) { + return Bindings.divide(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding divide(final ObservableNumberValue op1, int op2) { + return Bindings.divide(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding divide(int op1, final ObservableNumberValue op2) { + return Bindings.divide(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Equals + + private static BooleanBinding equal(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.doubleValue() - op2.doubleValue()) <= epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.floatValue() - op2.floatValue()) <= epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.longValue() - op2.longValue()) <= epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.intValue() - op2.intValue()) <= epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon) { + return Bindings.equal(op1, op2, epsilon, op1, op2); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return equal(op1, op2, 0.0, op1, op2); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final double op2, final double epsilon) { + return equal(op1, DoubleConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding equal(final double op1, final ObservableNumberValue op2, final double epsilon) { + return equal(DoubleConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final float op2, final double epsilon) { + return equal(op1, FloatConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding equal(final float op1, final ObservableNumberValue op2, final double epsilon) { + return equal(FloatConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final long op2, final double epsilon) { + return equal(op1, LongConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final long op2) { + return equal(op1, LongConstant.valueOf(op2), 0.0, op1); + } + + public static BooleanBinding equal(final long op1, final ObservableNumberValue op2, final double epsilon) { + return equal(LongConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding equal(final long op1, final ObservableNumberValue op2) { + return equal(LongConstant.valueOf(op1), op2, 0.0, op2); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final int op2, final double epsilon) { + return equal(op1, IntegerConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding equal(final ObservableNumberValue op1, final int op2) { + return equal(op1, IntegerConstant.valueOf(op2), 0.0, op1); + } + + public static BooleanBinding equal(final int op1, final ObservableNumberValue op2, final double epsilon) { + return equal(IntegerConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding equal(final int op1, final ObservableNumberValue op2) { + return equal(IntegerConstant.valueOf(op1), op2, 0.0, op2); + } + + // ================================================================================================================= + // Not Equal + + private static BooleanBinding notEqual(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.doubleValue() - op2.doubleValue()) > epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.floatValue() - op2.floatValue()) > epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.longValue() - op2.longValue()) > epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return Math.abs(op1.intValue() - op2.intValue()) > epsilon; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon) { + return Bindings.notEqual(op1, op2, epsilon, op1, op2); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return notEqual(op1, op2, 0.0, op1, op2); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final double op2, final double epsilon) { + return notEqual(op1, DoubleConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding notEqual(final double op1, final ObservableNumberValue op2, final double epsilon) { + return notEqual(DoubleConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final float op2, final double epsilon) { + return notEqual(op1, FloatConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding notEqual(final float op1, final ObservableNumberValue op2, final double epsilon) { + return notEqual(FloatConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final long op2, final double epsilon) { + return notEqual(op1, LongConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final long op2) { + return notEqual(op1, LongConstant.valueOf(op2), 0.0, op1); + } + + public static BooleanBinding notEqual(final long op1, final ObservableNumberValue op2, final double epsilon) { + return notEqual(LongConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding notEqual(final long op1, final ObservableNumberValue op2) { + return notEqual(LongConstant.valueOf(op1), op2, 0.0, op2); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final int op2, final double epsilon) { + return notEqual(op1, IntegerConstant.valueOf(op2), epsilon, op1); + } + + public static BooleanBinding notEqual(final ObservableNumberValue op1, final int op2) { + return notEqual(op1, IntegerConstant.valueOf(op2), 0.0, op1); + } + + public static BooleanBinding notEqual(final int op1, final ObservableNumberValue op2, final double epsilon) { + return notEqual(IntegerConstant.valueOf(op1), op2, epsilon, op2); + } + + public static BooleanBinding notEqual(final int op1, final ObservableNumberValue op2) { + return notEqual(IntegerConstant.valueOf(op1), op2, 0.0, op2); + } + + // ================================================================================================================= + // Greater Than + + private static BooleanBinding greaterThan(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.doubleValue() > op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.floatValue() > op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.longValue() > op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.intValue() > op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return Bindings.greaterThan(op1, op2, op1, op2); + } + + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final double op2) { + return greaterThan(op1, DoubleConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThan(final double op1, final ObservableNumberValue op2) { + return greaterThan(DoubleConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final float op2) { + return greaterThan(op1, FloatConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThan(final float op1, final ObservableNumberValue op2) { + return greaterThan(FloatConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final long op2) { + return greaterThan(op1, LongConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThan(final long op1, final ObservableNumberValue op2) { + return greaterThan(LongConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final int op2) { + return greaterThan(op1, IntegerConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThan(final int op1, final ObservableNumberValue op2) { + return greaterThan(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Less Than + + private static BooleanBinding lessThan(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + return greaterThan(op2, op1, dependencies); + } + + public static BooleanBinding lessThan(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return lessThan(op1, op2, op1, op2); + } + + public static BooleanBinding lessThan(final ObservableNumberValue op1, final double op2) { + return lessThan(op1, DoubleConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThan(final double op1, final ObservableNumberValue op2) { + return lessThan(DoubleConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThan(final ObservableNumberValue op1, final float op2) { + return lessThan(op1, FloatConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThan(final float op1, final ObservableNumberValue op2) { + return lessThan(FloatConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThan(final ObservableNumberValue op1, final long op2) { + return lessThan(op1, LongConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThan(final long op1, final ObservableNumberValue op2) { + return lessThan(LongConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThan(final ObservableNumberValue op1, final int op2) { + return lessThan(op1, IntegerConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThan(final int op1, final ObservableNumberValue op2) { + return lessThan(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Greater Than or Equal + + private static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.doubleValue() >= op2.doubleValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.floatValue() >= op2.floatValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.longValue() >= op2.longValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + return op1.intValue() >= op2.intValue(); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return greaterThanOrEqual(op1, op2, op1, op2); + } + + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final double op2) { + return greaterThanOrEqual(op1, DoubleConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThanOrEqual(final double op1, final ObservableNumberValue op2) { + return greaterThanOrEqual(DoubleConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final float op2) { + return greaterThanOrEqual(op1, FloatConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThanOrEqual(final float op1, final ObservableNumberValue op2) { + return greaterThanOrEqual(FloatConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final long op2) { + return greaterThanOrEqual(op1, LongConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThanOrEqual(final long op1, final ObservableNumberValue op2) { + return greaterThanOrEqual(LongConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final int op2) { + return greaterThanOrEqual(op1, IntegerConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThanOrEqual(final int op1, final ObservableNumberValue op2) { + return greaterThanOrEqual(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Less Than or Equal + + private static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2, Observable... dependencies) { + return greaterThanOrEqual(op2, op1, dependencies); + } + + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return lessThanOrEqual(op1, op2, op1, op2); + } + + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final double op2) { + return lessThanOrEqual(op1, DoubleConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThanOrEqual(final double op1, final ObservableNumberValue op2) { + return lessThanOrEqual(DoubleConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final float op2) { + return lessThanOrEqual(op1, FloatConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThanOrEqual(final float op1, final ObservableNumberValue op2) { + return lessThanOrEqual(FloatConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final long op2) { + return lessThanOrEqual(op1, LongConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThanOrEqual(final long op1, final ObservableNumberValue op2) { + return lessThanOrEqual(LongConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final int op2) { + return lessThanOrEqual(op1, IntegerConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThanOrEqual(final int op1, final ObservableNumberValue op2) { + return lessThanOrEqual(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Minimum + + private static NumberBinding min(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return Math.min(op1.doubleValue(), op2.doubleValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return Math.min(op1.floatValue(), op2.floatValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return Math.min(op1.longValue(), op2.longValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return Math.min(op1.intValue(), op2.intValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding min(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return min(op1, op2, op1, op2); + } + + public static DoubleBinding min(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) min(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding min(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) min(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding min(final ObservableNumberValue op1, final float op2) { + return min(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding min(final float op1, final ObservableNumberValue op2) { + return min(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding min(final ObservableNumberValue op1, final long op2) { + return min(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding min(final long op1, final ObservableNumberValue op2) { + return min(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding min(final ObservableNumberValue op1, final int op2) { + return min(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding min(final int op1, final ObservableNumberValue op2) { + return min(IntegerConstant.valueOf(op1), op2, op2); + } + + // ================================================================================================================= + // Maximum + + private static NumberBinding max(final ObservableNumberValue op1, final ObservableNumberValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + if ((op1 instanceof ObservableDoubleValue) || (op2 instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected double computeValue() { + return Math.max(op1.doubleValue(), op2.doubleValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableFloatValue) || (op2 instanceof ObservableFloatValue)) { + return new FloatBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected float computeValue() { + return Math.max(op1.floatValue(), op2.floatValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else if ((op1 instanceof ObservableLongValue) || (op2 instanceof ObservableLongValue)) { + return new LongBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected long computeValue() { + return Math.max(op1.longValue(), op2.longValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } else { + return new IntegerBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected int computeValue() { + return Math.max(op1.intValue(), op2.intValue()); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + } + + public static NumberBinding max(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return max(op1, op2, op1, op2); + } + + public static DoubleBinding max(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) max(op1, DoubleConstant.valueOf(op2), op1); + } + + public static DoubleBinding max(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) max(DoubleConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding max(final ObservableNumberValue op1, final float op2) { + return max(op1, FloatConstant.valueOf(op2), op1); + } + + public static NumberBinding max(final float op1, final ObservableNumberValue op2) { + return max(FloatConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding max(final ObservableNumberValue op1, final long op2) { + return max(op1, LongConstant.valueOf(op2), op1); + } + + public static NumberBinding max(final long op1, final ObservableNumberValue op2) { + return max(LongConstant.valueOf(op1), op2, op2); + } + + public static NumberBinding max(final ObservableNumberValue op1, final int op2) { + return max(op1, IntegerConstant.valueOf(op2), op1); + } + + public static NumberBinding max(final int op1, final ObservableNumberValue op2) { + return max(IntegerConstant.valueOf(op1), op2, op2); + } + + // boolean + // ================================================================================================================= + + private static class BooleanAndBinding extends BooleanBinding { + + private final ObservableBooleanValue op1; + private final ObservableBooleanValue op2; + private final InvalidationListener observer; + + public BooleanAndBinding(ObservableBooleanValue op1, ObservableBooleanValue op2) { + this.op1 = op1; + this.op2 = op2; + + observer = new ShortCircuitAndInvalidator(this); + + op1.addListener(observer); + op2.addListener(observer); + } + + + @Override + public void dispose() { + op1.removeListener(observer); + op2.removeListener(observer); + } + + @Override + protected boolean computeValue() { + return op1.get() && op2.get(); + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList<>(op1, op2); + } + } + + private static class ShortCircuitAndInvalidator implements InvalidationListener { + + private final WeakReference ref; + + private ShortCircuitAndInvalidator(BooleanAndBinding binding) { + assert binding != null; + ref = new WeakReference<>(binding); + } + + @Override + public void invalidated(Observable observable) { + final BooleanAndBinding binding = ref.get(); + if (binding == null) { + observable.removeListener(this); + } else { + // short-circuit invalidation. This BooleanBinding becomes + // only invalid if the first operator changes or the + // first parameter is true. + if ((binding.op1.equals(observable) || (binding.isValid() && binding.op1.get()))) { + binding.invalidate(); + } + } + } + + } + + public static BooleanBinding and(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanAndBinding(op1, op2); + } + + private static class BooleanOrBinding extends BooleanBinding { + + private final ObservableBooleanValue op1; + private final ObservableBooleanValue op2; + private final InvalidationListener observer; + + public BooleanOrBinding(ObservableBooleanValue op1, ObservableBooleanValue op2) { + this.op1 = op1; + this.op2 = op2; + observer = new ShortCircuitOrInvalidator(this); + op1.addListener(observer); + op2.addListener(observer); + } + + + @Override + public void dispose() { + op1.removeListener(observer); + op2.removeListener(observer); + } + + @Override + protected boolean computeValue() { + return op1.get() || op2.get(); + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList<>(op1, op2); + } + } + + + private static class ShortCircuitOrInvalidator implements InvalidationListener { + + private final WeakReference ref; + + private ShortCircuitOrInvalidator(BooleanOrBinding binding) { + assert binding != null; + ref = new WeakReference<>(binding); + } + + @Override + public void invalidated(Observable observable) { + final BooleanOrBinding binding = ref.get(); + if (binding == null) { + observable.removeListener(this); + } else { + // short circuit invalidation. This BooleanBinding becomes + // only invalid if the first operator changes or the + // first parameter is false. + if ((binding.op1.equals(observable) || (binding.isValid() && !binding.op1.get()))) { + binding.invalidate(); + } + } + } + + } + + public static BooleanBinding or(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanOrBinding(op1, op2); + } + + public static BooleanBinding not(final ObservableBooleanValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return !op.get(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding equal(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op1, op2); + } + + @Override + public void dispose() { + super.unbind(op1, op2); + } + + @Override + protected boolean computeValue() { + return op1.get() == op2.get(); + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op1, op2); + } + }; + } + + public static BooleanBinding notEqual(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op1, op2); + } + + @Override + public void dispose() { + super.unbind(op1, op2); + } + + @Override + protected boolean computeValue() { + return op1.get() != op2.get(); + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op1, op2); + } + }; + } + + // String + // ================================================================================================================= + + public static StringExpression convert(ObservableValue observableValue) { + return StringFormatter.convert(observableValue); + } + + public static StringExpression concat(Object... args) { + return StringFormatter.concat(args); + } + + public static StringExpression format(String format, Object... args) { + return StringFormatter.format(format, args); + } + + public static StringExpression format(Locale locale, String format, + Object... args) { + return StringFormatter.format(locale, format, args); + } + + private static String getStringSafe(String value) { + return value == null ? "" : value; + } + + private static BooleanBinding equal(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return s1.equals(s2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding equal(final ObservableStringValue op1, final ObservableStringValue op2) { + return equal(op1, op2, op1, op2); + } + + public static BooleanBinding equal(final ObservableStringValue op1, String op2) { + return equal(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding equal(String op1, final ObservableStringValue op2) { + return equal(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding notEqual(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return ! s1.equals(s2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding notEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return notEqual(op1, op2, op1, op2); + } + + public static BooleanBinding notEqual(final ObservableStringValue op1, String op2) { + return notEqual(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding notEqual(String op1, final ObservableStringValue op2) { + return notEqual(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding equalIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return s1.equalsIgnoreCase(s2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding equalIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2) { + return equalIgnoreCase(op1, op2, op1, op2); + } + + public static BooleanBinding equalIgnoreCase(final ObservableStringValue op1, String op2) { + return equalIgnoreCase(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding equalIgnoreCase(String op1, final ObservableStringValue op2) { + return equalIgnoreCase(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding notEqualIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return ! s1.equalsIgnoreCase(s2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding notEqualIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2) { + return notEqualIgnoreCase(op1, op2, op1, op2); + } + + public static BooleanBinding notEqualIgnoreCase(final ObservableStringValue op1, String op2) { + return notEqualIgnoreCase(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding notEqualIgnoreCase(String op1, final ObservableStringValue op2) { + return notEqualIgnoreCase(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding greaterThan(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return s1.compareTo(s2) > 0; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding greaterThan(final ObservableStringValue op1, final ObservableStringValue op2) { + return greaterThan(op1, op2, op1, op2); + } + + public static BooleanBinding greaterThan(final ObservableStringValue op1, String op2) { + return greaterThan(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThan(String op1, final ObservableStringValue op2) { + return greaterThan(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding lessThan(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + return greaterThan(op2, op1, dependencies); + } + + public static BooleanBinding lessThan(final ObservableStringValue op1, final ObservableStringValue op2) { + return lessThan(op1, op2, op1, op2); + } + + public static BooleanBinding lessThan(final ObservableStringValue op1, String op2) { + return lessThan(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThan(String op1, final ObservableStringValue op2) { + return lessThan(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding greaterThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final String s1 = getStringSafe(op1.get()); + final String s2 = getStringSafe(op2.get()); + return s1.compareTo(s2) >= 0; + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding greaterThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return greaterThanOrEqual(op1, op2, op1, op2); + } + + public static BooleanBinding greaterThanOrEqual(final ObservableStringValue op1, String op2) { + return greaterThanOrEqual(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding greaterThanOrEqual(String op1, final ObservableStringValue op2) { + return greaterThanOrEqual(StringConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding lessThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2, final Observable... dependencies) { + return greaterThanOrEqual(op2, op1, dependencies); + } + + public static BooleanBinding lessThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return lessThanOrEqual(op1, op2, op1, op2); + } + + public static BooleanBinding lessThanOrEqual(final ObservableStringValue op1, String op2) { + return lessThanOrEqual(op1, StringConstant.valueOf(op2), op1); + } + + public static BooleanBinding lessThanOrEqual(String op1, final ObservableStringValue op2) { + return lessThanOrEqual(StringConstant.valueOf(op1), op2, op2); + } + + public static IntegerBinding length(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + return getStringSafe(op.get()).length(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isEmpty(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return getStringSafe(op.get()).isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isNotEmpty(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return !getStringSafe(op.get()).isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + // Object + // ================================================================================================================= + + private static BooleanBinding equal(final ObservableObjectValue op1, final ObservableObjectValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final Object obj1 = op1.get(); + final Object obj2 = op2.get(); + return obj1 == null ? obj2 == null : obj1.equals(obj2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding equal(final ObservableObjectValue op1, final ObservableObjectValue op2) { + return equal(op1, op2, op1, op2); + } + + public static BooleanBinding equal(final ObservableObjectValue op1, Object op2) { + return equal(op1, ObjectConstant.valueOf(op2), op1); + } + + public static BooleanBinding equal(Object op1, final ObservableObjectValue op2) { + return equal(ObjectConstant.valueOf(op1), op2, op2); + } + + private static BooleanBinding notEqual(final ObservableObjectValue op1, final ObservableObjectValue op2, final Observable... dependencies) { + if ((op1 == null) || (op2 == null)) { + throw new NullPointerException("Operands cannot be null."); + } + assert (dependencies != null) && (dependencies.length > 0); + + return new BooleanBinding() { + { + super.bind(dependencies); + } + + @Override + public void dispose() { + super.unbind(dependencies); + } + + @Override + protected boolean computeValue() { + final Object obj1 = op1.get(); + final Object obj2 = op2.get(); + return obj1 == null ? obj2 != null : ! obj1.equals(obj2); + } + + @Override + public ObservableList getDependencies() { + return (dependencies.length == 1)? + FXCollections.singletonObservableList(dependencies[0]) + : new ImmutableObservableList(dependencies); + } + }; + } + + public static BooleanBinding notEqual(final ObservableObjectValue op1, final ObservableObjectValue op2) { + return notEqual(op1, op2, op1, op2); + } + + public static BooleanBinding notEqual(final ObservableObjectValue op1, Object op2) { + return notEqual(op1, ObjectConstant.valueOf(op2), op1); + } + + public static BooleanBinding notEqual(Object op1, final ObservableObjectValue op2) { + return notEqual(ObjectConstant.valueOf(op1), op2, op2); + } + + public static BooleanBinding isNull(final ObservableObjectValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return op.get() == null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isNotNull(final ObservableObjectValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return op.get() != null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + // List + // ================================================================================================================= + + public static IntegerBinding size(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + return op.size(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isEmpty(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isNotEmpty(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return !op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static ObjectBinding valueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new ObjectBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected E computeValue() { + try { + return op.get(index); + } catch (IndexOutOfBoundsException ex) { + + } + return null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static ObjectBinding valueAt(final ObservableList op, final ObservableIntegerValue index) { + return valueAt(op, (ObservableNumberValue)index); + } + + public static ObjectBinding valueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected E computeValue() { + try { + return op.get(index.intValue()); + } catch (IndexOutOfBoundsException ex) { + + } + return null; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static BooleanBinding booleanValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + try { + final Boolean value = op.get(index); + if (value == null) { + + } else { + return value; + } + } catch (IndexOutOfBoundsException ex) { + + } + return false; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding booleanValueAt(final ObservableList op, final ObservableIntegerValue index) { + return booleanValueAt(op, (ObservableNumberValue)index); + } + + public static BooleanBinding booleanValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected boolean computeValue() { + try { + final Boolean value = op.get(index.intValue()); + if (value == null) { + + } else { + return value; + } + } catch (IndexOutOfBoundsException ex) { + + } + return false; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static DoubleBinding doubleValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new DoubleBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected double computeValue() { + try { + final Number value = op.get(index); + if (value == null) { + + } else { + return value.doubleValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static DoubleBinding doubleValueAt(final ObservableList op, final ObservableIntegerValue index) { + return doubleValueAt(op, (ObservableNumberValue)index); + } + + public static DoubleBinding doubleValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new DoubleBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected double computeValue() { + try { + final Number value = op.get(index.intValue()); + if (value == null) { + + } else { + return value.doubleValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new FloatBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected float computeValue() { + try { + final Number value = op.get(index); + if (value == null) { + + } else { + return value.floatValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableList op, final ObservableIntegerValue index) { + return floatValueAt(op, (ObservableNumberValue)index); + } + + public static FloatBinding floatValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new FloatBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected float computeValue() { + try { + final Number value = op.get(index.intValue()); + if (value == null) { + + } else { + return value.floatValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + try { + final Number value = op.get(index); + if (value == null) { + + } else { + return value.intValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableList op, final ObservableIntegerValue index) { + return integerValueAt(op, (ObservableNumberValue)index); + } + + public static IntegerBinding integerValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected int computeValue() { + try { + final Number value = op.get(index.intValue()); + if (value == null) { + + } else { + return value.intValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static LongBinding longValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new LongBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected long computeValue() { + try { + final Number value = op.get(index); + if (value == null) { + + } else { + return value.longValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0L; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static LongBinding longValueAt(final ObservableList op, final ObservableIntegerValue index) { + return longValueAt(op, (ObservableNumberValue)index); + } + + public static LongBinding longValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new LongBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected long computeValue() { + try { + final Number value = op.get(index.intValue()); + if (value == null) { + + } else { + return value.longValue(); + } + } catch (IndexOutOfBoundsException ex) { + + } + return 0L; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + public static StringBinding stringValueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new StringBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected String computeValue() { + try { + return op.get(index); + } catch (IndexOutOfBoundsException ex) { + + } + return null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static StringBinding stringValueAt(final ObservableList op, final ObservableIntegerValue index) { + return stringValueAt(op, (ObservableNumberValue)index); + } + + public static StringBinding stringValueAt(final ObservableList op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new StringBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected String computeValue() { + try { + return op.get(index.intValue()); + } catch (IndexOutOfBoundsException ex) { + + } + return null; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, index); + } + }; + } + + // Set + // ================================================================================================================= + + public static IntegerBinding size(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("Set cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + return op.size(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isEmpty(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("Set cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isNotEmpty(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return !op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + // Array + // ================================================================================================================= + + public static IntegerBinding size(final ObservableArray op) { + if (op == null) { + throw new NullPointerException("Array cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + return op.size(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableFloatArray op, final int index) { + if (op == null) { + throw new NullPointerException("Array cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new FloatBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected float computeValue() { + try { + return op.get(index); + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableFloatArray op, final ObservableIntegerValue index) { + return floatValueAt(op, (ObservableNumberValue)index); + } + + public static FloatBinding floatValueAt(final ObservableFloatArray op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new FloatBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected float computeValue() { + try { + return op.get(index.intValue()); + } catch (IndexOutOfBoundsException ex) { + + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList<>(op, index); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableIntegerArray op, final int index) { + if (op == null) { + throw new NullPointerException("Array cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + try { + return op.get(index); + } catch (IndexOutOfBoundsException ex) { + + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableIntegerArray op, final ObservableIntegerValue index) { + return integerValueAt(op, (ObservableNumberValue)index); + } + + public static IntegerBinding integerValueAt(final ObservableIntegerArray op, final ObservableNumberValue index) { + if ((op == null) || (index == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op, index); + } + + @Override + public void dispose() { + super.unbind(op, index); + } + + @Override + protected int computeValue() { + try { + return op.get(index.intValue()); + } catch (IndexOutOfBoundsException ex) { + + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList<>(op, index); + } + }; + } + + // Map + // ================================================================================================================= + + public static IntegerBinding size(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + return op.size(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isEmpty(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding isNotEmpty(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + return !op.isEmpty(); + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static ObjectBinding valueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected V computeValue() { + try { + return op.get(key); + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static ObjectBinding valueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new ObjectBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected V computeValue() { + try { + return op.get(key.getValue()); + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return null; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static BooleanBinding booleanValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected boolean computeValue() { + try { + final Boolean value = op.get(key); + if (value == null) { + + } else { + return value; + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return false; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static BooleanBinding booleanValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new BooleanBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected boolean computeValue() { + try { + final Boolean value = op.get(key.getValue()); + if (value == null) { + + } else { + return value; + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return false; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static DoubleBinding doubleValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new DoubleBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected double computeValue() { + try { + final Number value = op.get(key); + if (value == null) { + + } else { + return value.doubleValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0.0; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static DoubleBinding doubleValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new DoubleBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected double computeValue() { + try { + final Number value = op.get(key.getValue()); + if (value == null) { + } else { + return value.doubleValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0.0; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new FloatBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected float computeValue() { + try { + final Number value = op.get(key); + if (value == null) { + } else { + return value.floatValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static FloatBinding floatValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new FloatBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected float computeValue() { + try { + final Number value = op.get(key.getValue()); + if (value == null) { + } else { + return value.floatValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0.0f; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected int computeValue() { + try { + final Number value = op.get(key); + if (value == null) { + } else { + return value.intValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static IntegerBinding integerValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new IntegerBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected int computeValue() { + try { + final Number value = op.get(key.getValue()); + if (value == null) { + } else { + return value.intValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static LongBinding longValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new LongBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected long computeValue() { + try { + final Number value = op.get(key); + if (value == null) { + } else { + return value.longValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0L; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static LongBinding longValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new LongBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected long computeValue() { + try { + final Number value = op.get(key.getValue()); + if (value == null) { + } else { + return value.longValue(); + } + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return 0L; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + public static StringBinding stringValueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + + return new StringBinding() { + { + super.bind(op); + } + + @Override + public void dispose() { + super.unbind(op); + } + + @Override + protected String computeValue() { + try { + return op.get(key); + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return null; + } + + @Override + public ObservableList getDependencies() { + return FXCollections.singletonObservableList(op); + } + }; + } + + public static StringBinding stringValueAt(final ObservableMap op, final ObservableValue key) { + if ((op == null) || (key == null)) { + throw new NullPointerException("Operands cannot be null."); + } + + return new StringBinding() { + { + super.bind(op, key); + } + + @Override + public void dispose() { + super.unbind(op, key); + } + + @Override + protected String computeValue() { + try { + return op.get(key.getValue()); + } catch (ClassCastException ex) { + // ignore + } catch (NullPointerException ex) { + // ignore + } + return null; + } + + @Override + public ObservableList getDependencies() { + return new ImmutableObservableList(op, key); + } + }; + } + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanBinding.java similarity index 51% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanBinding.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanBinding.java index f140af84..af6ce233 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanBinding.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanBinding.java @@ -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 Binding { @@ -31,6 +39,13 @@ public abstract class BooleanBinding extends BooleanExpression implements 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) { @@ -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) { if (observer != null) { 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 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 boolean get() { if (!valid) { @@ -64,6 +107,11 @@ public abstract class BooleanBinding extends BooleanExpression implements 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() { } @@ -81,8 +129,20 @@ public abstract class BooleanBinding extends BooleanExpression implements return valid; } + /** + * Calculates the current value of this binding. + *

+ * Classes extending {@code BooleanBinding} have to provide an + * implementation of {@code computeValue}. + * + * @return the current value + */ protected abstract boolean computeValue(); + /** + * Returns a string representation of this {@code BooleanBinding} object. + * @return a string representation of this {@code BooleanBinding} object. + */ @Override public String toString() { return valid ? "BooleanBinding [value: " + get() + "]" diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanExpression.java new file mode 100644 index 00000000..4682801d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/BooleanExpression.java @@ -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 getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + + public static BooleanExpression booleanExpression(final ObservableValue 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> 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 asObject() { + return new ObjectBinding() { + { + bind(BooleanExpression.this); + } + + @Override + public void dispose() { + unbind(BooleanExpression.this); + } + + @Override + protected Boolean computeValue() { + return BooleanExpression.this.getValue(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleBinding.java new file mode 100644 index 00000000..433f99b0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleBinding.java @@ -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 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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]"; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleExpression.java new file mode 100644 index 00000000..7d85b723 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/DoubleExpression.java @@ -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 getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + + public static DoubleExpression doubleExpression(final ObservableValue 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> 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 asObject() { + return new ObjectBinding() { + { + bind(DoubleExpression.this); + } + + @Override + public void dispose() { + unbind(DoubleExpression.this); + } + + @Override + protected Double computeValue() { + return DoubleExpression.this.getValue(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatBinding.java new file mode 100644 index 00000000..1a0715f1 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatBinding.java @@ -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 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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]"; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatExpression.java new file mode 100644 index 00000000..a34182ef --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/FloatExpression.java @@ -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 getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + + public static FloatExpression floatExpression(final ObservableValue 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> 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 asObject() { + return new ObjectBinding() { + { + bind(FloatExpression.this); + } + + @Override + public void dispose() { + unbind(FloatExpression.this); + } + + @Override + protected Float computeValue() { + return FloatExpression.this.getValue(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerBinding.java new file mode 100644 index 00000000..b9ceaca6 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerBinding.java @@ -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 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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]"; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerExpression.java new file mode 100644 index 00000000..301c1adb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/IntegerExpression.java @@ -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 getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + + public static IntegerExpression integerExpression(final ObservableValue 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> 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 asObject() { + return new ObjectBinding() { + { + bind(IntegerExpression.this); + } + + @Override + public void dispose() { + unbind(IntegerExpression.this); + } + + @Override + protected Integer computeValue() { + return IntegerExpression.this.getValue(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListBinding.java new file mode 100644 index 00000000..a0757e31 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListBinding.java @@ -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 extends ListExpression implements Binding> { + + /** + * Creates a default {@code ListBinding}. + */ + public ListBinding() { + } + + private final ListChangeListener listChangeListener = new ListChangeListener() { + @Override + public void onChanged(Change change) { + invalidateProperties(); + onInvalidating(); + ListExpressionHelper.fireValueChangedEvent(helper, change); + } + }; + + private ObservableList value; + private boolean valid = false; + private BindingHelperObserver observer; + private ListExpressionHelper 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> listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = ListExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(ListChangeListener listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ListChangeListener 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 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. + *

+ * Classes extending {@code ListBinding} have to provide an implementation + * of {@code computeValue}. + * + * @return the current value + */ + protected abstract ObservableList 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]"; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListExpression.java new file mode 100644 index 00000000..833cd9ad --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ListExpression.java @@ -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 implements ObservableListValue { + + private static final ObservableList EMPTY_LIST = FXCollections.emptyObservableList(); + + /** + * Creates a default {@code ListExpression}. + */ + public ListExpression() { + } + + @Override + public ObservableList getValue() { + return get(); + } + + public static ListExpression listExpression(final ObservableListValue value) { + if (value == null) { + throw new NullPointerException("List must be specified."); + } + return value instanceof ListExpression ? (ListExpression) value + : new ListBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected ObservableList computeValue() { + return value.get(); + } + + @Override + public ObservableList> 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 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 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 list = get(); + return (list == null)? EMPTY_LIST.size() : list.size(); + } + + @Override + public boolean isEmpty() { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.isEmpty() : list.isEmpty(); + } + + @Override + public boolean contains(Object obj) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.contains(obj) : list.contains(obj); + } + + @Override + public Iterator iterator() { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.iterator() : list.iterator(); + } + + @Override + public Object[] toArray() { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.toArray() : list.toArray(); + } + + @Override + public T[] toArray(T[] array) { + final ObservableList list = get(); + return (list == null)? (T[]) EMPTY_LIST.toArray(array) : list.toArray(array); + } + + @Override + public boolean add(E element) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.add(element) : list.add(element); + } + + @Override + public boolean remove(Object obj) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.remove(obj) : list.remove(obj); + } + + @Override + public boolean containsAll(Collection objects) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.contains(objects) : list.containsAll(objects); + } + + @Override + public boolean addAll(Collection elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.addAll(elements) : list.addAll(elements); + } + + @Override + public boolean addAll(int i, Collection elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.addAll(i, elements) : list.addAll(i, elements); + } + + @Override + public boolean removeAll(Collection objects) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.removeAll(objects) : list.removeAll(objects); + } + + @Override + public boolean retainAll(Collection objects) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.retainAll(objects) : list.retainAll(objects); + } + + @Override + public void clear() { + final ObservableList list = get(); + if (list == null) { + EMPTY_LIST.clear(); + } else { + list.clear(); + } + } + + @Override + public E get(int i) { + final ObservableList list = get(); + return (list == null)? (E) EMPTY_LIST.get(i) : list.get(i); + } + + @Override + public E set(int i, E element) { + final ObservableList 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 list = get(); + if (list == null) { + EMPTY_LIST.add(i, element); + } else { + list.add(i, element); + } + } + + @Override + public E remove(int i) { + final ObservableList list = get(); + return (list == null)? (E) EMPTY_LIST.remove(i) : list.remove(i); + } + + @Override + public int indexOf(Object obj) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.indexOf(obj) : list.indexOf(obj); + } + + @Override + public int lastIndexOf(Object obj) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.lastIndexOf(obj) : list.lastIndexOf(obj); + } + + @Override + public ListIterator listIterator() { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.listIterator() : list.listIterator(); + } + + @Override + public ListIterator listIterator(int i) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.listIterator(i) : list.listIterator(i); + } + + @Override + public List subList(int from, int to) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.subList(from, to) : list.subList(from, to); + } + + @Override + public boolean addAll(E... elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.addAll(elements) : list.addAll(elements); + } + + @Override + public boolean setAll(E... elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.setAll(elements) : list.setAll(elements); + } + + @Override + public boolean setAll(Collection elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.setAll(elements) : list.setAll(elements); + } + + @Override + public boolean removeAll(E... elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.removeAll(elements) : list.removeAll(elements); + } + + @Override + public boolean retainAll(E... elements) { + final ObservableList list = get(); + return (list == null)? EMPTY_LIST.retainAll(elements) : list.retainAll(elements); + } + + @Override + public void remove(int from, int to) { + final ObservableList list = get(); + if (list == null) { + EMPTY_LIST.remove(from, to); + } else { + list.remove(from, to); + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongBinding.java new file mode 100644 index 00000000..6c64d301 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongBinding.java @@ -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 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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]"; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongExpression.java new file mode 100644 index 00000000..912547d5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/LongExpression.java @@ -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 getDependencies() { + return FXCollections.singletonObservableList(value); + } + }; + } + + public static LongExpression longExpression(final ObservableValue 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> 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 asObject() { + return new ObjectBinding() { + { + bind(LongExpression.this); + } + + @Override + public void dispose() { + unbind(LongExpression.this); + } + + @Override + protected Long computeValue() { + return LongExpression.this.getValue(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapBinding.java new file mode 100644 index 00000000..9100f3b3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapBinding.java @@ -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 extends MapExpression implements Binding> { + + private final MapChangeListener mapChangeListener = new MapChangeListener() { + @Override + public void onChanged(Change change) { + invalidateProperties(); + onInvalidating(); + MapExpressionHelper.fireValueChangedEvent(helper, change); + } + }; + + private ObservableMap value; + private boolean valid = false; + private BindingHelperObserver observer; + private MapExpressionHelper 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> listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = MapExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(MapChangeListener listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(MapChangeListener 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 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. + *

+ * Classes extending {@code MapBinding} have to provide an implementation + * of {@code computeValue}. + * + * @return the current value + */ + protected abstract ObservableMap 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]"; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapExpression.java new file mode 100644 index 00000000..c36df725 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/MapExpression.java @@ -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 implements ObservableMapValue { + + private static final ObservableMap EMPTY_MAP = new EmptyObservableMap(); + + private static class EmptyObservableMap extends AbstractMap implements ObservableMap { + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } + + @Override + public void addListener(MapChangeListener mapChangeListener) { + // no-op + } + + @Override + public void removeListener(MapChangeListener mapChangeListener) { + // no-op + } + + @Override + public void addListener(InvalidationListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener listener) { + // no-op + } + } + + @Override + public ObservableMap getValue() { + return get(); + } + + /** + * Creates a default {@code MapExpression}. + */ + public MapExpression() { + } + + public static MapExpression mapExpression(final ObservableMapValue value) { + if (value == null) { + throw new NullPointerException("Map must be specified."); + } + return value instanceof MapExpression ? (MapExpression) value + : new MapBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected ObservableMap 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 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 valueAt(ObservableValue 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 map = get(); + return (map == null)? EMPTY_MAP.size() : map.size(); + } + + @Override + public boolean isEmpty() { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.isEmpty() : map.isEmpty(); + } + + @Override + public boolean containsKey(Object obj) { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.containsKey(obj) : map.containsKey(obj); + } + + @Override + public boolean containsValue(Object obj) { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.containsValue(obj) : map.containsValue(obj); + } + + @Override + public V put(K key, V value) { + final ObservableMap map = get(); + return (map == null)? (V) EMPTY_MAP.put(key, value) : map.put(key, value); + } + + @Override + public V remove(Object obj) { + final ObservableMap map = get(); + return (map == null)? (V) EMPTY_MAP.remove(obj) : map.remove(obj); + } + + @Override + public void putAll(Map elements) { + final ObservableMap map = get(); + if (map == null) { + EMPTY_MAP.putAll(elements); + } else { + map.putAll(elements); + } + } + + @Override + public void clear() { + final ObservableMap map = get(); + if (map == null) { + EMPTY_MAP.clear(); + } else { + map.clear(); + } + } + + @Override + public Set keySet() { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.keySet() : map.keySet(); + } + + @Override + public Collection values() { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.values() : map.values(); + } + + @Override + public Set> entrySet() { + final ObservableMap map = get(); + return (map == null)? EMPTY_MAP.entrySet() : map.entrySet(); + } + + @Override + public V get(Object key) { + final ObservableMap map = get(); + return (map == null)? (V) EMPTY_MAP.get(key) : map.get(key); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberBinding.java new file mode 100644 index 00000000..20f8d0e6 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberBinding.java @@ -0,0 +1,4 @@ +package com.tungsten.fclcore.fakefx.beans.binding; + +public interface NumberBinding extends Binding, NumberExpression { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpression.java new file mode 100644 index 00000000..c2df73ec --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpression.java @@ -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); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpressionBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpressionBase.java new file mode 100644 index 00000000..f393255a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/NumberExpressionBase.java @@ -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. + *

+ * 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 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); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectBinding.java new file mode 100644 index 00000000..cfb890c5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectBinding.java @@ -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 extends ObjectExpression implements + Binding { + + private T value; + private boolean valid = false; + private BindingHelperObserver observer; + private ExpressionHelper 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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. + *

+ * 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]"; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectExpression.java new file mode 100644 index 00000000..7b69aacd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/ObjectExpression.java @@ -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 implements ObservableObjectValue { + + @Override + public T getValue() { + return get(); + } + + /** + * Creates a default {@code ObjectExpression}. + */ + public ObjectExpression() { + } + + public static ObjectExpression objectExpression( + final ObservableObjectValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + return value instanceof ObjectExpression ? (ObjectExpression) value + : new ObjectBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected T computeValue() { + return value.get(); + } + + @Override + public ObservableList> 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); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetBinding.java new file mode 100644 index 00000000..80498a54 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetBinding.java @@ -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}. + *

+ * {@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. + *

+ * 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. + *

+ * See {@link DoubleBinding} for an example how this base class can be extended. + * + * @see Binding + * @see SetExpression + * + * @param + * the type of the {@code Set} elements + * @since JavaFX 2.1 + */ +public abstract class SetBinding extends SetExpression implements Binding> { + + /** + * Creates a default {@code SetBinding}. + */ + public SetBinding() { + } + + private final SetChangeListener setChangeListener = new SetChangeListener() { + @Override + public void onChanged(Change change) { + invalidateProperties(); + onInvalidating(); + SetExpressionHelper.fireValueChangedEvent(helper, change); + } + }; + + private ObservableSet value; + private boolean valid = false; + private BindingHelperObserver observer; + private SetExpressionHelper 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> listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = SetExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(SetChangeListener 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 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. + *

+ * Classes extending {@code SetBinding} have to provide an implementation + * of {@code computeValue}. + * + * @return the current value + */ + protected abstract ObservableSet 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]"; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetExpression.java new file mode 100644 index 00000000..01f9175f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/SetExpression.java @@ -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 implements ObservableSetValue { + + /** + * Creates a default {@code SetExpression}. + */ + public SetExpression() { + } + + private static final ObservableSet EMPTY_SET = new EmptyObservableSet(); + + private static class EmptyObservableSet extends AbstractSet implements ObservableSet { + + 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 iterator() { + return iterator; + } + + @Override + public int size() { + return 0; + } + + @Override + public void addListener(SetChangeListener setChangeListener) { + // no-op + } + + @Override + public void removeListener(SetChangeListener setChangeListener) { + // no-op + } + + @Override + public void addListener(InvalidationListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener listener) { + // no-op + } + } + + @Override + public ObservableSet getValue() { + return get(); + } + + public static SetExpression setExpression(final ObservableSetValue value) { + if (value == null) { + throw new NullPointerException("Set must be specified."); + } + return value instanceof SetExpression ? (SetExpression) value + : new SetBinding() { + { + super.bind(value); + } + + @Override + public void dispose() { + super.unbind(value); + } + + @Override + protected ObservableSet 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 set = get(); + return (set == null)? EMPTY_SET.size() : set.size(); + } + + @Override + public boolean isEmpty() { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.isEmpty() : set.isEmpty(); + } + + @Override + public boolean contains(Object obj) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.contains(obj) : set.contains(obj); + } + + @Override + public Iterator iterator() { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.iterator() : set.iterator(); + } + + @Override + public Object[] toArray() { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.toArray() : set.toArray(); + } + + @Override + public T[] toArray(T[] array) { + final ObservableSet set = get(); + return (set == null)? (T[]) EMPTY_SET.toArray(array) : set.toArray(array); + } + + @Override + public boolean add(E element) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.add(element) : set.add(element); + } + + @Override + public boolean remove(Object obj) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.remove(obj) : set.remove(obj); + } + + @Override + public boolean containsAll(Collection objects) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.contains(objects) : set.containsAll(objects); + } + + @Override + public boolean addAll(Collection elements) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.addAll(elements) : set.addAll(elements); + } + + @Override + public boolean removeAll(Collection objects) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.removeAll(objects) : set.removeAll(objects); + } + + @Override + public boolean retainAll(Collection objects) { + final ObservableSet set = get(); + return (set == null)? EMPTY_SET.retainAll(objects) : set.retainAll(objects); + } + + @Override + public void clear() { + final ObservableSet set = get(); + if (set == null) { + EMPTY_SET.clear(); + } else { + set.clear(); + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringBinding.java new file mode 100644 index 00000000..42dc5469 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringBinding.java @@ -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 { + + private String value; + private boolean valid = false; + private BindingHelperObserver observer; + private ExpressionHelper 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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener 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. + *

+ * 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]"; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringExpression.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringExpression.java new file mode 100644 index 00000000..eb40065b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/StringExpression.java @@ -0,0 +1,118 @@ +package com.tungsten.fclcore.fakefx.beans.binding; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableStringValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.StringFormatter; + +public abstract class StringExpression implements ObservableStringValue { + + public StringExpression() { + } + + @Override + public String getValue() { + return get(); + } + + public final String getValueSafe() { + final String value = get(); + return value == null ? "" : value; + } + + public static StringExpression stringExpression( + final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + return StringFormatter.convert(value); + } + + public StringExpression concat(Object other) { + return Bindings.concat(this, other); + } + + public BooleanBinding isEqualTo(final ObservableStringValue other) { + return Bindings.equal(this, other); + } + + public BooleanBinding isEqualTo(final String other) { + return Bindings.equal(this, other); + } + + public BooleanBinding isNotEqualTo(final ObservableStringValue other) { + return Bindings.notEqual(this, other); + } + + public BooleanBinding isNotEqualTo(final String other) { + return Bindings.notEqual(this, other); + } + + public BooleanBinding isEqualToIgnoreCase(final ObservableStringValue other) { + return Bindings.equalIgnoreCase(this, other); + } + + public BooleanBinding isEqualToIgnoreCase(final String other) { + return Bindings.equalIgnoreCase(this, other); + } + + public BooleanBinding isNotEqualToIgnoreCase( + final ObservableStringValue other) { + return Bindings.notEqualIgnoreCase(this, other); + } + + public BooleanBinding isNotEqualToIgnoreCase(final String other) { + return Bindings.notEqualIgnoreCase(this, other); + } + + public BooleanBinding greaterThan(final ObservableStringValue other) { + return Bindings.greaterThan(this, other); + } + + public BooleanBinding greaterThan(final String other) { + return Bindings.greaterThan(this, other); + } + + public BooleanBinding lessThan(final ObservableStringValue other) { + return Bindings.lessThan(this, other); + } + + public BooleanBinding lessThan(final String other) { + return Bindings.lessThan(this, other); + } + + public BooleanBinding greaterThanOrEqualTo(final ObservableStringValue other) { + return Bindings.greaterThanOrEqual(this, other); + } + + public BooleanBinding greaterThanOrEqualTo(final String other) { + return Bindings.greaterThanOrEqual(this, other); + } + + public BooleanBinding lessThanOrEqualTo(final ObservableStringValue other) { + return Bindings.lessThanOrEqual(this, other); + } + + public BooleanBinding lessThanOrEqualTo(final String other) { + return Bindings.lessThanOrEqual(this, other); + } + + public BooleanBinding isNull() { + return Bindings.isNull(this); + } + + public BooleanBinding isNotNull() { + return Bindings.isNotNull(this); + } + + public IntegerBinding length() { + return Bindings.length(this); + } + + public BooleanBinding isEmpty() { + return Bindings.isEmpty(this); + } + + public BooleanBinding isNotEmpty() { + return Bindings.isNotEmpty(this); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/When.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/When.java new file mode 100644 index 00000000..56af49aa --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/binding/When.java @@ -0,0 +1,773 @@ +package com.tungsten.fclcore.fakefx.beans.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.value.ObservableBooleanValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableDoubleValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableFloatValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableLongValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableObjectValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableStringValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.DoubleConstant; +import com.tungsten.fclcore.fakefx.binding.FloatConstant; +import com.tungsten.fclcore.fakefx.binding.IntegerConstant; +import com.tungsten.fclcore.fakefx.binding.LongConstant; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import java.lang.ref.WeakReference; + +public class When { + private final ObservableBooleanValue condition; + + /** + * The constructor of {@code When}. + * + * @param condition + * the condition of the ternary expression + */ + public When(final @NamedArg("condition") ObservableBooleanValue condition) { + if (condition == null) { + throw new NullPointerException("Condition must be specified."); + } + this.condition = condition; + } + + private static class WhenListener implements InvalidationListener { + + private final ObservableBooleanValue condition; + private final ObservableValue thenValue; + private final ObservableValue otherwiseValue; + private final WeakReference> ref; + + private WhenListener(Binding binding, ObservableBooleanValue condition, ObservableValue thenValue, ObservableValue otherwiseValue) { + this.ref = new WeakReference>(binding); + this.condition = condition; + this.thenValue = thenValue; + this.otherwiseValue = otherwiseValue; + } + + @Override + public void invalidated(Observable observable) { + final Binding binding = ref.get(); + if (binding == null) { + condition.removeListener(this); + if (thenValue != null) { + thenValue.removeListener(this); + } + if (otherwiseValue != null) { + otherwiseValue.removeListener(this); + } + } else { + // short-circuit invalidation. This Binding becomes + // only invalid if the condition changes or the + // active branch. + if (condition.equals(observable) || (binding.isValid() && (condition.get() == observable.equals(thenValue)))) { + binding.invalidate(); + } + } + } + + } + + private static NumberBinding createNumberCondition( + final ObservableBooleanValue condition, + final ObservableNumberValue thenValue, + final ObservableNumberValue otherwiseValue) { + if ((thenValue instanceof ObservableDoubleValue) || (otherwiseValue instanceof ObservableDoubleValue)) { + return new DoubleBinding() { + final InvalidationListener observer = new WhenListener(this, condition, thenValue, otherwiseValue); + { + condition.addListener(observer); + thenValue.addListener(observer); + otherwiseValue.addListener(observer); + } + + @Override + public void dispose() { + condition.removeListener(observer); + thenValue.removeListener(observer); + otherwiseValue.removeListener(observer); + } + + @Override + protected double computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? thenValue.doubleValue() : otherwiseValue.doubleValue(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList( + FXCollections.> observableArrayList(condition, thenValue, otherwiseValue)); + } + }; + } else if ((thenValue instanceof ObservableFloatValue) || (otherwiseValue instanceof ObservableFloatValue)) { + return new FloatBinding() { + final InvalidationListener observer = new WhenListener(this, condition, thenValue, otherwiseValue); + { + condition.addListener(observer); + thenValue.addListener(observer); + otherwiseValue.addListener(observer); + } + + @Override + public void dispose() { + condition.removeListener(observer); + thenValue.removeListener(observer); + otherwiseValue.removeListener(observer); + } + + @Override + protected float computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? thenValue.floatValue() : otherwiseValue.floatValue(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList( + FXCollections.> observableArrayList(condition, thenValue, otherwiseValue)); + } + }; + } else if ((thenValue instanceof ObservableLongValue) || (otherwiseValue instanceof ObservableLongValue)) { + return new LongBinding() { + final InvalidationListener observer = new WhenListener(this, condition, thenValue, otherwiseValue); + { + condition.addListener(observer); + thenValue.addListener(observer); + otherwiseValue.addListener(observer); + } + + @Override + public void dispose() { + condition.removeListener(observer); + thenValue.removeListener(observer); + otherwiseValue.removeListener(observer); + } + + @Override + protected long computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? thenValue.longValue() : otherwiseValue.longValue(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList( + FXCollections.> observableArrayList(condition, thenValue, otherwiseValue)); + } + }; + } else { + return new IntegerBinding() { + final InvalidationListener observer = new WhenListener(this, condition, thenValue, otherwiseValue); + { + condition.addListener(observer); + thenValue.addListener(observer); + otherwiseValue.addListener(observer); + } + + @Override + public void dispose() { + condition.removeListener(observer); + thenValue.removeListener(observer); + otherwiseValue.removeListener(observer); + } + + @Override + protected int computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? thenValue.intValue(): otherwiseValue.intValue(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList( + FXCollections.> observableArrayList(condition, thenValue, otherwiseValue)); + } + }; + } + } + + /** + * If-then-else expression returning a number. + * @since JavaFX 2.0 + */ + public class NumberConditionBuilder { + + private ObservableNumberValue thenValue; + + private NumberConditionBuilder(final ObservableNumberValue thenValue) { + this.thenValue = thenValue; + } + + public NumberBinding otherwise(ObservableNumberValue otherwiseValue) { + if (otherwiseValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + return When.createNumberCondition(condition, thenValue, otherwiseValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link DoubleBinding} + */ + public DoubleBinding otherwise(double otherwiseValue) { + return (DoubleBinding) otherwise(DoubleConstant.valueOf(otherwiseValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link NumberBinding} + */ + public NumberBinding otherwise(float otherwiseValue) { + return otherwise(FloatConstant.valueOf(otherwiseValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link NumberBinding} + */ + public NumberBinding otherwise(long otherwiseValue) { + return otherwise(LongConstant.valueOf(otherwiseValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link NumberBinding} + */ + public NumberBinding otherwise(int otherwiseValue) { + return otherwise(IntegerConstant.valueOf(otherwiseValue)); + } + } + + public NumberConditionBuilder then(final ObservableNumberValue thenValue) { + if (thenValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + return new NumberConditionBuilder(thenValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public NumberConditionBuilder then(double thenValue) { + return new NumberConditionBuilder(DoubleConstant.valueOf(thenValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public NumberConditionBuilder then(float thenValue) { + return new NumberConditionBuilder(FloatConstant.valueOf(thenValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public NumberConditionBuilder then(long thenValue) { + return new NumberConditionBuilder(LongConstant.valueOf(thenValue)); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public NumberConditionBuilder then(int thenValue) { + return new NumberConditionBuilder(IntegerConstant.valueOf(thenValue)); + } + + /** + * If-then-else expression returning Boolean. + */ + private class BooleanCondition extends BooleanBinding { + private final ObservableBooleanValue trueResult; + private final boolean trueResultValue; + + private final ObservableBooleanValue falseResult; + private final boolean falseResultValue; + + private final InvalidationListener observer; + + private BooleanCondition(final ObservableBooleanValue then, final ObservableBooleanValue otherwise) { + this.trueResult = then; + this.trueResultValue = false; + this.falseResult = otherwise; + this.falseResultValue = false; + this.observer = new WhenListener(this, condition, then, otherwise); + condition.addListener(observer); + then.addListener(observer); + otherwise.addListener(observer); + } + + private BooleanCondition(final boolean then, final ObservableBooleanValue otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = otherwise; + this.falseResultValue = false; + this.observer = new WhenListener(this, condition, null, otherwise); + condition.addListener(observer); + otherwise.addListener(observer); + } + + private BooleanCondition(final ObservableBooleanValue then, final boolean otherwise) { + this.trueResult = then; + this.trueResultValue = false; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = new WhenListener(this, condition, then, null); + condition.addListener(observer); + then.addListener(observer); + } + + private BooleanCondition(final boolean then, final boolean otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = null; + super.bind(condition); + } + + @Override + protected boolean computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? (trueResult != null ? trueResult.get() : trueResultValue) + : (falseResult != null ? falseResult.get() : falseResultValue); + } + + @Override + public void dispose() { + if (observer == null) { + super.unbind(condition); + } else { + condition.removeListener(observer); + if (trueResult != null) { + trueResult.removeListener(observer); + } + if (falseResult != null) { + falseResult.removeListener(observer); + } + } + } + + @Override + public ObservableList> getDependencies() { + assert condition != null; + final ObservableList> seq = FXCollections.> observableArrayList(condition); + if (trueResult != null) { + seq.add(trueResult); + } + if (falseResult != null) { + seq.add(falseResult); + } + return FXCollections.unmodifiableObservableList(seq); + } + } + + /** + * An intermediate class needed while assembling the ternary expression. It + * should not be used in another context. + * @since JavaFX 2.0 + */ + public class BooleanConditionBuilder { + + private ObservableBooleanValue trueResult; + private boolean trueResultValue; + + private BooleanConditionBuilder(final ObservableBooleanValue thenValue) { + this.trueResult = thenValue; + } + + private BooleanConditionBuilder(final boolean thenValue) { + this.trueResultValue = thenValue; + } + + public BooleanBinding otherwise(final ObservableBooleanValue otherwiseValue) { + if (otherwiseValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + if (trueResult != null) + return new BooleanCondition(trueResult, otherwiseValue); + else + return new BooleanCondition(trueResultValue, otherwiseValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link BooleanBinding} + */ + public BooleanBinding otherwise(final boolean otherwiseValue) { + if (trueResult != null) + return new BooleanCondition(trueResult, otherwiseValue); + else + return new BooleanCondition(trueResultValue, otherwiseValue); + } + } + + public BooleanConditionBuilder then(final ObservableBooleanValue thenValue) { + if (thenValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + return new BooleanConditionBuilder(thenValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public BooleanConditionBuilder then(final boolean thenValue) { + return new BooleanConditionBuilder(thenValue); + } + + /** + * If-then-else expression returning String. + */ + private class StringCondition extends StringBinding { + + private final ObservableStringValue trueResult; + private final String trueResultValue; + + private final ObservableStringValue falseResult; + private final String falseResultValue; + + private final InvalidationListener observer; + + private StringCondition(final ObservableStringValue then, final ObservableStringValue otherwise) { + this.trueResult = then; + this.trueResultValue = ""; + this.falseResult = otherwise; + this.falseResultValue = ""; + this.observer = new WhenListener(this, condition, then, otherwise); + condition.addListener(observer); + then.addListener(observer); + otherwise.addListener(observer); + } + + private StringCondition(final String then, final ObservableStringValue otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = otherwise; + this.falseResultValue = ""; + this.observer = new WhenListener(this, condition, null, otherwise); + condition.addListener(observer); + otherwise.addListener(observer); + } + + private StringCondition(final ObservableStringValue then, final String otherwise) { + this.trueResult = then; + this.trueResultValue = ""; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = new WhenListener(this, condition, then, null); + condition.addListener(observer); + then.addListener(observer); + } + + private StringCondition(final String then, final String otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = null; + super.bind(condition); + } + + @Override + protected String computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? (trueResult != null ? trueResult.get() : trueResultValue) + : (falseResult != null ? falseResult.get() : falseResultValue); + } + + @Override + public void dispose() { + if (observer == null) { + super.unbind(condition); + } else { + condition.removeListener(observer); + if (trueResult != null) { + trueResult.removeListener(observer); + } + if (falseResult != null) { + falseResult.removeListener(observer); + } + } + } + + + @Override + public ObservableList> getDependencies() { + assert condition != null; + final ObservableList> seq = FXCollections.> observableArrayList(condition); + if (trueResult != null) { + seq.add(trueResult); + } + if (falseResult != null) { + seq.add(falseResult); + } + return FXCollections.unmodifiableObservableList(seq); + } + } + + /** + * An intermediate class needed while assembling the ternary expression. It + * should not be used in another context. + * @since JavaFX 2.0 + */ + public class StringConditionBuilder { + + private ObservableStringValue trueResult; + private String trueResultValue; + + private StringConditionBuilder(final ObservableStringValue thenValue) { + this.trueResult = thenValue; + } + + private StringConditionBuilder(final String thenValue) { + this.trueResultValue = thenValue; + } + + public StringBinding otherwise(final ObservableStringValue otherwiseValue) { + if (trueResult != null) + return new StringCondition(trueResult, otherwiseValue); + else + return new StringCondition(trueResultValue, otherwiseValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link StringBinding} + */ + public StringBinding otherwise(final String otherwiseValue) { + if (trueResult != null) + return new StringCondition(trueResult, otherwiseValue); + else + return new StringCondition(trueResultValue, otherwiseValue); + } + } + + public StringConditionBuilder then(final ObservableStringValue thenValue) { + if (thenValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + return new StringConditionBuilder(thenValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public StringConditionBuilder then(final String thenValue) { + return new StringConditionBuilder(thenValue); + } + + /** + * If-then-else expression returning general objects. + */ + private class ObjectCondition extends ObjectBinding { + + private final ObservableObjectValue trueResult; + private final T trueResultValue; + + private final ObservableObjectValue falseResult; + private final T falseResultValue; + + private final InvalidationListener observer; + + private ObjectCondition(final ObservableObjectValue then, final ObservableObjectValue otherwise) { + this.trueResult = then; + this.trueResultValue = null; + this.falseResult = otherwise; + this.falseResultValue = null; + this.observer = new WhenListener(this, condition, then, otherwise); + condition.addListener(observer); + then.addListener(observer); + otherwise.addListener(observer); + } + + private ObjectCondition(final T then, final ObservableObjectValue otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = otherwise; + this.falseResultValue = null; + this.observer = new WhenListener(this, condition, null, otherwise); + condition.addListener(observer); + otherwise.addListener(observer); + } + + private ObjectCondition(final ObservableObjectValue then, final T otherwise) { + this.trueResult = then; + this.trueResultValue = null; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = new WhenListener(this, condition, then, null); + condition.addListener(observer); + then.addListener(observer); + } + + private ObjectCondition(final T then, final T otherwise) { + this.trueResult = null; + this.trueResultValue = then; + this.falseResult = null; + this.falseResultValue = otherwise; + this.observer = null; + super.bind(condition); + } + + @Override + protected T computeValue() { + final boolean conditionValue = condition.get(); + return conditionValue ? (trueResult != null ? trueResult.get() : trueResultValue) + : (falseResult != null ? falseResult.get() : falseResultValue); + } + + @Override + public void dispose() { + if (observer == null) { + super.unbind(condition); + } else { + condition.removeListener(observer); + if (trueResult != null) { + trueResult.removeListener(observer); + } + if (falseResult != null) { + falseResult.removeListener(observer); + } + } + } + + + @Override + public ObservableList> getDependencies() { + assert condition != null; + final ObservableList> seq = FXCollections.> observableArrayList(condition); + if (trueResult != null) { + seq.add(trueResult); + } + if (falseResult != null) { + seq.add(falseResult); + } + return FXCollections.unmodifiableObservableList(seq); + } + } + + /** + * An intermediate class needed while assembling the ternary expression. It + * should not be used in another context. + * @since JavaFX 2.0 + */ + public class ObjectConditionBuilder { + + private ObservableObjectValue trueResult; + private T trueResultValue; + + private ObjectConditionBuilder(final ObservableObjectValue thenValue) { + this.trueResult = thenValue; + } + + private ObjectConditionBuilder(final T thenValue) { + this.trueResultValue = thenValue; + } + + public ObjectBinding otherwise(final ObservableObjectValue otherwiseValue) { + if (otherwiseValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + if (trueResult != null) + return new ObjectCondition(trueResult, otherwiseValue); + else + return new ObjectCondition(trueResultValue, otherwiseValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned + * if the condition is {@code false}. + * + * @param otherwiseValue + * the value + * @return the complete {@link ObjectBinding} + */ + public ObjectBinding otherwise(final T otherwiseValue) { + if (trueResult != null) + return new ObjectCondition(trueResult, otherwiseValue); + else + return new ObjectCondition(trueResultValue, otherwiseValue); + } + } + + public ObjectConditionBuilder then(final ObservableObjectValue thenValue) { + if (thenValue == null) { + throw new NullPointerException("Value needs to be specified"); + } + return new ObjectConditionBuilder(thenValue); + } + + /** + * Defines a constant value of the ternary expression, that is returned if + * the condition is {@code true}. + * + * @param the type of the intermediate result + * @param thenValue + * the value + * @return the intermediate result which still requires the otherwise-branch + */ + public ObjectConditionBuilder then(final T thenValue) { + return new ObjectConditionBuilder(thenValue); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanProperty.java new file mode 100644 index 00000000..0d8dfd9d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanProperty.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableBooleanValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; + +import java.util.Objects; + +public abstract class BooleanProperty extends ReadOnlyBooleanProperty implements + Property, WritableBooleanValue { + + /** + * Sole constructor + */ + public BooleanProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(Boolean v) { + if (v == null) { + set(false); + } else { + set(v.booleanValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code BooleanProperty} object. + * @return a string representation of this {@code BooleanProperty} object. + */ + @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(); + } + + public static BooleanProperty booleanProperty(final Property property) { + Objects.requireNonNull(property, "Property cannot be null"); + return property instanceof BooleanProperty ? (BooleanProperty)property : new BooleanPropertyBase() { + { + BidirectionalBinding.bind(this, property); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ObjectProperty asObject() { + return new ObjectPropertyBase () { + { + BidirectionalBinding.bind(this, BooleanProperty.this); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return BooleanProperty.this.getName(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanPropertyBase.java similarity index 68% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanPropertyBase.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanPropertyBase.java index 8342ebbe..8bd784dd 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BooleanPropertyBase.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/BooleanPropertyBase.java @@ -1,7 +1,27 @@ -package com.tungsten.fclcore.fakefx; +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.BooleanBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableBooleanValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; import java.lang.ref.WeakReference; +/** + * The class {@code BooleanPropertyBase} is the base class for a property + * wrapping a {@code boolean} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see BooleanProperty + * @since JavaFX 2.0 + */ public abstract class BooleanPropertyBase extends BooleanProperty { private boolean value; @@ -10,9 +30,18 @@ public abstract class BooleanPropertyBase extends BooleanProperty { private boolean valid = true; private ExpressionHelper helper = null; + /** + * The constructor of the {@code BooleanPropertyBase}. + */ public BooleanPropertyBase() { } + /** + * The constructor of the {@code BooleanPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ public BooleanPropertyBase(boolean initialValue) { this.value = initialValue; } @@ -37,6 +66,15 @@ public abstract class BooleanPropertyBase extends BooleanProperty { helper = ExpressionHelper.removeListener(helper, listener); } + /** + * Sends notifications to all attached + * {@link InvalidationListener InvalidationListeners} and + * {@link javafx.beans.value.ChangeListener ChangeListeners}. + * + * This method is called when the value is changed, either manually by + * calling {@link #set(boolean)} or in case of a bound property, if the + * binding becomes invalid. + */ protected void fireValueChangedEvent() { ExpressionHelper.fireValueChangedEvent(helper); } @@ -49,19 +87,32 @@ public abstract class BooleanPropertyBase extends BooleanProperty { } } + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ protected void invalidated() { } + /** + * {@inheritDoc} + */ @Override public boolean get() { valid = true; return observable == null ? value : observable.get(); } + /** + * {@inheritDoc} + */ @Override public void set(boolean newValue) { if (isBound()) { - throw new java.lang.RuntimeException((getBean() != null && getName() != null ? + throw new RuntimeException((getBean() != null && getName() != null ? getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); } if (value != newValue) { @@ -70,11 +121,18 @@ public abstract class BooleanPropertyBase extends BooleanProperty { } } + /** + * {@inheritDoc} + */ @Override public boolean isBound() { return observable != null; } + /** + * {@inheritDoc} + * Note: + */ @Override public void bind(final ObservableValue rawObservable) { if (rawObservable == null) { @@ -95,6 +153,9 @@ public abstract class BooleanPropertyBase extends BooleanProperty { } } + /** + * {@inheritDoc} + */ @Override public void unbind() { if (observable != null) { @@ -107,6 +168,10 @@ public abstract class BooleanPropertyBase extends BooleanProperty { } } + /** + * Returns a string representation of this {@code BooleanPropertyBase} object. + * @return a string representation of this {@code BooleanPropertyBase} object. + */ @Override public String toString() { final Object bean = getBean(); @@ -175,4 +240,4 @@ public abstract class BooleanPropertyBase extends BooleanProperty { unbind(observable); } } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoubleProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoubleProperty.java new file mode 100644 index 00000000..8ca873cd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoubleProperty.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableDoubleValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; + +import java.util.Objects; + +public abstract class DoubleProperty extends ReadOnlyDoubleProperty implements + Property, WritableDoubleValue { + + /** + * Creates a default {@code DoubleProperty}. + */ + public DoubleProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(Number v) { + if (v == null) { + set(0.0); + } else { + set(v.doubleValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code DoubleProperty} object. + * @return a string representation of this {@code DoubleProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "DoubleProperty ["); + 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(); + } + + public static DoubleProperty doubleProperty(final Property property) { + Objects.requireNonNull(property, "Property cannot be null"); + return new DoublePropertyBase() { + { + BidirectionalBinding.bindNumber(this, property); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ObjectProperty asObject() { + return new ObjectPropertyBase () { + { + BidirectionalBinding.bindNumber(this, DoubleProperty.this); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return DoubleProperty.this.getName(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoublePropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoublePropertyBase.java new file mode 100644 index 00000000..52254b28 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/DoublePropertyBase.java @@ -0,0 +1,251 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.DoubleBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +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.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code DoublePropertyBase} is the base class for a property + * wrapping a {@code double} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see DoubleProperty + * + * + * @since JavaFX 2.0 + */ +public abstract class DoublePropertyBase extends DoubleProperty { + + private double value; + private ObservableDoubleValue observable = null; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code DoublePropertyBase}. + */ + public DoublePropertyBase() { + } + + /** + * The constructor of the {@code DoublePropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public DoublePropertyBase(double initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public double get() { + valid = true; + return observable == null ? value : observable.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(double newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(final ObservableValue rawObservable) { + if (rawObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + ObservableDoubleValue newObservable; + if (rawObservable instanceof ObservableDoubleValue) { + newObservable = (ObservableDoubleValue)rawObservable; + } else if (rawObservable instanceof ObservableNumberValue) { + final ObservableNumberValue numberValue = (ObservableNumberValue)rawObservable; + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected double computeValue() { + return numberValue.doubleValue(); + } + }; + } else { + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected double computeValue() { + final Number value = rawObservable.getValue(); + return (value == null)? 0.0 : value.doubleValue(); + } + }; + } + + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.get(); + observable.removeListener(listener); + if (observable instanceof ValueWrapper) { + ((ValueWrapper)observable).dispose(); + } + observable = null; + } + } + + /** + * Returns a string representation of this {@code DoublePropertyBase} object. + * @return a string representation of this {@code DoublePropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("DoubleProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference wref; + + public Listener(DoublePropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + DoublePropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } + + private abstract class ValueWrapper extends DoubleBinding { + + private ObservableValue observable; + + public ValueWrapper(ObservableValue observable) { + this.observable = observable; + bind(observable); + } + + @Override + public void dispose() { + unbind(observable); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatProperty.java new file mode 100644 index 00000000..e59795af --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatProperty.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableFloatValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; + +import java.util.Objects; + +public abstract class FloatProperty extends ReadOnlyFloatProperty implements + Property, WritableFloatValue { + + /** + * Creates a default {@code FloatProperty}. + */ + public FloatProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(Number v) { + if (v == null) { + set(0.0f); + } else { + set(v.floatValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code FloatProperty} object. + * @return a string representation of this {@code FloatProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "FloatProperty ["); + 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(); + } + + public static FloatProperty floatProperty(final Property property) { + Objects.requireNonNull(property, "Property cannot be null"); + return new FloatPropertyBase() { + { + BidirectionalBinding.bindNumber(this, property); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ObjectProperty asObject() { + return new ObjectPropertyBase () { + { + BidirectionalBinding.bindNumber(this, FloatProperty.this); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return FloatProperty.this.getName(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatPropertyBase.java new file mode 100644 index 00000000..52079863 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/FloatPropertyBase.java @@ -0,0 +1,261 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.FloatBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableFloatValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code FloatPropertyBase} is the base class for a property wrapping + * a {@code float} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see FloatProperty + * + * + * @since JavaFX 2.0 + */ +public abstract class FloatPropertyBase extends FloatProperty { + + private float value; + private ObservableFloatValue observable = null;; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code FloatPropertyBase}. + */ + public FloatPropertyBase() { + } + + /** + * The constructor of the {@code FloatPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public FloatPropertyBase(float initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * Sends notifications to all attached + * {@link InvalidationListener InvalidationListeners} and + * {@link javafx.beans.value.ChangeListener ChangeListeners}. + * + * This method is called when the value is changed, either manually by + * calling {@link #set(float)} or in case of a bound property, if the + * binding becomes invalid. + */ + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public float get() { + valid = true; + return observable == null ? value : observable.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(float newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(final ObservableValue rawObservable) { + if (rawObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + ObservableFloatValue newObservable; + if (rawObservable instanceof ObservableFloatValue) { + newObservable = (ObservableFloatValue)rawObservable; + } else if (rawObservable instanceof ObservableNumberValue) { + final ObservableNumberValue numberValue = (ObservableNumberValue)rawObservable; + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected float computeValue() { + return numberValue.floatValue(); + } + }; + } else { + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected float computeValue() { + final Number value = rawObservable.getValue(); + return (value == null)? 0.0f : value.floatValue(); + } + }; + } + + + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.get(); + observable.removeListener(listener); + if (observable instanceof ValueWrapper) { + ((ValueWrapper)observable).dispose(); + } + observable = null; + } + } + + /** + * Returns a string representation of this {@code FloatPropertyBase} object. + * @return a string representation of this {@code FloatPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("FloatProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference wref; + + public Listener(FloatPropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + FloatPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } + + private abstract class ValueWrapper extends FloatBinding { + + private ObservableValue observable; + + public ValueWrapper(ObservableValue observable) { + this.observable = observable; + bind(observable); + } + + @Override + public void dispose() { + unbind(observable); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerProperty.java new file mode 100644 index 00000000..73a8f620 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerProperty.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableIntegerValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; + +import java.util.Objects; + +public abstract class IntegerProperty extends ReadOnlyIntegerProperty implements + Property, WritableIntegerValue { + + /** + * Creates a default {@code IntegerProperty}. + */ + public IntegerProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(Number v) { + if (v == null) { + set(0); + } else { + set(v.intValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code IntegerProperty} object. + * @return a string representation of this {@code IntegerProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "IntegerProperty ["); + 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(); + } + + public static IntegerProperty integerProperty(final Property property) { + Objects.requireNonNull(property, "Property cannot be null"); + return new IntegerPropertyBase() { + { + BidirectionalBinding.bindNumber(this, property); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ObjectProperty asObject() { + return new ObjectPropertyBase () { + { + BidirectionalBinding.bindNumber(this, IntegerProperty.this); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return IntegerProperty.this.getName(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerPropertyBase.java new file mode 100644 index 00000000..add73c67 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/IntegerPropertyBase.java @@ -0,0 +1,260 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.IntegerBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableIntegerValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code IntegerPropertyBase} is the base class for a property + * wrapping a {@code int} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see IntegerProperty + * + * + * @since JavaFX 2.0 + */ +public abstract class IntegerPropertyBase extends IntegerProperty { + + private int value; + private ObservableIntegerValue observable = null;; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code IntegerPropertyBase}. + */ + public IntegerPropertyBase() { + } + + /** + * The constructor of the {@code IntegerPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public IntegerPropertyBase(int initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * Sends notifications to all attached + * {@link InvalidationListener InvalidationListeners} and + * {@link javafx.beans.value.ChangeListener ChangeListeners}. + * + * This method is called when the value is changed, either manually by + * calling {@link #set(int)} or in case of a bound property, if the + * binding becomes invalid. + */ + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public int get() { + valid = true; + return observable == null ? value : observable.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(int newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(final ObservableValue rawObservable) { + if (rawObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + ObservableIntegerValue newObservable; + if (rawObservable instanceof ObservableIntegerValue) { + newObservable = (ObservableIntegerValue)rawObservable; + } else if (rawObservable instanceof ObservableNumberValue) { + final ObservableNumberValue numberValue = (ObservableNumberValue)rawObservable; + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected int computeValue() { + return numberValue.intValue(); + } + }; + } else { + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected int computeValue() { + final Number value = rawObservable.getValue(); + return (value == null)? 0 : value.intValue(); + } + }; + } + + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.get(); + observable.removeListener(listener); + if (observable instanceof ValueWrapper) { + ((ValueWrapper)observable).dispose(); + } + observable = null; + } + } + + /** + * Returns a string representation of this {@code IntegerPropertyBase} object. + * @return a string representation of this {@code IntegerPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("IntegerProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference wref; + + public Listener(IntegerPropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + IntegerPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } + + private abstract class ValueWrapper extends IntegerBinding { + + private ObservableValue observable; + + public ValueWrapper(ObservableValue observable) { + this.observable = observable; + bind(observable); + } + + @Override + public void dispose() { + unbind(observable); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListProperty.java new file mode 100644 index 00000000..7bab6a3e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListProperty.java @@ -0,0 +1,59 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableListValue; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +public abstract class ListProperty extends ReadOnlyListProperty implements + Property>, WritableListValue { + + /** + * Creates a default {@code ListProperty}. + */ + public ListProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(ObservableList v) { + set(v); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property> other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property> other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code ListProperty} object. + * @return a string representation of this {@code ListProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ListProperty ["); + 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(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListPropertyBase.java new file mode 100644 index 00000000..b18613b5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ListPropertyBase.java @@ -0,0 +1,301 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ListExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import java.lang.ref.WeakReference; + +/** + * The class {@code ListPropertyBase} is the base class for a property + * wrapping an {@link ObservableList}. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see ObservableList + * @see ListProperty + * + * @param the type of the {@code List} elements + * @since JavaFX 2.1 + */ +public abstract class ListPropertyBase extends ListProperty { + + private final ListChangeListener listChangeListener = change -> { + invalidateProperties(); + invalidated(); + fireValueChangedEvent(change); + }; + + private ObservableList value; + private ObservableValue> observable = null; + private InvalidationListener listener = null; + private boolean valid = true; + private ListExpressionHelper helper = null; + + private SizeProperty size0; + private EmptyProperty empty0; + + /** + * The Constructor of {@code ListPropertyBase} + */ + public ListPropertyBase() {} + + /** + * The constructor of the {@code ListPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public ListPropertyBase(ObservableList initialValue) { + this.value = initialValue; + if (initialValue != null) { + initialValue.addListener(listChangeListener); + } + } + + @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 ListPropertyBase.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 ListPropertyBase.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> listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = ListExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(ListChangeListener listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ListChangeListener listener) { + helper = ListExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ListExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(ListChangeListener.Change change) { + ListExpressionHelper.fireValueChangedEvent(helper, change); + } + + private void invalidateProperties() { + if (size0 != null) { + size0.fireValueChangedEvent(); + } + if (empty0 != null) { + empty0.fireValueChangedEvent(); + } + } + + private void markInvalid(ObservableList oldValue) { + if (valid) { + if (oldValue != null) { + oldValue.removeListener(listChangeListener); + } + valid = false; + invalidateProperties(); + invalidated(); + fireValueChangedEvent(); + } + } + + + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + @Override + public ObservableList get() { + if (!valid) { + value = observable == null ? value : observable.getValue(); + valid = true; + if (value != null) { + value.addListener(listChangeListener); + } + } + return value; + } + + @Override + public void set(ObservableList newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + final ObservableList oldValue = value; + value = newValue; + markInvalid(oldValue); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + @Override + public void bind(final ObservableValue> newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (newObservable != observable) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener<>(this); + } + observable.addListener(listener); + markInvalid(value); + } + } + + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + /** + * Returns a string representation of this {@code ListPropertyBase} object. + * @return a string representation of this {@code ListPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("ListProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference> wref; + + public Listener(ListPropertyBase ref) { + this.wref = new WeakReference>(ref); + } + + @Override + public void invalidated(Observable observable) { + ListPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(ref.value); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongProperty.java new file mode 100644 index 00000000..b2c0678b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongProperty.java @@ -0,0 +1,102 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableLongValue; +import com.tungsten.fclcore.fakefx.binding.BidirectionalBinding; + +import java.util.Objects; + +public abstract class LongProperty extends ReadOnlyLongProperty implements + Property, WritableLongValue { + + /** + * Creates a default {@code LongProperty}. + */ + public LongProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(Number v) { + if (v == null) { + set(0L); + } else { + set(v.longValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code LongProperty} object. + * @return a string representation of this {@code LongProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("LongProperty ["); + 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(); + } + + public static LongProperty longProperty(final Property property) { + Objects.requireNonNull(property, "Property cannot be null"); + return new LongPropertyBase() { + { + BidirectionalBinding.bindNumber(this, property); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ObjectProperty asObject() { + return new ObjectPropertyBase () { + { + BidirectionalBinding.bindNumber(this, LongProperty.this); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return LongProperty.this.getName(); + } + }; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongPropertyBase.java new file mode 100644 index 00000000..914cad4f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/LongPropertyBase.java @@ -0,0 +1,251 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.LongBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableLongValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableNumberValue; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code LongPropertyBase} is the base class for a property wrapping + * a {@code long} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see LongProperty + * + * + * @since JavaFX 2.0 + */ +public abstract class LongPropertyBase extends LongProperty { + + private long value; + private ObservableLongValue observable = null;; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code LongPropertyBase}. + */ + public LongPropertyBase() { + } + + /** + * The constructor of the {@code LongPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public LongPropertyBase(long initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public long get() { + valid = true; + return observable == null ? value : observable.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(long newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(final ObservableValue rawObservable) { + if (rawObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + ObservableLongValue newObservable; + if (rawObservable instanceof ObservableLongValue) { + newObservable = (ObservableLongValue)rawObservable; + } else if (rawObservable instanceof ObservableNumberValue) { + final ObservableNumberValue numberValue = (ObservableNumberValue)rawObservable; + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected long computeValue() { + return numberValue.longValue(); + } + }; + } else { + newObservable = new ValueWrapper(rawObservable) { + + @Override + protected long computeValue() { + final Number value = rawObservable.getValue(); + return (value == null)? 0L : value.longValue(); + } + }; + } + + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.get(); + observable.removeListener(listener); + if (observable instanceof ValueWrapper) { + ((ValueWrapper)observable).dispose(); + } + observable = null; + } + } + + /** + * Returns a string representation of this {@code LongPropertyBase} object. + * @return a string representation of this {@code LongPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("LongProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference wref; + + public Listener(LongPropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + LongPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } + + private abstract class ValueWrapper extends LongBinding { + + private ObservableValue observable; + + public ValueWrapper(ObservableValue observable) { + this.observable = observable; + bind(observable); + } + + @Override + public void dispose() { + unbind(observable); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapProperty.java new file mode 100644 index 00000000..a2ff83b3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapProperty.java @@ -0,0 +1,59 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableMapValue; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +public abstract class MapProperty extends ReadOnlyMapProperty implements + Property>, WritableMapValue { + + /** + * Creates a default {@code MapProperty}. + */ + public MapProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(ObservableMap v) { + set(v); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property> other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property> other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code MapProperty} object. + * @return a string representation of this {@code MapProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "MapProperty ["); + 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(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapPropertyBase.java new file mode 100644 index 00000000..c5dc45f8 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/MapPropertyBase.java @@ -0,0 +1,288 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.MapExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +import java.lang.ref.WeakReference; + +public abstract class MapPropertyBase extends MapProperty { + + private final MapChangeListener mapChangeListener = change -> { + invalidateProperties(); + invalidated(); + fireValueChangedEvent(change); + }; + + private ObservableMap value; + private ObservableValue> observable = null; + private InvalidationListener listener = null; + private boolean valid = true; + private MapExpressionHelper helper = null; + + private SizeProperty size0; + private EmptyProperty empty0; + + /** + * The Constructor of {@code MapPropertyBase} + */ + public MapPropertyBase() {} + + /** + * The constructor of the {@code MapPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public MapPropertyBase(ObservableMap initialValue) { + this.value = initialValue; + if (initialValue != null) { + initialValue.addListener(mapChangeListener); + } + } + + @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 MapPropertyBase.this; + } + + @Override + public String getName() { + return "size"; + } + + @Override + 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 MapPropertyBase.this; + } + + @Override + public String getName() { + return "empty"; + } + + @Override + 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> listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = MapExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(MapChangeListener listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(MapChangeListener listener) { + helper = MapExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + MapExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(MapChangeListener.Change change) { + MapExpressionHelper.fireValueChangedEvent(helper, change); + } + + private void invalidateProperties() { + if (size0 != null) { + size0.fireValueChangedEvent(); + } + if (empty0 != null) { + empty0.fireValueChangedEvent(); + } + } + + private void markInvalid(ObservableMap oldValue) { + if (valid) { + if (oldValue != null) { + oldValue.removeListener(mapChangeListener); + } + valid = false; + invalidateProperties(); + invalidated(); + fireValueChangedEvent(); + } + } + + + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + @Override + public ObservableMap get() { + if (!valid) { + value = observable == null ? value : observable.getValue(); + valid = true; + if (value != null) { + value.addListener(mapChangeListener); + } + } + return value; + } + + @Override + public void set(ObservableMap newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + final ObservableMap oldValue = value; + value = newValue; + markInvalid(oldValue); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + @Override + public void bind(final ObservableValue> newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (newObservable != observable) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener<>(this); + } + observable.addListener(listener); + markInvalid(value); + } + } + + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + /** + * Returns a string representation of this {@code MapPropertyBase} object. + * @return a string representation of this {@code MapPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("MapProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference> wref; + + public Listener(MapPropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + MapPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(ref.value); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectProperty.java new file mode 100644 index 00000000..7adc5b66 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectProperty.java @@ -0,0 +1,58 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableObjectValue; + +public abstract class ObjectProperty extends ReadOnlyObjectProperty + implements Property, WritableObjectValue { + + /** + * Creates a default {@code ObjectProperty}. + */ + public ObjectProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(T v) { + set(v); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code ObjectProperty} object. + * @return a string representation of this {@code ObjectProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ObjectProperty ["); + 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(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectPropertyBase.java new file mode 100644 index 00000000..e6c26110 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ObjectPropertyBase.java @@ -0,0 +1,209 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code ObjectPropertyBase} is the base class for a property + * wrapping an arbitrary {@code Object}. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see ObjectProperty + * + * + * @param + * the type of the wrapped value + * @since JavaFX 2.0 + */ +public abstract class ObjectPropertyBase extends ObjectProperty { + + private T value; + private ObservableValue observable = null;; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code ObjectPropertyBase}. + */ + public ObjectPropertyBase() { + } + + /** + * The constructor of the {@code ObjectPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped {@code Object} + */ + public ObjectPropertyBase(T initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public T get() { + valid = true; + return observable == null ? value : observable.getValue(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(T newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!newObservable.equals(this.observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + /** + * Returns a string representation of this {@code ObjectPropertyBase} object. + * @return a string representation of this {@code ObjectPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("ObjectProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference> wref; + + public Listener(ObjectPropertyBase ref) { + this.wref = new WeakReference>(ref); + } + + @Override + public void invalidated(Observable observable) { + ObjectPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/Property.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/Property.java new file mode 100644 index 00000000..e30bbe3a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/Property.java @@ -0,0 +1,77 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.beans.value.WritableValue; + +/** + * Generic interface that defines the methods common to all (writable) + * properties, independent of their type. + * + * @param + * the type of the wrapped value + * @since JavaFX 2.0 + */ +public interface Property extends ReadOnlyProperty, WritableValue { + + /** + * Create a unidirection binding for this {@code Property}. + *

+ * Note that JavaFX has all the bind calls implemented through weak listeners. This means the bound property + * can be garbage collected and stopped from being updated. + * + * @param observable + * The observable this {@code Property} should be bound to. + * @throws NullPointerException + * if {@code observable} is {@code null} + */ + void bind(ObservableValue observable); + + void unbind(); + + boolean isBound(); + + /** + * Create a bidirectional binding between this {@code Property} and another + * one. + * Bidirectional bindings exists independently of unidirectional bindings. So it is possible to + * add unidirectional binding to a property with bidirectional binding and vice-versa. However, this practice is + * discouraged. + *

+ * It is possible to have multiple bidirectional bindings of one Property. + *

+ * JavaFX bidirectional binding implementation use weak listeners. This means bidirectional binding does not prevent + * properties from being garbage collected. + * + * @param other + * the other {@code Property} + * @throws NullPointerException + * if {@code other} is {@code null} + * @throws IllegalArgumentException + * if {@code other} is {@code this} + */ + void bindBidirectional(Property other); + + /** + * Removes a bidirectional binding between this {@code Property} and another + * one. + * + * If no bidirectional binding between the properties exists, calling this + * method has no effect. + * + * It is possible to unbind by a call on the second property. This code will work: + * + *

+     *     property1.bindBidirectional(property2);
+     *     property2.unbindBidirectional(property1);
+     * 
+ * + * @param other + * the other {@code Property} + * @throws NullPointerException + * if {@code other} is {@code null} + * @throws IllegalArgumentException + * if {@code other} is {@code this} + */ + void unbindBidirectional(Property other); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanProperty.java new file mode 100644 index 00000000..e581504f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanProperty.java @@ -0,0 +1,108 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.BooleanExpression; + +public abstract class ReadOnlyBooleanProperty extends BooleanExpression + implements ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyBooleanProperty}. + */ + public ReadOnlyBooleanProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyBooleanProperty} object. + * @return a string representation of this {@code ReadOnlyBooleanProperty} object. + */ + @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(); + } + + public static ReadOnlyBooleanProperty readOnlyBooleanProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + + return property instanceof ReadOnlyBooleanProperty ? (ReadOnlyBooleanProperty) property + : new ReadOnlyBooleanPropertyBase() { + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public boolean get() { + valid = true; + final Boolean value = property.getValue(); + return value == null ? false : value; + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + ReadOnlyBooleanProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return ReadOnlyBooleanProperty.this.getName(); + } + + @Override + public Boolean get() { + valid = true; + return ReadOnlyBooleanProperty.this.getValue(); + } + }; + }; + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanPropertyBase.java new file mode 100644 index 00000000..ada30f62 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanPropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping a {@code boolean}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyBooleanProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyBooleanPropertyBase extends ReadOnlyBooleanProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyBooleanPropertyBase}. + */ + public ReadOnlyBooleanPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanWrapper.java new file mode 100644 index 00000000..74d4d27d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyBooleanWrapper.java @@ -0,0 +1,99 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyBooleanWrapper extends SimpleBooleanProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyBooleanWrapper} + */ + public ReadOnlyBooleanWrapper() { + } + + /** + * The constructor of {@code ReadOnlyBooleanWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyBooleanWrapper(boolean initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyBooleanWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyBooleanProperty} + * @param name + * the name of this {@code ReadOnlyBooleanProperty} + */ + public ReadOnlyBooleanWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyBooleanWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyBooleanProperty} + * @param name + * the name of this {@code ReadOnlyBooleanProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyBooleanWrapper(Object bean, String name, + boolean initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyBooleanWrapper}. + * + * @return the readonly property + */ + public ReadOnlyBooleanProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyBooleanPropertyBase { + + @Override + public boolean get() { + return ReadOnlyBooleanWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyBooleanWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyBooleanWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleProperty.java new file mode 100644 index 00000000..baee2ea5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleProperty.java @@ -0,0 +1,110 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.DoubleExpression; + +public abstract class ReadOnlyDoubleProperty extends DoubleExpression implements + ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyDoubleProperty}. + */ + public ReadOnlyDoubleProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyDoubleProperty} object. + * @return a string representation of this {@code ReadOnlyDoubleProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyDoubleProperty ["); + 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(); + } + + public static ReadOnlyDoubleProperty readOnlyDoubleProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + + return property instanceof ReadOnlyDoubleProperty ? (ReadOnlyDoubleProperty) property: + new ReadOnlyDoublePropertyBase() { + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public double get() { + valid = true; + final T value = property.getValue(); + return value == null ? 0.0 : value.doubleValue(); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + ReadOnlyDoubleProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return ReadOnlyDoubleProperty.this.getName(); + } + + @Override + public Double get() { + valid = true; + return ReadOnlyDoubleProperty.this.getValue(); + } + }; + }; + + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoublePropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoublePropertyBase.java new file mode 100644 index 00000000..5abe27bf --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoublePropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping a {@code double}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyDoubleProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyDoublePropertyBase extends ReadOnlyDoubleProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyDoublePropertyBase}. + */ + public ReadOnlyDoublePropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleWrapper.java new file mode 100644 index 00000000..3280dc98 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyDoubleWrapper.java @@ -0,0 +1,99 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyDoubleWrapper extends SimpleDoubleProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyDoubleWrapper} + */ + public ReadOnlyDoubleWrapper() { + } + + /** + * The constructor of {@code ReadOnlyDoubleWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyDoubleWrapper(double initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyDoubleWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyDoubleProperty} + * @param name + * the name of this {@code ReadOnlyDoubleProperty} + */ + public ReadOnlyDoubleWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyDoubleWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyDoubleProperty} + * @param name + * the name of this {@code ReadOnlyDoubleProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyDoubleWrapper(Object bean, String name, + double initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the read-only property, that is synchronized with this + * {@code ReadOnlyDoubleWrapper}. + * + * @return the read-only property + */ + public ReadOnlyDoubleProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyDoublePropertyBase { + + @Override + public double get() { + return ReadOnlyDoubleWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyDoubleWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyDoubleWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatProperty.java new file mode 100644 index 00000000..1208f9d5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatProperty.java @@ -0,0 +1,108 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.FloatExpression; + +public abstract class ReadOnlyFloatProperty extends FloatExpression implements + ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyFloatProperty}. + */ + public ReadOnlyFloatProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyFloatProperty} object. + * @return a string representation of this {@code ReadOnlyFloatProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyFloatProperty ["); + 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(); + } + + public static ReadOnlyFloatProperty readOnlyFloatProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + + return property instanceof ReadOnlyFloatProperty ? (ReadOnlyFloatProperty) property: + new ReadOnlyFloatPropertyBase() { + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public float get() { + valid = true; + final T value = property.getValue(); + return value == null ? 0f : value.floatValue(); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + ReadOnlyFloatProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return ReadOnlyFloatProperty.this.getName(); + } + + @Override + public Float get() { + valid = true; + return ReadOnlyFloatProperty.this.getValue(); + } + }; + }; + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatPropertyBase.java new file mode 100644 index 00000000..fd5bf352 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatPropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping a {@code float}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyFloatProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyFloatPropertyBase extends ReadOnlyFloatProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyFloatPropertyBase}. + */ + public ReadOnlyFloatPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatWrapper.java new file mode 100644 index 00000000..1698b6a0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyFloatWrapper.java @@ -0,0 +1,98 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyFloatWrapper extends SimpleFloatProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyFloatWrapper} + */ + public ReadOnlyFloatWrapper() { + } + + /** + * The constructor of {@code ReadOnlyFloatWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyFloatWrapper(float initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyFloatWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyFloatProperty} + * @param name + * the name of this {@code ReadOnlyFloatProperty} + */ + public ReadOnlyFloatWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyFloatWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyFloatProperty} + * @param name + * the name of this {@code ReadOnlyFloatProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyFloatWrapper(Object bean, String name, float initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyFloatWrapper}. + * + * @return the readonly property + */ + public ReadOnlyFloatProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyFloatPropertyBase { + + @Override + public float get() { + return ReadOnlyFloatWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyFloatWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyFloatWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerProperty.java new file mode 100644 index 00000000..0b271297 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerProperty.java @@ -0,0 +1,109 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.IntegerExpression; + +public abstract class ReadOnlyIntegerProperty extends IntegerExpression + implements ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyIntegerProperty}. + */ + public ReadOnlyIntegerProperty() { + } + + + /** + * Returns a string representation of this {@code ReadOnlyIntegerProperty} object. + * @return a string representation of this {@code ReadOnlyIntegerProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyIntegerProperty ["); + 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(); + } + + public static ReadOnlyIntegerProperty readOnlyIntegerProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + + return property instanceof ReadOnlyIntegerProperty ? (ReadOnlyIntegerProperty) property: + new ReadOnlyIntegerPropertyBase() { + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public int get() { + valid = true; + final T value = property.getValue(); + return value == null ? 0 : value.intValue(); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + ReadOnlyIntegerProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return ReadOnlyIntegerProperty.this.getName(); + } + + @Override + public Integer get() { + valid = true; + return ReadOnlyIntegerProperty.this.getValue(); + } + }; + }; + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerPropertyBase.java new file mode 100644 index 00000000..240fc221 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerPropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping an {@code int}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyIntegerProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyIntegerPropertyBase extends ReadOnlyIntegerProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyIntegerPropertyBase}. + */ + public ReadOnlyIntegerPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerWrapper.java new file mode 100644 index 00000000..491bb482 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyIntegerWrapper.java @@ -0,0 +1,98 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyIntegerWrapper extends SimpleIntegerProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyIntegerWrapper} + */ + public ReadOnlyIntegerWrapper() { + } + + /** + * The constructor of {@code ReadOnlyIntegerWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyIntegerWrapper(int initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyIntegerWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyIntegerProperty} + * @param name + * the name of this {@code ReadOnlyIntegerProperty} + */ + public ReadOnlyIntegerWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyIntegerWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyIntegerProperty} + * @param name + * the name of this {@code ReadOnlyIntegerProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyIntegerWrapper(Object bean, String name, int initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyIntegerWrapper}. + * + * @return the readonly property + */ + public ReadOnlyIntegerProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyIntegerPropertyBase { + + @Override + public int get() { + return ReadOnlyIntegerWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyIntegerWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyIntegerWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListProperty.java new file mode 100644 index 00000000..e0a6408c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListProperty.java @@ -0,0 +1,127 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.binding.ListExpression; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import java.util.List; +import java.util.ListIterator; + +public abstract class ReadOnlyListProperty extends ListExpression + implements ReadOnlyProperty> { + + /** + * The constructor of {@code ReadOnlyListProperty}. + */ + public ReadOnlyListProperty() { + } + + /** + * Creates a bidirectional content binding of the {@link ObservableList}, that is + * wrapped in this {@code ReadOnlyListProperty}, and another {@code ObservableList}. + *

+ * A bidirectional content binding ensures that the content of two {@code ObservableLists} is the + * same. If the content of one of the lists changes, the other one will be updated automatically. + * + * @param list the {@code ObservableList} this property should be bound to + * @throws NullPointerException if {@code list} is {@code null} + * @throws IllegalArgumentException if {@code list} is the same list that this {@code ReadOnlyListProperty} points to + */ + public void bindContentBidirectional(ObservableList list) { + Bindings.bindContentBidirectional(this, list); + } + + /** + * Deletes a bidirectional content binding between the {@link ObservableList}, that is + * wrapped in this {@code ReadOnlyListProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the bidirectional binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same list that this {@code ReadOnlyListProperty} points to + */ + public void unbindContentBidirectional(Object object) { + Bindings.unbindContentBidirectional(this, object); + } + + /** + * Creates a content binding between the {@link ObservableList}, that is + * wrapped in this {@code ReadOnlyListProperty}, and another {@code ObservableList}. + *

+ * A content binding ensures that the content of the wrapped {@code ObservableLists} is the + * same as that of the other list. If the content of the other list changes, the wrapped list will be updated + * automatically. Once the wrapped list is bound to another list, you must not change it directly. + * + * @param list the {@code ObservableList} this property should be bound to + * @throws NullPointerException if {@code list} is {@code null} + * @throws IllegalArgumentException if {@code list} is the same list that this {@code ReadOnlyListProperty} points to + */ + public void bindContent(ObservableList list) { + Bindings.bindContent(this, list); + } + + /** + * Deletes a content binding between the {@link ObservableList}, that is + * wrapped in this {@code ReadOnlyListProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same list that this {@code ReadOnlyListProperty} points to + */ + public void unbindContent(Object object) { + Bindings.unbindContent(this, object); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof List)) { + return false; + } + final List list = (List)obj; + + if (size() != list.size()) { + return false; + } + + ListIterator e1 = listIterator(); + ListIterator e2 = list.listIterator(); + while (e1.hasNext() && e2.hasNext()) { + E o1 = e1.next(); + Object o2 = e2.next(); + if (!(o1==null ? o2==null : o1.equals(o2))) + return false; + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 1; + for (E e : this) + hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); + return hashCode; + } + + /** + * Returns a string representation of this {@code ReadOnlyListProperty} object. + * @return a string representation of this {@code ReadOnlyListProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyListProperty ["); + 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(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListPropertyBase.java new file mode 100644 index 00000000..1d20d598 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListPropertyBase.java @@ -0,0 +1,59 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ListExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +public abstract class ReadOnlyListPropertyBase extends ReadOnlyListProperty { + + private ListExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyListPropertyBase}. + */ + public ReadOnlyListPropertyBase() { + } + + @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> listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = ListExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(ListChangeListener listener) { + helper = ListExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ListChangeListener listener) { + helper = ListExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ListExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(ListChangeListener.Change change) { + ListExpressionHelper.fireValueChangedEvent(helper, change); + } + + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListWrapper.java new file mode 100644 index 00000000..c1bbd93f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyListWrapper.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.1 + */ +public class ReadOnlyListWrapper extends SimpleListProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyListWrapper} + */ + public ReadOnlyListWrapper() { + } + + /** + * The constructor of {@code ReadOnlyListWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyListWrapper(ObservableList initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyListWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyListWrapper} + * @param name + * the name of this {@code ReadOnlyListWrapper} + */ + public ReadOnlyListWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyListWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyListWrapper} + * @param name + * the name of this {@code ReadOnlyListWrapper} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyListWrapper(Object bean, String name, + ObservableList initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyListWrapper}. + * + * @return the readonly property + */ + public ReadOnlyListProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent(ListChangeListener.Change change) { + super.fireValueChangedEvent(change); + if (readOnlyProperty != null) { + change.reset(); + readOnlyProperty.fireValueChangedEvent(change); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyListPropertyBase { + + @Override + public ObservableList get() { + return ReadOnlyListWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyListWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyListWrapper.this.getName(); + } + + @Override + public ReadOnlyIntegerProperty sizeProperty() { + return ReadOnlyListWrapper.this.sizeProperty(); + } + + @Override + public ReadOnlyBooleanProperty emptyProperty() { + return ReadOnlyListWrapper.this.emptyProperty(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongProperty.java new file mode 100644 index 00000000..520fbc80 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongProperty.java @@ -0,0 +1,107 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.WeakInvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.LongExpression; + +public abstract class ReadOnlyLongProperty extends LongExpression implements + ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyLongProperty}. + */ + public ReadOnlyLongProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyLongProperty} object. + * @return a string representation of this {@code ReadOnlyLongProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("ReadOnlyLongProperty ["); + 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(); + } + + public static ReadOnlyLongProperty readOnlyLongProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + + return property instanceof ReadOnlyLongProperty ? (ReadOnlyLongProperty) property: + new ReadOnlyLongPropertyBase() { + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public long get() { + valid = true; + final T value = property.getValue(); + return value == null ? 0L : value.longValue(); + } + + @Override + public Object getBean() { + return null; // Virtual property, no bean + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + + private boolean valid = true; + private final InvalidationListener listener = observable -> { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + }; + + { + ReadOnlyLongProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Object getBean() { + return null; // Virtual property, does not exist on a bean + } + + @Override + public String getName() { + return ReadOnlyLongProperty.this.getName(); + } + + @Override + public Long get() { + valid = true; + return ReadOnlyLongProperty.this.getValue(); + } + }; + }; + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongPropertyBase.java new file mode 100644 index 00000000..ad86607f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongPropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping a {@code long}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyLongProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyLongPropertyBase extends ReadOnlyLongProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyLongPropertyBase}. + */ + public ReadOnlyLongPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongWrapper.java new file mode 100644 index 00000000..81309f6c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyLongWrapper.java @@ -0,0 +1,98 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyLongWrapper extends SimpleLongProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyLongWrapper} + */ + public ReadOnlyLongWrapper() { + } + + /** + * The constructor of {@code ReadOnlyLongWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyLongWrapper(long initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyLongWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyLongProperty} + * @param name + * the name of this {@code ReadOnlyLongProperty} + */ + public ReadOnlyLongWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyLongWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyLongProperty} + * @param name + * the name of this {@code ReadOnlyLongProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyLongWrapper(Object bean, String name, long initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyLongWrapper}. + * + * @return the readonly property + */ + public ReadOnlyLongProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyLongPropertyBase { + + @Override + public long get() { + return ReadOnlyLongWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyLongWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyLongWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapProperty.java new file mode 100644 index 00000000..f52cf2eb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapProperty.java @@ -0,0 +1,137 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.binding.MapExpression; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +import java.util.Map; + +public abstract class ReadOnlyMapProperty extends MapExpression implements ReadOnlyProperty> { + + /** + * The constructor of {@code ReadOnlyMapProperty}. + */ + public ReadOnlyMapProperty() { + } + + /** + * Creates a bidirectional content binding of the {@link ObservableMap}, that is + * wrapped in this {@code ReadOnlyMapProperty}, and another {@code ObservableMap}. + *

+ * A bidirectional content binding ensures that the content of two {@code ObservableMaps} is the + * same. If the content of one of the maps changes, the other one will be updated automatically. + * + * @param map the {@code ObservableMap} this property should be bound to + * @throws NullPointerException if {@code map} is {@code null} + * @throws IllegalArgumentException if {@code map} is the same map that this {@code ReadOnlyMapProperty} points to + */ + public void bindContentBidirectional(ObservableMap map) { + Bindings.bindContentBidirectional(this, map); + } + + /** + * Deletes a bidirectional content binding between the {@link ObservableMap}, that is + * wrapped in this {@code ReadOnlyMapProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the bidirectional binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same map that this {@code ReadOnlyMapProperty} points to + */ + public void unbindContentBidirectional(Object object) { + Bindings.unbindContentBidirectional(this, object); + } + + /** + * Creates a content binding between the {@link ObservableMap}, that is + * wrapped in this {@code ReadOnlyMapProperty}, and another {@code ObservableMap}. + *

+ * A content binding ensures that the content of the wrapped {@code ObservableMaps} is the + * same as that of the other map. If the content of the other map changes, the wrapped map will be updated + * automatically. Once the wrapped list is bound to another map, you must not change it directly. + * + * @param map the {@code ObservableMap} this property should be bound to + * @throws NullPointerException if {@code map} is {@code null} + * @throws IllegalArgumentException if {@code map} is the same map that this {@code ReadOnlyMapProperty} points to + */ + public void bindContent(ObservableMap map) { + Bindings.bindContent(this, map); + } + + /** + * Deletes a content binding between the {@link ObservableMap}, that is + * wrapped in this {@code ReadOnlyMapProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same map that this {@code ReadOnlyMapProperty} points to + */ + public void unbindContent(Object object) { + Bindings.unbindContent(this, object); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + + if (!(obj instanceof Map)) + return false; + Map m = (Map) obj; + if (m.size() != size()) + return false; + + try { + for (Entry e : entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + if (value == null) { + if (!(m.get(key)==null && m.containsKey(key))) + return false; + } else { + if (!value.equals(m.get(key))) + return false; + } + } + } catch (ClassCastException unused) { + return false; + } catch (NullPointerException unused) { + return false; + } + + return true; + } + + /** + * Returns a hash code for this {@code ReadOnlyMapProperty} object. + * @return a hash code for this {@code ReadOnlyMapProperty} object. + */ + @Override + public int hashCode() { + int h = 0; + for (Entry e : entrySet()) { + h += e.hashCode(); + } + return h; + } + + /** + * Returns a string representation of this {@code ReadOnlyMapProperty} object. + * @return a string representation of this {@code ReadOnlyMapProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyMapProperty ["); + 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(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapPropertyBase.java new file mode 100644 index 00000000..ec3ead0b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapPropertyBase.java @@ -0,0 +1,66 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.MapExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +/** + * Base class for all readonly properties wrapping an {@link ObservableMap}. + * This class provides a default implementation to attach listener. + * + * @see ReadOnlyMapProperty + * @since JavaFX 2.1 + */ +public abstract class ReadOnlyMapPropertyBase extends ReadOnlyMapProperty { + + private MapExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyMapPropertyBase}. + */ + public ReadOnlyMapPropertyBase() { + } + + @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> listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = MapExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(MapChangeListener listener) { + helper = MapExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(MapChangeListener listener) { + helper = MapExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + MapExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(MapChangeListener.Change change) { + MapExpressionHelper.fireValueChangedEvent(helper, change); + } + + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapWrapper.java new file mode 100644 index 00000000..14a72a6a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyMapWrapper.java @@ -0,0 +1,123 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.1 + */ +public class ReadOnlyMapWrapper extends SimpleMapProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyMapWrapper} + */ + public ReadOnlyMapWrapper() { + } + + /** + * The constructor of {@code ReadOnlyMapWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyMapWrapper(ObservableMap initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyMapWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyMapWrapper} + * @param name + * the name of this {@code ReadOnlyMapWrapper} + */ + public ReadOnlyMapWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyMapWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyMapWrapper} + * @param name + * the name of this {@code ReadOnlyMapWrapper} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyMapWrapper(Object bean, String name, + ObservableMap initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyMapWrapper}. + * + * @return the readonly property + */ + public ReadOnlyMapProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + super.fireValueChangedEvent(change); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(change); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyMapPropertyBase { + + @Override + public ObservableMap get() { + return ReadOnlyMapWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyMapWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyMapWrapper.this.getName(); + } + + @Override + public ReadOnlyIntegerProperty sizeProperty() { + return ReadOnlyMapWrapper.this.sizeProperty(); + } + + @Override + public ReadOnlyBooleanProperty emptyProperty() { + return ReadOnlyMapWrapper.this.emptyProperty(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectProperty.java new file mode 100644 index 00000000..885159b0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectProperty.java @@ -0,0 +1,34 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.ObjectExpression; + +public abstract class ReadOnlyObjectProperty extends ObjectExpression + implements ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyObjectProperty}. + */ + public ReadOnlyObjectProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyObjectProperty} object. + * @return a string representation of this {@code ReadOnlyObjectProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyObjectProperty ["); + 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(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectPropertyBase.java new file mode 100644 index 00000000..52f3ca72 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectPropertyBase.java @@ -0,0 +1,50 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping an arbitrary {@code Object}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyObjectProperty + * + * @param the type of the wrapped {@code Object} + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyObjectPropertyBase extends ReadOnlyObjectProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyObjectPropertyBase}. + */ + public ReadOnlyObjectPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectWrapper.java new file mode 100644 index 00000000..d49c7724 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyObjectWrapper.java @@ -0,0 +1,98 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyObjectWrapper extends SimpleObjectProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyObjectWrapper} + */ + public ReadOnlyObjectWrapper() { + } + + /** + * The constructor of {@code ReadOnlyObjectWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyObjectWrapper(T initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyObjectWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyObjectProperty} + * @param name + * the name of this {@code ReadOnlyObjectProperty} + */ + public ReadOnlyObjectWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyObjectWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyObjectProperty} + * @param name + * the name of this {@code ReadOnlyObjectProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyObjectWrapper(Object bean, String name, T initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyObjectWrapper}. + * + * @return the readonly property + */ + public ReadOnlyObjectProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyObjectPropertyBase { + + @Override + public T get() { + return ReadOnlyObjectWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyObjectWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyObjectWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyProperty.java new file mode 100644 index 00000000..6277de78 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyProperty.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; + +/** + * Generic interface that defines the methods common to all readable properties + * independent of their type. + * + * + * @param + * the type of the wrapped value + * @since JavaFX 2.0 + */ +public interface ReadOnlyProperty extends ObservableValue { + + /** + * Returns the {@code Object} that contains this property. If this property + * is not contained in an {@code Object}, {@code null} is returned. + * + * @return the containing {@code Object} or {@code null} + */ + Object getBean(); + + /** + * Returns the name of this property. If the property does not have a name, + * this method returns an empty {@code String}. + * + * @return the name or an empty {@code String} + */ + String getName(); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetProperty.java new file mode 100644 index 00000000..146a4fcd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetProperty.java @@ -0,0 +1,125 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.binding.SetExpression; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +import java.util.Set; + +public abstract class ReadOnlySetProperty extends SetExpression implements ReadOnlyProperty> { + + /** + * The constructor of {@code ReadOnlySetProperty}. + */ + public ReadOnlySetProperty() { + } + + /** + * Creates a bidirectional content binding of the {@link ObservableSet}, that is + * wrapped in this {@code ReadOnlySetProperty}, and another {@code ObservableSet}. + *

+ * A bidirectional content binding ensures that the content of two {@code ObservableSets} is the + * same. If the content of one of the sets changes, the other one will be updated automatically. + * + * @param set the {@code ObservableSet} this property should be bound to + * @throws NullPointerException if {@code set} is {@code null} + * @throws IllegalArgumentException if {@code set} is the same set that this {@code ReadOnlySetProperty} points to + */ + public void bindContentBidirectional(ObservableSet set) { + Bindings.bindContentBidirectional(this, set); + } + + /** + * Deletes a bidirectional content binding between the {@link ObservableSet}, that is + * wrapped in this {@code ReadOnlySetProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the bidirectional binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same set that this {@code ReadOnlySetProperty} points to + */ + public void unbindContentBidirectional(Object object) { + Bindings.unbindContentBidirectional(this, object); + } + + /** + * Creates a content binding between the {@link ObservableSet}, that is + * wrapped in this {@code ReadOnlySetProperty}, and another {@code ObservableSet}. + *

+ * A content binding ensures that the content of the wrapped {@code ObservableSets} is the + * same as that of the other set. If the content of the other set changes, the wrapped set will be updated + * automatically. Once the wrapped set is bound to another set, you must not change it directly. + * + * @param set the {@code ObservableSet} this property should be bound to + * @throws NullPointerException if {@code set} is {@code null} + * @throws IllegalArgumentException if {@code set} is the same set that this {@code ReadOnlySetProperty} points to + */ + public void bindContent(ObservableSet set) { + Bindings.bindContent(this, set); + } + + /** + * Deletes a content binding between the {@link ObservableSet}, that is + * wrapped in this {@code ReadOnlySetProperty}, and another {@code Object}. + * + * @param object the {@code Object} to which the binding should be removed + * @throws NullPointerException if {@code object} is {@code null} + * @throws IllegalArgumentException if {@code object} is the same set that this {@code ReadOnlySetProperty} points to + */ + public void unbindContent(Object object) { + Bindings.unbindContent(this, object); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + + if (!(obj instanceof Set)) + return false; + Set c = (Set) obj; + if (c.size() != size()) + return false; + try { + return containsAll(c); + } catch (ClassCastException unused) { + return false; + } catch (NullPointerException unused) { + return false; + } + } + + /** + * Returns a hash code for this {@code ReadOnlySetProperty} object. + * @return a hash code for this {@code ReadOnlySetProperty} object. + */ + @Override + public int hashCode() { + int h = 0; + for (E e : this) { + if (e != null) + h += e.hashCode(); + } + return h; + } + + /** + * Returns a string representation of this {@code ReadOnlySetProperty} object. + * @return a string representation of this {@code ReadOnlySetProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlySetProperty ["); + 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(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetPropertyBase.java new file mode 100644 index 00000000..ff420d34 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetPropertyBase.java @@ -0,0 +1,68 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.SetExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; +import com.tungsten.fclcore.fakefx.collections.SetChangeListener; + +/** + * Base class for all readonly properties wrapping an {@link ObservableSet}. + * This class provides a default implementation to attach listener. + * + * @see ReadOnlySetProperty + * + * @param the type of the {@code Set} elements + * @since JavaFX 2.1 + */ +public abstract class ReadOnlySetPropertyBase extends ReadOnlySetProperty { + + private SetExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlySetPropertyBase}. + */ + public ReadOnlySetPropertyBase() { + } + + @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> listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = SetExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(SetChangeListener listener) { + helper = SetExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + SetExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(SetChangeListener.Change change) { + SetExpressionHelper.fireValueChangedEvent(helper, change); + } + + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetWrapper.java new file mode 100644 index 00000000..064e562d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlySetWrapper.java @@ -0,0 +1,123 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.ObservableSet; +import com.tungsten.fclcore.fakefx.collections.SetChangeListener; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.1 + */ +public class ReadOnlySetWrapper extends SimpleSetProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlySetWrapper} + */ + public ReadOnlySetWrapper() { + } + + /** + * The constructor of {@code ReadOnlySetWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlySetWrapper(ObservableSet initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlySetWrapper} + * + * @param bean + * the bean of this {@code ReadOnlySetWrapper} + * @param name + * the name of this {@code ReadOnlySetWrapper} + */ + public ReadOnlySetWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlySetWrapper} + * + * @param bean + * the bean of this {@code ReadOnlySetWrapper} + * @param name + * the name of this {@code ReadOnlySetWrapper} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlySetWrapper(Object bean, String name, + ObservableSet initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlySetWrapper}. + * + * @return the readonly property + */ + public ReadOnlySetProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + super.fireValueChangedEvent(change); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(change); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlySetPropertyBase { + + @Override + public ObservableSet get() { + return ReadOnlySetWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlySetWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlySetWrapper.this.getName(); + } + + @Override + public ReadOnlyIntegerProperty sizeProperty() { + return ReadOnlySetWrapper.this.sizeProperty(); + } + + @Override + public ReadOnlyBooleanProperty emptyProperty() { + return ReadOnlySetWrapper.this.emptyProperty(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringProperty.java new file mode 100644 index 00000000..596e4b38 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringProperty.java @@ -0,0 +1,34 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.StringExpression; + +public abstract class ReadOnlyStringProperty extends StringExpression implements + ReadOnlyProperty { + + /** + * The constructor of {@code ReadOnlyStringProperty}. + */ + public ReadOnlyStringProperty() { + } + + /** + * Returns a string representation of this {@code ReadOnlyStringProperty} object. + * @return a string representation of this {@code ReadOnlyStringProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "ReadOnlyStringProperty ["); + 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(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringPropertyBase.java new file mode 100644 index 00000000..7bf9fa36 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringPropertyBase.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * Base class for all readonly properties wrapping a {@code String}. This class provides a default + * implementation to attach listener. + * + * @see ReadOnlyStringProperty + * @since JavaFX 2.0 + */ +public abstract class ReadOnlyStringPropertyBase extends ReadOnlyStringProperty { + + ExpressionHelper helper; + + /** + * Creates a default {@code ReadOnlyStringPropertyBase}. + */ + public ReadOnlyStringPropertyBase() { + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringWrapper.java new file mode 100644 index 00000000..eebb4471 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/ReadOnlyStringWrapper.java @@ -0,0 +1,99 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a convenient class to define read-only properties. It + * creates two properties that are synchronized. One property is read-only + * and can be passed to external users. The other property is read- and + * writable and should be used internally only. + * + * @since JavaFX 2.0 + */ +public class ReadOnlyStringWrapper extends SimpleStringProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /** + * The constructor of {@code ReadOnlyStringWrapper} + */ + public ReadOnlyStringWrapper() { + } + + /** + * The constructor of {@code ReadOnlyStringWrapper} + * + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyStringWrapper(String initialValue) { + super(initialValue); + } + + /** + * The constructor of {@code ReadOnlyStringWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyStringProperty} + * @param name + * the name of this {@code ReadOnlyStringProperty} + */ + public ReadOnlyStringWrapper(Object bean, String name) { + super(bean, name); + } + + /** + * The constructor of {@code ReadOnlyStringWrapper} + * + * @param bean + * the bean of this {@code ReadOnlyStringProperty} + * @param name + * the name of this {@code ReadOnlyStringProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public ReadOnlyStringWrapper(Object bean, String name, + String initialValue) { + super(bean, name, initialValue); + } + + /** + * Returns the readonly property, that is synchronized with this + * {@code ReadOnlyStringWrapper}. + * + * @return the readonly property + */ + public ReadOnlyStringProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + /** + * {@inheritDoc} + */ + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private class ReadOnlyPropertyImpl extends ReadOnlyStringPropertyBase { + + @Override + public String get() { + return ReadOnlyStringWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyStringWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyStringWrapper.this.getName(); + } + }; +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetProperty.java new file mode 100644 index 00000000..7d69c7fa --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetProperty.java @@ -0,0 +1,59 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableSetValue; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +public abstract class SetProperty extends ReadOnlySetProperty implements + Property>, WritableSetValue { + + /** + * Creates a default {@code SetProperty}. + */ + public SetProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(ObservableSet v) { + set(v); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property> other) { + Bindings.bindBidirectional(this, other); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property> other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code SetProperty} object. + * @return a string representation of this {@code SetProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "SetProperty ["); + 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(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetPropertyBase.java new file mode 100644 index 00000000..794e2cf9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SetPropertyBase.java @@ -0,0 +1,303 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.SetExpressionHelper; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; +import com.tungsten.fclcore.fakefx.collections.SetChangeListener; + +import java.lang.ref.WeakReference; + +/** + * The class {@code SetPropertyBase} is the base class for a property + * wrapping an {@link ObservableSet}. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see ObservableSet + * @see SetProperty + * + * @param the type of the {@code Set} elements + * @since JavaFX 2.1 + */ +public abstract class SetPropertyBase extends SetProperty { + + private final SetChangeListener setChangeListener = change -> { + invalidateProperties(); + invalidated(); + fireValueChangedEvent(change); + }; + + private ObservableSet value; + private ObservableValue> observable = null; + private InvalidationListener listener = null; + private boolean valid = true; + private SetExpressionHelper helper = null; + + private SizeProperty size0; + private EmptyProperty empty0; + + /** + * The Constructor of {@code SetPropertyBase} + */ + public SetPropertyBase() {} + + /** + * The constructor of the {@code SetPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped value + */ + public SetPropertyBase(ObservableSet initialValue) { + this.value = initialValue; + if (initialValue != null) { + initialValue.addListener(setChangeListener); + } + } + + @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 SetPropertyBase.this; + } + + @Override + public String getName() { + return "size"; + } + + @Override + 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 SetPropertyBase.this; + } + + @Override + public String getName() { + return "empty"; + } + + @Override + 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> listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = SetExpressionHelper.removeListener(helper, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + helper = SetExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(SetChangeListener listener) { + helper = SetExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + SetExpressionHelper.fireValueChangedEvent(helper); + } + + protected void fireValueChangedEvent(SetChangeListener.Change change) { + SetExpressionHelper.fireValueChangedEvent(helper, change); + } + + private void invalidateProperties() { + if (size0 != null) { + size0.fireValueChangedEvent(); + } + if (empty0 != null) { + empty0.fireValueChangedEvent(); + } + } + + private void markInvalid(ObservableSet oldValue) { + if (valid) { + if (oldValue != null) { + oldValue.removeListener(setChangeListener); + } + valid = false; + invalidateProperties(); + invalidated(); + fireValueChangedEvent(); + } + } + + + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + @Override + public ObservableSet get() { + if (!valid) { + value = observable == null ? value : observable.getValue(); + valid = true; + if (value != null) { + value.addListener(setChangeListener); + } + } + return value; + } + + @Override + public void set(ObservableSet newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if (value != newValue) { + final ObservableSet oldValue = value; + value = newValue; + markInvalid(oldValue); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + @Override + public void bind(final ObservableValue> newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (newObservable != this.observable) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener<>(this); + } + observable.addListener(listener); + markInvalid(value); + } + } + + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + /** + * Returns a string representation of this {@code SetPropertyBase} object. + * @return a string representation of this {@code SetPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("SetProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference> wref; + + public Listener(SetPropertyBase ref) { + this.wref = new WeakReference>(ref); + } + + @Override + public void invalidated(Observable observable) { + SetPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(ref.value); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleBooleanProperty.java new file mode 100644 index 00000000..96b7fbc5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleBooleanProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code boolean} value. + * + * @see BooleanPropertyBase + * + * @since JavaFX 2.0 + */ +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; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code BooleanProperty} + */ + public SimpleBooleanProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code BooleanProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleBooleanProperty(boolean initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code BooleanProperty} + * + * @param bean + * the bean of this {@code BooleanProperty} + * @param name + * the name of this {@code BooleanProperty} + */ + public SimpleBooleanProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code BooleanProperty} + * + * @param bean + * the bean of this {@code BooleanProperty} + * @param name + * the name of this {@code BooleanProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleBooleanProperty(Object bean, String name, boolean initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleDoubleProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleDoubleProperty.java new file mode 100644 index 00000000..f20ab965 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleDoubleProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code double} value. + * + * @see DoublePropertyBase + * + * @since JavaFX 2.0 + */ +public class SimpleDoubleProperty extends DoublePropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code DoubleProperty} + */ + public SimpleDoubleProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code DoubleProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleDoubleProperty(double initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code DoubleProperty} + * + * @param bean + * the bean of this {@code DoubleProperty} + * @param name + * the name of this {@code DoubleProperty} + */ + public SimpleDoubleProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code DoubleProperty} + * + * @param bean + * the bean of this {@code DoubleProperty} + * @param name + * the name of this {@code DoubleProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleDoubleProperty(Object bean, String name, double initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleFloatProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleFloatProperty.java new file mode 100644 index 00000000..9270e7a4 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleFloatProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code float} value. + * + * @see FloatPropertyBase + * + * @since JavaFX 2.0 + */ +public class SimpleFloatProperty extends FloatPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code FloatProperty} + */ + public SimpleFloatProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code FloatProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleFloatProperty(float initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code FloatProperty} + * + * @param bean + * the bean of this {@code FloatProperty} + * @param name + * the name of this {@code FloatProperty} + */ + public SimpleFloatProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code FloatProperty} + * + * @param bean + * the bean of this {@code FloatProperty} + * @param name + * the name of this {@code FloatProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleFloatProperty(Object bean, String name, float initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleIntegerProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleIntegerProperty.java new file mode 100644 index 00000000..91297e94 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleIntegerProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code int} value. + * + * @see IntegerPropertyBase + * + * @since JavaFX 2.0 + */ +public class SimpleIntegerProperty extends IntegerPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code IntegerProperty} + */ + public SimpleIntegerProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code IntegerProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleIntegerProperty(int initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code IntegerProperty} + * + * @param bean + * the bean of this {@code IntegerProperty} + * @param name + * the name of this {@code IntegerProperty} + */ + public SimpleIntegerProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code IntegerProperty} + * + * @param bean + * the bean of this {@code IntegerProperty} + * @param name + * the name of this {@code IntegerProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleIntegerProperty(Object bean, String name, int initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleListProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleListProperty.java new file mode 100644 index 00000000..8b1b5b96 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleListProperty.java @@ -0,0 +1,84 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +/** + * This class provides a full implementation of a {@link Property} wrapping an + * {@code ObservableList}. + * + * @see ListPropertyBase + * + * @param the type of the {@code List} elements + * @since JavaFX 2.1 + */ +public class SimpleListProperty extends ListPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code SimpleListProperty} + */ + public SimpleListProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code SimpleListProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleListProperty(ObservableList initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code SimpleListProperty} + * + * @param bean + * the bean of this {@code SetProperty} + * @param name + * the name of this {@code SetProperty} + */ + public SimpleListProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code SimpleListProperty} + * + * @param bean + * the bean of this {@code ListProperty} + * @param name + * the name of this {@code ListProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleListProperty(Object bean, String name, ObservableList initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleLongProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleLongProperty.java new file mode 100644 index 00000000..ca9bec7d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleLongProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code long} value. + * + * @see LongPropertyBase + * + * @since JavaFX 2.0 + */ +public class SimpleLongProperty extends LongPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code LongProperty} + */ + public SimpleLongProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code LongProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleLongProperty(long initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code LongProperty} + * + * @param bean + * the bean of this {@code LongProperty} + * @param name + * the name of this {@code LongProperty} + */ + public SimpleLongProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code LongProperty} + * + * @param bean + * the bean of this {@code LongProperty} + * @param name + * the name of this {@code LongProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleLongProperty(Object bean, String name, long initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleMapProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleMapProperty.java new file mode 100644 index 00000000..1c02edcc --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleMapProperty.java @@ -0,0 +1,85 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +/** + * This class provides a full implementation of a {@link Property} wrapping an + * {@code ObservableMap}. + * + * @see MapPropertyBase + * + * @param the type of the key elements of the {@code Map} + * @param the type of the value elements of the {@code Map} + * @since JavaFX 2.1 + */ +public class SimpleMapProperty extends MapPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code SimpleMapProperty} + */ + public SimpleMapProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code SimpleMapProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleMapProperty(ObservableMap initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code SimpleMapProperty} + * + * @param bean + * the bean of this {@code MapProperty} + * @param name + * the name of this {@code MapProperty} + */ + public SimpleMapProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code SimpleMapProperty} + * + * @param bean + * the bean of this {@code MapProperty} + * @param name + * the name of this {@code MapProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleMapProperty(Object bean, String name, ObservableMap initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleObjectProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleObjectProperty.java new file mode 100644 index 00000000..e8b2b9f0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleObjectProperty.java @@ -0,0 +1,84 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping an + * arbitrary {@code Object}. + * + * @see ObjectPropertyBase + * + * + * @param + * the type of the wrapped {@code Object} + * @since JavaFX 2.0 + */ +public class SimpleObjectProperty extends ObjectPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code ObjectProperty} + */ + public SimpleObjectProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code ObjectProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleObjectProperty(T initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code ObjectProperty} + * + * @param bean + * the bean of this {@code ObjectProperty} + * @param name + * the name of this {@code ObjectProperty} + */ + public SimpleObjectProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code ObjectProperty} + * + * @param bean + * the bean of this {@code ObjectProperty} + * @param name + * the name of this {@code ObjectProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleObjectProperty(Object bean, String name, T initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleSetProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleSetProperty.java new file mode 100644 index 00000000..97c338d7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleSetProperty.java @@ -0,0 +1,84 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +/** + * This class provides a full implementation of a {@link Property} wrapping an + * {@code ObservableSet}. + * + * @see SetPropertyBase + * + * @param the type of the {@code Set} elements + * @since JavaFX 2.1 + */ +public class SimpleSetProperty extends SetPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code SimpleSetProperty} + */ + public SimpleSetProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code SimpleSetProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleSetProperty(ObservableSet initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code SimpleSetProperty} + * + * @param bean + * the bean of this {@code SetProperty} + * @param name + * the name of this {@code SetProperty} + */ + public SimpleSetProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code SimpleSetProperty} + * + * @param bean + * the bean of this {@code SetProperty} + * @param name + * the name of this {@code SetProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleSetProperty(Object bean, String name, ObservableSet initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleStringProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleStringProperty.java new file mode 100644 index 00000000..3e8aae61 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/SimpleStringProperty.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +/** + * This class provides a full implementation of a {@link Property} wrapping a + * {@code String} value. + * + * @see StringPropertyBase + * + * @since JavaFX 2.0 + */ +public class SimpleStringProperty extends StringPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return bean; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * The constructor of {@code StringProperty} + */ + public SimpleStringProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /** + * The constructor of {@code StringProperty} + * + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleStringProperty(String initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /** + * The constructor of {@code StringProperty} + * + * @param bean + * the bean of this {@code StringProperty} + * @param name + * the name of this {@code StringProperty} + */ + public SimpleStringProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /** + * The constructor of {@code StringProperty} + * + * @param bean + * the bean of this {@code StringProperty} + * @param name + * the name of this {@code StringProperty} + * @param initialValue + * the initial value of the wrapped value + */ + public SimpleStringProperty(Object bean, String name, String initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringProperty.java new file mode 100644 index 00000000..d64f123b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringProperty.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.value.WritableStringValue; +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.text.Format; + +public abstract class StringProperty extends ReadOnlyStringProperty implements + Property, WritableStringValue { + + /** + * Creates a default {@code StringProperty}. + */ + public StringProperty() { + } + + /** + * {@inheritDoc} + */ + @Override + public void setValue(String v) { + set(v); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + /** + * Create a bidirectional binding between this {@code StringProperty} and another + * arbitrary property. Relies on an implementation of {@code Format} for conversion. + * + * @param other + * the other {@code Property} + * @param format + * the {@code Format} used to convert between this {@code StringProperty} + * and the other {@code Property} + * @throws NullPointerException + * if {@code other} or {@code format} is {@code null} + * @throws IllegalArgumentException + * if {@code other} is {@code this} + * @since JavaFX 2.1 + */ + public void bindBidirectional(Property other, Format format) { + Bindings.bindBidirectional(this, other, format); + } + + public void bindBidirectional(Property other, StringConverter converter) { + Bindings.bindBidirectional(this, other, converter); + } + + /** + * {@inheritDoc} + */ + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Remove a bidirectional binding between this {@code Property} and another + * one. + * + * If no bidirectional binding between the properties exists, calling this + * method has no effect. + * + * @param other + * the other {@code Property} + * @throws NullPointerException + * if {@code other} is {@code null} + * @throws IllegalArgumentException + * if {@code other} is {@code this} + * @since JavaFX 2.1 + */ + public void unbindBidirectional(Object other) { + Bindings.unbindBidirectional(this, other); + } + + /** + * Returns a string representation of this {@code StringProperty} object. + * @return a string representation of this {@code StringProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder( + "StringProperty ["); + 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(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringPropertyBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringPropertyBase.java new file mode 100644 index 00000000..0f059456 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/StringPropertyBase.java @@ -0,0 +1,206 @@ +package com.tungsten.fclcore.fakefx.beans.property; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +import java.lang.ref.WeakReference; + +/** + * The class {@code StringPropertyBase} is the base class for a property + * wrapping a {@code String} value. + * + * It provides all the functionality required for a property except for the + * {@link #getBean()} and {@link #getName()} methods, which must be implemented + * by extending classes. + * + * @see StringProperty + * + * + * @since JavaFX 2.0 + */ +public abstract class StringPropertyBase extends StringProperty { + + private String value; + private ObservableValue observable = null; + private InvalidationListener listener = null; + private boolean valid = true; + private ExpressionHelper helper = null; + + /** + * The constructor of the {@code StringPropertyBase}. + */ + public StringPropertyBase() { + } + + /** + * The constructor of the {@code StringPropertyBase}. + * + * @param initialValue + * the initial value of the wrapped {@code String} + */ + public StringPropertyBase(String initialValue) { + this.value = initialValue; + } + + @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 listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /** + * The method {@code invalidated()} can be overridden to receive + * invalidation notifications. This is the preferred option in + * {@code Objects} defining the property, because it requires less memory. + * + * The default implementation is empty. + */ + protected void invalidated() { + } + + /** + * {@inheritDoc} + */ + @Override + public String get() { + valid = true; + return observable == null ? value : observable.getValue(); + } + + /** + * {@inheritDoc} + */ + @Override + public void set(String newValue) { + if (isBound()) { + throw new RuntimeException((getBean() != null && getName() != null ? + getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set."); + } + if ((value == null)? newValue != null : !value.equals(newValue)) { + value = newValue; + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + /** + * Returns a string representation of this {@code StringPropertyBase} object. + * @return a string representation of this {@code StringPropertyBase} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("StringProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + if (valid) { + result.append("value: ").append(get()); + } else { + result.append("invalid"); + } + } else { + result.append("value: ").append(get()); + } + result.append("]"); + return result.toString(); + } + + private static class Listener implements InvalidationListener, WeakListener { + + private final WeakReference wref; + + public Listener(StringPropertyBase ref) { + this.wref = new WeakReference<>(ref); + } + + @Override + public void invalidated(Observable observable) { + StringPropertyBase ref = wref.get(); + if (ref == null) { + observable.removeListener(this); + } else { + ref.markInvalid(); + } + } + + @Override + public boolean wasGarbageCollected() { + return wref.get() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/DescriptorListenerCleaner.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/DescriptorListenerCleaner.java new file mode 100644 index 00000000..f20a97a0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/DescriptorListenerCleaner.java @@ -0,0 +1,24 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.ref.WeakReference; + +class DescriptorListenerCleaner implements Runnable{ + + private final ReadOnlyPropertyDescriptor pd; + private final WeakReference> lRef; + + DescriptorListenerCleaner(ReadOnlyPropertyDescriptor pd, ReadOnlyPropertyDescriptor.ReadOnlyListener l) { + this.pd = pd; + this.lRef = new WeakReference>(l); + } + + @Override + public void run() { + ReadOnlyPropertyDescriptor.ReadOnlyListener l = lRef.get(); + if (l != null) { + pd.removeListener(l); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanProperty.java new file mode 100644 index 00000000..461053c1 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanProperty.java @@ -0,0 +1,208 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class JavaBeanBooleanProperty extends BooleanProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanBooleanProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public boolean get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (Boolean) MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final boolean value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanBooleanProperty} object. + * @return a string representation of this {@code JavaBeanBooleanProperty} object. + */ + @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(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanPropertyBuilder.java new file mode 100644 index 00000000..8d82b325 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanBooleanPropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanBooleanPropertyBuilder} can be used to create + * {@link JavaBeanBooleanProperty JavaBeanBooleanProperties}. To create + * a {@code JavaBeanBooleanProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanBooleanPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanBooleanProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanBooleanPropertyBuilder { + + private final JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanBooleanPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanBooleanPropertyBuilder}. + * + * @return the new {@code JavaBeanBooleanPropertyBuilder} + */ + public static JavaBeanBooleanPropertyBuilder create() { + return new JavaBeanBooleanPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanBooleanProperty} with the current settings. + * + * @return the new {@code JavaBeanBooleanProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code boolean} or {@code Boolean} + */ + public JavaBeanBooleanProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!boolean.class.equals(descriptor.getType()) && !Boolean.class.equals(descriptor.getType())) { + throw new IllegalArgumentException("Not a boolean property"); + } + return new JavaBeanBooleanProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanBooleanPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoubleProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoubleProperty.java new file mode 100644 index 00000000..99657768 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoubleProperty.java @@ -0,0 +1,209 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class JavaBeanDoubleProperty extends DoubleProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanDoubleProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public double get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).doubleValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final double value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue().doubleValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanDoubleProperty} object. + * @return a string representation of this {@code JavaBeanDoubleProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("DoubleProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoublePropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoublePropertyBuilder.java new file mode 100644 index 00000000..c4d73962 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanDoublePropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanDoublePropertyBuilder} can be used to create + * {@link JavaBeanDoubleProperty JavaBeanDoubleProperties}. To create + * a {@code JavaBeanDoubleProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanDoublePropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanDoubleProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanDoublePropertyBuilder { + + private final JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanDoublePropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanDoublePropertyBuilder}. + * + * @return the new {@code JavaBeanDoublePropertyBuilder} + */ + public static JavaBeanDoublePropertyBuilder create() { + return new JavaBeanDoublePropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanDoubleProperty} with the current settings. + * + * @return the new {@code JavaBeanDoubleProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code double} or {@code Double} + */ + public JavaBeanDoubleProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!double.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a double property"); + } + return new JavaBeanDoubleProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanDoublePropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatProperty.java new file mode 100644 index 00000000..187c1267 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatProperty.java @@ -0,0 +1,208 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.FloatProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class JavaBeanFloatProperty extends FloatProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanFloatProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public float get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).floatValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final float value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue().floatValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanFloatProperty} object. + * @return a string representation of this {@code JavaBeanFloatProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("FloatProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatPropertyBuilder.java new file mode 100644 index 00000000..fc0c824f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanFloatPropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanFloatPropertyBuilder} can be used to create + * {@link JavaBeanFloatProperty JavaBeanFloatProperties}. To create + * a {@code JavaBeanFloatProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanFloatPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanFloatProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanFloatPropertyBuilder { + + private JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanFloatPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanFloatPropertyBuilder}. + * + * @return the new {@code JavaBeanFloatPropertyBuilder} + */ + public static JavaBeanFloatPropertyBuilder create() { + return new JavaBeanFloatPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanFloatProperty} with the current settings. + * + * @return the new {@code JavaBeanFloatProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code float} or {@code Float} + */ + public JavaBeanFloatProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!float.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a float property"); + } + return new JavaBeanFloatProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanFloatPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerProperty.java new file mode 100644 index 00000000..17561ddd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerProperty.java @@ -0,0 +1,208 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class JavaBeanIntegerProperty extends IntegerProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanIntegerProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public int get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).intValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final int value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue().intValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanIntegerProperty} object. + * @return a string representation of this {@code JavaBeanIntegerProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("IntegerProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerPropertyBuilder.java new file mode 100644 index 00000000..fd599b89 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanIntegerPropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanIntegerPropertyBuilder} can be used to create + * {@link JavaBeanIntegerProperty JavaBeanIntegerProperties}. To create + * a {@code JavaBeanIntegerProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanIntegerPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanIntegerProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanIntegerPropertyBuilder { + + private JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanIntegerPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanIntegerPropertyBuilder}. + * + * @return the new {@code JavaBeanIntegerPropertyBuilder} + */ + public static JavaBeanIntegerPropertyBuilder create() { + return new JavaBeanIntegerPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanIntegerProperty} with the current settings. + * + * @return the new {@code JavaBeanIntegerProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code int} or {@code Integer} + */ + public JavaBeanIntegerProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!int.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not an int property"); + } + return new JavaBeanIntegerProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanIntegerPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongProperty.java new file mode 100644 index 00000000..699ffed7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongProperty.java @@ -0,0 +1,208 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.LongProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class JavaBeanLongProperty extends LongProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanLongProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public long get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).longValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final long value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue().longValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanLongProperty} object. + * @return a string representation of this {@code JavaBeanLongProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("LongProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongPropertyBuilder.java new file mode 100644 index 00000000..cf6b1679 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanLongPropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanLongPropertyBuilder} can be used to create + * {@link JavaBeanLongProperty JavaBeanLongProperties}. To create + * a {@code JavaBeanLongProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanLongPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanLongProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanLongPropertyBuilder { + + private JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanLongPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanLongPropertyBuilder}. + * + * @return the new {@code JavaBeanLongPropertyBuilder} + */ + public static JavaBeanLongPropertyBuilder create() { + return new JavaBeanLongPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanLongProperty} with the current settings. + * + * @return the new {@code JavaBeanLongProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code long} or {@code Long} + */ + public JavaBeanLongProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!long.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a long property"); + } + return new JavaBeanLongProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanLongPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectProperty.java new file mode 100644 index 00000000..1c46bb15 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectProperty.java @@ -0,0 +1,209 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class JavaBeanObjectProperty extends ObjectProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanObjectProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings({"removal","unchecked"}) + @Override + public T get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (T)MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final T value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } + + /** + * Returns a string representation of this {@code JavaBeanObjectProperty} object. + * @return a string representation of this {@code JavaBeanObjectProperty} object. + */ + @Override + public String toString() { + final Object bean = getBean(); + final String name = getName(); + final StringBuilder result = new StringBuilder("ObjectProperty ["); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if ((name != null) && (!name.equals(""))) { + result.append("name: ").append(name).append(", "); + } + if (isBound()) { + result.append("bound, "); + } + result.append("value: ").append(get()); + result.append("]"); + return result.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectPropertyBuilder.java new file mode 100644 index 00000000..201da389 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanObjectPropertyBuilder.java @@ -0,0 +1,146 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanObjectPropertyBuilder} can be used to create + * {@link JavaBeanObjectProperty JavaBeanObjectProperties}. To create + * a {@code JavaBeanObjectProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanObjectPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanObjectProperty + * + * @param the type of the wrapped {@code Object} + * @since JavaFX 2.1 + */ +public final class JavaBeanObjectPropertyBuilder { + + private JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanObjectPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanObjectPropertyBuilder}. + * + * @return the new {@code JavaBeanObjectPropertyBuilder} + */ + public static JavaBeanObjectPropertyBuilder create() { + return new JavaBeanObjectPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanObjectProperty} with the current settings. + * + * @return the new {@code JavaBeanObjectProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + */ + public JavaBeanObjectProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + return new JavaBeanObjectProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanObjectPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanProperty.java new file mode 100644 index 00000000..86b59810 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanProperty.java @@ -0,0 +1,13 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.Property; + +/** + * {@code JavaBeanProperty} is the super interface of all adapters between + * writable Java Bean properties and JavaFX properties. + * + * @param The type of the wrapped property + * @since JavaFX 2.1 + */ +public interface JavaBeanProperty extends ReadOnlyJavaBeanProperty, Property { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringProperty.java new file mode 100644 index 00000000..46f0b44a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringProperty.java @@ -0,0 +1,184 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.property.StringProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class JavaBeanStringProperty extends StringProperty implements JavaBeanProperty { + + private final PropertyDescriptor descriptor; + private final PropertyDescriptor.Listener listener; + + private ObservableValue observable = null; + private ExpressionHelper helper = null; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + JavaBeanStringProperty(PropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new Listener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public String get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (String) MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public void set(final String value) { + if (isBound()) { + throw new RuntimeException("A bound value cannot be set."); + } + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + MethodHelper.invoke(descriptor.getSetter(), getBean(), new Object[] {value}); + ExpressionHelper.fireValueChangedEvent(helper); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + return null; + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public void bind(ObservableValue observable) { + if (observable == null) { + throw new NullPointerException("Cannot bind to null"); + } + + if (!observable.equals(this.observable)) { + unbind(); + set(observable.getValue()); + this.observable = observable; + this.observable.addListener(listener); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unbind() { + if (observable != null) { + observable.removeListener(listener); + observable = null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isBound() { + return observable != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringPropertyBuilder.java new file mode 100644 index 00000000..4e7f8745 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/JavaBeanStringPropertyBuilder.java @@ -0,0 +1,149 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.JavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.PropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code JavaBeanStringPropertyBuilder} can be used to create + * {@link JavaBeanStringProperty JavaBeanStringProperties}. To create + * a {@code JavaBeanStringProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the names of the getter and setter follow the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter and setter + * ({@link #getter(String)} and {@link #setter(String)}) or + * the getter and setter {@code Methods} directly ({@link #getter(Method)} + * and {@link #setter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code JavaBeanStringPropertyBuilder} + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see JavaBeanStringProperty + * @since JavaFX 2.1 + */ +public final class JavaBeanStringPropertyBuilder { + + private JavaBeanPropertyBuilderHelper helper = new JavaBeanPropertyBuilderHelper(); + + private JavaBeanStringPropertyBuilder() {} + + /** + * Creates a new instance of {@code JavaBeanStringPropertyBuilder}. + * + * @return the new {@code JavaBeanStringPropertyBuilder} + */ + public static JavaBeanStringPropertyBuilder create() { + return new JavaBeanStringPropertyBuilder(); + } + + /** + * Generates a new {@link JavaBeanStringProperty} with the current settings. + * + * @return the new {@code JavaBeanStringProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter and the setter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code String} + */ + public JavaBeanStringProperty build() throws NoSuchMethodException { + final PropertyDescriptor descriptor = helper.getDescriptor(); + if (!String.class.equals(descriptor.getType())) { + throw new IllegalArgumentException("Not a String property"); + } + return new JavaBeanStringProperty(descriptor, helper.getBean()); + } + + /** + * Sets the name of the property. + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Sets the Java Bean instance the adapter should connect to. + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Sets the Java Bean class in which the getter and setter should be searched. + * This can be useful if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Sets an alternative name for the getter. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Sets an alternative name for the setter. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the alternative name of the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder setter(String setter) { + helper.setterName(setter); + return this; + } + + /** + * Sets the getter method directly. This can be omitted if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } + + /** + * Sets the setter method directly. This can be omitted if the + * name of the setter follows Java Bean naming conventions. + * + * @param setter the setter + * @return a reference to this builder to enable method chaining + */ + public JavaBeanStringPropertyBuilder setter(Method setter) { + helper.setter(setter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanProperty.java new file mode 100644 index 00000000..ee6b2913 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanProperty.java @@ -0,0 +1,82 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyBooleanPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanBooleanProperty extends ReadOnlyBooleanPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanBooleanProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public boolean get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (Boolean) MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanPropertyBuilder.java new file mode 100644 index 00000000..db34f486 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanBooleanPropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanBooleanPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanBooleanProperty ReadOnlyJavaBeanBooleanProperties}. To create + * a {@code ReadOnlyJavaBeanBooleanProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanBooleanPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanBooleanProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanBooleanPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanBooleanPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanBooleanPropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanBooleanPropertyBuilder} + */ + public static ReadOnlyJavaBeanBooleanPropertyBuilder create() { + return new ReadOnlyJavaBeanBooleanPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanBooleanProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanBooleanProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code boolean} or {@code Boolean} + */ + public ReadOnlyJavaBeanBooleanProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!boolean.class.equals(descriptor.getType()) && !Boolean.class.equals(descriptor.getType())) { + throw new IllegalArgumentException("Not a boolean property"); + } + return new ReadOnlyJavaBeanBooleanProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanBooleanPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanBooleanPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanBooleanPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanBooleanPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanBooleanPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoubleProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoubleProperty.java new file mode 100644 index 00000000..80c5d141 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoubleProperty.java @@ -0,0 +1,83 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyDoublePropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanDoubleProperty extends ReadOnlyDoublePropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanDoubleProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public double get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).doubleValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoublePropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoublePropertyBuilder.java new file mode 100644 index 00000000..213dca84 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanDoublePropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanDoublePropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanDoubleProperty ReadOnlyJavaBeanDoubleProperties}. To create + * a {@code ReadOnlyJavaBeanDoubleProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanDoublePropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanDoubleProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanDoublePropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanDoublePropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanDoublePropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanDoublePropertyBuilder} + */ + public static ReadOnlyJavaBeanDoublePropertyBuilder create() { + return new ReadOnlyJavaBeanDoublePropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanDoubleProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanDoubleProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code double} or {@code Double} + */ + public ReadOnlyJavaBeanDoubleProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!double.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a double property"); + } + return new ReadOnlyJavaBeanDoubleProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanDoublePropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanDoublePropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanDoublePropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanDoublePropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanDoublePropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatProperty.java new file mode 100644 index 00000000..d484a80a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatProperty.java @@ -0,0 +1,83 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyFloatPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanFloatProperty extends ReadOnlyFloatPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanFloatProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public float get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).floatValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatPropertyBuilder.java new file mode 100644 index 00000000..40ddf0b2 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanFloatPropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanFloatPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanFloatProperty ReadOnlyJavaBeanFloatProperties}. To create + * a {@code ReadOnlyJavaBeanFloatProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanFloatPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanFloatProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanFloatPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanFloatPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanFloatPropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanFloatPropertyBuilder} + */ + public static ReadOnlyJavaBeanFloatPropertyBuilder create() { + return new ReadOnlyJavaBeanFloatPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanFloatProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanFloatProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code float} or {@code Float} + */ + public ReadOnlyJavaBeanFloatProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!float.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a float property"); + } + return new ReadOnlyJavaBeanFloatProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanFloatPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanFloatPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanFloatPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanFloatPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanFloatPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerProperty.java new file mode 100644 index 00000000..deffe627 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerProperty.java @@ -0,0 +1,83 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyIntegerPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanIntegerProperty extends ReadOnlyIntegerPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanIntegerProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public int get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).intValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerPropertyBuilder.java new file mode 100644 index 00000000..e9e08f75 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanIntegerPropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanIntegerPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanIntegerProperty ReadOnlyJavaBeanIntegerProperties}. To create + * a {@code ReadOnlyJavaBeanIntegerProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanIntegerPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanIntegerProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanIntegerPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanIntegerPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanIntegerPropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanIntegerPropertyBuilder} + */ + public static ReadOnlyJavaBeanIntegerPropertyBuilder create() { + return new ReadOnlyJavaBeanIntegerPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanIntegerProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanIntegerProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code int} or {@code Integer} + */ + public ReadOnlyJavaBeanIntegerProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!int.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not an int property"); + } + return new ReadOnlyJavaBeanIntegerProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanIntegerPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanIntegerPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanIntegerPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanIntegerPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanIntegerPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongProperty.java new file mode 100644 index 00000000..3d8536a9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongProperty.java @@ -0,0 +1,83 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyLongPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanLongProperty extends ReadOnlyLongPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanLongProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public long get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return ((Number) MethodHelper.invoke( + descriptor.getGetter(), getBean(), (Object[])null)).longValue(); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongPropertyBuilder.java new file mode 100644 index 00000000..3a0a7f9e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanLongPropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanLongPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanLongProperty ReadOnlyJavaBeanLongProperties}. To create + * a {@code ReadOnlyJavaBeanLongProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanLongPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanLongProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanLongPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanLongPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanLongPropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanLongPropertyBuilder} + */ + public static ReadOnlyJavaBeanLongPropertyBuilder create() { + return new ReadOnlyJavaBeanLongPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanLongProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanLongProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code long} or {@code Long} + */ + public ReadOnlyJavaBeanLongProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!long.class.equals(descriptor.getType()) && !Number.class.isAssignableFrom(descriptor.getType())) { + throw new IllegalArgumentException("Not a long property"); + } + return new ReadOnlyJavaBeanLongProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanLongPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanLongPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanLongPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanLongPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanLongPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectProperty.java new file mode 100644 index 00000000..91087891 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectProperty.java @@ -0,0 +1,83 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyObjectPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanObjectProperty extends ReadOnlyObjectPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanObjectProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public T get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (T) MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectPropertyBuilder.java new file mode 100644 index 00000000..c635d099 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanObjectPropertyBuilder.java @@ -0,0 +1,122 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanObjectPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanObjectProperty ReadOnlyJavaBeanObjectProperties}. To create + * a {@code ReadOnlyJavaBeanObjectProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanObjectPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanObjectProperty + * + * @param the type of the wrapped {@code Object} + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanObjectPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanObjectPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanObjectPropertyBuilder} + * + * @param the type of the wrapped {@code Object} + * @return the new {@code ReadOnlyJavaBeanObjectPropertyBuilder} + */ + public static ReadOnlyJavaBeanObjectPropertyBuilder create() { + return new ReadOnlyJavaBeanObjectPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanObjectProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanObjectProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + */ + public ReadOnlyJavaBeanObjectProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + return new ReadOnlyJavaBeanObjectProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanObjectPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanObjectPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanObjectPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanObjectPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanObjectPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanProperty.java new file mode 100644 index 00000000..c64b8c52 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanProperty.java @@ -0,0 +1,27 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyProperty; + +/** + * {@code JavaBeanProperty} is the super interface of all adapters between + * readonly Java Bean properties and JavaFX properties. + * + * @param The type of the wrapped property + * @since JavaFX 2.1 + */ +public interface ReadOnlyJavaBeanProperty extends ReadOnlyProperty { + /** + * This method can be called to notify the adapter of a change of the Java + * Bean value, if the Java Bean property is not bound (i.e. it does not + * support PropertyChangeListeners). + */ + void fireValueChangedEvent(); + + /** + * Signals to the JavaFX property that it will not be used anymore and any + * references can be removed. A call of this method usually results in the + * property stopping to observe the Java Bean property by unregistering its + * listener(s). + */ + void dispose(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringProperty.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringProperty.java new file mode 100644 index 00000000..c3e4ad96 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringProperty.java @@ -0,0 +1,82 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringPropertyBase; +import com.tungsten.fclcore.fakefx.property.MethodHelper; +import com.tungsten.fclcore.fakefx.property.adapter.Disposer; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +import java.security.AccessController; +import java.security.AccessControlContext; +import java.security.PrivilegedAction; + +public final class ReadOnlyJavaBeanStringProperty extends ReadOnlyStringPropertyBase implements ReadOnlyJavaBeanProperty { + + private final ReadOnlyPropertyDescriptor descriptor; + private final ReadOnlyPropertyDescriptor.ReadOnlyListener listener; + + @SuppressWarnings("removal") + private final AccessControlContext acc = AccessController.getContext(); + + ReadOnlyJavaBeanStringProperty(ReadOnlyPropertyDescriptor descriptor, Object bean) { + this.descriptor = descriptor; + this.listener = descriptor.new ReadOnlyListener(bean, this); + descriptor.addListener(listener); + Disposer.addRecord(this, new DescriptorListenerCleaner(descriptor, listener)); + } + + /** + * {@inheritDoc} + * + * @throws UndeclaredThrowableException if calling the getter of the Java Bean + * property throws an {@code IllegalAccessException} or an + * {@code InvocationTargetException}. + */ + @SuppressWarnings("removal") + @Override + public String get() { + return AccessController.doPrivileged((PrivilegedAction) () -> { + try { + return (String) MethodHelper.invoke(descriptor.getGetter(), getBean(), (Object[])null); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } catch (InvocationTargetException e) { + throw new UndeclaredThrowableException(e); + } + }, acc); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getBean() { + return listener.getBean(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return descriptor.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void fireValueChangedEvent() { + super.fireValueChangedEvent(); + } + + /** + * {@inheritDoc} + */ + @Override + public void dispose() { + descriptor.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringPropertyBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringPropertyBuilder.java new file mode 100644 index 00000000..dbf633e9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/property/adapter/ReadOnlyJavaBeanStringPropertyBuilder.java @@ -0,0 +1,124 @@ +package com.tungsten.fclcore.fakefx.beans.property.adapter; + +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyJavaBeanPropertyBuilderHelper; +import com.tungsten.fclcore.fakefx.property.adapter.ReadOnlyPropertyDescriptor; + +import java.lang.reflect.Method; + +/** + * A {@code ReadOnlyJavaBeanStringPropertyBuilder} can be used to create + * {@link ReadOnlyJavaBeanStringProperty ReadOnlyJavaBeanStringProperties}. To create + * a {@code ReadOnlyJavaBeanStringProperty} one first has to call {@link #create()} + * to generate a builder, set the required properties, and then one can + * call {@link #build()} to generate the property. + *

+ * Not all properties of a builder have to specified, there are several + * combinations possible. As a minimum the {@link #name(String)} of + * the property and the {@link #bean(Object)} have to be specified. + * If the name of the getter follows the conventions, this is sufficient. + * Otherwise it is possible to specify an alternative name for the getter + * ({@link #getter(String)}) or + * the getter {@code Methods} directly ({@link #getter(Method)}). + *

+ * All methods to change properties return a reference to this builder, to enable + * method chaining. + *

+ * If you have to generate adapters for the same property of several instances + * of the same class, you can reuse a {@code ReadOnlyJavaBeanStringPropertyBuilder}. + * by switching the Java Bean instance (with {@link #bean(Object)} and + * calling {@link #build()}. + * + * @see ReadOnlyJavaBeanStringProperty + * @since JavaFX 2.1 + */ +public final class ReadOnlyJavaBeanStringPropertyBuilder { + + private final ReadOnlyJavaBeanPropertyBuilderHelper helper = new ReadOnlyJavaBeanPropertyBuilderHelper(); + + private ReadOnlyJavaBeanStringPropertyBuilder() {} + + /** + * Create a new instance of {@code ReadOnlyJavaBeanStringPropertyBuilder} + * + * @return the new {@code ReadOnlyJavaBeanStringPropertyBuilder} + */ + public static ReadOnlyJavaBeanStringPropertyBuilder create() { + return new ReadOnlyJavaBeanStringPropertyBuilder(); + } + + /** + * Generate a new {@link ReadOnlyJavaBeanStringProperty} with the current settings. + * + * @return the new {@code ReadOnlyJavaBeanStringProperty} + * @throws NoSuchMethodException if the settings were not sufficient to find + * the getter of the Java Bean property + * @throws IllegalArgumentException if the Java Bean property is not of type + * {@code String} + */ + public ReadOnlyJavaBeanStringProperty build() throws NoSuchMethodException { + final ReadOnlyPropertyDescriptor descriptor = helper.getDescriptor(); + if (!String.class.equals(descriptor.getType())) { + throw new IllegalArgumentException("Not a String property"); + } + return new ReadOnlyJavaBeanStringProperty(descriptor, helper.getBean()); + } + + /** + * Set the name of the property + * + * @param name the name of the property + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanStringPropertyBuilder name(String name) { + helper.name(name); + return this; + } + + /** + * Set the Java Bean instance the adapter should connect to + * + * @param bean the Java Bean instance + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanStringPropertyBuilder bean(Object bean) { + helper.bean(bean); + return this; + } + + /** + * Set the Java Bean class in which the getter should be searched. + * This can be useful, if the builder should generate adapters for several + * Java Beans of different types. + * + * @param beanClass the Java Bean class + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanStringPropertyBuilder beanClass(Class beanClass) { + helper.beanClass(beanClass); + return this; + } + + /** + * Set an alternative name for the getter. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the alternative name of the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanStringPropertyBuilder getter(String getter) { + helper.getterName(getter); + return this; + } + + /** + * Set the getter method directly. This can be omitted, if the + * name of the getter follows Java Bean naming conventions. + * + * @param getter the getter + * @return a reference to this builder to enable method chaining + */ + public ReadOnlyJavaBeanStringPropertyBuilder getter(Method getter) { + helper.getter(getter); + return this; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ChangeListener.java new file mode 100644 index 00000000..8ed87aea --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ChangeListener.java @@ -0,0 +1,37 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A {@code ChangeListener} is notified whenever the value of an + * {@link ObservableValue} changes. It can be registered and unregistered with + * {@link ObservableValue#addListener(ChangeListener)} respectively + * {@link ObservableValue#removeListener(ChangeListener)} + *

+ * For an in-depth explanation of change events and how they differ from + * invalidation events, see the documentation of {@code ObservableValue}. + *

+ * The same instance of {@code ChangeListener} can be registered to listen to + * multiple {@code ObservableValues}. + * + * @see ObservableValue + * + * + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface ChangeListener { + + /** + * Called when the value of an {@link ObservableValue} changes. + *

+ * In general, it is considered bad practice to modify the observed value in + * this method. + * + * @param observable + * The {@code ObservableValue} which value changed + * @param oldValue + * The old value + * @param newValue + * The new value + */ + void changed(ObservableValue observable, T oldValue, T newValue); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableBooleanValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableBooleanValue.java new file mode 100644 index 00000000..57c77970 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableBooleanValue.java @@ -0,0 +1,19 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable boolean value. + * + * @see ObservableValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableBooleanValue extends ObservableValue { + + /** + * Returns the current value of this {@code ObservableBooleanValue}. + * + * @return The current value + */ + boolean get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableDoubleValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableDoubleValue.java new file mode 100644 index 00000000..1a8f7639 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableDoubleValue.java @@ -0,0 +1,20 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable double value. + * + * @see ObservableValue + * @see ObservableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableDoubleValue extends ObservableNumberValue { + + /** + * Returns the current value of this {@code ObservableDoubleValue}. + * + * @return The current value + */ + double get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableFloatValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableFloatValue.java new file mode 100644 index 00000000..3d2d8c9f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableFloatValue.java @@ -0,0 +1,20 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable float value. + * + * @see ObservableValue + * @see ObservableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableFloatValue extends ObservableNumberValue { + + /** + * Returns the current value of this {@code ObservableFloatValue}. + * + * @return The current value + */ + float get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableIntegerValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableIntegerValue.java new file mode 100644 index 00000000..f91a6be5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableIntegerValue.java @@ -0,0 +1,20 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable integer value. + * + * @see ObservableValue + * @see ObservableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableIntegerValue extends ObservableNumberValue { + + /** + * Returns the current value of this {@code ObservableIntegerValue}. + * + * @return The current value + */ + int get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableListValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableListValue.java new file mode 100644 index 00000000..3162756d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableListValue.java @@ -0,0 +1,6 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +public interface ObservableListValue extends ObservableObjectValue>, ObservableList { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableLongValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableLongValue.java new file mode 100644 index 00000000..c2eed359 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableLongValue.java @@ -0,0 +1,20 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable long value. + * + * @see ObservableValue + * @see ObservableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableLongValue extends ObservableNumberValue { + + /** + * Returns the current value of this {@code ObservableLongValue}. + * + * @return The current value + */ + long get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableMapValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableMapValue.java new file mode 100644 index 00000000..2f5fa101 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableMapValue.java @@ -0,0 +1,17 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +/** + * An observable reference to an {@link ObservableMap}. + * + * @see ObservableMap + * @see ObservableObjectValue + * @see ObservableValue + * + * @param the type of the key elements of the {@code Map} + * @param the type of the value elements of the {@code Map} + * @since JavaFX 2.1 + */ +public interface ObservableMapValue extends ObservableObjectValue>, ObservableMap { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableNumberValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableNumberValue.java new file mode 100644 index 00000000..b94325cf --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableNumberValue.java @@ -0,0 +1,54 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A common interface of all sub-interfaces of {@link ObservableValue} that wrap + * a number. + * + * @see ObservableValue + * @see ObservableDoubleValue + * @see ObservableFloatValue + * @see ObservableIntegerValue + * @see ObservableLongValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableNumberValue extends ObservableValue { + + /** + * Returns the value of this {@code ObservableNumberValue} as an {@code int} + * . If the value is not an {@code int}, a standard cast is performed. + * + * @return The value of this {@code ObservableNumberValue} as an {@code int} + */ + int intValue(); + + /** + * Returns the value of this {@code ObservableNumberValue} as a {@code long} + * . If the value is not a {@code long}, a standard cast is performed. + * + * @return The value of this {@code ObservableNumberValue} as a {@code long} + */ + long longValue(); + + /** + * Returns the value of this {@code ObservableNumberValue} as a + * {@code float}. If the value is not a {@code float}, a standard cast is + * performed. + * + * @return The value of this {@code ObservableNumberValue} as a + * {@code float} + */ + float floatValue(); + + /** + * Returns the value of this {@code ObservableNumberValue} as a + * {@code double}. If the value is not a {@code double}, a standard cast is + * performed. + * + * @return The value of this {@code ObservableNumberValue} as a + * {@code double} + */ + double doubleValue(); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableObjectValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableObjectValue.java new file mode 100644 index 00000000..1a6ac75e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableObjectValue.java @@ -0,0 +1,19 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable typed {@code Object} value. + * + * @see ObservableValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableObjectValue extends ObservableValue { + + /** + * Returns the current value of this {@code ObservableObjectValue}. + * + * @return The current value + */ + T get(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableSetValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableSetValue.java new file mode 100644 index 00000000..a672982b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableSetValue.java @@ -0,0 +1,16 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +/** + * An observable reference to an {@link ObservableSet}. + * + * @see ObservableSet + * @see ObservableObjectValue + * @see ObservableValue + * + * @param the type of the {@code Set} elements + * @since JavaFX 2.1 + */ +public interface ObservableSetValue extends ObservableObjectValue>, ObservableSet { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableStringValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableStringValue.java new file mode 100644 index 00000000..f70c9c96 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableStringValue.java @@ -0,0 +1,13 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * An observable String value. + * + * @see ObservableObjectValue + * @see ObservableValue + * + * + * @since JavaFX 2.0 + */ +public interface ObservableStringValue extends ObservableObjectValue { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValue.java new file mode 100644 index 00000000..41b1b907 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValue.java @@ -0,0 +1,29 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.binding.FlatMappedBinding; +import com.tungsten.fclcore.fakefx.binding.MappedBinding; +import com.tungsten.fclcore.fakefx.binding.OrElseBinding; + +import java.util.function.Function; + +public interface ObservableValue extends Observable { + + void addListener(ChangeListener listener); + + void removeListener(ChangeListener listener); + + T getValue(); + + default ObservableValue map(Function mapper) { + return new MappedBinding<>(this, mapper); + } + + default ObservableValue orElse(T constant) { + return new OrElseBinding<>(this, constant); + } + + default ObservableValue flatMap(Function> mapper) { + return new FlatMappedBinding<>(this, mapper); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValueBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValueBase.java new file mode 100644 index 00000000..dd64172d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/ObservableValueBase.java @@ -0,0 +1,69 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelper; + +/** + * A convenience class for creating implementations of {@link ObservableValue}. + * It contains all of the infrastructure support for value invalidation- and + * change event notification. + * + * This implementation can handle adding and removing listeners while the + * observers are being notified, but it is not thread-safe. + * + * + * @since JavaFX 2.0 + */ +public abstract class ObservableValueBase implements ObservableValue { + + private ExpressionHelper helper; + + /** + * Creates a default {@code ObservableValueBase}. + */ + public ObservableValueBase() { + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(ChangeListener listener) { + helper = ExpressionHelper.addListener(helper, this, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(ChangeListener listener) { + helper = ExpressionHelper.removeListener(helper, listener); + } + + /** + * Notify the currently registered observers of a value change. + * + * This implementation will ignore all adds and removes of observers that + * are done while a notification is processed. The changes take effect in + * the following call to fireValueChangedEvent. + */ + protected void fireValueChangedEvent() { + ExpressionHelper.fireValueChangedEvent(helper); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WeakChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WeakChangeListener.java new file mode 100644 index 00000000..33e8057e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WeakChangeListener.java @@ -0,0 +1,70 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.WeakListener; + +import java.lang.ref.WeakReference; + +/** + * A {@code WeakChangeListener} can be used if an {@link ObservableValue} + * should only maintain a weak reference to the listener. This helps to avoid + * memory leaks which can occur if observers are not unregistered from observed + * objects after use. + *

+ * {@code WeakChangeListener} instances are created by passing in the original + * {@link ChangeListener}. The {@code WeakChangeListener} should then be + * registered to listen for changes of the observed object. + *

+ * Note: You have to keep a reference to the {@code ChangeListener} that + * was passed in for as long as it is in use, otherwise it will be garbage collected + * too soon. + * + * @see ChangeListener + * @see ObservableValue + * + * @param The type of the observed value + * + * @since JavaFX 2.0 + */ +public final class WeakChangeListener implements ChangeListener, WeakListener { + + private final WeakReference> ref; + + /** + * The constructor of {@code WeakChangeListener}. + * + * @param listener The original listener that should be notified + */ + public WeakChangeListener(@NamedArg("listener") ChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference>(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean wasGarbageCollected() { + return (ref.get() == null); + } + + /** + * {@inheritDoc} + */ + @Override + public void changed(ObservableValue observable, T oldValue, + T newValue) { + ChangeListener listener = ref.get(); + if (listener != null) { + listener.changed(observable, oldValue, newValue); + } else { + // The weakly reference listener has been garbage collected, + // so this WeakListener will now unhook itself from the + // source bean + observable.removeListener(this); + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableBooleanValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableBooleanValue.java new file mode 100644 index 00000000..c831c8fa --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableBooleanValue.java @@ -0,0 +1,45 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable boolean value. + * + * @see WritableValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableBooleanValue extends WritableValue { + + /** + * Get the wrapped value. + * Unlike {@link #getValue()}, + * this method returns primitive boolean. + * Needs to be identical to {@link #getValue()}. + * + * @return The current value + */ + boolean get(); + + /** + * Set the wrapped value. + * Unlike {@link #setValue(Boolean) }, + * this method uses primitive boolean. + * + * @param value + * The new value + */ + void set(boolean value); + + /** + * Set the wrapped value. + *

+ * Note: this method should accept null without throwing an exception, + * setting "false" instead. + * + * @param value + * The new value + */ + @Override + void setValue(Boolean value); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableDoubleValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableDoubleValue.java new file mode 100644 index 00000000..4f6ff7cb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableDoubleValue.java @@ -0,0 +1,45 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable double value. + * + * @see WritableValue + * @see WritableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableDoubleValue extends WritableNumberValue { + + /** + * Get the wrapped value. + * Unlike {@link #getValue()}, + * this method returns primitive double. + * Needs to be identical to {@link #getValue()}. + * + * @return The current value + */ + double get(); + + /** + * Set the wrapped value. + * Unlike {@link #setValue(Number) }, + * this method uses primitive double. + * + * @param value + * The new value + */ + void set(double value); + + /** + * Set the wrapped value. + *

+ * Note: this method should accept null without throwing an exception, + * setting "0.0" instead. + * + * @param value + * The new value + */ + @Override + void setValue(Number value); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableFloatValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableFloatValue.java new file mode 100644 index 00000000..8b77c0ed --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableFloatValue.java @@ -0,0 +1,45 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable float value. + * + * @see WritableValue + * @see WritableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableFloatValue extends WritableNumberValue { + + /** + * Get the wrapped value. + * Unlike {@link #getValue()}, + * this method returns primitive float. + * Needs to be identical to {@link #getValue()}. + * + * @return The current value + */ + float get(); + + /** + * Set the wrapped value. + * Unlike {@link #setValue(Number) }, + * this method uses primitive float. + * + * @param value + * The new value + */ + void set(float value); + + /** + * Set the wrapped value. + *

+ * Note: this method should accept null without throwing an exception, + * setting "0.0" instead. + * + * @param value + * The new value + */ + @Override + void setValue(Number value); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableIntegerValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableIntegerValue.java new file mode 100644 index 00000000..7fe1f693 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableIntegerValue.java @@ -0,0 +1,45 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable int value. + * + * @see WritableValue + * @see WritableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableIntegerValue extends WritableNumberValue { + + /** + * Get the wrapped value. + * Unlike {@link #getValue()}, + * this method returns primitive int. + * Needs to be identical to {@link #getValue()}. + * + * @return The current value + */ + int get(); + + /** + * Set the wrapped value. + * Unlike {@link #setValue(Number) }, + * this method uses primitive int. + * + * @param value + * The new value + */ + void set(int value); + + /** + * Set the wrapped value. + *

+ * Note: this method should accept null without throwing an exception, + * setting "0" instead. + * + * @param value + * The new value + */ + @Override + void setValue(Number value); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableListValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableListValue.java new file mode 100644 index 00000000..eaae6114 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableListValue.java @@ -0,0 +1,6 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +public interface WritableListValue extends WritableObjectValue>, ObservableList { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableLongValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableLongValue.java new file mode 100644 index 00000000..7f7b7127 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableLongValue.java @@ -0,0 +1,45 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable long value. + * + * @see WritableValue + * @see WritableNumberValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableLongValue extends WritableNumberValue { + + /** + * Get the wrapped value. + * Unlike {@link #getValue()}, + * this method returns primitive long. + * Needs to be identical to {@link #getValue()}. + * + * @return The current value + */ + long get(); + + /** + * Set the wrapped value. + * Unlike {@link #setValue(Number) }, + * this method uses primitive long. + * + * @param value + * The new value + */ + void set(long value); + + /** + * Set the wrapped value. + *

+ * Note: this method should accept null without throwing an exception, + * setting "0" instead. + * + * @param value + * The new value + */ + @Override + void setValue(Number value); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableMapValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableMapValue.java new file mode 100644 index 00000000..64e4c92f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableMapValue.java @@ -0,0 +1,6 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +public interface WritableMapValue extends WritableObjectValue>, ObservableMap { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableNumberValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableNumberValue.java new file mode 100644 index 00000000..6987d21b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableNumberValue.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A tagging interface that marks all sub-interfaces of {@link WritableValue} + * that wrap a number. + * + * @see WritableValue + * @see WritableDoubleValue + * @see WritableFloatValue + * @see WritableIntegerValue + * @see WritableLongValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableNumberValue extends WritableValue { + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableObjectValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableObjectValue.java new file mode 100644 index 00000000..668bd4b9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableObjectValue.java @@ -0,0 +1,35 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable typed value. + * + * @param the type of the wrapped value + * + * @see WritableValue + * + * + * @since JavaFX 2.0 + */ +public interface WritableObjectValue extends WritableValue { + + /** + * Get the wrapped value. This must be identical to + * the value returned from {@link #getValue()}. + *

+ * This method exists only to align WritableObjectValue API with + * {@link WritableBooleanValue} and subclasses of {@link WritableNumberValue} + * @return The current value + */ + T get(); + + /** + * Set the wrapped value. + * Should be equivalent to {@link #setValue(Object) } + * @see #get() + * + * @param value + * The new value + */ + void set(T value); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableSetValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableSetValue.java new file mode 100644 index 00000000..76391cc5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableSetValue.java @@ -0,0 +1,6 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +public interface WritableSetValue extends WritableObjectValue>, ObservableSet { +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableStringValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableStringValue.java new file mode 100644 index 00000000..bac642ba --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableStringValue.java @@ -0,0 +1,12 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A writable String. + * + * @see WritableValue + * @see WritableObjectValue + * @since JavaFX 2.0 + */ +public interface WritableStringValue extends WritableObjectValue { + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableValue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableValue.java new file mode 100644 index 00000000..f9b178d6 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/beans/value/WritableValue.java @@ -0,0 +1,40 @@ +package com.tungsten.fclcore.fakefx.beans.value; + +/** + * A {@code WritableValue} is an entity that wraps a value that can be read and + * set. In general this interface should not be implemented directly but one of + * its sub-interfaces ({@code WritableBooleanValue} etc.). + * + * @see WritableBooleanValue + * @see WritableDoubleValue + * @see WritableFloatValue + * @see WritableIntegerValue + * @see WritableLongValue + * @see WritableNumberValue + * @see WritableObjectValue + * @see WritableStringValue + * + * @param + * The type of the wrapped value + * + * + * @since JavaFX 2.0 + */ +public interface WritableValue { + + /** + * Get the wrapped value. + * + * @return The current value + */ + T getValue(); + + /** + * Set the wrapped value. + * + * @param value + * The new value + */ + void setValue(T value); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalBinding.java new file mode 100644 index 00000000..83f3bf03 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalBinding.java @@ -0,0 +1,873 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty; +import com.tungsten.fclcore.fakefx.beans.property.FloatProperty; +import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty; +import com.tungsten.fclcore.fakefx.beans.property.LongProperty; +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.lang.ref.WeakReference; +import java.text.Format; +import java.text.ParseException; +import java.util.Objects; + +/** + * @implNote Bidirectional bindings are implemented with InvalidationListeners, which are fired once + * when the changed property was valid, but elided for any future changes until the property + * is again validated by calling its getValue()/get() method. + * Since bidirectional bindings require that we observe all property changes, independent + * of whether the property was validated by user code, we manually validate both properties + * by calling their getter method in all relevant places. + */ +public abstract class BidirectionalBinding implements InvalidationListener, WeakListener { + + private static void checkParameters(Object property1, Object property2) { + Objects.requireNonNull(property1, "Both properties must be specified."); + Objects.requireNonNull(property2, "Both properties must be specified."); + if (property1 == property2) { + throw new IllegalArgumentException("Cannot bind property to itself"); + } + } + + public static BidirectionalBinding bind(Property property1, Property property2) { + checkParameters(property1, property2); + final BidirectionalBinding binding = + ((property1 instanceof DoubleProperty) && (property2 instanceof DoubleProperty)) ? + new BidirectionalDoubleBinding((DoubleProperty) property1, (DoubleProperty) property2) + : ((property1 instanceof FloatProperty) && (property2 instanceof FloatProperty)) ? + new BidirectionalFloatBinding((FloatProperty) property1, (FloatProperty) property2) + : ((property1 instanceof IntegerProperty) && (property2 instanceof IntegerProperty)) ? + new BidirectionalIntegerBinding((IntegerProperty) property1, (IntegerProperty) property2) + : ((property1 instanceof LongProperty) && (property2 instanceof LongProperty)) ? + new BidirectionalLongBinding((LongProperty) property1, (LongProperty) property2) + : ((property1 instanceof BooleanProperty) && (property2 instanceof BooleanProperty)) ? + new BidirectionalBooleanBinding((BooleanProperty) property1, (BooleanProperty) property2) + : new TypedGenericBidirectionalBinding(property1, property2); + property1.setValue(property2.getValue()); + property1.getValue(); + property1.addListener(binding); + property2.addListener(binding); + return binding; + } + + public static Object bind(Property stringProperty, Property otherProperty, Format format) { + checkParameters(stringProperty, otherProperty); + Objects.requireNonNull(format, "Format cannot be null"); + final StringFormatBidirectionalBinding binding = new StringFormatBidirectionalBinding(stringProperty, otherProperty, format); + stringProperty.setValue(format.format(otherProperty.getValue())); + stringProperty.getValue(); + stringProperty.addListener(binding); + otherProperty.addListener(binding); + return binding; + } + + public static Object bind(Property stringProperty, Property otherProperty, StringConverter converter) { + checkParameters(stringProperty, otherProperty); + Objects.requireNonNull(converter, "Converter cannot be null"); + final StringConverterBidirectionalBinding binding = new StringConverterBidirectionalBinding<>(stringProperty, otherProperty, converter); + stringProperty.setValue(converter.toString(otherProperty.getValue())); + stringProperty.getValue(); + stringProperty.addListener(binding); + otherProperty.addListener(binding); + return binding; + } + + public static void unbind(Property property1, Property property2) { + checkParameters(property1, property2); + final BidirectionalBinding binding = new UntypedGenericBidirectionalBinding(property1, property2); + property1.removeListener(binding); + property2.removeListener(binding); + } + + public static void unbind(Object property1, Object property2) { + checkParameters(property1, property2); + final BidirectionalBinding binding = new UntypedGenericBidirectionalBinding(property1, property2); + if (property1 instanceof ObservableValue) { + ((ObservableValue)property1).removeListener(binding); + } + if (property2 instanceof ObservableValue) { + ((ObservableValue)property2).removeListener(binding); + } + } + + public static BidirectionalBinding bindNumber(Property property1, IntegerProperty property2) { + return bindNumber(property1, (Property)property2); + } + + public static BidirectionalBinding bindNumber(Property property1, LongProperty property2) { + return bindNumber(property1, (Property)property2); + } + + public static BidirectionalBinding bindNumber(Property property1, FloatProperty property2) { + return bindNumber(property1, (Property)property2); + } + + public static BidirectionalBinding bindNumber(Property property1, DoubleProperty property2) { + return bindNumber(property1, (Property)property2); + } + + public static BidirectionalBinding bindNumber(IntegerProperty property1, Property property2) { + return bindNumberObject(property1, property2); + } + + public static BidirectionalBinding bindNumber(LongProperty property1, Property property2) { + return bindNumberObject(property1, property2); + } + + public static BidirectionalBinding bindNumber(FloatProperty property1, Property property2) { + return bindNumberObject(property1, property2); + } + + public static BidirectionalBinding bindNumber(DoubleProperty property1, Property property2) { + return bindNumberObject(property1, property2); + } + + private static BidirectionalBinding bindNumberObject(Property property1, Property property2) { + checkParameters(property1, property2); + + final BidirectionalBinding binding = new TypedNumberBidirectionalBinding<>(property2, property1); + + property1.setValue(property2.getValue()); + property1.getValue(); + property1.addListener(binding); + property2.addListener(binding); + return binding; + } + + private static BidirectionalBinding bindNumber(Property property1, Property property2) { + checkParameters(property1, property2); + + final BidirectionalBinding binding = new TypedNumberBidirectionalBinding<>(property1, property2); + + property1.setValue((T)property2.getValue()); + property1.getValue(); + property1.addListener(binding); + property2.addListener(binding); + return binding; + } + + private final int cachedHashCode; + + private BidirectionalBinding(Object property1, Object property2) { + cachedHashCode = property1.hashCode() * property2.hashCode(); + } + + protected abstract Object getProperty1(); + + protected abstract Object getProperty2(); + + @Override + public int hashCode() { + return cachedHashCode; + } + + @Override + public boolean wasGarbageCollected() { + return (getProperty1() == null) || (getProperty2() == null); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Object propertyA1 = getProperty1(); + final Object propertyA2 = getProperty2(); + if ((propertyA1 == null) || (propertyA2 == null)) { + return false; + } + + if (obj instanceof BidirectionalBinding) { + final BidirectionalBinding otherBinding = (BidirectionalBinding) obj; + final Object propertyB1 = otherBinding.getProperty1(); + final Object propertyB2 = otherBinding.getProperty2(); + if ((propertyB1 == null) || (propertyB2 == null)) { + return false; + } + + if (propertyA1 == propertyB1 && propertyA2 == propertyB2) { + return true; + } + if (propertyA1 == propertyB2 && propertyA2 == propertyB1) { + return true; + } + } + return false; + } + + private static class BidirectionalBooleanBinding extends BidirectionalBinding { + private final WeakReference propertyRef1; + private final WeakReference propertyRef2; + private boolean oldValue; + private boolean updating; + + private BidirectionalBooleanBinding(BooleanProperty property1, BooleanProperty property2) { + super(property1, property2); + oldValue = property1.get(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final BooleanProperty property1 = propertyRef1.get(); + final BooleanProperty property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + boolean newValue = property1.get(); + property2.set(newValue); + property2.get(); + oldValue = newValue; + } else { + boolean newValue = property2.get(); + property1.set(newValue); + property1.get(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.set(oldValue); + property1.get(); + } else { + property2.set(oldValue); + property2.get(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class BidirectionalDoubleBinding extends BidirectionalBinding { + private final WeakReference propertyRef1; + private final WeakReference propertyRef2; + private double oldValue; + private boolean updating = false; + + private BidirectionalDoubleBinding(DoubleProperty property1, DoubleProperty property2) { + super(property1, property2); + oldValue = property1.get(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final DoubleProperty property1 = propertyRef1.get(); + final DoubleProperty property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + double newValue = property1.get(); + property2.set(newValue); + property2.get(); + oldValue = newValue; + } else { + double newValue = property2.get(); + property1.set(newValue); + property1.get(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.set(oldValue); + property1.get(); + } else { + property2.set(oldValue); + property2.get(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class BidirectionalFloatBinding extends BidirectionalBinding { + private final WeakReference propertyRef1; + private final WeakReference propertyRef2; + private float oldValue; + private boolean updating = false; + + private BidirectionalFloatBinding(FloatProperty property1, FloatProperty property2) { + super(property1, property2); + oldValue = property1.get(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final FloatProperty property1 = propertyRef1.get(); + final FloatProperty property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + float newValue = property1.get(); + property2.set(newValue); + property2.get(); + oldValue = newValue; + } else { + float newValue = property2.get(); + property1.set(newValue); + property1.get(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.set(oldValue); + property1.get(); + } else { + property2.set(oldValue); + property2.get(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class BidirectionalIntegerBinding extends BidirectionalBinding { + private final WeakReference propertyRef1; + private final WeakReference propertyRef2; + private int oldValue; + private boolean updating = false; + + private BidirectionalIntegerBinding(IntegerProperty property1, IntegerProperty property2) { + super(property1, property2); + oldValue = property1.get(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final IntegerProperty property1 = propertyRef1.get(); + final IntegerProperty property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + int newValue = property1.get(); + property2.set(newValue); + property2.get(); + oldValue = newValue; + } else { + int newValue = property2.get(); + property1.set(newValue); + property1.get(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.set(oldValue); + property1.get(); + } else { + property2.set(oldValue); + property2.get(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class BidirectionalLongBinding extends BidirectionalBinding { + private final WeakReference propertyRef1; + private final WeakReference propertyRef2; + private long oldValue; + private boolean updating = false; + + private BidirectionalLongBinding(LongProperty property1, LongProperty property2) { + super(property1, property2); + oldValue = property1.get(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final LongProperty property1 = propertyRef1.get(); + final LongProperty property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + long newValue = property1.get(); + property2.set(newValue); + property2.get(); + oldValue = newValue; + } else { + long newValue = property2.get(); + property1.set(newValue); + property1.get(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.set(oldValue); + property1.get(); + } else { + property2.set(oldValue); + property2.get(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class TypedGenericBidirectionalBinding extends BidirectionalBinding { + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + private T oldValue; + private boolean updating = false; + + private TypedGenericBidirectionalBinding(Property property1, Property property2) { + super(property1, property2); + oldValue = property1.getValue(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final Property property1 = propertyRef1.get(); + final Property property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + T newValue = property1.getValue(); + property2.setValue(newValue); + property2.getValue(); + oldValue = newValue; + } else { + T newValue = property2.getValue(); + property1.setValue(newValue); + property1.getValue(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.setValue(oldValue); + property1.getValue(); + } else { + property2.setValue(oldValue); + property2.getValue(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class TypedNumberBidirectionalBinding extends BidirectionalBinding { + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + private T oldValue; + private boolean updating = false; + + private TypedNumberBidirectionalBinding(Property property1, Property property2) { + super(property1, property2); + oldValue = property1.getValue(); + propertyRef1 = new WeakReference<>(property1); + propertyRef2 = new WeakReference<>(property2); + } + + @Override + protected Property getProperty1() { + return propertyRef1.get(); + } + + @Override + protected Property getProperty2() { + return propertyRef2.get(); + } + + @Override + public void invalidated(Observable sourceProperty) { + if (!updating) { + final Property property1 = propertyRef1.get(); + final Property property2 = propertyRef2.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == sourceProperty) { + T newValue = property1.getValue(); + property2.setValue(newValue); + property2.getValue(); + oldValue = newValue; + } else { + T newValue = (T)property2.getValue(); + property1.setValue(newValue); + property1.getValue(); + oldValue = newValue; + } + } catch (RuntimeException e) { + try { + if (property1 == sourceProperty) { + property1.setValue((T)oldValue); + property1.getValue(); + } else { + property2.setValue(oldValue); + property2.getValue(); + } + } catch (Exception e2) { + e2.addSuppressed(e); + unbind(property1, property2); + throw new RuntimeException( + "Bidirectional binding failed together with an attempt" + + " to restore the source property to the previous value." + + " Removing the bidirectional binding from properties " + + property1 + " and " + property2, e2); + } + throw new RuntimeException( + "Bidirectional binding failed, setting to the previous value", e); + } finally { + updating = false; + } + } + } + } + } + + private static class UntypedGenericBidirectionalBinding extends BidirectionalBinding { + private final Object property1; + private final Object property2; + + public UntypedGenericBidirectionalBinding(Object property1, Object property2) { + super(property1, property2); + this.property1 = property1; + this.property2 = property2; + } + + @Override + protected Object getProperty1() { + return property1; + } + + @Override + protected Object getProperty2() { + return property2; + } + + @Override + public void invalidated(Observable sourceProperty) { + throw new RuntimeException("Should not reach here"); + } + } + + public abstract static class StringConversionBidirectionalBinding extends BidirectionalBinding { + private final WeakReference> stringPropertyRef; + private final WeakReference> otherPropertyRef; + private boolean updating; + + public StringConversionBidirectionalBinding(Property stringProperty, Property otherProperty) { + super(stringProperty, otherProperty); + stringPropertyRef = new WeakReference<>(stringProperty); + otherPropertyRef = new WeakReference<>(otherProperty); + } + + protected abstract String toString(T value); + + protected abstract T fromString(String value) throws ParseException; + + @Override + protected Object getProperty1() { + return stringPropertyRef.get(); + } + + @Override + protected Object getProperty2() { + return otherPropertyRef.get(); + } + + @Override + public void invalidated(Observable observable) { + if (!updating) { + final Property property1 = stringPropertyRef.get(); + final Property property2 = otherPropertyRef.get(); + if ((property1 == null) || (property2 == null)) { + if (property1 != null) { + property1.removeListener(this); + } + if (property2 != null) { + property2.removeListener(this); + } + } else { + try { + updating = true; + if (property1 == observable) { + try { + property2.setValue(fromString(property1.getValue())); + property2.getValue(); + } catch (Exception e) { + property2.setValue(null); + property2.getValue(); + } + } else { + try { + property1.setValue(toString(property2.getValue())); + property1.getValue(); + } catch (Exception e) { + property1.setValue(""); + property1.getValue(); + } + } + } finally { + updating = false; + } + } + } + } + } + + private static class StringFormatBidirectionalBinding extends StringConversionBidirectionalBinding { + private final Format format; + + @SuppressWarnings("unchecked") + public StringFormatBidirectionalBinding(Property stringProperty, Property otherProperty, Format format) { + super(stringProperty, otherProperty); + this.format = format; + } + + @Override + protected String toString(Object value) { + return format.format(value); + } + + @Override + protected Object fromString(String value) throws ParseException { + return format.parseObject(value); + } + } + + private static class StringConverterBidirectionalBinding extends StringConversionBidirectionalBinding { + private final StringConverter converter; + + public StringConverterBidirectionalBinding(Property stringProperty, Property otherProperty, StringConverter converter) { + super(stringProperty, otherProperty); + this.converter = converter; + } + + @Override + protected String toString(T value) { + return converter.toString(value); + } + + @Override + protected T fromString(String value) throws ParseException { + return converter.fromString(value); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalContentBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalContentBinding.java new file mode 100644 index 00000000..acdf6b2b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BidirectionalContentBinding.java @@ -0,0 +1,347 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +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.fakefx.collections.SetChangeListener; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.Set; + +/** + */ +public class BidirectionalContentBinding { + + private static void checkParameters(Object property1, Object property2) { + if ((property1 == null) || (property2 == null)) { + throw new NullPointerException("Both parameters must be specified."); + } + if (property1 == property2) { + throw new IllegalArgumentException("Cannot bind object to itself"); + } + } + + public static Object bind(ObservableList list1, ObservableList list2) { + checkParameters(list1, list2); + final ListContentBinding binding = new ListContentBinding(list1, list2); + list1.setAll(list2); + list1.addListener(binding); + list2.addListener(binding); + return binding; + } + + public static Object bind(ObservableSet set1, ObservableSet set2) { + checkParameters(set1, set2); + final SetContentBinding binding = new SetContentBinding(set1, set2); + set1.clear(); + set1.addAll(set2); + set1.addListener(binding); + set2.addListener(binding); + return binding; + } + + public static Object bind(ObservableMap map1, ObservableMap map2) { + checkParameters(map1, map2); + final MapContentBinding binding = new MapContentBinding(map1, map2); + map1.clear(); + map1.putAll(map2); + map1.addListener(binding); + map2.addListener(binding); + return binding; + } + + public static void unbind(Object obj1, Object obj2) { + checkParameters(obj1, obj2); + if ((obj1 instanceof ObservableList) && (obj2 instanceof ObservableList)) { + final ObservableList list1 = (ObservableList)obj1; + final ObservableList list2 = (ObservableList)obj2; + final ListContentBinding binding = new ListContentBinding(list1, list2); + list1.removeListener(binding); + list2.removeListener(binding); + } else if ((obj1 instanceof ObservableSet) && (obj2 instanceof ObservableSet)) { + final ObservableSet set1 = (ObservableSet)obj1; + final ObservableSet set2 = (ObservableSet)obj2; + final SetContentBinding binding = new SetContentBinding(set1, set2); + set1.removeListener(binding); + set2.removeListener(binding); + } else if ((obj1 instanceof ObservableMap) && (obj2 instanceof ObservableMap)) { + final ObservableMap map1 = (ObservableMap)obj1; + final ObservableMap map2 = (ObservableMap)obj2; + final MapContentBinding binding = new MapContentBinding(map1, map2); + map1.removeListener(binding); + map2.removeListener(binding); + } + } + + private static class ListContentBinding implements ListChangeListener, WeakListener { + + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + + private boolean updating = false; + + + public ListContentBinding(ObservableList list1, ObservableList list2) { + propertyRef1 = new WeakReference>(list1); + propertyRef2 = new WeakReference>(list2); + } + + @Override + public void onChanged(Change change) { + if (!updating) { + final ObservableList list1 = propertyRef1.get(); + final ObservableList list2 = propertyRef2.get(); + if ((list1 == null) || (list2 == null)) { + if (list1 != null) { + list1.removeListener(this); + } + if (list2 != null) { + list2.removeListener(this); + } + } else { + try { + updating = true; + final ObservableList dest = (list1 == change.getList())? list2 : list1; + while (change.next()) { + if (change.wasPermutated()) { + dest.remove(change.getFrom(), change.getTo()); + dest.addAll(change.getFrom(), change.getList().subList(change.getFrom(), change.getTo())); + } else { + if (change.wasRemoved()) { + dest.remove(change.getFrom(), change.getFrom() + change.getRemovedSize()); + } + if (change.wasAdded()) { + dest.addAll(change.getFrom(), change.getAddedSubList()); + } + } + } + } finally { + updating = false; + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return (propertyRef1.get() == null) || (propertyRef2.get() == null); + } + + @Override + public int hashCode() { + final ObservableList list1 = propertyRef1.get(); + final ObservableList list2 = propertyRef2.get(); + final int hc1 = (list1 == null)? 0 : list1.hashCode(); + final int hc2 = (list2 == null)? 0 : list2.hashCode(); + return hc1 * hc2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Object propertyA1 = propertyRef1.get(); + final Object propertyA2 = propertyRef2.get(); + if ((propertyA1 == null) || (propertyA2 == null)) { + return false; + } + + if (obj instanceof ListContentBinding) { + final ListContentBinding otherBinding = (ListContentBinding) obj; + final Object propertyB1 = otherBinding.propertyRef1.get(); + final Object propertyB2 = otherBinding.propertyRef2.get(); + if ((propertyB1 == null) || (propertyB2 == null)) { + return false; + } + + if ((propertyA1 == propertyB1) && (propertyA2 == propertyB2)) { + return true; + } + if ((propertyA1 == propertyB2) && (propertyA2 == propertyB1)) { + return true; + } + } + return false; + } + } + + private static class SetContentBinding implements SetChangeListener, WeakListener { + + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + + private boolean updating = false; + + + public SetContentBinding(ObservableSet list1, ObservableSet list2) { + propertyRef1 = new WeakReference>(list1); + propertyRef2 = new WeakReference>(list2); + } + + @Override + public void onChanged(Change change) { + if (!updating) { + final ObservableSet set1 = propertyRef1.get(); + final ObservableSet set2 = propertyRef2.get(); + if ((set1 == null) || (set2 == null)) { + if (set1 != null) { + set1.removeListener(this); + } + if (set2 != null) { + set2.removeListener(this); + } + } else { + try { + updating = true; + final Set dest = (set1 == change.getSet())? set2 : set1; + if (change.wasRemoved()) { + dest.remove(change.getElementRemoved()); + } else { + dest.add(change.getElementAdded()); + } + } finally { + updating = false; + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return (propertyRef1.get() == null) || (propertyRef2.get() == null); + } + + @Override + public int hashCode() { + final ObservableSet set1 = propertyRef1.get(); + final ObservableSet set2 = propertyRef2.get(); + final int hc1 = (set1 == null)? 0 : set1.hashCode(); + final int hc2 = (set2 == null)? 0 : set2.hashCode(); + return hc1 * hc2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Object propertyA1 = propertyRef1.get(); + final Object propertyA2 = propertyRef2.get(); + if ((propertyA1 == null) || (propertyA2 == null)) { + return false; + } + + if (obj instanceof SetContentBinding) { + final SetContentBinding otherBinding = (SetContentBinding) obj; + final Object propertyB1 = otherBinding.propertyRef1.get(); + final Object propertyB2 = otherBinding.propertyRef2.get(); + if ((propertyB1 == null) || (propertyB2 == null)) { + return false; + } + + if ((propertyA1 == propertyB1) && (propertyA2 == propertyB2)) { + return true; + } + if ((propertyA1 == propertyB2) && (propertyA2 == propertyB1)) { + return true; + } + } + return false; + } + } + + private static class MapContentBinding implements MapChangeListener, WeakListener { + + private final WeakReference> propertyRef1; + private final WeakReference> propertyRef2; + + private boolean updating = false; + + + public MapContentBinding(ObservableMap list1, ObservableMap list2) { + propertyRef1 = new WeakReference>(list1); + propertyRef2 = new WeakReference>(list2); + } + + @Override + public void onChanged(Change change) { + if (!updating) { + final ObservableMap map1 = propertyRef1.get(); + final ObservableMap map2 = propertyRef2.get(); + if ((map1 == null) || (map2 == null)) { + if (map1 != null) { + map1.removeListener(this); + } + if (map2 != null) { + map2.removeListener(this); + } + } else { + try { + updating = true; + final Map dest = (map1 == change.getMap())? map2 : map1; + if (change.wasRemoved()) { + dest.remove(change.getKey()); + } + if (change.wasAdded()) { + dest.put(change.getKey(), change.getValueAdded()); + } + } finally { + updating = false; + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return (propertyRef1.get() == null) || (propertyRef2.get() == null); + } + + @Override + public int hashCode() { + final ObservableMap map1 = propertyRef1.get(); + final ObservableMap map2 = propertyRef2.get(); + final int hc1 = (map1 == null)? 0 : map1.hashCode(); + final int hc2 = (map2 == null)? 0 : map2.hashCode(); + return hc1 * hc2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Object propertyA1 = propertyRef1.get(); + final Object propertyA2 = propertyRef2.get(); + if ((propertyA1 == null) || (propertyA2 == null)) { + return false; + } + + if (obj instanceof MapContentBinding) { + final MapContentBinding otherBinding = (MapContentBinding) obj; + final Object propertyB1 = otherBinding.propertyRef1.get(); + final Object propertyB2 = otherBinding.propertyRef2.get(); + if ((propertyB1 == null) || (propertyB2 == null)) { + return false; + } + + if ((propertyA1 == propertyB1) && (propertyA2 == propertyB2)) { + return true; + } + if ((propertyA1 == propertyB2) && (propertyA2 == propertyB1)) { + return true; + } + } + return false; + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BindingHelperObserver.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BindingHelperObserver.java similarity index 73% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BindingHelperObserver.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BindingHelperObserver.java index 9bfa7c2c..169ddfb2 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/BindingHelperObserver.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/BindingHelperObserver.java @@ -1,4 +1,9 @@ -package com.tungsten.fclcore.fakefx; +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.binding.Binding; import java.lang.ref.WeakReference; @@ -27,4 +32,4 @@ public class BindingHelperObserver implements InvalidationListener, WeakListener public boolean wasGarbageCollected() { return ref.get() == null; } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ContentBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ContentBinding.java new file mode 100644 index 00000000..4c73d1de --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ContentBinding.java @@ -0,0 +1,241 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +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.fakefx.collections.SetChangeListener; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + */ +public class ContentBinding { + + private static void checkParameters(Object property1, Object property2) { + if ((property1 == null) || (property2 == null)) { + throw new NullPointerException("Both parameters must be specified."); + } + if (property1 == property2) { + throw new IllegalArgumentException("Cannot bind object to itself"); + } + } + + public static Object bind(List list1, ObservableList list2) { + checkParameters(list1, list2); + final ListContentBinding contentBinding = new ListContentBinding(list1); + if (list1 instanceof ObservableList) { + ((ObservableList) list1).setAll(list2); + } else { + list1.clear(); + list1.addAll(list2); + } + list2.removeListener(contentBinding); + list2.addListener(contentBinding); + return contentBinding; + } + + public static Object bind(Set set1, ObservableSet set2) { + checkParameters(set1, set2); + final SetContentBinding contentBinding = new SetContentBinding(set1); + set1.clear(); + set1.addAll(set2); + set2.removeListener(contentBinding); + set2.addListener(contentBinding); + return contentBinding; + } + + public static Object bind(Map map1, ObservableMap map2) { + checkParameters(map1, map2); + final MapContentBinding contentBinding = new MapContentBinding(map1); + map1.clear(); + map1.putAll(map2); + map2.removeListener(contentBinding); + map2.addListener(contentBinding); + return contentBinding; + } + + public static void unbind(Object obj1, Object obj2) { + checkParameters(obj1, obj2); + if ((obj1 instanceof List) && (obj2 instanceof ObservableList)) { + ((ObservableList)obj2).removeListener(new ListContentBinding((List)obj1)); + } else if ((obj1 instanceof Set) && (obj2 instanceof ObservableSet)) { + ((ObservableSet)obj2).removeListener(new SetContentBinding((Set)obj1)); + } else if ((obj1 instanceof Map) && (obj2 instanceof ObservableMap)) { + ((ObservableMap)obj2).removeListener(new MapContentBinding((Map)obj1)); + } + } + + private static class ListContentBinding implements ListChangeListener, WeakListener { + + private final WeakReference> listRef; + + public ListContentBinding(List list) { + this.listRef = new WeakReference>(list); + } + + @Override + public void onChanged(Change change) { + final List list = listRef.get(); + if (list == null) { + change.getList().removeListener(this); + } else { + while (change.next()) { + if (change.wasPermutated()) { + list.subList(change.getFrom(), change.getTo()).clear(); + list.addAll(change.getFrom(), change.getList().subList(change.getFrom(), change.getTo())); + } else { + if (change.wasRemoved()) { + list.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear(); + } + if (change.wasAdded()) { + list.addAll(change.getFrom(), change.getAddedSubList()); + } + } + } + } + } + + @Override + public boolean wasGarbageCollected() { + return listRef.get() == null; + } + + @Override + public int hashCode() { + final List list = listRef.get(); + return (list == null)? 0 : list.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final List list1 = listRef.get(); + if (list1 == null) { + return false; + } + + if (obj instanceof ListContentBinding) { + final ListContentBinding other = (ListContentBinding) obj; + final List list2 = other.listRef.get(); + return list1 == list2; + } + return false; + } + } + + private static class SetContentBinding implements SetChangeListener, WeakListener { + + private final WeakReference> setRef; + + public SetContentBinding(Set set) { + this.setRef = new WeakReference>(set); + } + + @Override + public void onChanged(Change change) { + final Set set = setRef.get(); + if (set == null) { + change.getSet().removeListener(this); + } else { + if (change.wasRemoved()) { + set.remove(change.getElementRemoved()); + } else { + set.add(change.getElementAdded()); + } + } + } + + @Override + public boolean wasGarbageCollected() { + return setRef.get() == null; + } + + @Override + public int hashCode() { + final Set set = setRef.get(); + return (set == null)? 0 : set.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Set set1 = setRef.get(); + if (set1 == null) { + return false; + } + + if (obj instanceof SetContentBinding) { + final SetContentBinding other = (SetContentBinding) obj; + final Set set2 = other.setRef.get(); + return set1 == set2; + } + return false; + } + } + + private static class MapContentBinding implements MapChangeListener, WeakListener { + + private final WeakReference> mapRef; + + public MapContentBinding(Map map) { + this.mapRef = new WeakReference>(map); + } + + @Override + public void onChanged(Change change) { + final Map map = mapRef.get(); + if (map == null) { + change.getMap().removeListener(this); + } else { + if (change.wasRemoved()) { + map.remove(change.getKey()); + } + if (change.wasAdded()) { + map.put(change.getKey(), change.getValueAdded()); + } + } + } + + @Override + public boolean wasGarbageCollected() { + return mapRef.get() == null; + } + + @Override + public int hashCode() { + final Map map = mapRef.get(); + return (map == null)? 0 : map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + final Map map1 = mapRef.get(); + if (map1 == null) { + return false; + } + + if (obj instanceof MapContentBinding) { + final MapContentBinding other = (MapContentBinding) obj; + final Map map2 = other.mapRef.get(); + return map1 == map2; + } + return false; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/DoubleConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/DoubleConstant.java new file mode 100644 index 00000000..a2cd2eb5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/DoubleConstant.java @@ -0,0 +1,71 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableDoubleValue; + +/** + * A simple DoubleExpression that represents a single constant value. + */ +public final class DoubleConstant implements ObservableDoubleValue { + + private final double value; + + private DoubleConstant(double value) { + this.value = value; + } + + public static DoubleConstant valueOf(double value) { + return new DoubleConstant(value); + } + + @Override + public double get() { + return value; + } + + @Override + public Double getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener listener) { + // no-op + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public float floatValue() { + return (float) value; + } + + @Override + public double doubleValue() { + return value; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelper.java similarity index 98% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelper.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelper.java index a9448280..8062a74a 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelper.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelper.java @@ -1,4 +1,8 @@ -package com.tungsten.fclcore.fakefx; +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; import java.util.Arrays; diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelperBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelperBase.java similarity index 89% rename from FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelperBase.java rename to FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelperBase.java index 7b48e925..75528a94 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/ExpressionHelperBase.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ExpressionHelperBase.java @@ -1,4 +1,6 @@ -package com.tungsten.fclcore.fakefx; +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.WeakListener; import java.util.function.Predicate; @@ -29,4 +31,4 @@ public class ExpressionHelperBase { return size; } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FlatMappedBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FlatMappedBinding.java new file mode 100644 index 00000000..d8901bfb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FlatMappedBinding.java @@ -0,0 +1,85 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; + +import java.util.Objects; +import java.util.function.Function; + +/** + * A binding holding the value of an indirect source. The indirect source results from + * applying a mapping to the given source. + * + *

Implementation: + * + *

In a flat mapped binding there are always two subscriptions involved: + *

    + *
  • The subscription on its source
  • + *
  • The subscription on the value resulting from the mapping of the source: the indirect source
  • + *
+ * The subscription on its given source is present when this binding itself is observed and not present otherwise. + * + *

The subscription on the indirect source must change whenever the value of the given source changes or is invalidated. More + * specifically, when the given source is invalidated the indirect subscription should be removed, and when it is revalidated it + * should resubscribe to the newly calculated indirect source. The binding avoids resubscribing when only the value of + * the indirect source changes. + * + * @param the type of the source + * @param the type of the resulting binding + */ +public class FlatMappedBinding extends LazyObjectBinding { + + private final ObservableValue source; + private final Function> mapper; + + private Subscription indirectSourceSubscription = Subscription.EMPTY; + private ObservableValue indirectSource; + + public FlatMappedBinding(ObservableValue source, Function> mapper) { + this.source = Objects.requireNonNull(source, "source cannot be null"); + this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null"); + } + + @Override + protected T computeValue() { + S value = source.getValue(); + ObservableValue newIndirectSource = value == null ? null : mapper.apply(value); + + if (isObserved() && indirectSource != newIndirectSource) { // only resubscribe when observed and the indirect source changed + indirectSourceSubscription.unsubscribe(); + indirectSourceSubscription = newIndirectSource == null ? Subscription.EMPTY : Subscription.subscribeInvalidations(newIndirectSource, this::invalidate); + indirectSource = newIndirectSource; + } + + return newIndirectSource == null ? null : newIndirectSource.getValue(); + } + + @Override + protected Subscription observeSources() { + Subscription subscription = Subscription.subscribeInvalidations(source, this::invalidateAll); + + return () -> { + subscription.unsubscribe(); + unsubscribeIndirectSource(); + }; + } + + /** + * Called when the primary source changes. Invalidates this binding and unsubscribes the indirect source + * to avoid holding a strong reference to it. If the binding becomes valid later, {@link #computeValue()} will + * subscribe to a newly calculated indirect source. + * + *

Note that this only needs to be called for changes of the primary source; changes in the indirect + * source only need to invalidate this binding without also unsubscribing, as it would be wasteful to resubscribe + * to the same indirect source for each invalidation of that source. + */ + private void invalidateAll() { + unsubscribeIndirectSource(); + invalidate(); + } + + private void unsubscribeIndirectSource() { + indirectSourceSubscription.unsubscribe(); + indirectSourceSubscription = Subscription.EMPTY; + indirectSource = null; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FloatConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FloatConstant.java new file mode 100644 index 00000000..9c0fe29d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/FloatConstant.java @@ -0,0 +1,71 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableFloatValue; + +/** + * A simple FloatExpression that represents a single constant value. + */ +public final class FloatConstant implements ObservableFloatValue { + + private final float value; + + private FloatConstant(float value) { + this.value = value; + } + + public static FloatConstant valueOf(float value) { + return new FloatConstant(value); + } + + @Override + public float get() { + return value; + } + + @Override + public Float getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener listener) { + // no-op + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/IntegerConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/IntegerConstant.java new file mode 100644 index 00000000..c81b9e6e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/IntegerConstant.java @@ -0,0 +1,71 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableIntegerValue; + +/** + * A simple IntegerExpression that represents a single constant value. + */ +public final class IntegerConstant implements ObservableIntegerValue { + + private final int value; + + private IntegerConstant(int value) { + this.value = value; + } + + public static IntegerConstant valueOf(int value) { + return new IntegerConstant(value); + } + + @Override + public int get() { + return value; + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener listener) { + // no-op + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LazyObjectBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LazyObjectBinding.java new file mode 100644 index 00000000..79449f8b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LazyObjectBinding.java @@ -0,0 +1,101 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; + +/** + * Extends {@link ObjectBinding} with the ability to lazily register and eagerly unregister listeners on its + * dependencies. + * + * @param the type of the wrapped {@code Object} + */ +abstract class LazyObjectBinding extends ObjectBinding { + + private Subscription subscription; + private boolean wasObserved; + + @Override + public void addListener(ChangeListener listener) { + super.addListener(listener); + + updateSubscriptionAfterAdd(); + } + + @Override + public void removeListener(ChangeListener listener) { + super.removeListener(listener); + + updateSubscriptionAfterRemove(); + } + + @Override + public void addListener(InvalidationListener listener) { + super.addListener(listener); + + updateSubscriptionAfterAdd(); + } + + @Override + public void removeListener(InvalidationListener listener) { + super.removeListener(listener); + + updateSubscriptionAfterRemove(); + } + + @Override + protected boolean allowValidation() { + return isObserved(); + } + + /** + * Called after a listener was added to start observing inputs if they're not observed already. + */ + private void updateSubscriptionAfterAdd() { + if (!wasObserved) { // was first observer registered? + subscription = observeSources(); // start observing source + + /* + * Although the act of registering a listener already attempts to make + * this binding valid, allowValidation won't allow it as the binding is + * not observed yet. This is because isObserved will not yet return true + * when the process of registering the listener hasn't completed yet. + * + * As the binding must be valid after it becomes observed the first time + * 'get' is called again. + * + * See com.sun.javafx.binding.ExpressionHelper (which is used + * by ObjectBinding) where it will do a call to ObservableValue#getValue + * BEFORE adding the actual listener. This results in ObjectBinding#get + * to be called in which the #allowValidation call will block it from + * becoming valid as the condition is "isObserved()"; this is technically + * correct as the listener wasn't added yet, but means we must call + * #get again to make this binding valid. + */ + + get(); // make binding valid as source wasn't tracked until now + wasObserved = true; + } + } + + /** + * Called after a listener was removed to stop observing inputs if this was the last listener + * observing this binding. + */ + private void updateSubscriptionAfterRemove() { + if (wasObserved && !isObserved()) { // was last observer unregistered? + subscription.unsubscribe(); + subscription = null; + invalidate(); // make binding invalid as source is no longer tracked + wasObserved = false; + } + } + + /** + * Called when this binding was previously not observed and a new observer was added. Implementors must return a + * {@link Subscription} which will be cancelled when this binding no longer has any observers. + * + * @return a {@link Subscription} which will be cancelled when this binding no longer has any observers, never null + */ + protected abstract Subscription observeSources(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ListExpressionHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ListExpressionHelper.java new file mode 100644 index 00000000..f0e0e31d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ListExpressionHelper.java @@ -0,0 +1,568 @@ +package com.tungsten.fclcore.fakefx.binding; + +import java.util.Arrays; +import java.util.List; + +import static com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableListValue; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.NonIterableChange; +import com.tungsten.fclcore.fakefx.collections.ObservableList; +import com.tungsten.fclcore.fakefx.collections.SourceAdapterChange; + +public abstract class ListExpressionHelper extends ExpressionHelperBase { + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static ListExpressionHelper addListener(ListExpressionHelper helper, ObservableListValue observable, InvalidationListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + observable.getValue(); // validate observable + return (helper == null)? new SingleInvalidation(observable, listener) : helper.addListener(listener); + } + + public static ListExpressionHelper removeListener(ListExpressionHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static ListExpressionHelper addListener(ListExpressionHelper helper, ObservableListValue observable, ChangeListener> listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(observable, listener) : helper.addListener(listener); + } + + public static ListExpressionHelper removeListener(ListExpressionHelper helper, ChangeListener> listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static ListExpressionHelper addListener(ListExpressionHelper helper, ObservableListValue observable, ListChangeListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleListChange(observable, listener) : helper.addListener(listener); + } + + public static ListExpressionHelper removeListener(ListExpressionHelper helper, ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(ListExpressionHelper helper) { + if (helper != null) { + helper.fireValueChangedEvent(); + } + } + + public static void fireValueChangedEvent(ListExpressionHelper helper, Change change) { + if (helper != null) { + helper.fireValueChangedEvent(change); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected final ObservableListValue observable; + + protected ListExpressionHelper(ObservableListValue observable) { + this.observable = observable; + } + + protected abstract ListExpressionHelper addListener(InvalidationListener listener); + protected abstract ListExpressionHelper removeListener(InvalidationListener listener); + + protected abstract ListExpressionHelper addListener(ChangeListener> listener); + protected abstract ListExpressionHelper removeListener(ChangeListener> listener); + + protected abstract ListExpressionHelper addListener(ListChangeListener listener); + protected abstract ListExpressionHelper removeListener(ListChangeListener listener); + + protected abstract void fireValueChangedEvent(); + protected abstract void fireValueChangedEvent(Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends ListExpressionHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(ObservableListValue observable, InvalidationListener listener) { + super(observable); + this.listener = listener; + } + + @Override + protected ListExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected ListExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected ListExpressionHelper addListener(ListChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(ListChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + listener.invalidated(observable); + } + + @Override + protected void fireValueChangedEvent(Change change) { + listener.invalidated(observable); + } + } + + private static class SingleChange extends ListExpressionHelper { + + private final ChangeListener> listener; + private ObservableList currentValue; + + private SingleChange(ObservableListValue observable, ChangeListener> listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected ListExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected ListExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected ListExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(ChangeListener> listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected ListExpressionHelper addListener(ListChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(ListChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableList oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + listener.changed(observable, oldValue, currentValue); + } + } + + @Override + protected void fireValueChangedEvent(Change change) { + listener.changed(observable, currentValue, currentValue); + } + } + + private static class SingleListChange extends ListExpressionHelper { + + private final ListChangeListener listener; + private ObservableList currentValue; + + private SingleListChange(ObservableListValue observable, ListChangeListener listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected ListExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected ListExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected ListExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected ListExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected ListExpressionHelper addListener(ListChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ListExpressionHelper removeListener(ListChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableList oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + final int safeSize = (currentValue == null)? 0 : currentValue.size(); + final ObservableList safeOldValue = (oldValue == null)? + FXCollections.emptyObservableList() + : FXCollections.unmodifiableObservableList(oldValue); + final Change change = new NonIterableChange.GenericAddRemoveChange(0, safeSize, safeOldValue, observable); + listener.onChanged(change); + } + } + + @Override + protected void fireValueChangedEvent(final Change change) { + listener.onChanged(new SourceAdapterChange<>(observable, change)); + } + } + + private static class Generic extends ListExpressionHelper { + + private InvalidationListener[] invalidationListeners; + private ChangeListener>[] changeListeners; + private ListChangeListener[] listChangeListeners; + private int invalidationSize; + private int changeSize; + private int listChangeSize; + private boolean locked; + private ObservableList currentValue; + + private Generic(ObservableListValue observable, InvalidationListener listener0, InvalidationListener listener1) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(ObservableListValue observable, ChangeListener> listener0, ChangeListener> listener1) { + super(observable); + this.changeListeners = new ChangeListener[] {listener0, listener1}; + this.changeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableListValue observable, ListChangeListener listener0, ListChangeListener listener1) { + super(observable); + this.listChangeListeners = new ListChangeListener[] {listener0, listener1}; + this.listChangeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableListValue observable, InvalidationListener invalidationListener, ChangeListener> changeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableListValue observable, InvalidationListener invalidationListener, ListChangeListener listChangeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.listChangeListeners = new ListChangeListener[] {listChangeListener}; + this.listChangeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableListValue observable, ChangeListener> changeListener, ListChangeListener listChangeListener) { + super(observable); + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.listChangeListeners = new ListChangeListener[] {listChangeListener}; + this.listChangeSize = 1; + this.currentValue = observable.getValue(); + } + + @Override + protected ListExpressionHelper addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected ListExpressionHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if ((changeSize == 1) && (listChangeSize == 0)) { + return new SingleChange(observable, changeListeners[0]); + } else if ((changeSize == 0) && (listChangeSize == 1)) { + return new SingleListChange(observable, listChangeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0) && (listChangeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected ListExpressionHelper addListener(ChangeListener> listener) { + if (changeListeners == null) { + changeListeners = new ChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + if (changeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected ListExpressionHelper removeListener(ChangeListener> listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if ((invalidationSize == 1) && (listChangeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (listChangeSize == 1)) { + return new SingleListChange(observable, listChangeListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0) && (listChangeSize == 0)) { + return new SingleChange(observable, changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final ChangeListener>[] oldListeners = changeListeners; + if (locked) { + changeListeners = new ChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected ListExpressionHelper addListener(ListChangeListener listener) { + if (listChangeListeners == null) { + listChangeListeners = new ListChangeListener[] {listener}; + listChangeSize = 1; + } else { + final int oldCapacity = listChangeListeners.length; + if (locked) { + final int newCapacity = (listChangeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + listChangeListeners = Arrays.copyOf(listChangeListeners, newCapacity); + } else if (listChangeSize == oldCapacity) { + listChangeSize = trim(listChangeSize, listChangeListeners); + if (listChangeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + listChangeListeners = Arrays.copyOf(listChangeListeners, newCapacity); + } + } + listChangeListeners[listChangeSize++] = listener; + } + if (listChangeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected ListExpressionHelper removeListener(ListChangeListener listener) { + if (listChangeListeners != null) { + for (int index = 0; index < listChangeSize; index++) { + if (listener.equals(listChangeListeners[index])) { + if (listChangeSize == 1) { + if ((invalidationSize == 1) && (changeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (changeSize == 1)) { + return new SingleChange(observable, changeListeners[0]); + } + listChangeListeners = null; + listChangeSize = 0; + } else if ((listChangeSize == 2) && (invalidationSize == 0) && (changeSize == 0)) { + return new SingleListChange(observable, listChangeListeners[1-index]); + } else { + final int numMoved = listChangeSize - index - 1; + final ListChangeListener[] oldListeners = listChangeListeners; + if (locked) { + listChangeListeners = new ListChangeListener[listChangeListeners.length]; + System.arraycopy(oldListeners, 0, listChangeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, listChangeListeners, index, numMoved); + } + listChangeSize--; + if (!locked) { + listChangeListeners[listChangeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent() { + if ((changeSize == 0) && (listChangeSize == 0)) { + notifyListeners(currentValue, null, false); + } else { + final ObservableList oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + Change change = null; + if (listChangeSize > 0) { + final int safeSize = (currentValue == null)? 0 : currentValue.size(); + final ObservableList safeOldValue = (oldValue == null)? + FXCollections.emptyObservableList() + : FXCollections.unmodifiableObservableList(oldValue); + change = new NonIterableChange.GenericAddRemoveChange(0, safeSize, safeOldValue, observable); + } + notifyListeners(oldValue, change, false); + } else { + notifyListeners(currentValue, null, true); + } + } + } + + @Override + protected void fireValueChangedEvent(final Change change) { + final Change mappedChange = (listChangeSize == 0)? null : new SourceAdapterChange<>(observable, change); + notifyListeners(currentValue, mappedChange, false); + } + + private void notifyListeners(ObservableList oldValue, Change change, boolean noChange) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final ChangeListener>[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + final ListChangeListener[] curListChangeList = listChangeListeners; + final int curListChangeSize = listChangeSize; + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + curInvalidationList[i].invalidated(observable); + } + if (!noChange) { + for (int i = 0; i < curChangeSize; i++) { + curChangeList[i].changed(observable, oldValue, currentValue); + } + if (change != null) { + for (int i = 0; i < curListChangeSize; i++) { + change.reset(); + curListChangeList[i].onChanged(change); + } + } + } + } finally { + locked = false; + } + } + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LongConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LongConstant.java new file mode 100644 index 00000000..1c89ff6c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/LongConstant.java @@ -0,0 +1,71 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableLongValue; + +/** + * A simple LongExpression that represents a single constant value. + */ +public final class LongConstant implements ObservableLongValue { + + private final long value; + + private LongConstant(long value) { + this.value = value; + } + + public static LongConstant valueOf(long value) { + return new LongConstant(value); + } + + @Override + public long get() { + return value; + } + + @Override + public Long getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener observer) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener observer) { + // no-op + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public float floatValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MapExpressionHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MapExpressionHelper.java new file mode 100644 index 00000000..2e51d383 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MapExpressionHelper.java @@ -0,0 +1,711 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableMapValue; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +import java.util.Arrays; +import java.util.Map; + +/** +*/ +public abstract class MapExpressionHelper extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static MapExpressionHelper addListener(MapExpressionHelper helper, ObservableMapValue observable, InvalidationListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + observable.getValue(); // validate observable + return (helper == null)? new SingleInvalidation(observable, listener) : helper.addListener(listener); + } + + public static MapExpressionHelper removeListener(MapExpressionHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static MapExpressionHelper addListener(MapExpressionHelper helper, ObservableMapValue observable, ChangeListener> listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(observable, listener) : helper.addListener(listener); + } + + public static MapExpressionHelper removeListener(MapExpressionHelper helper, ChangeListener> listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static MapExpressionHelper addListener(MapExpressionHelper helper, ObservableMapValue observable, MapChangeListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleMapChange(observable, listener) : helper.addListener(listener); + } + + public static MapExpressionHelper removeListener(MapExpressionHelper helper, MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(MapExpressionHelper helper) { + if (helper != null) { + helper.fireValueChangedEvent(); + } + } + + public static void fireValueChangedEvent(MapExpressionHelper helper, MapChangeListener.Change change) { + if (helper != null) { + helper.fireValueChangedEvent(change); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected final ObservableMapValue observable; + + protected MapExpressionHelper(ObservableMapValue observable) { + this.observable = observable; + } + + protected abstract MapExpressionHelper addListener(InvalidationListener listener); + protected abstract MapExpressionHelper removeListener(InvalidationListener listener); + + protected abstract MapExpressionHelper addListener(ChangeListener> listener); + protected abstract MapExpressionHelper removeListener(ChangeListener> listener); + + protected abstract MapExpressionHelper addListener(MapChangeListener listener); + protected abstract MapExpressionHelper removeListener(MapChangeListener listener); + + protected abstract void fireValueChangedEvent(); + protected abstract void fireValueChangedEvent(MapChangeListener.Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends MapExpressionHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(ObservableMapValue observable, InvalidationListener listener) { + super(observable); + this.listener = listener; + } + + @Override + protected MapExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected MapExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected MapExpressionHelper addListener(MapChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(MapChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + listener.invalidated(observable); + } + + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + listener.invalidated(observable); + } + } + + private static class SingleChange extends MapExpressionHelper { + + private final ChangeListener> listener; + private ObservableMap currentValue; + + private SingleChange(ObservableMapValue observable, ChangeListener> listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected MapExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected MapExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected MapExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(ChangeListener> listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected MapExpressionHelper addListener(MapChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(MapChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableMap oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + listener.changed(observable, oldValue, currentValue); + } + } + + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + listener.changed(observable, currentValue, currentValue); + } + } + + private static class SingleMapChange extends MapExpressionHelper { + + private final MapChangeListener listener; + private ObservableMap currentValue; + + private SingleMapChange(ObservableMapValue observable, MapChangeListener listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected MapExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected MapExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected MapExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected MapExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected MapExpressionHelper addListener(MapChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected MapExpressionHelper removeListener(MapChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableMap oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + final SimpleChange change = new SimpleChange(observable); + if (currentValue == null) { + for (final Map.Entry element : oldValue.entrySet()) { + listener.onChanged(change.setRemoved(element.getKey(), element.getValue())); + } + } else if (oldValue == null) { + for (final Map.Entry element : currentValue.entrySet()) { + listener.onChanged(change.setAdded(element.getKey(), element.getValue())); + } + } else { + for (final Map.Entry element : oldValue.entrySet()) { + final K key = element.getKey(); + final V oldEntry = element.getValue(); + if (currentValue.containsKey(key)) { + final V newEntry = currentValue.get(key); + if (oldEntry == null ? newEntry != null : !newEntry.equals(oldEntry)) { + listener.onChanged(change.setPut(key, oldEntry, newEntry)); + } + } else { + listener.onChanged(change.setRemoved(key, oldEntry)); + } + } + for (final Map.Entry element : currentValue.entrySet()) { + final K key = element.getKey(); + if (!oldValue.containsKey(key)) { + listener.onChanged(change.setAdded(key, element.getValue())); + } + } + } + } + } + + @Override + protected void fireValueChangedEvent(final MapChangeListener.Change change) { + listener.onChanged(new SimpleChange(observable, change)); + } + } + + private static class Generic extends MapExpressionHelper { + + private InvalidationListener[] invalidationListeners; + private ChangeListener>[] changeListeners; + private MapChangeListener[] mapChangeListeners; + private int invalidationSize; + private int changeSize; + private int mapChangeSize; + private boolean locked; + private ObservableMap currentValue; + + private Generic(ObservableMapValue observable, InvalidationListener listener0, InvalidationListener listener1) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(ObservableMapValue observable, ChangeListener> listener0, ChangeListener> listener1) { + super(observable); + this.changeListeners = new ChangeListener[] {listener0, listener1}; + this.changeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableMapValue observable, MapChangeListener listener0, MapChangeListener listener1) { + super(observable); + this.mapChangeListeners = new MapChangeListener[] {listener0, listener1}; + this.mapChangeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableMapValue observable, InvalidationListener invalidationListener, ChangeListener> changeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableMapValue observable, InvalidationListener invalidationListener, MapChangeListener listChangeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.mapChangeListeners = new MapChangeListener[] {listChangeListener}; + this.mapChangeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableMapValue observable, ChangeListener> changeListener, MapChangeListener listChangeListener) { + super(observable); + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.mapChangeListeners = new MapChangeListener[] {listChangeListener}; + this.mapChangeSize = 1; + this.currentValue = observable.getValue(); + } + + @Override + protected MapExpressionHelper addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected MapExpressionHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if ((changeSize == 1) && (mapChangeSize == 0)) { + return new SingleChange(observable, changeListeners[0]); + } else if ((changeSize == 0) && (mapChangeSize == 1)) { + return new SingleMapChange(observable, mapChangeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0) && (mapChangeSize == 0)) { + return new SingleInvalidation<>(observable, invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected MapExpressionHelper addListener(ChangeListener> listener) { + if (changeListeners == null) { + changeListeners = new ChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + if (changeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected MapExpressionHelper removeListener(ChangeListener> listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if ((invalidationSize == 1) && (mapChangeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (mapChangeSize == 1)) { + return new SingleMapChange(observable, mapChangeListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0) && (mapChangeSize == 0)) { + return new SingleChange<>(observable, changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final ChangeListener>[] oldListeners = changeListeners; + if (locked) { + changeListeners = new ChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected MapExpressionHelper addListener(MapChangeListener listener) { + if (mapChangeListeners == null) { + mapChangeListeners = new MapChangeListener[] {listener}; + mapChangeSize = 1; + } else { + final int oldCapacity = mapChangeListeners.length; + if (locked) { + final int newCapacity = (mapChangeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + mapChangeListeners = Arrays.copyOf(mapChangeListeners, newCapacity); + } else if (mapChangeSize == oldCapacity) { + mapChangeSize = trim(mapChangeSize, mapChangeListeners); + if (mapChangeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + mapChangeListeners = Arrays.copyOf(mapChangeListeners, newCapacity); + } + } + mapChangeListeners[mapChangeSize++] = listener; + } + if (mapChangeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected MapExpressionHelper removeListener(MapChangeListener listener) { + if (mapChangeListeners != null) { + for (int index = 0; index < mapChangeSize; index++) { + if (listener.equals(mapChangeListeners[index])) { + if (mapChangeSize == 1) { + if ((invalidationSize == 1) && (changeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (changeSize == 1)) { + return new SingleChange(observable, changeListeners[0]); + } + mapChangeListeners = null; + mapChangeSize = 0; + } else if ((mapChangeSize == 2) && (invalidationSize == 0) && (changeSize == 0)) { + return new SingleMapChange<>(observable, mapChangeListeners[1-index]); + } else { + final int numMoved = mapChangeSize - index - 1; + final MapChangeListener[] oldListeners = mapChangeListeners; + if (locked) { + mapChangeListeners = new MapChangeListener[mapChangeListeners.length]; + System.arraycopy(oldListeners, 0, mapChangeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, mapChangeListeners, index, numMoved); + } + mapChangeSize--; + if (!locked) { + mapChangeListeners[mapChangeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent() { + if ((changeSize == 0) && (mapChangeSize == 0)) { + notifyListeners(currentValue, null); + } else { + final ObservableMap oldValue = currentValue; + currentValue = observable.getValue(); + notifyListeners(oldValue, null); + } + } + + @Override + protected void fireValueChangedEvent(final MapChangeListener.Change change) { + final SimpleChange mappedChange = (mapChangeSize == 0)? null : new SimpleChange(observable, change); + notifyListeners(currentValue, mappedChange); + } + + private void notifyListeners(ObservableMap oldValue, SimpleChange change) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final ChangeListener>[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + final MapChangeListener[] curListChangeList = mapChangeListeners; + final int curListChangeSize = mapChangeSize; + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + curInvalidationList[i].invalidated(observable); + } + if ((currentValue != oldValue) || (change != null)) { + for (int i = 0; i < curChangeSize; i++) { + curChangeList[i].changed(observable, oldValue, currentValue); + } + if (curListChangeSize > 0) { + if (change != null) { + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } else { + change = new SimpleChange(observable); + if (currentValue == null) { + for (final Map.Entry element : oldValue.entrySet()) { + change.setRemoved(element.getKey(), element.getValue()); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } else if (oldValue == null) { + for (final Map.Entry element : currentValue.entrySet()) { + change.setAdded(element.getKey(), element.getValue()); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } else { + for (final Map.Entry element : oldValue.entrySet()) { + final K key = element.getKey(); + final V oldEntry = element.getValue(); + if (currentValue.containsKey(key)) { + final V newEntry = currentValue.get(key); + if (oldEntry == null ? newEntry != null : !newEntry.equals(oldEntry)) { + change.setPut(key, oldEntry, newEntry); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } else { + change.setRemoved(key, oldEntry); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } + for (final Map.Entry element : currentValue.entrySet()) { + final K key = element.getKey(); + if (!oldValue.containsKey(key)) { + change.setAdded(key, element.getValue()); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } + } + } + } + } + } finally { + locked = false; + } + } + + } + + public static class SimpleChange extends MapChangeListener.Change { + + private K key; + private V old; + private V added; + private boolean removeOp; + private boolean addOp; + + public SimpleChange(ObservableMap set) { + super(set); + } + + public SimpleChange(ObservableMap set, MapChangeListener.Change source) { + super(set); + key = source.getKey(); + old = source.getValueRemoved(); + added = source.getValueAdded(); + addOp = source.wasAdded(); + removeOp = source.wasRemoved(); + } + + public SimpleChange setRemoved(K key, V old) { + this.key = key; + this.old = old; + this.added = null; + addOp = false; + removeOp = true; + return this; + } + + public SimpleChange setAdded(K key, V added) { + this.key = key; + this.old = null; + this.added = added; + addOp = true; + removeOp = false; + return this; + } + + public SimpleChange setPut(K key, V old, V added) { + this.key = key; + this.old = old; + this.added = added; + addOp = true; + removeOp = true; + return this; + } + + @Override + public boolean wasAdded() { + return addOp; + } + + @Override + public boolean wasRemoved() { + return removeOp; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValueAdded() { + return added; + } + + @Override + public V getValueRemoved() { + return old; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (addOp) { + if (removeOp) { + builder.append("replaced ").append(old).append(" by ").append(added); + } else { + builder.append("added ").append(added); + } + } else { + builder.append("removed ").append(old); + } + builder.append(" at key ").append(key); + return builder.toString(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MappedBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MappedBinding.java new file mode 100644 index 00000000..ebad7f68 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/MappedBinding.java @@ -0,0 +1,29 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; + +import java.util.Objects; +import java.util.function.Function; + +public class MappedBinding extends LazyObjectBinding { + + private final ObservableValue source; + private final Function mapper; + + public MappedBinding(ObservableValue source, Function mapper) { + this.source = Objects.requireNonNull(source, "source cannot be null"); + this.mapper = Objects.requireNonNull(mapper, "mapper cannot be null"); + } + + @Override + protected T computeValue() { + S value = source.getValue(); + + return value == null ? null : mapper.apply(value); + } + + @Override + protected Subscription observeSources() { + return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ObjectConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ObjectConstant.java new file mode 100644 index 00000000..e2b2388d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/ObjectConstant.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableObjectValue; + +public class ObjectConstant implements ObservableObjectValue { + + private final T value; + + private ObjectConstant(T value) { + this.value = value; + } + + public static ObjectConstant valueOf(T value) { + return new ObjectConstant(value); + } + + @Override + public T get() { + return value; + } + + @Override + public T getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener observer) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener observer) { + // no-op + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/OrElseBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/OrElseBinding.java new file mode 100644 index 00000000..59a35dcd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/OrElseBinding.java @@ -0,0 +1,28 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; + +import java.util.Objects; + +public class OrElseBinding extends LazyObjectBinding { + + private final ObservableValue source; + private final T constant; + + public OrElseBinding(ObservableValue source, T constant) { + this.source = Objects.requireNonNull(source, "source cannot be null"); + this.constant = constant; + } + + @Override + protected T computeValue() { + T value = source.getValue(); + + return value == null ? constant : value; + } + + @Override + protected Subscription observeSources() { + return Subscription.subscribeInvalidations(source, this::invalidate); // start observing source + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SelectBinding.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SelectBinding.java new file mode 100644 index 00000000..5c2d4690 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SelectBinding.java @@ -0,0 +1,515 @@ +package com.tungsten.fclcore.fakefx.binding; + +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.binding.Binding; +import com.tungsten.fclcore.fakefx.beans.binding.BooleanBinding; +import com.tungsten.fclcore.fakefx.beans.binding.DoubleBinding; +import com.tungsten.fclcore.fakefx.beans.binding.FloatBinding; +import com.tungsten.fclcore.fakefx.beans.binding.IntegerBinding; +import com.tungsten.fclcore.fakefx.beans.binding.LongBinding; +import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; +import com.tungsten.fclcore.fakefx.beans.binding.StringBinding; +import com.tungsten.fclcore.fakefx.beans.value.ObservableBooleanValue; +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; +import com.tungsten.fclcore.fakefx.property.JavaBeanAccessHelper; +import com.tungsten.fclcore.fakefx.property.PropertyReference; + +import java.util.Arrays; + +/** + * A binding used to get a member, such as a.b.c. The value of the + * binding will be "c", or null if c could not be reached (due to "b" not having + * a "c" property, or "b" being null). "a" must be passed to the constructor of + * the SelectBinding and may be any dependency. All subsequent links are simply + * PropertyReferences. + *

+ * With a SelectBinding, "a" must always exist. Usually "a" will refer to + * "this", or some concrete object. "b"* will be some intermediate step in the + * select binding. + */ +public class SelectBinding { + + private SelectBinding() {} + + public static class AsObject extends ObjectBinding { + + private final SelectBindingHelper helper; + + public AsObject(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsObject(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @SuppressWarnings("unchecked") + @Override + protected T computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return null; + } + try { + return (T)observable.getValue(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return null; + } + + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsBoolean extends BooleanBinding { + + private static final boolean DEFAULT_VALUE = false; + + private final SelectBindingHelper helper; + + public AsBoolean(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsBoolean(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected boolean computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + if (observable instanceof ObservableBooleanValue) { + return ((ObservableBooleanValue)observable).get(); + } + try { + return (Boolean)observable.getValue(); + } catch (NullPointerException ex) { + ex.printStackTrace(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return DEFAULT_VALUE; + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsDouble extends DoubleBinding { + + private static final double DEFAULT_VALUE = 0.0; + + private final SelectBindingHelper helper; + + public AsDouble(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsDouble(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected double computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + if (observable instanceof ObservableNumberValue) { + return ((ObservableNumberValue)observable).doubleValue(); + } + try { + return ((Number)observable.getValue()).doubleValue(); + } catch (NullPointerException ex) { + ex.printStackTrace(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return DEFAULT_VALUE; + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsFloat extends FloatBinding { + + private static final float DEFAULT_VALUE = 0.0f; + + private final SelectBindingHelper helper; + + public AsFloat(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsFloat(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected float computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + if (observable instanceof ObservableNumberValue) { + return ((ObservableNumberValue)observable).floatValue(); + } + try { + return ((Number)observable.getValue()).floatValue(); + } catch (NullPointerException ex) { + ex.printStackTrace(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return DEFAULT_VALUE; + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsInteger extends IntegerBinding { + + private static final int DEFAULT_VALUE = 0; + + private final SelectBindingHelper helper; + + public AsInteger(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsInteger(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected int computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + if (observable instanceof ObservableNumberValue) { + return ((ObservableNumberValue)observable).intValue(); + } + try { + return ((Number)observable.getValue()).intValue(); + } catch (NullPointerException ex) { + ex.printStackTrace(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return DEFAULT_VALUE; + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsLong extends LongBinding { + + private static final long DEFAULT_VALUE = 0L; + + private final SelectBindingHelper helper; + + public AsLong(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsLong(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected long computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + if (observable instanceof ObservableNumberValue) { + return ((ObservableNumberValue)observable).longValue(); + } + try { + return ((Number)observable.getValue()).longValue(); + } catch (NullPointerException ex) { + ex.printStackTrace(); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + return DEFAULT_VALUE; + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + public static class AsString extends StringBinding { + + private static final String DEFAULT_VALUE = null; + + private final SelectBindingHelper helper; + + public AsString(ObservableValue root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + public AsString(Object root, String... steps) { + helper = new SelectBindingHelper(this, root, steps); + } + + @Override + public void dispose() { + helper.unregisterListener(); + } + + @Override + protected void onInvalidating() { + helper.unregisterListener(); + } + + @Override + protected String computeValue() { + final ObservableValue observable = helper.getObservableValue(); + if (observable == null) { + return DEFAULT_VALUE; + } + try { + return observable.getValue().toString(); + } catch (RuntimeException ex) { + ex.printStackTrace(); + // return default + return DEFAULT_VALUE; + } + } + + @Override + public ObservableList> getDependencies() { + return helper.getDependencies(); + } + + } + + private static class SelectBindingHelper implements InvalidationListener { + + private final Binding binding; + private final String[] propertyNames; + private final ObservableValue[] properties; + private final PropertyReference[] propRefs; + private final WeakInvalidationListener observer; + + private ObservableList> dependencies; + + private SelectBindingHelper(Binding binding, ObservableValue firstProperty, String... steps) { + if (firstProperty == null) { + throw new NullPointerException("Must specify the root"); + } + if (steps == null) { + steps = new String[0]; + } + + this.binding = binding; + + final int n = steps.length; + for (int i = 0; i < n; i++) { + if (steps[i] == null) { + throw new NullPointerException("all steps must be specified"); + } + } + + observer = new WeakInvalidationListener(this); + propertyNames = new String[n]; + System.arraycopy(steps, 0, propertyNames, 0, n); + propRefs = new PropertyReference[n]; + properties = new ObservableValue[n + 1]; + properties[0] = firstProperty; + properties[0].addListener(observer); + } + + private static ObservableValue checkAndCreateFirstStep(Object root, String[] steps) { + if (root == null || steps == null || steps[0] == null) { + throw new NullPointerException("Must specify the root and the first property"); + } + try { + return JavaBeanAccessHelper.createReadOnlyJavaBeanProperty(root, steps[0]); + } catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("The first property '" + steps[0] + "' doesn't exist"); + } + } + + private SelectBindingHelper(Binding binding, Object root, String... steps) { + this(binding, checkAndCreateFirstStep(root, steps), Arrays.copyOfRange(steps, 1, steps.length)); + } + + @Override + public void invalidated(Observable observable) { + binding.invalidate(); + } + + public ObservableValue getObservableValue() { + // Step through each of the steps, and at each step add a listener as + // appropriate, accumulating the result. + final int n = properties.length; + for (int i = 0; i < n - 1; i++) { + final Object obj = properties[i].getValue(); + try { + if ((propRefs[i] == null) + || (!obj.getClass().equals( + propRefs[i].getContainingClass()))) { + propRefs[i] = new PropertyReference(obj.getClass(), + propertyNames[i]); + } + if (propRefs[i].hasProperty()) { + properties[i + 1] = propRefs[i].getProperty(obj); + } else { + properties[i + 1] = JavaBeanAccessHelper.createReadOnlyJavaBeanProperty(obj, propRefs[i].getName()); + } + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + // return default + updateDependencies(); + return null; + } catch (RuntimeException ex) { + ex.printStackTrace(); + // return default + updateDependencies(); + return null; + } + properties[i + 1].addListener(observer); + } + updateDependencies(); + final ObservableValue result = properties[n-1]; + if (result == null) { + + } + return result; + } + + private String stepsToString() { + return Arrays.toString(propertyNames); + } + + private void unregisterListener() { + final int n = properties.length; + for (int i = 1; i < n; i++) { + if (properties[i] == null) { + break; + } + properties[i].removeListener(observer); + properties[i] = null; + } + updateDependencies(); + } + + private void updateDependencies() { + if (dependencies != null) { + dependencies.clear(); + final int n = properties.length; + for (int i = 0; i < n; i++) { + if (properties[i] == null) { + break; + } + dependencies.add(properties[i]); + } + } + } + + public ObservableList> getDependencies() { + if (dependencies == null) { + dependencies = FXCollections.observableArrayList(); + updateDependencies(); + } + + return FXCollections.unmodifiableObservableList(dependencies); + } + + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SetExpressionHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SetExpressionHelper.java new file mode 100644 index 00000000..9f0d4ed2 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/SetExpressionHelper.java @@ -0,0 +1,660 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableSetValue; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; +import com.tungsten.fclcore.fakefx.collections.SetChangeListener; + +import java.util.Arrays; + +/** +*/ +public abstract class SetExpressionHelper extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static SetExpressionHelper addListener(SetExpressionHelper helper, ObservableSetValue observable, InvalidationListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + observable.getValue(); // validate observable + return (helper == null)? new SingleInvalidation(observable, listener) : helper.addListener(listener); + } + + public static SetExpressionHelper removeListener(SetExpressionHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static SetExpressionHelper addListener(SetExpressionHelper helper, ObservableSetValue observable, ChangeListener> listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(observable, listener) : helper.addListener(listener); + } + + public static SetExpressionHelper removeListener(SetExpressionHelper helper, ChangeListener> listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static SetExpressionHelper addListener(SetExpressionHelper helper, ObservableSetValue observable, SetChangeListener listener) { + if ((observable == null) || (listener == null)) { + throw new NullPointerException(); + } + return (helper == null)? new SingleSetChange(observable, listener) : helper.addListener(listener); + } + + public static SetExpressionHelper removeListener(SetExpressionHelper helper, SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(SetExpressionHelper helper) { + if (helper != null) { + helper.fireValueChangedEvent(); + } + } + + public static void fireValueChangedEvent(SetExpressionHelper helper, SetChangeListener.Change change) { + if (helper != null) { + helper.fireValueChangedEvent(change); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected final ObservableSetValue observable; + + protected SetExpressionHelper(ObservableSetValue observable) { + this.observable = observable; + } + + protected abstract SetExpressionHelper addListener(InvalidationListener listener); + protected abstract SetExpressionHelper removeListener(InvalidationListener listener); + + protected abstract SetExpressionHelper addListener(ChangeListener> listener); + protected abstract SetExpressionHelper removeListener(ChangeListener> listener); + + protected abstract SetExpressionHelper addListener(SetChangeListener listener); + protected abstract SetExpressionHelper removeListener(SetChangeListener listener); + + protected abstract void fireValueChangedEvent(); + protected abstract void fireValueChangedEvent(SetChangeListener.Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends SetExpressionHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(ObservableSetValue observable, InvalidationListener listener) { + super(observable); + this.listener = listener; + } + + @Override + protected SetExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected SetExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected SetExpressionHelper addListener(SetChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(SetChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + listener.invalidated(observable); + } + + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + listener.invalidated(observable); + } + } + + private static class SingleChange extends SetExpressionHelper { + + private final ChangeListener> listener; + private ObservableSet currentValue; + + private SingleChange(ObservableSetValue observable, ChangeListener> listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected SetExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected SetExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected SetExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(ChangeListener> listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected SetExpressionHelper addListener(SetChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(SetChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableSet oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + listener.changed(observable, oldValue, currentValue); + } + } + + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + listener.changed(observable, currentValue, currentValue); + } + } + + private static class SingleSetChange extends SetExpressionHelper { + + private final SetChangeListener listener; + private ObservableSet currentValue; + + private SingleSetChange(ObservableSetValue observable, SetChangeListener listener) { + super(observable); + this.listener = listener; + this.currentValue = observable.getValue(); + } + + @Override + protected SetExpressionHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected SetExpressionHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected SetExpressionHelper addListener(ChangeListener> listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected SetExpressionHelper removeListener(ChangeListener> listener) { + return this; + } + + @Override + protected SetExpressionHelper addListener(SetChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected SetExpressionHelper removeListener(SetChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent() { + final ObservableSet oldValue = currentValue; + currentValue = observable.getValue(); + if (currentValue != oldValue) { + final SimpleChange change = new SimpleChange(observable); + if (currentValue == null) { + for (final E element : oldValue) { + listener.onChanged(change.setRemoved(element)); + } + } else if (oldValue == null) { + for (final E element : currentValue) { + listener.onChanged(change.setAdded(element)); + } + } else { + for (final E element : oldValue) { + if (!currentValue.contains(element)) { + listener.onChanged(change.setRemoved(element)); + } + } + for (final E element : currentValue) { + if (!oldValue.contains(element)) { + listener.onChanged(change.setAdded(element)); + } + } + } + } + } + + @Override + protected void fireValueChangedEvent(final SetChangeListener.Change change) { + listener.onChanged(new SimpleChange(observable, change)); + } + } + + private static class Generic extends SetExpressionHelper { + + private InvalidationListener[] invalidationListeners; + private ChangeListener>[] changeListeners; + private SetChangeListener[] setChangeListeners; + private int invalidationSize; + private int changeSize; + private int setChangeSize; + private boolean locked; + private ObservableSet currentValue; + + private Generic(ObservableSetValue observable, InvalidationListener listener0, InvalidationListener listener1) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(ObservableSetValue observable, ChangeListener> listener0, ChangeListener> listener1) { + super(observable); + this.changeListeners = new ChangeListener[] {listener0, listener1}; + this.changeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableSetValue observable, SetChangeListener listener0, SetChangeListener listener1) { + super(observable); + this.setChangeListeners = new SetChangeListener[] {listener0, listener1}; + this.setChangeSize = 2; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableSetValue observable, InvalidationListener invalidationListener, ChangeListener> changeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableSetValue observable, InvalidationListener invalidationListener, SetChangeListener listChangeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.setChangeListeners = new SetChangeListener[] {listChangeListener}; + this.setChangeSize = 1; + this.currentValue = observable.getValue(); + } + + private Generic(ObservableSetValue observable, ChangeListener> changeListener, SetChangeListener listChangeListener) { + super(observable); + this.changeListeners = new ChangeListener[] {changeListener}; + this.changeSize = 1; + this.setChangeListeners = new SetChangeListener[] {listChangeListener}; + this.setChangeSize = 1; + this.currentValue = observable.getValue(); + } + + @Override + protected SetExpressionHelper addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected SetExpressionHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if ((changeSize == 1) && (setChangeSize == 0)) { + return new SingleChange(observable, changeListeners[0]); + } else if ((changeSize == 0) && (setChangeSize == 1)) { + return new SingleSetChange(observable, setChangeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0) && (setChangeSize == 0)) { + return new SingleInvalidation<>(observable, invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected SetExpressionHelper addListener(ChangeListener> listener) { + if (changeListeners == null) { + changeListeners = new ChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + if (changeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected SetExpressionHelper removeListener(ChangeListener> listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if ((invalidationSize == 1) && (setChangeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (setChangeSize == 1)) { + return new SingleSetChange(observable, setChangeListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0) && (setChangeSize == 0)) { + return new SingleChange<>(observable, changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final ChangeListener>[] oldListeners = changeListeners; + if (locked) { + changeListeners = new ChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected SetExpressionHelper addListener(SetChangeListener listener) { + if (setChangeListeners == null) { + setChangeListeners = new SetChangeListener[] {listener}; + setChangeSize = 1; + } else { + final int oldCapacity = setChangeListeners.length; + if (locked) { + final int newCapacity = (setChangeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + setChangeListeners = Arrays.copyOf(setChangeListeners, newCapacity); + } else if (setChangeSize == oldCapacity) { + setChangeSize = trim(setChangeSize, setChangeListeners); + if (setChangeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + setChangeListeners = Arrays.copyOf(setChangeListeners, newCapacity); + } + } + setChangeListeners[setChangeSize++] = listener; + } + if (setChangeSize == 1) { + currentValue = observable.getValue(); + } + return this; + } + + @Override + protected SetExpressionHelper removeListener(SetChangeListener listener) { + if (setChangeListeners != null) { + for (int index = 0; index < setChangeSize; index++) { + if (listener.equals(setChangeListeners[index])) { + if (setChangeSize == 1) { + if ((invalidationSize == 1) && (changeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } else if ((invalidationSize == 0) && (changeSize == 1)) { + return new SingleChange(observable, changeListeners[0]); + } + setChangeListeners = null; + setChangeSize = 0; + } else if ((setChangeSize == 2) && (invalidationSize == 0) && (changeSize == 0)) { + return new SingleSetChange<>(observable, setChangeListeners[1-index]); + } else { + final int numMoved = setChangeSize - index - 1; + final SetChangeListener[] oldListeners = setChangeListeners; + if (locked) { + setChangeListeners = new SetChangeListener[setChangeListeners.length]; + System.arraycopy(oldListeners, 0, setChangeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, setChangeListeners, index, numMoved); + } + setChangeSize--; + if (!locked) { + setChangeListeners[setChangeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent() { + if ((changeSize == 0) && (setChangeSize == 0)) { + notifyListeners(currentValue, null); + } else { + final ObservableSet oldValue = currentValue; + currentValue = observable.getValue(); + notifyListeners(oldValue, null); + } + } + + @Override + protected void fireValueChangedEvent(final SetChangeListener.Change change) { + final SimpleChange mappedChange = (setChangeSize == 0)? null : new SimpleChange(observable, change); + notifyListeners(currentValue, mappedChange); + } + + private void notifyListeners(ObservableSet oldValue, SimpleChange change) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final ChangeListener>[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + final SetChangeListener[] curListChangeList = setChangeListeners; + final int curListChangeSize = setChangeSize; + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + curInvalidationList[i].invalidated(observable); + } + if ((currentValue != oldValue) || (change != null)) { + for (int i = 0; i < curChangeSize; i++) { + curChangeList[i].changed(observable, oldValue, currentValue); + } + if (curListChangeSize > 0) { + if (change != null) { + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } else { + change = new SimpleChange(observable); + if (currentValue == null) { + for (final E element : oldValue) { + change.setRemoved(element); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } else if (oldValue == null) { + for (final E element : currentValue) { + change.setAdded(element); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } else { + for (final E element : oldValue) { + if (!currentValue.contains(element)) { + change.setRemoved(element); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } + for (final E element : currentValue) { + if (!oldValue.contains(element)) { + change.setAdded(element); + for (int i = 0; i < curListChangeSize; i++) { + curListChangeList[i].onChanged(change); + } + } + } + } + + } + } + } + } finally { + locked = false; + } + } + + } + + public static class SimpleChange extends SetChangeListener.Change { + + private E old; + private E added; + private boolean addOp; + + public SimpleChange(ObservableSet set) { + super(set); + } + + public SimpleChange(ObservableSet set, SetChangeListener.Change source) { + super(set); + old = source.getElementRemoved(); + added = source.getElementAdded(); + addOp = source.wasAdded(); + } + + public SimpleChange setRemoved(E old) { + this.old = old; + this.added = null; + addOp = false; + return this; + } + + public SimpleChange setAdded(E added) { + this.old = null; + this.added = added; + addOp = true; + return this; + } + + @Override + public boolean wasAdded() { + return addOp; + } + + @Override + public boolean wasRemoved() { + return !addOp; + } + + @Override + public E getElementAdded() { + return added; + } + + @Override + public E getElementRemoved() { + return old; + } + + @Override + public String toString() { + return addOp ? "added " + added : "removed " + old; + } + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringConstant.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringConstant.java new file mode 100644 index 00000000..657d3582 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringConstant.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.binding.StringExpression; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; + +public class StringConstant extends StringExpression { + + private final String value; + + private StringConstant(String value) { + this.value = value; + } + + public static StringConstant valueOf(String value) { + return new StringConstant(value); + } + + @Override + public String get() { + return value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public void addListener(InvalidationListener observer) { + // no-op + } + + @Override + public void addListener(ChangeListener observer) { + // no-op + } + + @Override + public void removeListener(InvalidationListener observer) { + // no-op + } + + @Override + public void removeListener(ChangeListener observer) { + // no-op + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringFormatter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringFormatter.java new file mode 100644 index 00000000..201a5081 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/StringFormatter.java @@ -0,0 +1,179 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.binding.StringBinding; +import com.tungsten.fclcore.fakefx.beans.binding.StringExpression; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public abstract class StringFormatter extends StringBinding { + + private static Object extractValue(Object obj) { + return obj instanceof ObservableValue ? ((ObservableValue)obj).getValue() : obj; + } + + private static Object[] extractValues(Object[] objs) { + final int n = objs.length; + final Object[] values = new Object[n]; + for (int i = 0; i < n; i++) { + values[i] = extractValue(objs[i]); + } + return values; + } + + private static ObservableValue[] extractDependencies(Object... args) { + final List> dependencies = new ArrayList>(); + for (final Object obj : args) { + if (obj instanceof ObservableValue) { + dependencies.add((ObservableValue) obj); + } + } + return dependencies.toArray(new ObservableValue[dependencies.size()]); + } + + public static StringExpression convert(final ObservableValue observableValue) { + if (observableValue == null) { + throw new NullPointerException("ObservableValue must be specified"); + } + if (observableValue instanceof StringExpression) { + return (StringExpression) observableValue; + } else { + return new StringBinding() { + { + super.bind(observableValue); + } + + @Override + public void dispose() { + super.unbind(observableValue); + } + + @Override + protected String computeValue() { + final Object value = observableValue.getValue(); + return (value == null)? "null" : value.toString(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.> singletonObservableList(observableValue); + } + }; + } + } + + public static StringExpression concat(final Object... args) { + if ((args == null) || (args.length == 0)) { + return StringConstant.valueOf(""); + } + if (args.length == 1) { + final Object cur = args[0]; + return cur instanceof ObservableValue ? convert((ObservableValue) cur) + : StringConstant.valueOf(cur.toString()); + } + if (extractDependencies(args).length == 0) { + final StringBuilder builder = new StringBuilder(); + for (final Object obj : args) { + builder.append(obj); + } + return StringConstant.valueOf(builder.toString()); + } + return new StringFormatter() { + { + super.bind(extractDependencies(args)); + } + + @Override + public void dispose() { + super.unbind(extractDependencies(args)); + } + + @Override + protected String computeValue() { + final StringBuilder builder = new StringBuilder(); + for (final Object obj : args) { + builder.append(extractValue(obj)); + } + return builder.toString(); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList(FXCollections + .observableArrayList(extractDependencies(args))); + } + }; + } + + public static StringExpression format(final Locale locale, final String format, final Object... args) { + if (format == null) { + throw new NullPointerException("Format cannot be null."); + } + if (extractDependencies(args).length == 0) { + return StringConstant.valueOf(String.format(locale, format, args)); + } + final StringFormatter formatter = new StringFormatter() { + { + super.bind(extractDependencies(args)); + } + + @Override + public void dispose() { + super.unbind(extractDependencies(args)); + } + + @Override + protected String computeValue() { + final Object[] values = extractValues(args); + return String.format(locale, format, values); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList(FXCollections + .observableArrayList(extractDependencies(args))); + } + }; + // Force calculation to check format + formatter.get(); + return formatter; + } + + public static StringExpression format(final String format, final Object... args) { + if (format == null) { + throw new NullPointerException("Format cannot be null."); + } + if (extractDependencies(args).length == 0) { + return StringConstant.valueOf(String.format(format, args)); + } + final StringFormatter formatter = new StringFormatter() { + { + super.bind(extractDependencies(args)); + } + + @Override + public void dispose() { + super.unbind(extractDependencies(args)); + } + + @Override + protected String computeValue() { + final Object[] values = extractValues(args); + return String.format(format, values); + } + + @Override + public ObservableList> getDependencies() { + return FXCollections.unmodifiableObservableList(FXCollections + .observableArrayList(extractDependencies(args))); + } + }; + // Force calculation to check format + formatter.get(); + return formatter; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/Subscription.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/Subscription.java new file mode 100644 index 00000000..a1975f9f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/binding/Subscription.java @@ -0,0 +1,97 @@ +package com.tungsten.fclcore.fakefx.binding; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * A subscription encapsulates how to cancel it without having + * to keep track of how it was created. + * + *

For example: + * + *

{@code Subscription s = property.subscribe(System.out::println)} + * + *

The function passed in to {@code subscribe} does not need to be stored + * in order to clean up the subscription later. + */ +@FunctionalInterface +public interface Subscription { + + /** + * An empty subscription. Does nothing when cancelled. + */ + static final Subscription EMPTY = () -> {}; + + /** + * Cancels this subscription. + */ + void unsubscribe(); + + /** + * Combines this {@link Subscription} with the given {@code Subscription} + * and returns a new {@code Subscription} which will cancel both when + * cancelled. + * + * @param other another {@link Subscription}, cannot be {@code null} + * @return a combined {@link Subscription} which will cancel both when + * cancelled, never {@code null} + * @throws NullPointerException when {@code other} is {@code null} + */ + default Subscription and(Subscription other) { + Objects.requireNonNull(other); + + return () -> { + unsubscribe(); + other.unsubscribe(); + }; + } + + /** + * Creates a {@link Subscription} on this {@link ObservableValue} which + * immediately provides its current value to the given {@code subscriber}, + * followed by any subsequent changes in value. + * + * @param subscriber a {@link Consumer} to supply with the values of this + * {@link ObservableValue}, cannot be {@code null} + * @return a {@link Subscription} which can be used to cancel this + * subscription, never {@code null} + * @throws NullPointerException when {@code observableValue} or {@code subscriber} is {@code null} + */ + static Subscription subscribe(ObservableValue observableValue, Consumer subscriber) { + Objects.requireNonNull(observableValue); + Objects.requireNonNull(subscriber); + + ChangeListener listener = (obs, old, current) -> subscriber.accept(current); + + subscriber.accept(observableValue.getValue()); // eagerly send current value + observableValue.addListener(listener); + + return () -> observableValue.removeListener(listener); + } + + /** + * Creates a {@link Subscription} on this {@link ObservableValue} which + * calls the given {@code runnable} whenever this {@code ObservableValue} + * becomes invalid. + * + * @param runnable a {@link Runnable} to call whenever this + * {@link ObservableValue} becomes invalid, cannot be @{code null} + * @return a {@link Subscription} which can be used to cancel this + * subscription, never @{code null} + * @throws NullPointerException when {@code observableValue} or {@code runnable} is {@code null} + */ + static Subscription subscribeInvalidations(ObservableValue observableValue, Runnable runnable) { + Objects.requireNonNull(observableValue); + Objects.requireNonNull(runnable); + + InvalidationListener listener = obs -> runnable.run(); + + observableValue.addListener(listener); + + return () -> observableValue.removeListener(listener); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayChangeListener.java new file mode 100644 index 00000000..0c129578 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayChangeListener.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + * Interface that receives notifications of changes to an ObservableArray. + * @since JavaFX 8.0 + */ +public interface ArrayChangeListener> { + + /** + * Called after a change has been made to an ObservableArray. + * + * @param observableArray the array that changed + * @param sizeChanged indicates size of array changed + * @param from A beginning (inclusive) of an interval related to the change + * @param to An end (exclusive) of an interval related to the change. + */ + public void onChanged(T observableArray, boolean sizeChanged, int from, int to); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayListenerHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayListenerHelper.java new file mode 100644 index 00000000..88a60199 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ArrayListenerHelper.java @@ -0,0 +1,321 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelperBase; + +import java.util.Arrays; + +/** + */ +public abstract class ArrayListenerHelper> extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static > ArrayListenerHelper addListener(ArrayListenerHelper helper, T observable, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleInvalidation(observable, listener) : helper.addListener(listener); + } + + public static ArrayListenerHelper removeListener(ArrayListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static > ArrayListenerHelper addListener(ArrayListenerHelper helper, T observable, ArrayChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(observable, listener) : helper.addListener(listener); + } + + public static ArrayListenerHelper removeListener(ArrayListenerHelper helper, ArrayChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(ArrayListenerHelper helper, boolean sizeChanged, int from, int to) { + if (helper != null && (from < to || sizeChanged)) { + helper.fireValueChangedEvent(sizeChanged, from, to); + } + } + + public static boolean hasListeners(ArrayListenerHelper helper) { + return helper != null; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected final T observable; + + public ArrayListenerHelper(T observable) { + this.observable = observable; + } + + protected abstract ArrayListenerHelper addListener(InvalidationListener listener); + protected abstract ArrayListenerHelper removeListener(InvalidationListener listener); + + protected abstract ArrayListenerHelper addListener(ArrayChangeListener listener); + protected abstract ArrayListenerHelper removeListener(ArrayChangeListener listener); + + protected abstract void fireValueChangedEvent(boolean sizeChanged, int from, int to); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation> extends ArrayListenerHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(T observable, InvalidationListener listener) { + super(observable); + this.listener = listener; + } + + @Override + protected ArrayListenerHelper addListener(InvalidationListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ArrayListenerHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected ArrayListenerHelper addListener(ArrayChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ArrayListenerHelper removeListener(ArrayChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent(boolean sizeChanged, int from, int to) { + try { + listener.invalidated(observable); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class SingleChange> extends ArrayListenerHelper { + + private final ArrayChangeListener listener; + + private SingleChange(T observable, ArrayChangeListener listener) { + super(observable); + this.listener = listener; + } + + @Override + protected ArrayListenerHelper addListener(InvalidationListener listener) { + return new Generic(observable, listener, this.listener); + } + + @Override + protected ArrayListenerHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected ArrayListenerHelper addListener(ArrayChangeListener listener) { + return new Generic(observable, this.listener, listener); + } + + @Override + protected ArrayListenerHelper removeListener(ArrayChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent(boolean sizeChanged, int from, int to) { + try { + listener.onChanged(observable, sizeChanged, from, to); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class Generic> extends ArrayListenerHelper { + + private InvalidationListener[] invalidationListeners; + private ArrayChangeListener[] changeListeners; + private int invalidationSize; + private int changeSize; + private boolean locked; + + private Generic(T observable, InvalidationListener listener0, InvalidationListener listener1) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(T observable, ArrayChangeListener listener0, ArrayChangeListener listener1) { + super(observable); + this.changeListeners = new ArrayChangeListener[] {listener0, listener1}; + this.changeSize = 2; + } + + private Generic(T observable, InvalidationListener invalidationListener, ArrayChangeListener changeListener) { + super(observable); + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new ArrayChangeListener[] {changeListener}; + this.changeSize = 1; + } + + @Override + protected Generic addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected ArrayListenerHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if (changeSize == 1) { + return new SingleChange(observable, changeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0)) { + return new SingleInvalidation(observable, invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected ArrayListenerHelper addListener(ArrayChangeListener listener) { + if (changeListeners == null) { + changeListeners = new ArrayChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + return this; + } + + @Override + protected ArrayListenerHelper removeListener(ArrayChangeListener listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if (invalidationSize == 1) { + return new SingleInvalidation(observable, invalidationListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0)) { + return new SingleChange(observable, changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final ArrayChangeListener[] oldListeners = changeListeners; + if (locked) { + changeListeners = new ArrayChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index+1); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent(boolean sizeChanged, int from, int to) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final ArrayChangeListener[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + try { + curInvalidationList[i].invalidated(observable); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + for (int i = 0; i < curChangeSize; i++) { + try { + curChangeList[i].onChanged(observable, sizeChanged, from, to); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } finally { + locked = false; + } + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ChangeHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ChangeHelper.java new file mode 100644 index 00000000..f28fa741 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ChangeHelper.java @@ -0,0 +1,33 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Arrays; +import java.util.List; + +public class ChangeHelper { + public static String addRemoveChangeToString(int from, int to, List list, List removed) { + StringBuilder b = new StringBuilder(); + + if (removed.isEmpty()) { + b.append(list.subList(from, to)); + b.append(" added at ").append(from); + } else { + b.append(removed); + if (from == to) { + b.append(" removed at ").append(from); + } else { + b.append(" replaced by "); + b.append(list.subList(from, to)); + b.append(" at ").append(from); + } + } + return b.toString(); + } + + public static String permChangeToString(int[] permutation) { + return "permutated by " + Arrays.toString(permutation); + } + + public static String updateChangeToString(int from, int to) { + return "updated at range [" + from + ", " + to + ")"; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObservableListDecorator.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObservableListDecorator.java new file mode 100644 index 00000000..d9e5cefe --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObservableListDecorator.java @@ -0,0 +1,232 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.util.Callback; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.RandomAccess; + +public final class ElementObservableListDecorator extends ObservableListBase implements ObservableList { + + private final ObservableList decoratedList; + private final ListChangeListener listener; + private ElementObserver observer; + + + public ElementObservableListDecorator(ObservableList decorated, + Callback extractor) { + this.observer = new ElementObserver(extractor, new Callback() { + + @Override + public InvalidationListener call(final E e) { + return new InvalidationListener() { + + @Override + public void invalidated(Observable observable) { + beginChange(); + int i = 0; + if (decoratedList instanceof RandomAccess) { + final int size = size(); + for (; i < size; ++i) { + if (get(i) == e) { + nextUpdate(i); + } + } + } else { + for (Iterator it = iterator(); it.hasNext();) { + if (it.next() == e) { + nextUpdate(i); + } + ++i; + } + } + endChange(); + } + }; + } + }, this); + this.decoratedList = decorated; + final int sz = decoratedList.size(); + for (int i = 0; i < sz; ++i) { + observer.attachListener(decoratedList.get(i)); + } + listener = new ListChangeListener() { + + @Override + public void onChanged(Change c) { + while (c.next()) { + if (c.wasAdded() || c.wasRemoved()) { + final int removedSize = c.getRemovedSize(); + final List removed = c.getRemoved(); + for (int i = 0; i < removedSize; ++i) { + observer.detachListener(removed.get(i)); + } + if (decoratedList instanceof RandomAccess) { + final int to = c.getTo(); + for (int i = c.getFrom(); i < to; ++i) { + observer.attachListener(decoratedList.get(i)); + } + } else { + for (E e : c.getAddedSubList()) { + observer.attachListener(e); + } + } + } + } + c.reset(); + fireChange(c); + } + }; + this.decoratedList.addListener(new WeakListChangeListener (listener)); + } + + @Override + public T[] toArray(T[] a) { + return decoratedList.toArray(a); + } + + @Override + public Object[] toArray() { + return decoratedList.toArray(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return decoratedList.subList(fromIndex, toIndex); + } + + @Override + public int size() { + return decoratedList.size(); + } + + @Override + public E set(int index, E element) { + return decoratedList.set(index, element); + } + + @Override + public boolean retainAll(Collection c) { + return decoratedList.retainAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return decoratedList.removeAll(c); + } + + @Override + public E remove(int index) { + return decoratedList.remove(index); + } + + @Override + public boolean remove(Object o) { + return decoratedList.remove(o); + } + + @Override + public ListIterator listIterator(int index) { + return decoratedList.listIterator(index); + } + + @Override + public ListIterator listIterator() { + return decoratedList.listIterator(); + } + + @Override + public int lastIndexOf(Object o) { + return decoratedList.lastIndexOf(o); + } + + @Override + public Iterator iterator() { + return decoratedList.iterator(); + } + + @Override + public boolean isEmpty() { + return decoratedList.isEmpty(); + } + + @Override + public int indexOf(Object o) { + return decoratedList.indexOf(o); + } + + @Override + public E get(int index) { + return decoratedList.get(index); + } + + @Override + public boolean containsAll(Collection c) { + return decoratedList.containsAll(c); + } + + @Override + public boolean contains(Object o) { + return decoratedList.contains(o); + } + + @Override + public void clear() { + decoratedList.clear(); + } + + @Override + public boolean addAll(int index, Collection c) { + return decoratedList.addAll(index, c); + } + + @Override + public boolean addAll(Collection c) { + return decoratedList.addAll(c); + } + + @Override + public void add(int index, E element) { + decoratedList.add(index, element); + } + + @Override + public boolean add(E e) { + return decoratedList.add(e); + } + + @Override + public boolean setAll(Collection col) { + return decoratedList.setAll(col); + } + + @Override + public boolean setAll(E... elements) { + return decoratedList.setAll(elements); + } + + @Override + public boolean retainAll(E... elements) { + return decoratedList.retainAll(elements); + } + + @Override + public boolean removeAll(E... elements) { + return decoratedList.removeAll(elements); + } + + @Override + public void remove(int from, int to) { + decoratedList.remove(from, to); + } + + @Override + public boolean addAll(E... elements) { + return decoratedList.addAll(elements); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObserver.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObserver.java new file mode 100644 index 00000000..db4b941f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ElementObserver.java @@ -0,0 +1,72 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.util.Callback; + +import java.util.IdentityHashMap; + +final class ElementObserver { + + private static class ElementsMapElement { + InvalidationListener listener; + int counter; + + public ElementsMapElement(InvalidationListener listener) { + this.listener = listener; + this.counter = 1; + } + + public void increment() { + counter++; + } + + public int decrement() { + return --counter; + } + + private InvalidationListener getListener() { + return listener; + } + } + + private Callback extractor; + private final Callback listenerGenerator; + private final ObservableListBase list; + private IdentityHashMap elementsMap = + new IdentityHashMap(); + + ElementObserver(Callback extractor, Callback listenerGenerator, ObservableListBase list) { + this.extractor = extractor; + this.listenerGenerator = listenerGenerator; + this.list = list; + } + + + void attachListener(final E e) { + if (elementsMap != null && e != null) { + if (elementsMap.containsKey(e)) { + elementsMap.get(e).increment(); + } else { + InvalidationListener listener = listenerGenerator.call(e); + for (Observable o : extractor.call(e)) { + o.addListener(listener); + } + elementsMap.put(e, new ElementsMapElement(listener)); + } + } + } + + void detachListener(E e) { + if (elementsMap != null && e != null) { + ElementsMapElement el = elementsMap.get(e); + for (Observable o : extractor.call(e)) { + o.removeListener(el.getListener()); + } + if (el.decrement() == 0) { + elementsMap.remove(e); + } + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FXCollections.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FXCollections.java new file mode 100644 index 00000000..fa7c5bdf --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FXCollections.java @@ -0,0 +1,2492 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.collections.ListListenerHelper; +import com.tungsten.fclcore.fakefx.collections.MapListenerHelper; +import com.tungsten.fclcore.fakefx.collections.SetListenerHelper; +import com.tungsten.fclcore.fakefx.util.Callback; + +import java.lang.reflect.Array; +import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; + +import java.util.RandomAccess; + +public class FXCollections { + /** Not to be instantiated. */ + private FXCollections() { } + + public static ObservableList observableList(List list) { + if (list == null) { + throw new NullPointerException(); + } + return list instanceof RandomAccess ? new ObservableListWrapper(list) : + new ObservableSequentialListWrapper(list); + } + + public static ObservableList observableList(List list, Callback extractor) { + if (list == null || extractor == null) { + throw new NullPointerException(); + } + return list instanceof RandomAccess ? new ObservableListWrapper(list, extractor) : + new ObservableSequentialListWrapper(list, extractor); + } + + public static ObservableMap observableMap(Map map) { + if (map == null) { + throw new NullPointerException(); + } + return new ObservableMapWrapper(map); + } + + public static ObservableSet observableSet(Set set) { + if (set == null) { + throw new NullPointerException(); + } + return new ObservableSetWrapper(set); + } + + public static ObservableSet observableSet(E... elements) { + if (elements == null) { + throw new NullPointerException(); + } + Set set = new HashSet(elements.length); + Collections.addAll(set, elements); + return new ObservableSetWrapper(set); + } + + public static ObservableMap unmodifiableObservableMap(ObservableMap map) { + if (map == null) { + throw new NullPointerException(); + } + return new UnmodifiableObservableMap(map); + } + + public static ObservableMap checkedObservableMap(ObservableMap map, Class keyType, Class valueType) { + if (map == null || keyType == null || valueType == null) { + throw new NullPointerException(); + } + return new CheckedObservableMap(map, keyType, valueType); + } + + public static ObservableMap synchronizedObservableMap(ObservableMap map) { + if (map == null) { + throw new NullPointerException(); + } + return new SynchronizedObservableMap(map); + } + + private static ObservableMap EMPTY_OBSERVABLE_MAP = new EmptyObservableMap(); + + @SuppressWarnings("unchecked") + public static ObservableMap emptyObservableMap() { + return EMPTY_OBSERVABLE_MAP; + } + + public static ObservableIntegerArray observableIntegerArray() { + return new ObservableIntegerArrayImpl(); + } + + public static ObservableIntegerArray observableIntegerArray(int... values) { + return new ObservableIntegerArrayImpl(values); + } + + public static ObservableIntegerArray observableIntegerArray(ObservableIntegerArray array) { + return new ObservableIntegerArrayImpl(array); + } + + public static ObservableFloatArray observableFloatArray() { + return new ObservableFloatArrayImpl(); + } + + public static ObservableFloatArray observableFloatArray(float... values) { + return new ObservableFloatArrayImpl(values); + } + + public static ObservableFloatArray observableFloatArray(ObservableFloatArray array) { + return new ObservableFloatArrayImpl(array); + } + + @SuppressWarnings("unchecked") + public static ObservableList observableArrayList() { + return observableList(new ArrayList()); + } + + public static ObservableList observableArrayList(Callback extractor) { + return observableList(new ArrayList(), extractor); + } + + public static ObservableList observableArrayList(E... items) { + return observableList(new ArrayList<>(Arrays.asList(items))); + } + + public static ObservableList observableArrayList(Collection col) { + return observableList(new ArrayList<>(col)); + } + + public static ObservableMap observableHashMap() { + return observableMap(new HashMap()); + } + + public static ObservableList concat(ObservableList... lists) { + if (lists.length == 0 ) { + return observableArrayList(); + } + if (lists.length == 1) { + return observableArrayList(lists[0]); + } + ArrayList backingList = new ArrayList(); + for (ObservableList s : lists) { + backingList.addAll(s); + } + + return observableList(backingList); + } + + public static ObservableList unmodifiableObservableList(ObservableList list) { + if (list == null) { + throw new NullPointerException(); + } + return new UnmodifiableObservableListImpl(list); + } + + public static ObservableList checkedObservableList(ObservableList list, Class type) { + if (list == null) { + throw new NullPointerException(); + } + return new CheckedObservableList(list, type); + } + + public static ObservableList synchronizedObservableList(ObservableList list) { + if (list == null) { + throw new NullPointerException(); + } + return new SynchronizedObservableList(list); + } + + private static ObservableList EMPTY_OBSERVABLE_LIST = new EmptyObservableList(); + + @SuppressWarnings("unchecked") + public static ObservableList emptyObservableList() { + return EMPTY_OBSERVABLE_LIST; + } + + public static ObservableList singletonObservableList(E e) { + return new SingletonObservableList(e); + } + + public static ObservableSet unmodifiableObservableSet(ObservableSet set) { + if (set == null) { + throw new NullPointerException(); + } + return new UnmodifiableObservableSet(set); + } + + public static ObservableSet checkedObservableSet(ObservableSet set, Class type) { + if (set == null) { + throw new NullPointerException(); + } + return new CheckedObservableSet(set, type); + } + + public static ObservableSet synchronizedObservableSet(ObservableSet set) { + if (set == null) { + throw new NullPointerException(); + } + return new SynchronizedObservableSet(set); + } + + private static ObservableSet EMPTY_OBSERVABLE_SET = new EmptyObservableSet(); + + @SuppressWarnings("unchecked") + public static ObservableSet emptyObservableSet() { + return EMPTY_OBSERVABLE_SET; + } + + @SuppressWarnings("unchecked") + public static void copy(ObservableList dest, List src) { + final int srcSize = src.size(); + if (srcSize > dest.size()) { + throw new IndexOutOfBoundsException("Source does not fit in dest"); + } + T[] destArray = (T[]) dest.toArray(); + System.arraycopy(src.toArray(), 0, destArray, 0, srcSize); + dest.setAll(destArray); + } + + @SuppressWarnings("unchecked") + public static void fill(ObservableList list, T obj) { + T[] newContent = (T[]) new Object[list.size()]; + Arrays.fill(newContent, obj); + list.setAll(newContent); + } + + @SuppressWarnings("unchecked") + public static boolean replaceAll(ObservableList list, T oldVal, T newVal) { + T[] newContent = (T[]) list.toArray(); + boolean modified = false; + for (int i = 0 ; i < newContent.length; ++i) { + if (newContent[i].equals(oldVal)) { + newContent[i] = newVal; + modified = true; + } + } + if (modified) { + list.setAll(newContent); + } + return modified; + } + + @SuppressWarnings("unchecked") + public static void reverse(ObservableList list) { + Object[] newContent = list.toArray(); + for (int i = 0; i < newContent.length / 2; ++i) { + Object tmp = newContent[i]; + newContent[i] = newContent[newContent.length - i - 1]; + newContent[newContent.length -i - 1] = tmp; + } + list.setAll(newContent); + } + + /** + * Rotates the list by distance. + * Fires only one change notification on the list. + * @param list the list to be rotated + * @param distance the distance of rotation + * @see Collections#rotate(List, int) + */ + @SuppressWarnings("unchecked") + public static void rotate(ObservableList list, int distance) { + Object[] newContent = list.toArray(); + + int size = list.size(); + distance = distance % size; + if (distance < 0) + distance += size; + if (distance == 0) + return; + + for (int cycleStart = 0, nMoved = 0; nMoved != size; cycleStart++) { + Object displaced = newContent[cycleStart]; + Object tmp; + int i = cycleStart; + do { + i += distance; + if (i >= size) + i -= size; + tmp = newContent[i]; + newContent[i] = displaced; + displaced = tmp; + nMoved ++; + } while(i != cycleStart); + } + list.setAll(newContent); + } + + /** + * Shuffles all elements in the observable list. + * Fires only one change notification on the list. + * @param list the list to shuffle + * @see Collections#shuffle(List) + */ + public static void shuffle(ObservableList list) { + if (r == null) { + r = new Random(); + } + shuffle(list, r); + } + private static Random r; + + /** + * Shuffles all elements in the observable list. + * Fires only one change notification on the list. + * @param list the list to be shuffled + * @param rnd the random generator used for shuffling + * @see Collections#shuffle(List, Random) + */ + @SuppressWarnings("unchecked") + public static void shuffle(ObservableList list, Random rnd) { + Object newContent[] = list.toArray(); + + for (int i = list.size(); i > 1; i--) { + swap(newContent, i - 1, rnd.nextInt(i)); + } + + list.setAll(newContent); + } + + private static void swap(Object[] arr, int i, int j) { + Object tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + + /** + * Sorts the provided observable list. + * Fires only one change notification on the list. + * @param The type of List to be wrapped + * @param list the list to be sorted + * @see Collections#sort(List) + */ + @SuppressWarnings("unchecked") + public static > void sort(ObservableList list) { + if (list instanceof SortableList) { + ((SortableList)list).sort(); + } else { + List newContent = new ArrayList(list); + Collections.sort(newContent); + list.setAll((Collection)newContent); + } + } + + /** + * Sorts the provided observable list using the c comparator. + * Fires only one change notification on the list. + * @param The type of List to be wrapped + * @param list the list to sort + * @param c comparator used for sorting. Null if natural ordering is required. + * @see Collections#sort(List, Comparator) + */ + @SuppressWarnings("unchecked") + public static void sort(ObservableList list, Comparator c) { + if (list instanceof SortableList) { + ((SortableList)list).sort(c); + } else { + List newContent = new ArrayList(list); + Collections.sort(newContent, c); + list.setAll((Collection)newContent); + } + } + + private static class EmptyObservableList extends AbstractList implements ObservableList { + + private static final ListIterator iterator = new ListIterator() { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Object next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPrevious() { + return false; + } + + @Override + public Object previous() { + throw new NoSuchElementException(); + } + + @Override + public int nextIndex() { + return 0; + } + + @Override + public int previousIndex() { + return -1; + } + + @Override + public void set(Object e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(Object e) { + throw new UnsupportedOperationException(); + } + }; + + public EmptyObservableList() { + } + + @Override + public final void addListener(InvalidationListener listener) { + } + + @Override + public final void removeListener(InvalidationListener listener) { + } + + + @Override + public void addListener(ListChangeListener o) { + } + + @Override + public void removeListener(ListChangeListener o) { + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean contains(Object o) { + return false; + } + + @Override + @SuppressWarnings("unchecked") + public Iterator iterator() { + return iterator; + } + + @Override + public boolean containsAll(Collection c) { + return c.isEmpty(); + } + + @Override + public E get(int index) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int indexOf(Object o) { + return -1; + } + + @Override + public int lastIndexOf(Object o) { + return -1; + } + + @Override + @SuppressWarnings("unchecked") + public ListIterator listIterator() { + return iterator; + } + + @Override + @SuppressWarnings("unchecked") + public ListIterator listIterator(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException(); + } + return iterator; + } + + @Override + public List subList(int fromIndex, int toIndex) { + if (fromIndex != 0 || toIndex != 0) { + throw new IndexOutOfBoundsException(); + } + return this; + } + + @Override + public boolean addAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int from, int to) { + throw new UnsupportedOperationException(); + } + } + + private static class SingletonObservableList extends AbstractList implements ObservableList { + + private final E element; + + public SingletonObservableList(E element) { + if (element == null) { + throw new NullPointerException(); + } + this.element = element; + } + + @Override + public boolean addAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int from, int to) { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(ListChangeListener o) { + } + + @Override + public void removeListener(ListChangeListener o) { + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean contains(Object o) { + return element.equals(o); + } + + @Override + public E get(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException(); + } + return element; + } + + } + + private static class UnmodifiableObservableListImpl extends ObservableListBase implements ObservableList { + + private final ObservableList backingList; + private final ListChangeListener listener; + + public UnmodifiableObservableListImpl(ObservableList backingList) { + this.backingList = backingList; + listener = c -> { + fireChange(new SourceAdapterChange(UnmodifiableObservableListImpl.this, c)); + }; + this.backingList.addListener(new WeakListChangeListener(listener)); + } + + @Override + public T get(int index) { + return backingList.get(index); + } + + @Override + public int size() { + return backingList.size(); + } + + @Override + public boolean addAll(T... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(T... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(T... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(T... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int from, int to) { + throw new UnsupportedOperationException(); + } + + } + + private static class SynchronizedList implements List { + final Object mutex; + private final List backingList; + + SynchronizedList(List list, Object mutex) { + this.backingList = list; + this.mutex = mutex; + } + + SynchronizedList(List list) { + this.backingList = list; + this.mutex = this; + } + + @Override + public int size() { + synchronized(mutex) { + return backingList.size(); + } + } + + @Override + public boolean isEmpty() { + synchronized(mutex) { + return backingList.isEmpty(); + } + } + + @Override + public boolean contains(Object o) { + synchronized(mutex) { + return backingList.contains(o); + } + } + + @Override + public Iterator iterator() { + return backingList.iterator(); + } + + @Override + public Object[] toArray() { + synchronized(mutex) { + return backingList.toArray(); + } + } + + @Override + public X[] toArray(X[] a) { + synchronized(mutex) { + return backingList.toArray(a); + } + } + + @Override + public boolean add(T e) { + synchronized(mutex) { + return backingList.add(e); + } + } + + @Override + public boolean remove(Object o) { + synchronized(mutex) { + return backingList.remove(o); + } + } + + @Override + public boolean containsAll(Collection c) { + synchronized(mutex) { + return backingList.containsAll(c); + } + } + + @Override + public boolean addAll(Collection c) { + synchronized(mutex) { + return backingList.addAll(c); + } + } + + @Override + public boolean addAll(int index, Collection c) { + synchronized(mutex) { + return backingList.addAll(index, c); + + } + } + + @Override + public boolean removeAll(Collection c) { + synchronized(mutex) { + return backingList.removeAll(c); + } + } + + @Override + public boolean retainAll(Collection c) { + synchronized(mutex) { + return backingList.retainAll(c); + } + } + + @Override + public void clear() { + synchronized(mutex) { + backingList.clear(); + } + } + + @Override + public T get(int index) { + synchronized(mutex) { + return backingList.get(index); + } + } + + @Override + public T set(int index, T element) { + synchronized(mutex) { + return backingList.set(index, element); + } + } + + @Override + public void add(int index, T element) { + synchronized(mutex) { + backingList.add(index, element); + } + } + + @Override + public T remove(int index) { + synchronized(mutex) { + return backingList.remove(index); + } + } + + @Override + public int indexOf(Object o) { + synchronized(mutex) { + return backingList.indexOf(o); + } + } + + @Override + public int lastIndexOf(Object o) { + synchronized(mutex) { + return backingList.lastIndexOf(o); + } + } + + @Override + public ListIterator listIterator() { + return backingList.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + synchronized(mutex) { + return backingList.listIterator(index); + } + } + + @Override + public List subList(int fromIndex, int toIndex) { + synchronized(mutex) { + return new SynchronizedList(backingList.subList(fromIndex, toIndex), + mutex); + } + } + + @Override + public String toString() { + synchronized(mutex) { + return backingList.toString(); + } + } + + @Override + public int hashCode() { + synchronized(mutex) { + return backingList.hashCode(); + } + } + + @Override + public boolean equals(Object o) { + synchronized(mutex) { + return backingList.equals(o); + } + } + + } + + private static class SynchronizedObservableList extends SynchronizedList implements ObservableList { + + private ListListenerHelper helper; + + private final ObservableList backingList; + private final ListChangeListener listener; + + SynchronizedObservableList(ObservableList seq) { + super(seq); + this.backingList = seq; + listener = c -> { + ListListenerHelper.fireValueChangedEvent(helper, new SourceAdapterChange(SynchronizedObservableList.this, c)); + }; + backingList.addListener(new WeakListChangeListener(listener)); + } + + @Override + public boolean addAll(T... elements) { + synchronized(mutex) { + return backingList.addAll(elements); + } + } + + @Override + public boolean setAll(T... elements) { + synchronized(mutex) { + return backingList.setAll(elements); + } + } + + @Override + public boolean removeAll(T... elements) { + synchronized(mutex) { + return backingList.removeAll(elements); + } + } + + @Override + public boolean retainAll(T... elements) { + synchronized(mutex) { + return backingList.retainAll(elements); + } + } + + @Override + public void remove(int from, int to) { + synchronized(mutex) { + backingList.remove(from, to); + } + } + + @Override + public boolean setAll(Collection col) { + synchronized(mutex) { + return backingList.setAll(col); + } + } + + @Override + public final void addListener(InvalidationListener listener) { + synchronized (mutex) { + helper = ListListenerHelper.addListener(helper, listener); + } + } + + @Override + public final void removeListener(InvalidationListener listener) { + synchronized (mutex) { + helper = ListListenerHelper.removeListener(helper, listener); + } + } + + @Override + public void addListener(ListChangeListener listener) { + synchronized (mutex) { + helper = ListListenerHelper.addListener(helper, listener); + } + } + + @Override + public void removeListener(ListChangeListener listener) { + synchronized (mutex) { + helper = ListListenerHelper.removeListener(helper, listener); + } + } + + + } + + private static class CheckedObservableList extends ObservableListBase implements ObservableList { + + private final ObservableList list; + private final Class type; + private final ListChangeListener listener; + + CheckedObservableList(ObservableList list, Class type) { + if (list == null || type == null) { + throw new NullPointerException(); + } + this.list = list; + this.type = type; + listener = c -> { + fireChange(new SourceAdapterChange(CheckedObservableList.this, c)); + }; + list.addListener(new WeakListChangeListener(listener)); + } + + void typeCheck(Object o) { + if (o != null && !type.isInstance(o)) { + throw new ClassCastException("Attempt to insert " + + o.getClass() + " element into collection with element type " + + type); + } + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public X[] toArray(X[] a) { + return list.toArray(a); + } + + @Override + public String toString() { + return list.toString(); + } + + @Override + public boolean remove(Object o) { + return list.remove(o); + } + + @Override + public boolean containsAll(Collection coll) { + return list.containsAll(coll); + } + + @Override + public boolean removeAll(Collection coll) { + return list.removeAll(coll); + } + + @Override + public boolean retainAll(Collection coll) { + return list.retainAll(coll); + } + + @Override + public boolean removeAll(T... elements) { + return list.removeAll(elements); + } + + @Override + public boolean retainAll(T... elements) { + return list.retainAll(elements); + } + + @Override + public void remove(int from, int to) { + list.remove(from, to); + } + + @Override + public void clear() { + list.clear(); + } + + @Override + public boolean equals(Object o) { + return o == this || list.equals(o); + } + + @Override + public int hashCode() { + return list.hashCode(); + } + + @Override + public T get(int index) { + return list.get(index); + } + + @Override + public T remove(int index) { + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public T set(int index, T element) { + typeCheck(element); + return list.set(index, element); + } + + @Override + public void add(int index, T element) { + typeCheck(element); + list.add(index, element); + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(int index, Collection c) { + T[] a = null; + try { + a = c.toArray((T[]) Array.newInstance(type, 0)); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + + return this.list.addAll(index, Arrays.asList(a)); + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(Collection coll) { + T[] a = null; + try { + a = coll.toArray((T[]) Array.newInstance(type, 0)); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + + return this.list.addAll(Arrays.asList(a)); + } + + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @Override + public ListIterator listIterator(final int index) { + return new ListIterator() { + + ListIterator i = list.listIterator(index); + + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public T next() { + return i.next(); + } + + @Override + public boolean hasPrevious() { + return i.hasPrevious(); + } + + @Override + public T previous() { + return i.previous(); + } + + @Override + public int nextIndex() { + return i.nextIndex(); + } + + @Override + public int previousIndex() { + return i.previousIndex(); + } + + @Override + public void remove() { + i.remove(); + } + + @Override + public void set(T e) { + typeCheck(e); + i.set(e); + } + + @Override + public void add(T e) { + typeCheck(e); + i.add(e); + } + }; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private final Iterator it = list.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public T next() { + return it.next(); + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + @Override + public boolean add(T e) { + typeCheck(e); + return list.add(e); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return Collections.checkedList(list.subList(fromIndex, toIndex), type); + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(T... elements) { + try { + T[] array = (T[]) Array.newInstance(type, elements.length); + System.arraycopy(elements, 0, array, 0, elements.length); + return list.addAll(array); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + } + + @Override + @SuppressWarnings("unchecked") + public boolean setAll(T... elements) { + try { + T[] array = (T[]) Array.newInstance(type, elements.length); + System.arraycopy(elements, 0, array, 0, elements.length); + return list.setAll(array); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + } + + @Override + @SuppressWarnings("unchecked") + public boolean setAll(Collection col) { + T[] a = null; + try { + a = col.toArray((T[]) Array.newInstance(type, 0)); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + + return list.setAll(Arrays.asList(a)); + } + } + + private static class EmptyObservableSet extends AbstractSet implements ObservableSet { + + public EmptyObservableSet() { + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(SetChangeListener listener) { + } + + @Override + public void removeListener(SetChangeListener listener) { + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean contains(Object obj) { + return false; + } + + @Override + public boolean containsAll(Collection c) { + return c.isEmpty(); + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public X[] toArray(X[] a) { + if (a.length > 0) + a[0] = null; + return a; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Object next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + } + + private static class UnmodifiableObservableSet extends AbstractSet implements ObservableSet { + + private final ObservableSet backingSet; + private SetListenerHelper listenerHelper; + private SetChangeListener listener; + + public UnmodifiableObservableSet(ObservableSet backingSet) { + this.backingSet = backingSet; + this.listener = null; + } + + private void initListener() { + if (listener == null) { + listener = c -> { + callObservers(new SetAdapterChange(UnmodifiableObservableSet.this, c)); + }; + this.backingSet.addListener(new WeakSetChangeListener(listener)); + } + } + + private void callObservers(SetChangeListener.Change change) { + SetListenerHelper.fireValueChangedEvent(listenerHelper, change); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator i = backingSet.iterator(); + + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public E next() { + return i.next(); + } + }; + } + + @Override + public int size() { + return backingSet.size(); + } + + @Override + public boolean isEmpty() { + return backingSet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingSet.contains(o); + } + + @Override + public void addListener(InvalidationListener listener) { + initListener(); + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + initListener(); + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(SetChangeListener listener) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } + + private static class SynchronizedSet implements Set { + final Object mutex; + private final Set backingSet; + + SynchronizedSet(Set set, Object mutex) { + this.backingSet = set; + this.mutex = mutex; + } + + SynchronizedSet(Set set) { + this.backingSet = set; + this.mutex = this; + } + + @Override + public int size() { + synchronized(mutex) { + return backingSet.size(); + } + } + + @Override + public boolean isEmpty() { + synchronized(mutex) { + return backingSet.isEmpty(); + } + } + + @Override + public boolean contains(Object o) { + synchronized(mutex) { + return backingSet.contains(o); + } + } + + @Override + public Iterator iterator() { + return backingSet.iterator(); + } + + @Override + public Object[] toArray() { + synchronized(mutex) { + return backingSet.toArray(); + } + } + + @Override + public X[] toArray(X[] a) { + synchronized(mutex) { + return backingSet.toArray(a); + } + } + + @Override + public boolean add(E e) { + synchronized(mutex) { + return backingSet.add(e); + } + } + + @Override + public boolean remove(Object o) { + synchronized(mutex) { + return backingSet.remove(o); + } + } + + @Override + public boolean containsAll(Collection c) { + synchronized(mutex) { + return backingSet.containsAll(c); + } + } + + @Override + public boolean addAll(Collection c) { + synchronized(mutex) { + return backingSet.addAll(c); + } + } + + @Override + public boolean retainAll(Collection c) { + synchronized(mutex) { + return backingSet.retainAll(c); + } + } + + @Override + public boolean removeAll(Collection c) { + synchronized(mutex) { + return backingSet.removeAll(c); + } + } + + @Override + public void clear() { + synchronized(mutex) { + backingSet.clear(); + } + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + synchronized(mutex) { + return backingSet.equals(o); + } + } + + @Override + public int hashCode() { + synchronized (mutex) { + return backingSet.hashCode(); + } + } + } + + private static class SynchronizedObservableSet extends SynchronizedSet implements ObservableSet { + + private final ObservableSet backingSet; + private SetListenerHelper listenerHelper; + private final SetChangeListener listener; + + SynchronizedObservableSet(ObservableSet set) { + super(set); + backingSet = set; + listener = c -> { + SetListenerHelper.fireValueChangedEvent(listenerHelper, new SetAdapterChange(SynchronizedObservableSet.this, c)); + }; + backingSet.addListener(new WeakSetChangeListener(listener)); + } + + @Override + public void addListener(InvalidationListener listener) { + synchronized (mutex) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + } + + @Override + public void removeListener(InvalidationListener listener) { + synchronized (mutex) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + } + @Override + public void addListener(SetChangeListener listener) { + synchronized (mutex) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + } + + @Override + public void removeListener(SetChangeListener listener) { + synchronized (mutex) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + } + } + + private static class CheckedObservableSet extends AbstractSet implements ObservableSet { + + private final ObservableSet backingSet; + private final Class type; + private SetListenerHelper listenerHelper; + private final SetChangeListener listener; + + CheckedObservableSet(ObservableSet set, Class type) { + if (set == null || type == null) { + throw new NullPointerException(); + } + backingSet = set; + this.type = type; + listener = c -> { + callObservers(new SetAdapterChange(CheckedObservableSet.this, c)); + }; + backingSet.addListener(new WeakSetChangeListener(listener)); + } + + private void callObservers(SetChangeListener.Change c) { + SetListenerHelper.fireValueChangedEvent(listenerHelper, c); + } + + void typeCheck(Object o) { + if (o != null && !type.isInstance(o)) { + throw new ClassCastException("Attempt to insert " + + o.getClass() + " element into collection with element type " + + type); + } + } + + @Override + public void addListener(InvalidationListener listener) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(SetChangeListener listener) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public int size() { + return backingSet.size(); + } + + @Override + public boolean isEmpty() { + return backingSet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingSet.contains(o); + } + + @Override + public Object[] toArray() { + return backingSet.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return backingSet.toArray(a); + } + + @Override + public boolean add(E e) { + typeCheck(e); + return backingSet.add(e); + } + + @Override + public boolean remove(Object o) { + return backingSet.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return backingSet.containsAll(c); + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(Collection c) { + E[] a = null; + try { + a = c.toArray((E[]) Array.newInstance(type, 0)); + } catch (ArrayStoreException e) { + throw new ClassCastException(); + } + + return backingSet.addAll(Arrays.asList(a)); + } + + @Override + public boolean retainAll(Collection c) { + return backingSet.retainAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return backingSet.removeAll(c); + } + + @Override + public void clear() { + backingSet.clear(); + } + + @Override + public boolean equals(Object o) { + return o == this || backingSet.equals(o); + } + + @Override + public int hashCode() { + return backingSet.hashCode(); + } + + @Override + public Iterator iterator() { + final Iterator it = backingSet.iterator(); + + return new Iterator() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public E next() { + return it.next(); + } + + @Override + public void remove() { + it.remove(); + } + }; + } + + } + + private static class EmptyObservableMap extends AbstractMap implements ObservableMap { + + public EmptyObservableMap() { + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(MapChangeListener listener) { + } + + @Override + public void removeListener(MapChangeListener listener) { + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public V get(Object key) { + return null; + } + + @Override + public Set keySet() { + return emptyObservableSet(); + } + + @Override + public Collection values() { + return emptyObservableSet(); + } + + @Override + public Set> entrySet() { + return emptyObservableSet(); + } + + @Override + public boolean equals(Object o) { + return (o instanceof Map) && ((Map)o).isEmpty(); + } + + @Override + public int hashCode() { + return 0; + } + } + + private static class CheckedObservableMap extends AbstractMap implements ObservableMap { + + private final ObservableMap backingMap; + private final Class keyType; + private final Class valueType; + private MapListenerHelper listenerHelper; + private final MapChangeListener listener; + + CheckedObservableMap(ObservableMap map, Class keyType, Class valueType) { + backingMap = map; + this.keyType = keyType; + this.valueType = valueType; + listener = c -> { + callObservers(new MapAdapterChange(CheckedObservableMap.this, c)); + }; + backingMap.addListener(new WeakMapChangeListener(listener)); + } + + private void callObservers(MapChangeListener.Change c) { + MapListenerHelper.fireValueChangedEvent(listenerHelper, c); + } + + void typeCheck(Object key, Object value) { + if (key != null && !keyType.isInstance(key)) { + throw new ClassCastException("Attempt to insert " + + key.getClass() + " key into map with key type " + + keyType); + } + + if (value != null && !valueType.isInstance(value)) { + throw new ClassCastException("Attempt to insert " + + value.getClass() + " value into map with value type " + + valueType); + } + } + + @Override + public void addListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public void addListener(MapChangeListener listener) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(MapChangeListener listener) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return backingMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return backingMap.containsValue(value); + } + + @Override + public V get(Object key) { + return backingMap.get(key); + } + + @Override + public V put(K key, V value) { + typeCheck(key, value); + return backingMap.put(key, value); + } + + @Override + public V remove(Object key) { + return backingMap.remove(key); + } + + @Override + @SuppressWarnings("unchecked") + public void putAll(Map t) { + // Satisfy the following goals: + // - good diagnostics in case of type mismatch + // - all-or-nothing semantics + // - protection from malicious t + // - correct behavior if t is a concurrent map + Object[] entries = t.entrySet().toArray(); + List> checked = + new ArrayList>(entries.length); + for (Object o : entries) { + Entry e = (Entry) o; + Object k = e.getKey(); + Object v = e.getValue(); + typeCheck(k, v); + checked.add( + new SimpleImmutableEntry((K) k, (V) v)); + } + for (Entry e : checked) + backingMap.put(e.getKey(), e.getValue()); + } + + @Override + public void clear() { + backingMap.clear(); + } + + @Override + public Set keySet() { + return backingMap.keySet(); + } + + @Override + public Collection values() { + return backingMap.values(); + } + + private transient Set> entrySet = null; + + @Override + public Set entrySet() { + if (entrySet==null) + entrySet = new CheckedEntrySet(backingMap.entrySet(), valueType); + return entrySet; + } + + @Override + public boolean equals(Object o) { + return o == this || backingMap.equals(o); + } + + @Override + public int hashCode() { + return backingMap.hashCode(); + } + + static class CheckedEntrySet implements Set> { + private final Set> s; + private final Class valueType; + + CheckedEntrySet(Set> s, Class valueType) { + this.s = s; + this.valueType = valueType; + } + + @Override + public int size() { + return s.size(); + } + + @Override + public boolean isEmpty() { + return s.isEmpty(); + } + + @Override + public String toString() { + return s.toString(); + } + + @Override + public int hashCode() { + return s.hashCode(); + } + + @Override + public void clear() { + s.clear(); + } + + @Override + public boolean add(Entry e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> coll) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator> iterator() { + final Iterator> i = s.iterator(); + final Class valueType = this.valueType; + + return new Iterator>() { + @Override + public boolean hasNext() { + return i.hasNext(); + } + + @Override + public void remove() { + i.remove(); + } + + @Override + public Entry next() { + return checkedEntry(i.next(), valueType); + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public Object[] toArray() { + Object[] source = s.toArray(); + + /* + * Ensure that we don't get an ArrayStoreException even if + * s.toArray returns an array of something other than Object + */ + Object[] dest = (CheckedEntry.class.isInstance( + source.getClass().getComponentType()) ? source : + new Object[source.length]); + + for (int i = 0; i < source.length; i++) + dest[i] = checkedEntry((Entry)source[i], + valueType); + return dest; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + // We don't pass a to s.toArray, to avoid window of + // vulnerability wherein an unscrupulous multithreaded client + // could get his hands on raw (unwrapped) Entries from s. + T[] arr = s.toArray(a.length==0 ? a : Arrays.copyOf(a, 0)); + + for (int i=0; i)arr[i], + valueType); + if (arr.length > a.length) + return arr; + + System.arraycopy(arr, 0, a, 0, arr.length); + if (a.length > arr.length) + a[arr.length] = null; + return a; + } + + /** + * This method is overridden to protect the backing set against + * an object with a nefarious equals function that senses + * that the equality-candidate is Map.Entry and calls its + * setValue method. + */ + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Entry e = (Entry) o; + return s.contains( + (e instanceof CheckedEntry) ? e : checkedEntry(e, valueType)); + } + + /** + * The bulk collection methods are overridden to protect + * against an unscrupulous collection whose contains(Object o) + * method senses when o is a Map.Entry, and calls o.setValue. + */ + @Override + public boolean containsAll(Collection c) { + for (Object o : c) + if (!contains(o)) // Invokes safe contains() above + return false; + return true; + } + + @Override + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) + return false; + return s.remove(new SimpleImmutableEntry + ((Entry)o)); + } + + @Override + public boolean removeAll(Collection c) { + return batchRemove(c, false); + } + + @Override + public boolean retainAll(Collection c) { + return batchRemove(c, true); + } + + private boolean batchRemove(Collection c, boolean complement) { + boolean modified = false; + Iterator> it = iterator(); + while (it.hasNext()) { + if (c.contains(it.next()) != complement) { + it.remove(); + modified = true; + } + } + return modified; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof Set)) + return false; + Set that = (Set) o; + return that.size() == s.size() + && containsAll(that); // Invokes safe containsAll() above + } + + static CheckedEntry checkedEntry(Entry e, + Class valueType) { + return new CheckedEntry(e, valueType); + } + + /** + * This "wrapper class" serves two purposes: it prevents + * the client from modifying the backing Map, by short-circuiting + * the setValue method, and it protects the backing Map against + * an ill-behaved Map.Entry that attempts to modify another + * Map.Entry when asked to perform an equality check. + */ + private static class CheckedEntry implements Entry { + private final Entry e; + private final Class valueType; + + CheckedEntry(Entry e, Class valueType) { + this.e = e; + this.valueType = valueType; + } + + @Override + public K getKey() { + return e.getKey(); + } + + @Override + public V getValue() { + return e.getValue(); + } + + @Override + public int hashCode() { + return e.hashCode(); + } + + @Override + public String toString() { + return e.toString(); + } + + @Override + public V setValue(V value) { + if (value != null && !valueType.isInstance(value)) + throw new ClassCastException(badValueMsg(value)); + return e.setValue(value); + } + + private String badValueMsg(Object value) { + return "Attempt to insert " + value.getClass() + + " value into map with value type " + valueType; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof Map.Entry)) + return false; + return e.equals(new SimpleImmutableEntry + ((Entry)o)); + } + } + } + + } + + private static class SynchronizedMap implements Map { + final Object mutex; + private final Map backingMap; + + SynchronizedMap(Map map) { + backingMap = map; + this.mutex = this; + } + + @Override + public int size() { + synchronized (mutex) { + return backingMap.size(); + } + } + + @Override + public boolean isEmpty() { + synchronized (mutex) { + return backingMap.isEmpty(); + } + } + + @Override + public boolean containsKey(Object key) { + synchronized (mutex) { + return backingMap.containsKey(key); + } + } + + @Override + public boolean containsValue(Object value) { + synchronized (mutex) { + return backingMap.containsValue(value); + } + } + + @Override + public V get(Object key) { + synchronized (mutex) { + return backingMap.get(key); + } + } + + @Override + public V put(K key, V value) { + synchronized (mutex) { + return backingMap.put(key, value); + } + } + + @Override + public V remove(Object key) { + synchronized (mutex) { + return backingMap.remove(key); + } + } + + @Override + public void putAll(Map m) { + synchronized (mutex) { + backingMap.putAll(m); + } + } + + @Override + public void clear() { + synchronized (mutex) { + backingMap.clear(); + } + } + + private transient Set keySet = null; + private transient Set> entrySet = null; + private transient Collection values = null; + + @Override + public Set keySet() { + synchronized(mutex) { + if (keySet==null) + keySet = new SynchronizedSet(backingMap.keySet(), mutex); + return keySet; + } + } + + @Override + public Collection values() { + synchronized(mutex) { + if (values==null) + values = new SynchronizedCollection(backingMap.values(), mutex); + return values; + } + } + + @Override + public Set> entrySet() { + synchronized(mutex) { + if (entrySet==null) + entrySet = new SynchronizedSet>(backingMap.entrySet(), mutex); + return entrySet; + } + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + synchronized(mutex) { + return backingMap.equals(o); + } + } + + @Override + public int hashCode() { + synchronized(mutex) { + return backingMap.hashCode(); + } + } + + } + + private static class SynchronizedCollection implements Collection { + + private final Collection backingCollection; + final Object mutex; + + SynchronizedCollection(Collection c, Object mutex) { + backingCollection = c; + this.mutex = mutex; + } + + SynchronizedCollection(Collection c) { + this(c, new Object()); + } + + @Override + public int size() { + synchronized (mutex) { + return backingCollection.size(); + } + } + + @Override + public boolean isEmpty() { + synchronized (mutex) { + return backingCollection.isEmpty(); + } + } + + @Override + public boolean contains(Object o) { + synchronized (mutex) { + return backingCollection.contains(o); + } + } + + @Override + public Iterator iterator() { + return backingCollection.iterator(); + } + + @Override + public Object[] toArray() { + synchronized (mutex) { + return backingCollection.toArray(); + } + } + + @Override + public T[] toArray(T[] a) { + synchronized (mutex) { + return backingCollection.toArray(a); + } + } + + @Override + public boolean add(E e) { + synchronized (mutex) { + return backingCollection.add(e); + } + } + + @Override + public boolean remove(Object o) { + synchronized (mutex) { + return backingCollection.remove(o); + } + } + + @Override + public boolean containsAll(Collection c) { + synchronized (mutex) { + return backingCollection.containsAll(c); + } + } + + @Override + public boolean addAll(Collection c) { + synchronized (mutex) { + return backingCollection.addAll(c); + } + } + + @Override + public boolean removeAll(Collection c) { + synchronized (mutex) { + return backingCollection.removeAll(c); + } + } + + @Override + public boolean retainAll(Collection c) { + synchronized (mutex) { + return backingCollection.retainAll(c); + } + } + + @Override + public void clear() { + synchronized (mutex) { + backingCollection.clear(); + } + } + } + + private static class SynchronizedObservableMap extends SynchronizedMap implements ObservableMap { + + private final ObservableMap backingMap; + private MapListenerHelper listenerHelper; + private final MapChangeListener listener; + + SynchronizedObservableMap(ObservableMap map) { + super(map); + backingMap = map; + listener = c -> { + MapListenerHelper.fireValueChangedEvent(listenerHelper, new MapAdapterChange(SynchronizedObservableMap.this, c)); + }; + backingMap.addListener(new WeakMapChangeListener(listener)); + } + + @Override + public void addListener(InvalidationListener listener) { + synchronized (mutex) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + } + + @Override + public void removeListener(InvalidationListener listener) { + synchronized (mutex) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + } + + @Override + public void addListener(MapChangeListener listener) { + synchronized (mutex) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + } + + @Override + public void removeListener(MapChangeListener listener) { + synchronized (mutex) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + } + + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FloatArraySyncer.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FloatArraySyncer.java new file mode 100644 index 00000000..771c2a47 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/FloatArraySyncer.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + */ +public interface FloatArraySyncer { + + /** + * This method is used to sync arrays on pulses. This method expects + * the same array was synced before. The usage is similar to toArray method + * so always use it as following: {@code dest = source.syncTo(dest);} + * @param array previously synced array + * @param fromAndLengthIndices an int array of 2 elements that states the + * start and length of elements modified. + * @return a synced array, which is the same or new array (depending on + * the change). + */ + float[] syncTo(float[] array, int[] fromAndLengthIndices); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ImmutableObservableList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ImmutableObservableList.java new file mode 100644 index 00000000..c4df6205 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ImmutableObservableList.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.Collection; + +public class ImmutableObservableList extends AbstractList implements ObservableList { + + private final E[] elements; + + public ImmutableObservableList(E... elements) { + this.elements = ((elements == null) || (elements.length == 0))? + null : Arrays.copyOf(elements, elements.length); + } + + @Override + public void addListener(InvalidationListener listener) { + // no-op + } + + @Override + public void removeListener(InvalidationListener listener) { + // no-op + } + + @Override + public void addListener(ListChangeListener listener) { + // no-op + } + + @Override + public void removeListener(ListChangeListener listener) { + // no-op + } + + @Override + public boolean addAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int from, int to) { + throw new UnsupportedOperationException(); + } + + @Override + public E get(int index) { + if ((index < 0) || (index >= size())) { + throw new IndexOutOfBoundsException(); + } + return elements[index]; + } + + @Override + public int size() { + return (elements == null)? 0 : elements.length; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/IntegerArraySyncer.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/IntegerArraySyncer.java new file mode 100644 index 00000000..4117effe --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/IntegerArraySyncer.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + */ +public interface IntegerArraySyncer { + + /** + * This method is used to sync arrays on pulses. This method expects + * the same array was synced before. The usage is similar to toArray method + * so always use it as following: {@code dest = source.syncTo(dest);} + * @param array previously synced array + * @param fromAndLengthIndices an int array of 2 elements that states the + * start and length of elements modified. + * @return a synced array, which is the same or new array (depending on + * the change). + */ + int[] syncTo(int[] array, int[] fromAndLengthIndices); +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeBuilder.java new file mode 100644 index 00000000..8afe9767 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeBuilder.java @@ -0,0 +1,695 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; + +final class ListChangeBuilder { + + private static final int[] EMPTY_PERM = new int[0]; + private final ObservableListBase list; + private int changeLock; + private List> addRemoveChanges; + private List> updateChanges; + private SubChange permutationChange; + + private void checkAddRemoveList() { + if (addRemoveChanges == null) { + addRemoveChanges = new ArrayList>(); + } + } + + private void checkState() { + if (changeLock == 0) { + throw new IllegalStateException("beginChange was not called on this builder"); + } + } + + private int findSubChange(int idx, final List> list) { + int from = 0; + int to = list.size() - 1; + + while (from <= to) { + int changeIdx = (from + to) / 2; + SubChange change = list.get(changeIdx); + + if (idx >= change.to) { + from = changeIdx + 1; + } else if (idx < change.from) { + to = changeIdx - 1; + } else { + return changeIdx; + } + } + return ~from; + } + + private void insertUpdate(int pos) { + int idx = findSubChange(pos, updateChanges); + if (idx < 0) { //If not found + idx = ~idx; + SubChange change; + if (idx > 0 && (change = updateChanges.get(idx - 1)).to == pos) { + change.to = pos + 1; + } else if (idx < updateChanges.size() && (change = updateChanges.get(idx)).from == pos + 1) { + change.from = pos; + } else { + updateChanges.add(idx, new SubChange(pos, pos + 1, null, EMPTY_PERM, true)); + } + } // If found, no need to do another update + } + + private void insertRemoved(int pos, final E removed) { + int idx = findSubChange(pos, addRemoveChanges); + if (idx < 0) { // Not found + idx = ~idx; + SubChange change; + + if (idx > 0 && (change = addRemoveChanges.get(idx - 1)).to == pos) { + change.removed.add(removed); + --idx; // Idx index will be used as a starting point for update + } else if (idx < addRemoveChanges.size() && (change = addRemoveChanges.get(idx)).from == pos + 1) { + change.from--; + change.to--; + change.removed.add(0, removed); + } else { + ArrayList removedList = new ArrayList(); + removedList.add(removed); + addRemoveChanges.add(idx, new SubChange(pos, pos, removedList, EMPTY_PERM, false)); + } + } else { + SubChange change = addRemoveChanges.get(idx); + change.to--; // Removed one element from the previously added list + if (change.from == change.to && (change.removed == null || change.removed.isEmpty())) { + addRemoveChanges.remove(idx); + } + } + for (int i = idx + 1; i < addRemoveChanges.size(); ++i) { + SubChange change = addRemoveChanges.get(i); + change.from--; + change.to--; + } + } + + private void insertAdd(int from, int to) { + int idx = findSubChange(from, addRemoveChanges); + final int numberOfAdded = to - from; + + if (idx < 0) { // Not found + idx = ~idx; + + SubChange change; + if (idx > 0 && (change = addRemoveChanges.get(idx - 1)).to == from) { + change.to = to; + --idx; + } else { + addRemoveChanges.add(idx, new SubChange(from, to, new ArrayList(), EMPTY_PERM, false)); + } + } else { + SubChange change = addRemoveChanges.get(idx); + change.to += numberOfAdded; + } + + for (int i = idx + 1; i < addRemoveChanges.size(); ++i) { + SubChange change = addRemoveChanges.get(i); + change.from += numberOfAdded; + change.to += numberOfAdded; + } + } + + private int compress(List> list) { + int removed = 0; + + SubChange prev = list.get(0); + for (int i = 1, sz = list.size(); i < sz; ++i) { + SubChange cur = list.get(i); + if (prev.to == cur.from) { + prev.to = cur.to; + if (prev.removed != null || cur.removed != null) { + if (prev.removed == null) { + prev.removed = new ArrayList(); + } + prev.removed.addAll(cur.removed); + } + list.set(i, null); + ++removed; + } else { + prev = cur; + } + } + return removed; + + } + + private static class SubChange { + + int from, to; + List removed; + int[] perm; + boolean updated; + + public SubChange(int from, int to, List removed, int[] perm, boolean updated) { + this.from = from; + this.to = to; + this.removed = removed; + this.perm = perm; + this.updated = updated; + } + } + + ListChangeBuilder(ObservableListBase list) { + this.list = list; + } + + public void nextRemove(int idx, E removed) { + checkState(); + checkAddRemoveList(); + + final SubChange last = addRemoveChanges.isEmpty() ? null + : addRemoveChanges.get(addRemoveChanges.size() - 1); + + if (last != null && last.to == idx) { + last.removed.add(removed); + } else if (last != null && last.from == idx + 1) { + last.from--; + last.to--; + last.removed.add(0, removed); + } else { + insertRemoved(idx, removed); + } + + if (updateChanges != null && !updateChanges.isEmpty()) { + int uPos = findSubChange(idx, updateChanges); + if (uPos < 0) { + uPos = ~uPos; + } else { + final SubChange change = updateChanges.get(uPos); + if (change.from == change.to - 1) { + updateChanges.remove(uPos); + } else { + change.to--; + ++uPos; // Do the update from the next position + } + } + for (int i = uPos; i < updateChanges.size(); ++i) { + updateChanges.get(i).from--; + updateChanges.get(i).to--; + } + } + + } + + public void nextRemove(int idx, List removed) { + checkState(); + + for (int i = 0; i < removed.size(); ++i) { + nextRemove(idx, removed.get(i)); + } + } + + public void nextAdd(int from, int to) { + checkState(); + checkAddRemoveList(); + final SubChange last = addRemoveChanges.isEmpty() ? null : + addRemoveChanges.get(addRemoveChanges.size() - 1); + final int numberOfAdded = to - from; + + if (last != null && last.to == from) { + last.to = to; + } else if (last != null && from >= last.from && from < last.to) { // Adding to the middle + last.to += numberOfAdded; + } else { + insertAdd(from, to); + } + + if (updateChanges != null && !updateChanges.isEmpty()) { + int uPos = findSubChange(from, updateChanges); + if (uPos < 0) { + uPos = ~uPos; + } else { + // We have to split the change into 2 + SubChange change = updateChanges.get(uPos); + updateChanges.add(uPos + 1, new SubChange(to, change.to + to - from, null, EMPTY_PERM, true)); + change.to = from; + uPos += 2; // skip those 2 for the update + } + for (int i = uPos; i < updateChanges.size(); ++i) { + updateChanges.get(i).from += numberOfAdded; + updateChanges.get(i).to += numberOfAdded; + } + } + + } + + public void nextPermutation(int from, int to, int[] perm) { + checkState(); + + int prePermFrom = from; + int prePermTo = to; + int[] prePerm = perm; + + if ((addRemoveChanges != null && !addRemoveChanges.isEmpty())) { + //Because there were already some changes to the list, we need + // to "reconstruct" the original list and create a permutation + // as-if there were no changes to the list. We can then + // merge this with the permutation we already did + + // This maps elements from current list to the original list. + // -1 means the map was not in the original list. + // Note that for performance reasons, the map is permutated when created + // by the permutation. So it basically contains the order in which the original + // items were permutated by our new permutation. + int[] mapToOriginal = new int[list.size()]; + // Marks the original-list indexes that were removed + Set removed = new TreeSet(); + int last = 0; + int offset = 0; + for (int i = 0, sz = addRemoveChanges.size(); i < sz; ++i) { + SubChange change = addRemoveChanges.get(i); + for (int j = last; j < change.from; ++j) { + mapToOriginal[j < from || j >= to ? j : perm[j - from]] = j + offset; + } + for (int j = change.from; j < change.to; ++j) { + mapToOriginal[j < from || j >= to ? j : perm[j - from]] = -1; + } + last = change.to; + int removedSize = (change.removed != null ? change.removed.size() : 0); + for (int j = change.from + offset, upTo = change.from + offset + removedSize; + j < upTo; ++j) { + removed.add(j); + } + offset += removedSize - (change.to - change.from); + + } + // from the last add/remove change to the end of the list + for (int i = last; i < mapToOriginal.length; ++i) { + mapToOriginal[i < from || i >= to ? i : perm[i - from]] = i + offset; + } + + int[] newPerm = new int[list.size() + offset]; + int mapPtr = 0; + for (int i = 0; i < newPerm.length; ++i) { + if (removed.contains(i)) { + newPerm[i] = i; + } else { + while(mapToOriginal[mapPtr] == -1) { + mapPtr++; + } + newPerm[mapToOriginal[mapPtr++]] = i; + } + } + + // We could theoretically find the first and last items such that + // newPerm[i] != i and trim the permutation, but it is not necessary + prePermFrom = 0; + prePermTo = newPerm.length; + prePerm = newPerm; + } + + + + if (permutationChange != null) { + if (prePermFrom == permutationChange.from && prePermTo == permutationChange.to) { + for (int i = 0; i < prePerm.length; ++i) { + permutationChange.perm[i] = prePerm[permutationChange.perm[i] - prePermFrom]; + } + } else { + final int newTo = Math.max(permutationChange.to, prePermTo); + final int newFrom = Math.min(permutationChange.from, prePermFrom); + int[] newPerm = new int[newTo - newFrom]; + + for (int i = newFrom; i < newTo; ++i) { + if (i < permutationChange.from || i >= permutationChange.to) { + newPerm[i - newFrom] = prePerm[i - prePermFrom]; + } else { + int p = permutationChange.perm[i - permutationChange.from]; + if (p < prePermFrom || p >= prePermTo) { + newPerm[i - newFrom] = p; + } else { + newPerm[i - newFrom] = prePerm[p - prePermFrom]; + } + } + } + + permutationChange.from = newFrom; + permutationChange.to = newTo; + permutationChange.perm = newPerm; + } + } else { + permutationChange = new SubChange(prePermFrom, prePermTo, null, prePerm, false); + } + + if ((addRemoveChanges != null && !addRemoveChanges.isEmpty())) { + Set newAdded = new TreeSet(); + Map> newRemoved = new HashMap>(); + for (int i = 0, sz = addRemoveChanges.size(); i < sz; ++i) { + SubChange change = addRemoveChanges.get(i); + for (int cIndex = change.from; cIndex < change.to; ++cIndex) { + if (cIndex < from || cIndex >= to) { + newAdded.add(cIndex); + } else { + newAdded.add(perm[cIndex - from]); + } + } + if (change.removed != null) { + if (change.from < from || change.from >= to) { + newRemoved.put(change.from, change.removed); + } else { + newRemoved.put(perm[change.from - from], change.removed); + } + } + } + addRemoveChanges.clear(); + SubChange lastChange = null; + for (Integer i : newAdded) { + if (lastChange == null || lastChange.to != i) { + lastChange = new SubChange(i, i + 1, null, EMPTY_PERM, false); + addRemoveChanges.add(lastChange); + } else { + lastChange.to = i + 1; + } + List removed = newRemoved.remove(i); + if (removed != null) { + if (lastChange.removed != null) { + lastChange.removed.addAll(removed); + } else { + lastChange.removed = removed; + } + } + } + + for(Entry> e : newRemoved.entrySet()) { + final Integer at = e.getKey(); + int idx = findSubChange(at, addRemoveChanges); + assert(idx < 0); + addRemoveChanges.add(~idx, new SubChange(at, at, e.getValue(), new int[0], false)); + } + } + + if (updateChanges != null && !updateChanges.isEmpty()) { + Set newUpdated = new TreeSet(); + for (int i = 0, sz = updateChanges.size(); i < sz; ++i) { + SubChange change = updateChanges.get(i); + for (int cIndex = change.from; cIndex < change.to; ++cIndex) { + if (cIndex < from || cIndex >= to) { + newUpdated.add(cIndex); + } else { + newUpdated.add(perm[cIndex - from]); + } + } + } + updateChanges.clear(); + SubChange lastUpdateChange = null; + for (Integer i : newUpdated) { + if (lastUpdateChange == null || lastUpdateChange.to != i) { + lastUpdateChange = new SubChange(i, i + 1, null, EMPTY_PERM, true); + updateChanges.add(lastUpdateChange); + } else { + lastUpdateChange.to = i + 1; + } + } + } + } + + + public void nextReplace(int from, int to, List removed) { + nextRemove(from, removed); + nextAdd(from, to); + } + + public void nextSet(int idx, E old) { + nextRemove(idx, old); + nextAdd(idx, idx + 1); + + } + + public void nextUpdate(int idx) { + checkState(); + if (updateChanges == null) { + updateChanges = new ArrayList>(); + } + final SubChange last = updateChanges.isEmpty() ? null : updateChanges.get(updateChanges.size() - 1); + if (last != null && last.to == idx) { + last.to = idx + 1; + } else { + insertUpdate(idx); + } + } + + private void commit() { + final boolean addRemoveNotEmpty = addRemoveChanges != null && !addRemoveChanges.isEmpty(); + final boolean updateNotEmpty = updateChanges != null && !updateChanges.isEmpty(); + if (changeLock == 0 + && (addRemoveNotEmpty + || updateNotEmpty + || permutationChange != null)) { + int totalSize = (updateChanges != null ? updateChanges.size() : 0) + + (addRemoveChanges != null ? addRemoveChanges.size() : 0) + (permutationChange != null ? 1 : 0); + if (totalSize == 1) { + if (addRemoveNotEmpty) { + list.fireChange(new SingleChange(finalizeSubChange(addRemoveChanges.get(0)), list)); + addRemoveChanges.clear(); + } else if (updateNotEmpty) { + list.fireChange(new SingleChange(finalizeSubChange(updateChanges.get(0)), list)); + updateChanges.clear(); + } else { + list.fireChange(new SingleChange(finalizeSubChange(permutationChange), list)); + permutationChange = null; + } + } else { + if (updateNotEmpty) { + int removed = compress(updateChanges); + totalSize -= removed; + } + if (addRemoveNotEmpty) { + int removed = compress(addRemoveChanges); + totalSize -= removed; + } + + SubChange[] array = new SubChange[totalSize]; + int ptr = 0; + if (permutationChange != null) { + array[ptr++] = permutationChange; + } + if (addRemoveNotEmpty) { + int sz = addRemoveChanges.size(); + for (int i = 0; i < sz; ++i) { + final SubChange change = addRemoveChanges.get(i); + if (change != null) { + array[ptr++] = change; + } + } + } + if (updateNotEmpty) { + int sz = updateChanges.size(); + for (int i = 0; i < sz; ++i) { + final SubChange change = updateChanges.get(i); + if (change != null) { + array[ptr++] = change; + } + } + } + list.fireChange(new IterableChange(finalizeSubChangeArray(array), list)); + if (addRemoveChanges != null) addRemoveChanges.clear(); + if (updateChanges != null) updateChanges.clear(); + permutationChange = null; + } + } + } + + public void beginChange() { + changeLock++; + } + + public void endChange() { + if (changeLock <= 0) { + throw new IllegalStateException("Called endChange before beginChange"); + } + changeLock--; + commit(); + } + + private static SubChange[] finalizeSubChangeArray(final SubChange[] changes) { + for (SubChange c : changes) { + finalizeSubChange(c); + } + return changes; + } + + private static SubChange finalizeSubChange(final SubChange c) { + if (c.perm == null) { + c.perm = EMPTY_PERM; + } + if (c.removed == null) { + c.removed = Collections.emptyList(); + } else { + c.removed = Collections.unmodifiableList(c.removed); + } + return c; + } + + private static class SingleChange extends Change { + private final SubChange change; + private boolean onChange; + + public SingleChange(SubChange change, ObservableListBase list) { + super(list); + this.change = change; + } + + @Override + public boolean next() { + if (onChange) { + return false; + } + onChange = true; + return true; + } + + @Override + public void reset() { + onChange = false; + } + + @Override + public int getFrom() { + checkState(); + return change.from; + } + + @Override + public int getTo() { + checkState(); + return change.to; + } + + @Override + public List getRemoved() { + checkState(); + return change.removed; + } + + @Override + protected int[] getPermutation() { + checkState(); + return change.perm; + } + + @Override + public boolean wasUpdated() { + checkState(); + return change.updated; + } + + private void checkState() { + if (!onChange) { + throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change."); + } + } + + @Override + public String toString() { + String ret; + if (change.perm.length != 0) { + ret = ChangeHelper.permChangeToString(change.perm); + } else if (change.updated) { + ret = ChangeHelper.updateChangeToString(change.from, change.to); + } else { + ret = ChangeHelper.addRemoveChangeToString(change.from, change.to, getList(), change.removed); + } + return "{ " + ret + " }"; + } + + } + + + private static class IterableChange extends Change { + + private SubChange[] changes; + private int cursor = -1; + + private IterableChange(SubChange[] changes, ObservableList list) { + super(list); + this.changes = changes; + } + + @Override + public boolean next() { + if (cursor + 1 < changes.length) { + ++cursor; + return true; + } + return false; + } + + @Override + public void reset() { + cursor = -1; + } + + @Override + public int getFrom() { + checkState(); + return changes[cursor].from; + } + + @Override + public int getTo() { + checkState(); + return changes[cursor].to; + } + + @Override + public List getRemoved() { + checkState(); + return changes[cursor].removed; + } + + @Override + protected int[] getPermutation() { + checkState(); + return changes[cursor].perm; + } + + @Override + public boolean wasUpdated() { + checkState(); + return changes[cursor].updated; + } + + private void checkState() { + if (cursor == -1) { + throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change."); + } + } + + @Override + public String toString() { + int c = 0; + StringBuilder b = new StringBuilder(); + b.append("{ "); + while (c < changes.length) { + if (changes[c].perm.length != 0) { + b.append(ChangeHelper.permChangeToString(changes[c].perm)); + } else if (changes[c].updated) { + b.append(ChangeHelper.updateChangeToString(changes[c].from, changes[c].to)); + } else { + b.append(ChangeHelper.addRemoveChangeToString(changes[c].from, changes[c].to, getList(), changes[c].removed)); + } + if (c != changes.length - 1) { + b.append(", "); + } + ++c; + } + b.append(" }"); + return b.toString(); + } + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeListener.java new file mode 100644 index 00000000..144fa5bb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListChangeListener.java @@ -0,0 +1,281 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Collections; +import java.util.List; + +/** + * Interface that receives notifications of changes to an ObservableList. + * + * @param the list element type + * @see Change + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface ListChangeListener { + + /** + * Represents a report of changes done to an {@link ObservableList}. The change may consist of one or more actual + * changes and must be iterated by calling the {@link #next()} method. + * + * Each change must be one of the following: + *

    + *
  • Permutation change : {@link #wasPermutated()} returns true in this case. + * The permutation happened at range between {@link #getFrom() from} (inclusive) and {@link #getTo() to} (exclusive) and + * can be queried by calling {@link #getPermutation(int)} method. + *
  • Add or remove change : In this case, at least one of the {@link #wasAdded()}, {@link #wasRemoved()} returns true. + * If both methods return true, {@link #wasReplaced()} will also return true. + *

    The {@link #getRemoved()} method returns a list of elements that have been + * replaced or removed from the list. + *

    The range between {@link #getFrom() from} (inclusive) and {@link #getTo() to} (exclusive) + * denotes the sublist of the list that contain new elements. Note that this is a half-open + * interval, so if no elements were added, {@code getFrom()} is equal to {@code getTo()}. + *

    It is possible to get a list of added elements by calling getAddedSubList(). + *

    Note that in order to maintain correct indexes of the separate add/remove changes, these changes + * must be sorted by their {@code from} index. + *

  • Update change : {@link #wasUpdated()} return true on an update change. + * All elements between {@link #getFrom() from} (inclusive) and {@link #getTo() to} (exclusive) were updated. + *
+ * + * Important: It's necessary to call {@link #next()} method before calling + * any other method of {@code Change}. The same applies after calling {@link #reset()}. + * The only methods that works at any time is {@link #getList()}. + * + *

+ * Typical usage is to observe changes on an ObservableList in order + * to hook or unhook (or add or remove a listener) or in order to maintain + * some invariant on every element in that ObservableList. A common code + * pattern for doing this looks something like the following:
+ * + *

+     * ObservableList<Item> theList = ...;
+     *
+     * theList.addListener(new ListChangeListener<Item>() {
+     *     public void onChanged(Change<Item> c) {
+     *         while (c.next()) {
+     *             if (c.wasPermutated()) {
+     *                 for (int i = c.getFrom(); i < c.getTo(); ++i) {
+     *                      //permutate
+     *                 }
+     *             } else if (c.wasUpdated()) {
+     *                      //update item
+     *             } else {
+     *                 for (Item remitem : c.getRemoved()) {
+     *                     remitem.remove(Outer.this);
+     *                 }
+     *                 for (Item additem : c.getAddedSubList()) {
+     *                     additem.add(Outer.this);
+     *                 }
+     *             }
+     *         }
+     *     });
+     *
+     * }
+ *

+ * Warning: This class directly accesses the source list to acquire information about the changes. + *
This effectively makes the Change object invalid when another change occurs on the list. + *
For this reason it is not safe to use this class on a different thread. + *
It also means the source list cannot be modified inside the listener since that would invalidate this Change object + * for all subsequent listeners. + *

+ * Note: in case the change contains multiple changes of different type, these changes must be in the following order: + * permutation change(s), add or remove changes, update changes + * This is because permutation changes cannot go after add/remove changes as they would change the position of added elements. + * And on the other hand, update changes must go after add/remove changes because they refer with their indexes to the current + * state of the list, which means with all add/remove changes applied. + * @param the list element type + * @since JavaFX 2.0 + */ + public abstract static class Change { + private final ObservableList list; + + /** + * Goes to the next change. + * The Change instance, in its initial state, is invalid and requires a call to {@code next()} before + * calling other methods. The first {@code next()} call will make this object + * represent the first change. + * @return {@code true} if switched to the next change, {@code false} if this is the last change + */ + public abstract boolean next(); + + /** + * Resets to the initial stage. After this call, {@link #next()} must be called + * before working with the first change. + */ + public abstract void reset(); + + /** + * Constructs a new Change instance on the given list. + * @param list The list that was changed + */ + public Change(ObservableList list) { + this.list = list; + } + + /** + * The source list of the change. + * @return a list that was changed + */ + public ObservableList getList() { + return list; + } + + /** + * If {@link #wasAdded()} is true, the interval contains all the values that were added. + * If {@link #wasPermutated()} is true, the interval marks the values that were permutated. + * If {@link #wasRemoved()} is true and {@code wasAdded} is false, {@link #getFrom()} and {@link #getTo()} + * should return the same number - the place where the removed elements were positioned in the list. + * @return a beginning (inclusive) of an interval related to the change + * @throws IllegalStateException if this Change instance is in initial state + */ + public abstract int getFrom(); + + /** + * The end of the change interval. + * @return an end (exclusive) of an interval related to the change + * @throws IllegalStateException if this Change instance is in initial state + * @see #getFrom() + */ + public abstract int getTo(); + + /** + * An immutable list of removed/replaced elements. If no elements + * were removed from the list, an empty list is returned. + * @return a list with all the removed elements + * @throws IllegalStateException if this Change instance is in initial state + */ + public abstract List getRemoved(); + + /** + * Indicates if the change was only a permutation. + * @return {@code true} if the change was just a permutation + * @throws IllegalStateException if this Change instance is in initial state + */ + public boolean wasPermutated() { + return getPermutation().length != 0; + } + + /** + * Indicates if elements were added during this change. + * @return {@code true} if something was added to the list + * @throws IllegalStateException if this Change instance is in initial state + */ + public boolean wasAdded() { + return !wasPermutated() && !wasUpdated() && getFrom() < getTo(); + } + + /** + * Indicates if elements were removed during this change. + * Note that using set will also produce a change with {@code wasRemoved()} returning + * true. See {@link #wasReplaced()}. + * @return {@code true} if something was removed from the list + * @throws IllegalStateException if this Change instance is in initial state + */ + public boolean wasRemoved() { + return !getRemoved().isEmpty(); + } + + /** + * Indicates if elements were replaced during this change. + * This is usually true when set is called on the list. + * Set operation will act like remove and add operation at the same time. + *

+ * Usually, it's not necessary to use this method directly. + * Handling remove operation and then add operation, as in the example in + * the {@link Change} class javadoc, will effectively handle the set operation. + * + * @return same as {@code wasAdded() && wasRemoved()} + * @throws IllegalStateException if this Change instance is in initial state + */ + public boolean wasReplaced() { + return wasAdded() && wasRemoved(); + } + + /** + * Indicates that the elements between {@link #getFrom()} (inclusive) + * to {@link #getTo()} exclusive has changed. + * This is the only optional event type and may not be + * fired by all ObservableLists. + * @return {@code true} if the current change is an update change + * @since JavaFX 2.1 + */ + public boolean wasUpdated() { + return false; + } + + /** + * Returns a subList view of the list that contains only the elements added. This is actually a shortcut to + * c.getList().subList(c.getFrom(), c.getTo()); + * + *

{@code
+         * for (Node n : change.getAddedSubList()) {
+         *       // do something
+         * }
+         * }
+ * @return the newly created sublist view that contains all the added elements + * @throws IllegalStateException if this Change instance is in initial state + */ + public List getAddedSubList() { + return wasAdded()? getList().subList(getFrom(), getTo()) : Collections.emptyList(); + } + + /** + * Returns the size of {@link #getRemoved()} list. + * @return the number of removed items + * @throws IllegalStateException if this Change instance is in initial state + */ + public int getRemovedSize() { + return getRemoved().size(); + } + + /** + * Returns the size of the interval that was added. + * @return the number of added items + * @throws IllegalStateException if this Change instance is in initial state + */ + public int getAddedSize() { + return wasAdded() ? getTo() - getFrom() : 0; + } + + /** + * If this change is a permutation, it returns an integer array + * that describes the permutation. + * This array maps directly from the previous indexes to the new ones. + * This method is not publicly accessible and therefore can return an array safely. + * The 0 index of the array corresponds to index {@link #getFrom()} of the list. The same applies + * for the last index and {@link #getTo()}. + * The method is used by {@link #wasPermutated() } and {@link #getPermutation(int)} methods. + * @return empty array if this is not permutation or an integer array containing the permutation + * @throws IllegalStateException if this Change instance is in initial state + */ + protected abstract int[] getPermutation(); + + /** + * This method allows developers to observe the permutations that occurred in this change. In order to get the + * new position of an element, you must call: + *
+         *    change.getPermutation(oldIndex);
+         * 
+ * + * Note: default implementation of this method takes the information + * from {@link #getPermutation()} method. You don't have to override this method. + * @param i the old index that contained the element prior to this change + * @return the new index of the same element + * @throws IndexOutOfBoundsException if i is out of the bounds of the list + * @throws IllegalStateException if this is not a permutation change + */ + public int getPermutation(int i) { + if (!wasPermutated()) { + throw new IllegalStateException("Not a permutation change"); + } + return getPermutation()[i - getFrom()]; + } + + } + /** + * Called after a change has been made to an ObservableList. + * + * @param c an object representing the change that was done + * @see Change + */ + public void onChanged(Change c); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListListenerHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListListenerHelper.java new file mode 100644 index 00000000..29b43cf7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ListListenerHelper.java @@ -0,0 +1,313 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelperBase; + +import java.util.Arrays; + +/** + */ +public abstract class ListListenerHelper extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static ListListenerHelper addListener(ListListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleInvalidation(listener) : helper.addListener(listener); + } + + public static ListListenerHelper removeListener(ListListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static ListListenerHelper addListener(ListListenerHelper helper, ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(listener) : helper.addListener(listener); + } + + public static ListListenerHelper removeListener(ListListenerHelper helper, ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(ListListenerHelper helper, ListChangeListener.Change change) { + if (helper != null) { + change.reset(); + helper.fireValueChangedEvent(change); + } + } + + public static boolean hasListeners(ListListenerHelper helper) { + return helper != null; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected abstract ListListenerHelper addListener(InvalidationListener listener); + protected abstract ListListenerHelper removeListener(InvalidationListener listener); + + protected abstract ListListenerHelper addListener(ListChangeListener listener); + protected abstract ListListenerHelper removeListener(ListChangeListener listener); + + protected abstract void fireValueChangedEvent(ListChangeListener.Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends ListListenerHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(InvalidationListener listener) { + this.listener = listener; + } + + @Override + protected ListListenerHelper addListener(InvalidationListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected ListListenerHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected ListListenerHelper addListener(ListChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected ListListenerHelper removeListener(ListChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent(ListChangeListener.Change change) { + try { + listener.invalidated(change.getList()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class SingleChange extends ListListenerHelper { + + private final ListChangeListener listener; + + private SingleChange(ListChangeListener listener) { + this.listener = listener; + } + + @Override + protected ListListenerHelper addListener(InvalidationListener listener) { + return new Generic(listener, this.listener); + } + + @Override + protected ListListenerHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected ListListenerHelper addListener(ListChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected ListListenerHelper removeListener(ListChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent(ListChangeListener.Change change) { + try { + listener.onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class Generic extends ListListenerHelper { + + private InvalidationListener[] invalidationListeners; + private ListChangeListener[] changeListeners; + private int invalidationSize; + private int changeSize; + private boolean locked; + + private Generic(InvalidationListener listener0, InvalidationListener listener1) { + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(ListChangeListener listener0, ListChangeListener listener1) { + this.changeListeners = new ListChangeListener[] {listener0, listener1}; + this.changeSize = 2; + } + + private Generic(InvalidationListener invalidationListener, ListChangeListener changeListener) { + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new ListChangeListener[] {changeListener}; + this.changeSize = 1; + } + + @Override + protected Generic addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected ListListenerHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if (changeSize == 1) { + return new SingleChange(changeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0)) { + return new SingleInvalidation(invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected ListListenerHelper addListener(ListChangeListener listener) { + if (changeListeners == null) { + changeListeners = new ListChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + return this; + } + + @Override + protected ListListenerHelper removeListener(ListChangeListener listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if (invalidationSize == 1) { + return new SingleInvalidation(invalidationListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0)) { + return new SingleChange(changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final ListChangeListener[] oldListeners = changeListeners; + if (locked) { + changeListeners = new ListChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent(ListChangeListener.Change change) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final ListChangeListener[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + try { + curInvalidationList[i].invalidated(change.getList()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + for (int i = 0; i < curChangeSize; i++) { + change.reset(); + try { + curChangeList[i].onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } finally { + locked = false; + } + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapAdapterChange.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapAdapterChange.java new file mode 100644 index 00000000..b670bea8 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapAdapterChange.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.collections.MapChangeListener.Change; + +public class MapAdapterChange extends MapChangeListener.Change { + private final Change change; + + public MapAdapterChange(ObservableMap map, Change change) { + super(map); + this.change = change; + } + + @Override + public boolean wasAdded() { + return change.wasAdded(); + } + + @Override + public boolean wasRemoved() { + return change.wasRemoved(); + } + + @Override + public K getKey() { + return change.getKey(); + } + + @Override + public V getValueAdded() { + return change.getValueAdded(); + } + + @Override + public V getValueRemoved() { + return change.getValueRemoved(); + } + + @Override + public String toString() { + return change.toString(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapChangeListener.java new file mode 100644 index 00000000..80775e90 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapChangeListener.java @@ -0,0 +1,88 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + * Interface that receives notifications of changes to an ObservableMap. + * @param the key element type + * @param the value element type + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface MapChangeListener { + + /** + * An elementary change done to an ObservableMap. + * Change contains information about a put or remove operation. + * Note that put operation might remove an element if there was + * already a value associated with the same key. In this case + * wasAdded() and wasRemoved() will both return true. + * + * @param key type + * @param value type + * @since JavaFX 2.0 + */ + public static abstract class Change { + + private final ObservableMap map; + + /** + * Constructs a change associated with a map. + * @param map the source of the change + */ + public Change(ObservableMap map) { + this.map = map; + } + + /** + * An observable map that is associated with the change. + * @return the source map + */ + public ObservableMap getMap() { + return map; + } + + /** + * If this change is a result of add operation. + * @return true if a new value (or key-value) entry was added to the map + */ + public abstract boolean wasAdded(); + + /** + * If this change is a result of removal operation. + * Note that an element might be removed even as a result of put operation. + * @return true if an old value (or key-value) entry was removed from the map + */ + public abstract boolean wasRemoved(); + + /** + * A key associated with the change. + * If the change is a remove change, the key no longer exist in a map. + * Otherwise, the key got set to a new value. + * @return the key that changed + */ + public abstract K getKey(); + + /** + * Get the new value of the key. Return null if this is a removal. + * @return the value that is now associated with the key + */ + public abstract V getValueAdded(); + + /** + * Get the old value of the key. This is null if and only if the value was + * added to the key that was not previously in the map. + * @return the value previously associated with the key + */ + public abstract V getValueRemoved(); + + } + + /** + * Called after a change has been made to an ObservableMap. + * This method is called on every elementary change (put/remove) once. + * This means, complex changes like keySet().removeAll(Collection) or clear() + * may result in more than one call of onChanged method. + * + * @param change the change that was made + */ + void onChanged(Change change); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapListenerHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapListenerHelper.java new file mode 100644 index 00000000..5e890a9d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MapListenerHelper.java @@ -0,0 +1,312 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelperBase; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener; + +import java.util.Arrays; + +/** + */ +public abstract class MapListenerHelper extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static MapListenerHelper addListener(MapListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleInvalidation(listener) : helper.addListener(listener); + } + + public static MapListenerHelper removeListener(MapListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static MapListenerHelper addListener(MapListenerHelper helper, MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(listener) : helper.addListener(listener); + } + + public static MapListenerHelper removeListener(MapListenerHelper helper, MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(MapListenerHelper helper, MapChangeListener.Change change) { + if (helper != null) { + helper.fireValueChangedEvent(change); + } + } + + public static boolean hasListeners(MapListenerHelper helper) { + return helper != null; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected abstract MapListenerHelper addListener(InvalidationListener listener); + protected abstract MapListenerHelper removeListener(InvalidationListener listener); + + protected abstract MapListenerHelper addListener(MapChangeListener listener); + protected abstract MapListenerHelper removeListener(MapChangeListener listener); + + protected abstract void fireValueChangedEvent(MapChangeListener.Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends MapListenerHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(InvalidationListener listener) { + this.listener = listener; + } + + @Override + protected MapListenerHelper addListener(InvalidationListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected MapListenerHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected MapListenerHelper addListener(MapChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected MapListenerHelper removeListener(MapChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + try { + listener.invalidated(change.getMap()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class SingleChange extends MapListenerHelper { + + private final MapChangeListener listener; + + private SingleChange(MapChangeListener listener) { + this.listener = listener; + } + + @Override + protected MapListenerHelper addListener(InvalidationListener listener) { + return new Generic(listener, this.listener); + } + + @Override + protected MapListenerHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected MapListenerHelper addListener(MapChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected MapListenerHelper removeListener(MapChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + try { + listener.onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class Generic extends MapListenerHelper { + + private InvalidationListener[] invalidationListeners; + private MapChangeListener[] changeListeners; + private int invalidationSize; + private int changeSize; + private boolean locked; + + private Generic(InvalidationListener listener0, InvalidationListener listener1) { + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(MapChangeListener listener0, MapChangeListener listener1) { + this.changeListeners = new MapChangeListener[] {listener0, listener1}; + this.changeSize = 2; + } + + private Generic(InvalidationListener invalidationListener, MapChangeListener changeListener) { + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new MapChangeListener[] {changeListener}; + this.changeSize = 1; + } + + @Override + protected Generic addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected MapListenerHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if (changeSize == 1) { + return new SingleChange(changeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0)) { + return new SingleInvalidation(invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected MapListenerHelper addListener(MapChangeListener listener) { + if (changeListeners == null) { + changeListeners = new MapChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + return this; + } + + @Override + protected MapListenerHelper removeListener(MapChangeListener listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if (invalidationSize == 1) { + return new SingleInvalidation(invalidationListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0)) { + return new SingleChange(changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final MapChangeListener[] oldListeners = changeListeners; + if (locked) { + changeListeners = new MapChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent(MapChangeListener.Change change) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final MapChangeListener[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + try { + curInvalidationList[i].invalidated(change.getMap()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + for (int i = 0; i < curChangeSize; i++) { + try { + curChangeList[i].onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } finally { + locked = false; + } + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MappingChange.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MappingChange.java new file mode 100644 index 00000000..c3eb1e62 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/MappingChange.java @@ -0,0 +1,131 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.AbstractList; +import java.util.List; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; + +public final class MappingChange extends Change{ + private final Map map; + private final Change original; + private List removed; + + public static final Map NOOP_MAP = new Map() { + + @Override + public Object map(Object original) { + return original; + } + }; + + public static interface Map { + F map(E original); + } + + public MappingChange(Change original, Map map, ObservableList list) { + super(list); + this.original = original; + this.map = map; + } + + @Override + public boolean next() { + return original.next(); + } + + @Override + public void reset() { + original.reset(); + } + + @Override + public int getFrom() { + return original.getFrom(); + } + + @Override + public int getTo() { + return original.getTo(); + } + + @Override + public List getRemoved() { + if (removed == null) { + removed = new AbstractList() { + + @Override + public F get(int index) { + return map.map(original.getRemoved().get(index)); + } + + @Override + public int size() { + return original.getRemovedSize(); + } + }; + } + return removed; + } + + @Override + protected int[] getPermutation() { + return new int[0]; + } + + @Override + public boolean wasPermutated() { + return original.wasPermutated(); + } + + @Override + public boolean wasUpdated() { + return original.wasUpdated(); + } + + @Override + public int getPermutation(int i) { + return original.getPermutation(i); + } + + @Override + public String toString() { + // Get the current position. We don't want to store the current position explicitely, + // just for the toString(), so we need to iterate the changes twice. This shouldn't + // be an issue, given the average number of change sub-parts + int posToEnd = 0; + while (next()) { + posToEnd++; + } + + int size = 0; + reset(); + while (next()) { + size++; + } + reset(); + StringBuilder b = new StringBuilder(); + b.append("{ "); + int pos = 0; + while (next()) { + if (wasPermutated()) { + b.append(ChangeHelper.permChangeToString(getPermutation())); + } else if (wasUpdated()) { + b.append(ChangeHelper.updateChangeToString(getFrom(), getTo())); + } else { + b.append(ChangeHelper.addRemoveChangeToString(getFrom(), getTo(), getList(), getRemoved())); + } + if (pos != size) { + b.append(", "); + } + } + b.append(" }"); + + reset(); + pos = size - posToEnd; + while (pos-- > 0) { + next(); + } + + return b.toString(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ModifiableObservableListBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ModifiableObservableListBase.java new file mode 100644 index 00000000..489840af --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ModifiableObservableListBase.java @@ -0,0 +1,398 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Abstract class that serves as a base class for {@link ObservableList} implementations that are modifiable. + * + * To implement a modifiable {@code ObservableList} class, you just need to implement the following set of methods: + *
    + *
  • {@link #get(int) get(int)} + *
  • {@link #size() size()} + *
  • {@link #doAdd(int, Object) doAdd(int, Object)} + *
  • {@link #doRemove(int) doRemove(int)} + *
  • {@link #doSet(int, Object) doSet(int, Object)} + *
+ * + * and the notifications and built and fired automatically for you. + * + *

Example of a simple {@code ObservableList} delegating to another {@code List} would look like this: + * + *

+ *
+ *   public class ArrayObservableList<E> extends ModifiableObservableList<E> {
+ *
+ *   private final List<E> delegate = new ArrayList<>();
+ *
+ *   public E get(int index) {
+ *       return delegate.get(index);
+ *   }
+ *
+ *   public int size() {
+ *       return delegate.size();
+ *   }
+ *
+ *   protected void doAdd(int index, E element) {
+ *       delegate.add(index, element);
+ *   }
+ *
+ *   protected E doSet(int index, E element) {
+ *       return delegate.set(index, element);
+ *   }
+ *
+ *   protected E doRemove(int index) {
+ *       return delegate.remove(index);
+ *   }
+ *
+ * 
+ * + * @param the type of the elements contained in the List + * @see ObservableListBase + * @since JavaFX 8.0 + */ +public abstract class ModifiableObservableListBase extends ObservableListBase { + + /** + * Creates a default {@code ModifiableObservableListBase}. + */ + public ModifiableObservableListBase() { + } + + @Override + public boolean setAll(Collection col) { + if (isEmpty() && col.isEmpty()) return false; + beginChange(); + try { + clear(); + addAll(col); + return true; + } finally { + endChange(); + } + } + + @Override + public boolean addAll(Collection c) { + beginChange(); + try { + boolean res = super.addAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public boolean addAll(int index, Collection c) { + beginChange(); + try { + boolean res = super.addAll(index, c); + return res; + } finally { + endChange(); + } + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + beginChange(); + try { + super.removeRange(fromIndex, toIndex); + } finally { + endChange(); + } + } + + @Override + public boolean removeAll(Collection c) { + beginChange(); + try { + boolean res = super.removeAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public boolean retainAll(Collection c) { + beginChange(); + try { + boolean res = super.retainAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public void add(int index, E element) { + doAdd(index, element); + beginChange(); + nextAdd(index, index + 1); + ++modCount; + endChange(); + } + + @Override + public E set(int index, E element) { + E old = doSet(index, element); + beginChange(); + nextSet(index, old); + endChange(); + return old; + } + + @Override + public boolean remove(Object o) { + int i = indexOf(o); + if (i != - 1) { + remove(i); + return true; + } + return false; + } + + @Override + public E remove(int index) { + E old = doRemove(index); + beginChange(); + nextRemove(index, old); + ++modCount; + endChange(); + return old; + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new SubObservableList(super.subList(fromIndex, toIndex)); + } + + @Override + public abstract E get(int index); + + @Override + public abstract int size(); + + /** + * Adds the {@code element} to the List at the position of {@code index}. + * + *

For the description of possible exceptions, please refer to the documentation + * of {@link #add(Object) } method. + * + * @param index the position where to add the element + * @param element the element that will be added + + * @throws ClassCastException if the type of the specified element is + * incompatible with this list + * @throws NullPointerException if the specified arguments contain one or + * more null elements + * @throws IllegalArgumentException if some property of this element + * prevents it from being added to this list + * @throws IndexOutOfBoundsException if the index is out of range + * {@code (index < 0 || index > size())} + */ + protected abstract void doAdd(int index, E element); + + /** + * Sets the {@code element} in the List at the position of {@code index}. + * + *

For the description of possible exceptions, please refer to the documentation + * of {@link #set(int, Object) } method. + * + * @param index the position where to set the element + * @param element the element that will be set at the specified position + * @return the old element at the specified position + * + * @throws ClassCastException if the type of the specified element is + * incompatible with this list + * @throws NullPointerException if the specified arguments contain one or + * more null elements + * @throws IllegalArgumentException if some property of this element + * prevents it from being added to this list + * @throws IndexOutOfBoundsException if the index is out of range + * {@code (index < 0 || index >= size())} + */ + protected abstract E doSet(int index, E element); + + /** + * Removes the element at position of {@code index}. + * + * @param index the index of the removed element + * @return the removed element + * + * @throws IndexOutOfBoundsException if the index is out of range + * {@code (index < 0 || index >= size())} + */ + protected abstract E doRemove(int index); + + private class SubObservableList implements List { + + public SubObservableList(List sublist) { + this.sublist = sublist; + } + private List sublist; + + @Override + public int size() { + return sublist.size(); + } + + @Override + public boolean isEmpty() { + return sublist.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return sublist.contains(o); + } + + @Override + public Iterator iterator() { + return sublist.iterator(); + } + + @Override + public Object[] toArray() { + return sublist.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return sublist.toArray(a); + } + + @Override + public boolean add(E e) { + return sublist.add(e); + } + + @Override + public boolean remove(Object o) { + return sublist.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return sublist.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + beginChange(); + try { + boolean res = sublist.addAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public boolean addAll(int index, Collection c) { + beginChange(); + try { + boolean res = sublist.addAll(index, c); + return res; + } finally { + endChange(); + } + } + + @Override + public boolean removeAll(Collection c) { + beginChange(); + try { + boolean res = sublist.removeAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public boolean retainAll(Collection c) { + beginChange(); + try { + boolean res = sublist.retainAll(c); + return res; + } finally { + endChange(); + } + } + + @Override + public void clear() { + beginChange(); + try { + sublist.clear(); + } finally { + endChange(); + } + } + + @Override + public E get(int index) { + return sublist.get(index); + } + + @Override + public E set(int index, E element) { + return sublist.set(index, element); + } + + @Override + public void add(int index, E element) { + sublist.add(index, element); + } + + @Override + public E remove(int index) { + return sublist.remove(index); + } + + @Override + public int indexOf(Object o) { + return sublist.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return sublist.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return sublist.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return sublist.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new SubObservableList(sublist.subList(fromIndex, toIndex)); + } + + @Override + public boolean equals(Object obj) { + return sublist.equals(obj); + } + + @Override + public int hashCode() { + return sublist.hashCode(); + } + + @Override + public String toString() { + return sublist.toString(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/NonIterableChange.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/NonIterableChange.java new file mode 100644 index 00000000..65d30aee --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/NonIterableChange.java @@ -0,0 +1,179 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Collections; +import java.util.List; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; + +public abstract class NonIterableChange extends Change { + + private final int from; + private final int to; + private boolean invalid = true; + + protected NonIterableChange(int from, int to, ObservableList list) { + super(list); + this.from = from; + this.to = to; + } + + @Override + public int getFrom() { + checkState(); + return from; + } + + @Override + public int getTo() { + checkState(); + return to; + } + + private static final int[] EMPTY_PERM = new int[0]; + + @Override + protected int[] getPermutation() { + checkState(); + return EMPTY_PERM; + } + + @Override + public boolean next() { + if (invalid) { + invalid = false; + return true; + } + return false; + } + + @Override + public void reset() { + invalid = true; + } + + public void checkState() { + if (invalid) { + throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change."); + } + } + + @Override + public String toString() { + boolean oldInvalid = invalid; + invalid = false; + String ret; + if (wasPermutated()) { + ret = ChangeHelper.permChangeToString(getPermutation()); + } else if (wasUpdated()) { + ret = ChangeHelper.updateChangeToString(from, to); + } else { + ret = ChangeHelper.addRemoveChangeToString(from, to, getList(), getRemoved()); + } + invalid = oldInvalid; + return "{ " + ret + " }"; + } + + public static class GenericAddRemoveChange extends NonIterableChange { + + private final List removed; + + public GenericAddRemoveChange(int from, int to, List removed, ObservableList list) { + super(from, to, list); + this.removed = removed; + } + + @Override + public List getRemoved() { + checkState(); + return removed; + } + + } + + public static class SimpleRemovedChange extends NonIterableChange { + + private final List removed; + public SimpleRemovedChange(int from, int to, E removed, ObservableList list) { + super(from, to, list); + this.removed = Collections.singletonList(removed); + } + + @Override + public boolean wasRemoved() { + checkState(); + return true; + } + + @Override + public List getRemoved() { + checkState(); + return removed; + } + + } + + public static class SimpleAddChange extends NonIterableChange { + + public SimpleAddChange(int from, int to, ObservableList list) { + super(from, to, list); + } + + @Override + public boolean wasRemoved() { + checkState(); + return false; + } + + @Override + public List getRemoved() { + checkState(); + return Collections.emptyList(); + } + + } + + public static class SimplePermutationChange extends NonIterableChange{ + + private final int[] permutation; + + public SimplePermutationChange(int from, int to, int[] permutation, ObservableList list) { + super(from, to, list); + this.permutation = permutation; + } + + + @Override + public List getRemoved() { + checkState(); + return Collections.emptyList(); + } + + @Override + protected int[] getPermutation() { + checkState(); + return permutation; + } + } + + public static class SimpleUpdateChange extends NonIterableChange{ + + public SimpleUpdateChange(int position, ObservableList list) { + this(position, position + 1, list); + } + + public SimpleUpdateChange(int from, int to, ObservableList list) { + super(from, to, list); + } + + @Override + public List getRemoved() { + return Collections.emptyList(); + } + + @Override + public boolean wasUpdated() { + return true; + } + + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArray.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArray.java new file mode 100644 index 00000000..6de420ff --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArray.java @@ -0,0 +1,72 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.Observable; + +/** + * {@code ObservableArray} is an array that allows listeners to track changes + * when they occur. In order to track changes, the internal array + * is encapsulated and there is no direct access available from the outside. + * Bulk operations are supported but they always do a copy of the data range. + * You can find them in subclasses as they deal with primitive arrays directly. + * + *

Implementations have both {@code capacity}, which is internal array length, + * and {@code size}. If size needs to be increased beyond capacity, the capacity + * increases to match that new size. Use {@link #trimToSize()} method + * to shrink it. + * + * @see ArrayChangeListener + * @param actual array instance type + * @since JavaFX 8.0 + */ +public interface ObservableArray> extends Observable { + + /** + * Add a listener to this observable array. + * @param listener the listener for listening to the array changes + * @throws NullPointerException if {@code listener} is {@code null} + */ + public void addListener(ArrayChangeListener listener); + + /** + * Tries to remove a listener from this observable array. If the listener is not + * attached to this array, nothing happens. + * @param listener a listener to remove + * @throws NullPointerException if {@code listener} is {@code null} + */ + public void removeListener(ArrayChangeListener listener); + + /** + * Sets new length of data in this array. This method grows capacity + * if necessary but never shrinks it. Resulting array will contain existing + * data for indexes that are less than the current size and zeroes for + * indexes that are greater than the current size. + * @param size new length of data in this array + * @throws NegativeArraySizeException if size is negative + */ + public void resize(int size); + + /** + * Grows the capacity of this array if the current capacity is less than + * given {@code capacity}, does nothing if it already exceeds + * the {@code capacity}. + * @param capacity the capacity of this array + */ + public void ensureCapacity(int capacity); + + /** + * Shrinks the capacity to the current size of data in the array. + */ + public void trimToSize(); + + /** + * Empties the array by resizing it to 0. Capacity is not changed. + * @see #trimToSize() + */ + public void clear(); + + /** + * Retrieves length of data in this array. + * @return length of data in this array + */ + public int size(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArrayBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArrayBase.java new file mode 100644 index 00000000..33ed1e3a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableArrayBase.java @@ -0,0 +1,51 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +/** + * Abstract class that serves as a base class for {@link ObservableArray} implementations. + * The base class provides listener handling functionality by implementing + * {@code addListener} and {@code removeListener} methods. + * {@link #fireChange(boolean, int, int) } method is provided + * for notifying the listeners. + * @param actual array instance type + * @see ObservableArray + * @see ArrayChangeListener + * @since JavaFX 8.0 + */ +public abstract class ObservableArrayBase> implements ObservableArray { + + private ArrayListenerHelper listenerHelper; + + /** + * Creates a default {@code ObservableArrayBase}. + */ + public ObservableArrayBase() { + } + + @Override public final void addListener(InvalidationListener listener) { + listenerHelper = ArrayListenerHelper.addListener(listenerHelper, (T) this, listener); + } + + @Override public final void removeListener(InvalidationListener listener) { + listenerHelper = ArrayListenerHelper.removeListener(listenerHelper, listener); + } + + @Override public final void addListener(ArrayChangeListener listener) { + listenerHelper = ArrayListenerHelper.addListener(listenerHelper, (T) this, listener); + } + + @Override public final void removeListener(ArrayChangeListener listener) { + listenerHelper = ArrayListenerHelper.removeListener(listenerHelper, listener); + } + + /** + * Notifies all listeners of a change + * @param sizeChanged indicates size of array changed + * @param from A beginning (inclusive) of an interval related to the change + * @param to An end (exclusive) of an interval related to the change. + */ + protected final void fireChange(boolean sizeChanged, int from, int to) { + ArrayListenerHelper.fireValueChangedEvent(listenerHelper, sizeChanged, from, to); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArray.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArray.java new file mode 100644 index 00000000..dc3804f7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArray.java @@ -0,0 +1,176 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + * {@code ObservableFloatArray} is a {@code float[]} array that allows listeners + * to track changes when they occur. In order to track changes, the internal + * array is encapsulated and there is no direct access available from the outside. + * Bulk operations are supported but they always do a copy of the data range. + * + * @see ArrayChangeListener + * @since JavaFX 8.0 + */ +public interface ObservableFloatArray extends ObservableArray { + + /** + * Copies specified portion of array into {@code dest} array. Throws + * the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param srcIndex starting position in the observable array + * @param dest destination array + * @param destIndex starting position in destination array + * @param length length of portion to copy + */ + public void copyTo(int srcIndex, float[] dest, int destIndex, int length); + + /** + * Copies specified portion of array into {@code dest} observable array. + * Throws the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param srcIndex starting position in the observable array + * @param dest destination observable array + * @param destIndex starting position in destination observable array + * @param length length of portion to copy + */ + public void copyTo(int srcIndex, ObservableFloatArray dest, int destIndex, int length); + + /** + * Gets a single value of array. This is generally as fast as direct access + * to an array and eliminates necessity to make a copy of array. + * @param index index of element to get + * @return value at the given index + * @throws ArrayIndexOutOfBoundsException if {@code index} is outside + * array bounds + */ + public float get(int index); + + /** + * Appends given {@code elements} to the end of this array. Capacity is increased + * if necessary to match the new size of the data. + * @param elements elements to append + */ + public void addAll(float... elements); + + /** + * Appends content of a given observable array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src observable array with elements to append + */ + public void addAll(ObservableFloatArray src); + + /** + * Appends a portion of given array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source array + * @param srcIndex starting position in source array + * @param length length of portion to append + */ + public void addAll(float[] src, int srcIndex, int length); + + /** + * Appends a portion of given observable array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array + * @param srcIndex starting position in source array + * @param length length of portion to append + */ + public void addAll(ObservableFloatArray src, int srcIndex, int length); + + /** + * Replaces this observable array content with given elements. + * Capacity is increased if necessary to match the new size of the data. + * @param elements elements to put into array content + * @throws NullPointerException if {@code src} is null + */ + public void setAll(float... elements); + + /** + * Replaces this observable array content with a copy of portion of + * a given array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source array to copy. + * @param srcIndex starting position in source observable array + * @param length length of a portion to copy + * @throws NullPointerException if {@code src} is null + */ + public void setAll(float[] src, int srcIndex, int length); + + /** + * Replaces this observable array content with a copy of given observable array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array to copy. + * @throws NullPointerException if {@code src} is null + */ + public void setAll(ObservableFloatArray src); + + /** + * Replaces this observable array content with a portion of a given + * observable array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array to copy. + * @param srcIndex starting position in source observable array + * @param length length of a portion to copy + * @throws NullPointerException if {@code src} is null + */ + public void setAll(ObservableFloatArray src, int srcIndex, int length); + + /** + * Copies a portion of specified array into this observable array. Throws + * the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param destIndex the starting destination position in this observable array + * @param src source array to copy + * @param srcIndex starting position in source array + * @param length length of portion to copy + */ + public void set(int destIndex, float[] src, int srcIndex, int length); + + /** + * Copies a portion of specified observable array into this observable array. + * Throws the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param destIndex the starting destination position in this observable array + * @param src source observable array to copy + * @param srcIndex starting position in source array + * @param length length of portion to copy + */ + public void set(int destIndex, ObservableFloatArray src, int srcIndex, int length); + + /** + * Sets a single value in the array. Avoid using this method if many values + * are updated, use {@linkplain #set(int, float[], int, int)} update method + * instead with as minimum number of invocations as possible. + * @param index index of the value to set + * @param value new value for the given index + * @throws ArrayIndexOutOfBoundsException if {@code index} is outside + * array bounds + */ + public void set(int index, float value); + + /** + * Returns an array containing copy of the observable array. + * If the observable array fits in the specified array, it is copied therein. + * Otherwise, a new array is allocated with the size of the observable array. + * + * @param dest the array into which the observable array to be copied, + * if it is big enough; otherwise, a new float array is allocated. + * Ignored, if null. + * @return a float array containing the copy of the observable array + */ + public float[] toArray(float[] dest); + + /** + * Returns an array containing copy of specified portion of the observable array. + * If specified portion of the observable array fits in the specified array, + * it is copied therein. Otherwise, a new array of given length is allocated. + * + * @param srcIndex starting position in the observable array + * @param dest the array into which specified portion of the observable array + * to be copied, if it is big enough; + * otherwise, a new float array is allocated. + * Ignored, if null. + * @param length length of portion to copy + * @return a float array containing the copy of specified portion the observable array + */ + public float[] toArray(int srcIndex, float[] dest, int length); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArrayImpl.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArrayImpl.java new file mode 100644 index 00000000..0b2864d7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableFloatArrayImpl.java @@ -0,0 +1,287 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Arrays; + +/** + * ObservableFloatArray default implementation. + */ +public final class ObservableFloatArrayImpl extends ObservableArrayBase implements ObservableFloatArray { + + private static final float[] INITIAL = new float[0]; + + private float[] array = INITIAL; + private int size = 0; + + /** + * Creates empty observable float array + */ + public ObservableFloatArrayImpl() { + } + + /** + * Creates observable float array with copy of given initial values + * @param elements initial values to copy to observable float array + */ + public ObservableFloatArrayImpl(float... elements) { + setAll(elements); + } + + /** + * Creates observable float array with copy of given observable float array + * @param src observable float array to copy + */ + public ObservableFloatArrayImpl(ObservableFloatArray src) { + setAll(src); + } + + @Override + public void clear() { + resize(0); + } + + @Override + public int size() { + return size; + } + + private void addAllInternal(ObservableFloatArray src, int srcIndex, int length) { + growCapacity(length); + src.copyTo(srcIndex, array, size, length); + size += length; + fireChange(length != 0, size - length, size); + } + + private void addAllInternal(float[] src, int srcIndex, int length) { + growCapacity(length); + System.arraycopy(src, srcIndex, array, size, length); + size += length; + fireChange(length != 0, size - length, size); + } + + @Override + public void addAll(ObservableFloatArray src) { + addAllInternal(src, 0, src.size()); + } + + @Override + public void addAll(float... elements) { + addAllInternal(elements, 0, elements.length); + } + + @Override + public void addAll(ObservableFloatArray src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + addAllInternal(src, srcIndex, length); + } + + @Override + public void addAll(float[] src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + addAllInternal(src, srcIndex, length); + } + + private void setAllInternal(ObservableFloatArray src, int srcIndex, int length) { + boolean sizeChanged = size() != length; + if (src == this) { + if (srcIndex == 0) { + resize(length); + } else { + System.arraycopy(array, srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + } else { + size = 0; + ensureCapacity(length); + src.copyTo(srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + } + + private void setAllInternal(float[] src, int srcIndex, int length) { + boolean sizeChanged = size() != length; + size = 0; + ensureCapacity(length); + System.arraycopy(src, srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + + @Override + public void setAll(ObservableFloatArray src) { + setAllInternal(src, 0, src.size()); + } + + @Override + public void setAll(ObservableFloatArray src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + setAllInternal(src, srcIndex, length); + } + + @Override + public void setAll(float[] src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + setAllInternal(src, srcIndex, length); + } + + @Override + public void setAll(float... src) { + setAllInternal(src, 0, src.length); + } + + @Override + public void set(int destIndex, float[] src, int srcIndex, int length) { + rangeCheck(destIndex + length); + System.arraycopy(src, srcIndex, array, destIndex, length); + fireChange(false, destIndex, destIndex + length); + } + + @Override + public void set(int destIndex, ObservableFloatArray src, int srcIndex, int length) { + rangeCheck(destIndex + length); + src.copyTo(srcIndex, array, destIndex, length); + fireChange(false, destIndex, destIndex + length); + } + + @Override + public float[] toArray(float[] dest) { + if ((dest == null) || (size() > dest.length)) { + dest = new float[size()]; + } + System.arraycopy(array, 0, dest, 0, size()); + return dest; + } + + @Override + public float get(int index) { + rangeCheck(index + 1); + return array[index]; + } + + @Override + public void set(int index, float value) { + rangeCheck(index + 1); + array[index] = value; + fireChange(false, index, index + 1); + } + + @Override + public float[] toArray(int index, float[] dest, int length) { + rangeCheck(index + length); + if ((dest == null) || (length > dest.length)) { + dest = new float[length]; + } + System.arraycopy(array, index, dest, 0, length); + return dest; + } + + @Override + public void copyTo(int srcIndex, float[] dest, int destIndex, int length) { + rangeCheck(srcIndex + length); + System.arraycopy(array, srcIndex, dest, destIndex, length); + } + + @Override + public void copyTo(int srcIndex, ObservableFloatArray dest, int destIndex, int length) { + rangeCheck(srcIndex + length); + dest.set(destIndex, array, srcIndex, length); + } + + @Override + public void resize(int newSize) { + if (newSize < 0) { + throw new NegativeArraySizeException("Can't resize to negative value: " + newSize); + } + ensureCapacity(newSize); + int minSize = Math.min(size, newSize); + boolean sizeChanged = size != newSize; + size = newSize; + Arrays.fill(array, minSize, size, 0); + fireChange(sizeChanged, minSize, newSize); + } + + /** + * The maximum size of array to allocate. + * Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in + * OutOfMemoryError: Requested array size exceeds VM limit + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private void growCapacity(int length) { + int minCapacity = size + length; + int oldCapacity = array.length; + if (minCapacity > array.length) { + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity < minCapacity) newCapacity = minCapacity; + if (newCapacity > MAX_ARRAY_SIZE) newCapacity = hugeCapacity(minCapacity); + ensureCapacity(newCapacity); + } else if (length > 0 && minCapacity < 0) { + throw new OutOfMemoryError(); // overflow + } + } + + @Override + public void ensureCapacity(int capacity) { + if (array.length < capacity) { + array = Arrays.copyOf(array, capacity); + } + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; + } + + @Override + public void trimToSize() { + if (array.length != size) { + float[] newArray = new float[size]; + System.arraycopy(array, 0, newArray, 0, size); + array = newArray; + } + } + + private void rangeCheck(int size) { + if (size > this.size) throw new ArrayIndexOutOfBoundsException(this.size); + } + + private void rangeCheck(ObservableFloatArray src, int srcIndex, int length) { + if (src == null) throw new NullPointerException(); + if (srcIndex < 0 || srcIndex + length > src.size()) { + throw new ArrayIndexOutOfBoundsException(src.size()); + } + if (length < 0) throw new ArrayIndexOutOfBoundsException(-1); + } + + private void rangeCheck(float[] src, int srcIndex, int length) { + if (src == null) throw new NullPointerException(); + if (srcIndex < 0 || srcIndex + length > src.length) { + throw new ArrayIndexOutOfBoundsException(src.length); + } + if (length < 0) throw new ArrayIndexOutOfBoundsException(-1); + } + + @Override + public String toString() { + if (array == null) + return "null"; + + int iMax = size() - 1; + if (iMax == -1) + return "[]"; + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0; ; i++) { + b.append(array[i]); + if (i == iMax) + return b.append(']').toString(); + b.append(", "); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArray.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArray.java new file mode 100644 index 00000000..5f4ed674 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArray.java @@ -0,0 +1,176 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + * {@code ObservableIntegerArray} is an {@code int[]} array that allows listeners + * to track changes when they occur. In order to track changes, the internal + * array is encapsulated and there is no direct access available from the outside. + * Bulk operations are supported but they always do a copy of the data range. + * + * @see ArrayChangeListener + * @since JavaFX 8.0 + */ +public interface ObservableIntegerArray extends ObservableArray { + + /** + * Copies specified portion of array into {@code dest} array. Throws + * the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param srcIndex starting position in the observable array + * @param dest destination array + * @param destIndex starting position in destination array + * @param length length of portion to copy + */ + public void copyTo(int srcIndex, int[] dest, int destIndex, int length); + + /** + * Copies specified portion of array into {@code dest} observable array. + * Throws the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param srcIndex starting position in the observable array + * @param dest destination observable array + * @param destIndex starting position in destination observable array + * @param length length of portion to copy + */ + public void copyTo(int srcIndex, ObservableIntegerArray dest, int destIndex, int length); + + /** + * Gets a single value of array. This is generally as fast as direct access + * to an array and eliminates necessity to make a copy of array. + * @param index index of element to get + * @return value at the given index + * @throws ArrayIndexOutOfBoundsException if {@code index} is outside + * array bounds + */ + public int get(int index); + + /** + * Appends given {@code elements} to the end of this array. Capacity is increased + * if necessary to match the new size of the data. + * @param elements elements to append + */ + public void addAll(int... elements); + + /** + * Appends content of a given observable array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src observable array with elements to append + */ + public void addAll(ObservableIntegerArray src); + + /** + * Appends a portion of given array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source array + * @param srcIndex starting position in source array + * @param length length of portion to append + */ + public void addAll(int[] src, int srcIndex, int length); + + /** + * Appends a portion of given observable array to the end of this array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array + * @param srcIndex starting position in source array + * @param length length of portion to append + */ + public void addAll(ObservableIntegerArray src, int srcIndex, int length); + + /** + * Replaces this observable array content with given elements. + * Capacity is increased if necessary to match the new size of the data. + * @param elements elements to put into array content + * @throws NullPointerException if {@code src} is null + */ + public void setAll(int... elements); + + /** + * Replaces this observable array content with a copy of portion of + * a given array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source array to copy. + * @param srcIndex starting position in source observable array + * @param length length of a portion to copy + * @throws NullPointerException if {@code src} is null + */ + public void setAll(int[] src, int srcIndex, int length); + + /** + * Replaces this observable array content with a copy of given observable array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array to copy. + * @throws NullPointerException if {@code src} is null + */ + public void setAll(ObservableIntegerArray src); + + /** + * Replaces this observable array content with a portion of a given + * observable array. + * Capacity is increased if necessary to match the new size of the data. + * @param src source observable array to copy. + * @param srcIndex starting position in source observable array + * @param length length of a portion to copy + * @throws NullPointerException if {@code src} is null + */ + public void setAll(ObservableIntegerArray src, int srcIndex, int length); + + /** + * Copies a portion of specified array into this observable array. Throws + * the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param destIndex the starting destination position in this observable array + * @param src source array to copy + * @param srcIndex starting position in source array + * @param length length of portion to copy + */ + public void set(int destIndex, int[] src, int srcIndex, int length); + + /** + * Copies a portion of specified observable array into this observable array. + * Throws the same exceptions as {@link System#arraycopy(Object, + * int, Object, int, int) System.arraycopy()} method. + * @param destIndex the starting destination position in this observable array + * @param src source observable array to copy + * @param srcIndex starting position in source array + * @param length length of portion to copy + */ + public void set(int destIndex, ObservableIntegerArray src, int srcIndex, int length); + + /** + * Sets a single value in the array. Avoid using this method if many values + * are updated, use {@linkplain #set(int, int[], int, int)} update method + * instead with as minimum number of invocations as possible. + * @param index index of the value to set + * @param value new value for the given index + * @throws ArrayIndexOutOfBoundsException if {@code index} is outside + * array bounds + */ + public void set(int index, int value); + + /** + * Returns an array containing copy of the observable array. + * If the observable array fits in the specified array, it is copied therein. + * Otherwise, a new array is allocated with the size of the observable array. + * + * @param dest the array into which the observable array to be copied, + * if it is big enough; otherwise, a new int array is allocated. + * Ignored, if null. + * @return an int array containing the copy of the observable array + */ + public int[] toArray(int[] dest); + + /** + * Returns an array containing copy of specified portion of the observable array. + * If specified portion of the observable array fits in the specified array, + * it is copied therein. Otherwise, a new array of given length is allocated. + * + * @param srcIndex starting position in the observable array + * @param dest the array into which specified portion of the observable array + * to be copied, if it is big enough; + * otherwise, a new int array is allocated. + * Ignored, if null. + * @param length length of portion to copy + * @return an int array containing the copy of specified portion the observable array + */ + public int[] toArray(int srcIndex, int[] dest, int length); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArrayImpl.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArrayImpl.java new file mode 100644 index 00000000..cc2dab4b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableIntegerArrayImpl.java @@ -0,0 +1,287 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Arrays; + +/** + * ObservableIntegerArray default implementation. + */ +public class ObservableIntegerArrayImpl extends ObservableArrayBase implements ObservableIntegerArray { + + private static final int[] INITIAL = new int[0]; + + private int[] array = INITIAL; + private int size = 0; + + /** + * Creates empty observable integer array + */ + public ObservableIntegerArrayImpl() { + } + + /** + * Creates observable integer array with copy of given initial values + * @param elements initial values to copy to observable integer array + */ + public ObservableIntegerArrayImpl(int... elements) { + setAll(elements); + } + + /** + * Creates observable integer array with copy of given observable integer array + * @param src observable integer array to copy + */ + public ObservableIntegerArrayImpl(ObservableIntegerArray src) { + setAll(src); + } + + @Override + public void clear() { + resize(0); + } + + @Override + public int size() { + return size; + } + + private void addAllInternal(ObservableIntegerArray src, int srcIndex, int length) { + growCapacity(length); + src.copyTo(srcIndex, array, size, length); + size += length; + fireChange(length != 0, size - length, size); + } + + private void addAllInternal(int[] src, int srcIndex, int length) { + growCapacity(length); + System.arraycopy(src, srcIndex, array, size, length); + size += length; + fireChange(length != 0, size - length, size); + } + + @Override + public void addAll(ObservableIntegerArray src) { + addAllInternal(src, 0, src.size()); + } + + @Override + public void addAll(int... elements) { + addAllInternal(elements, 0, elements.length); + } + + @Override + public void addAll(ObservableIntegerArray src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + addAllInternal(src, srcIndex, length); + } + + @Override + public void addAll(int[] src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + addAllInternal(src, srcIndex, length); + } + + private void setAllInternal(ObservableIntegerArray src, int srcIndex, int length) { + boolean sizeChanged = size() != length; + if (src == this) { + if (srcIndex == 0) { + resize(length); + } else { + System.arraycopy(array, srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + } else { + size = 0; + ensureCapacity(length); + src.copyTo(srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + } + + private void setAllInternal(int[] src, int srcIndex, int length) { + boolean sizeChanged = size() != length; + size = 0; + ensureCapacity(length); + System.arraycopy(src, srcIndex, array, 0, length); + size = length; + fireChange(sizeChanged, 0, size); + } + + @Override + public void setAll(ObservableIntegerArray src) { + setAllInternal(src, 0, src.size()); + } + + @Override + public void setAll(ObservableIntegerArray src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + setAllInternal(src, srcIndex, length); + } + + @Override + public void setAll(int[] src, int srcIndex, int length) { + rangeCheck(src, srcIndex, length); + setAllInternal(src, srcIndex, length); + } + + @Override + public void setAll(int... src) { + setAllInternal(src, 0, src.length); + } + + @Override + public void set(int destIndex, int[] src, int srcIndex, int length) { + rangeCheck(destIndex + length); + System.arraycopy(src, srcIndex, array, destIndex, length); + fireChange(false, destIndex, destIndex + length); + } + + @Override + public void set(int destIndex, ObservableIntegerArray src, int srcIndex, int length) { + rangeCheck(destIndex + length); + src.copyTo(srcIndex, array, destIndex, length); + fireChange(false, destIndex, destIndex + length); + } + + @Override + public int[] toArray(int[] dest) { + if ((dest == null) || (size() > dest.length)) { + dest = new int[size()]; + } + System.arraycopy(array, 0, dest, 0, size()); + return dest; + } + + @Override + public int get(int index) { + rangeCheck(index + 1); + return array[index]; + } + + @Override + public void set(int index, int value) { + rangeCheck(index + 1); + array[index] = value; + fireChange(false, index, index + 1); + } + + @Override + public int[] toArray(int index, int[] dest, int length) { + rangeCheck(index + length); + if ((dest == null) || (length > dest.length)) { + dest = new int[length]; + } + System.arraycopy(array, index, dest, 0, length); + return dest; + } + + @Override + public void copyTo(int srcIndex, int[] dest, int destIndex, int length) { + rangeCheck(srcIndex + length); + System.arraycopy(array, srcIndex, dest, destIndex, length); + } + + @Override + public void copyTo(int srcIndex, ObservableIntegerArray dest, int destIndex, int length) { + rangeCheck(srcIndex + length); + dest.set(destIndex, array, srcIndex, length); + } + + @Override + public void resize(int newSize) { + if (newSize < 0) { + throw new NegativeArraySizeException("Can't resize to negative value: " + newSize); + } + ensureCapacity(newSize); + int minSize = Math.min(size, newSize); + boolean sizeChanged = size != newSize; + size = newSize; + Arrays.fill(array, minSize, size, 0); + fireChange(sizeChanged, minSize, newSize); + } + + /** + * The maximum size of array to allocate. + * Some VMs reserve some header words in an array. + * Attempts to allocate larger arrays may result in + * OutOfMemoryError: Requested array size exceeds VM limit + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private void growCapacity(int length) { + int minCapacity = size + length; + int oldCapacity = array.length; + if (minCapacity > array.length) { + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity < minCapacity) newCapacity = minCapacity; + if (newCapacity > MAX_ARRAY_SIZE) newCapacity = hugeCapacity(minCapacity); + ensureCapacity(newCapacity); + } else if (length > 0 && minCapacity < 0) { + throw new OutOfMemoryError(); // overflow + } + } + + @Override + public void ensureCapacity(int capacity) { + if (array.length < capacity) { + array = Arrays.copyOf(array, capacity); + } + } + + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; + } + + @Override + public void trimToSize() { + if (array.length != size) { + int[] newArray = new int[size]; + System.arraycopy(array, 0, newArray, 0, size); + array = newArray; + } + } + + private void rangeCheck(int size) { + if (size > this.size) throw new ArrayIndexOutOfBoundsException(this.size); + } + + private void rangeCheck(ObservableIntegerArray src, int srcIndex, int length) { + if (src == null) throw new NullPointerException(); + if (srcIndex < 0 || srcIndex + length > src.size()) { + throw new ArrayIndexOutOfBoundsException(src.size()); + } + if (length < 0) throw new ArrayIndexOutOfBoundsException(-1); + } + + private void rangeCheck(int[] src, int srcIndex, int length) { + if (src == null) throw new NullPointerException(); + if (srcIndex < 0 || srcIndex + length > src.length) { + throw new ArrayIndexOutOfBoundsException(src.length); + } + if (length < 0) throw new ArrayIndexOutOfBoundsException(-1); + } + + @Override + public String toString() { + if (array == null) + return "null"; + + int iMax = size() - 1; + if (iMax == -1) + return "[]"; + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0; ; i++) { + b.append(array[i]); + if (i == iMax) + return b.append(']').toString(); + b.append(", "); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableList.java new file mode 100644 index 00000000..1d4dcf5a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableList.java @@ -0,0 +1,125 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.collections.transformation.FilteredList; +import com.tungsten.fclcore.fakefx.collections.transformation.SortedList; + +import java.text.Collator; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +public interface ObservableList extends List, Observable { + /** + * Add a listener to this observable list. + * @param listener the listener for listening to the list changes + */ + public void addListener(ListChangeListener listener); + + /** + * Tries to remove a listener from this observable list. If the listener is not + * attached to this list, nothing happens. + * @param listener a listener to remove + */ + public void removeListener(ListChangeListener listener); + + /** + * A convenience method for var-arg addition of elements. + * @param elements the elements to add + * @return true (as specified by Collection.add(E)) + */ + public boolean addAll(E... elements); + + /** + * Clears the ObservableList and adds all the elements passed as var-args. + * @param elements the elements to set + * @return true (as specified by Collection.add(E)) + * @throws NullPointerException if the specified arguments contain one or more null elements + */ + public boolean setAll(E... elements); + + /** + * Clears the ObservableList and adds all elements from the collection. + * @param col the collection with elements that will be added to this observableArrayList + * @return true (as specified by Collection.add(E)) + * @throws NullPointerException if the specified collection contains one or more null elements + */ + public boolean setAll(Collection col); + + /** + * A convenience method for var-arg usage of the {@link #removeAll(Collection) removeAll} method. + * @param elements the elements to be removed + * @return true if list changed as a result of this call + */ + public boolean removeAll(E... elements); + + /** + * A convenience method for var-arg usage of the {@link #retainAll(Collection) retainAll} method. + * @param elements the elements to be retained + * @return true if list changed as a result of this call + */ + public boolean retainAll(E... elements); + + /** + * A simplified way of calling {@code sublist(from, to).clear()}. As this is a common operation, + * ObservableList has this method for convenient usage. + * @param from the start of the range to remove (inclusive) + * @param to the end of the range to remove (exclusive) + * @throws IndexOutOfBoundsException if an illegal range is provided + */ + public void remove(int from, int to); + + /** + * Creates a {@link FilteredList} wrapper of this list using + * the specified predicate. + * @param predicate the predicate to use + * @return new {@code FilteredList} + * @since JavaFX 8.0 + */ + public default FilteredList filtered(Predicate predicate) { + return new FilteredList<>(this, predicate); + } + + /** + * Creates a {@link SortedList} wrapper of this list using + * the specified comparator. + * @param comparator the comparator to use or null for unordered List + * @return new {@code SortedList} + * @since JavaFX 8.0 + */ + public default SortedList sorted(Comparator comparator) { + return new SortedList<>(this, comparator); + } + + /** + * Creates a {@link SortedList} wrapper of this list with the natural + * ordering. + * @return new {@code SortedList} + * @since JavaFX 8.0 + */ + public default SortedList sorted() { + Comparator naturalOrder = new Comparator() { + + @Override + public int compare(E o1, E o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + if (o1 instanceof Comparable) { + return ((Comparable) o1).compareTo(o2); + } + + return Collator.getInstance().compare(o1.toString(), o2.toString()); + } + }; + return sorted(naturalOrder); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListBase.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListBase.java new file mode 100644 index 00000000..777ec388 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListBase.java @@ -0,0 +1,198 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +public abstract class ObservableListBase extends AbstractList implements ObservableList { + + private ListListenerHelper listenerHelper; + private final ListChangeBuilder changeBuilder = new ListChangeBuilder(this); + + /** + * Creates a default {@code ObservableListBase}. + */ + public ObservableListBase() { + } + + /** + * Adds a new update operation to the change. + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param pos the position in the list where the updated element resides. + */ + protected final void nextUpdate(int pos) { + changeBuilder.nextUpdate(pos); + } + + /** + * Adds a new set operation to the change. + * Equivalent to {@code nextRemove(idx); nextAdd(idx, idx + 1); }. + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param idx the index of the item that was set + * @param old the old value at the {@code idx} position. + */ + protected final void nextSet(int idx, E old) { + changeBuilder.nextSet(idx, old); + } + + /** + * Adds a new replace operation to the change. + * Equivalent to {@code nextRemove(from, removed); nextAdd(from, to); } + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param from the index where the items were replaced + * @param to the end index (exclusive) of the range where the new items reside + * @param removed the list of items that were removed + */ + protected final void nextReplace(int from, int to, List removed) { + changeBuilder.nextReplace(from, to, removed); + } + + /** + * Adds a new remove operation to the change with multiple items removed. + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param idx the index where the items were removed + * @param removed the list of items that were removed + */ + protected final void nextRemove(int idx, List removed) { + changeBuilder.nextRemove(idx, removed); + } + + /** + * Adds a new remove operation to the change with single item removed. + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param idx the index where the item was removed + * @param removed the item that was removed + */ + protected final void nextRemove(int idx, E removed) { + changeBuilder.nextRemove(idx, removed); + } + + /** + * Adds a new permutation operation to the change. + * The permutation on index {@code "i"} contains the index, where the item from the index {@code "i"} was moved. + *

It's not necessary to provide the smallest permutation possible. It's correct to always call this method + * with {@code nextPermutation(0, size(), permutation); } + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param from marks the beginning (inclusive) of the range that was permutated + * @param to marks the end (exclusive) of the range that was permutated + * @param perm the permutation in that range. Even if {@code from != 0}, the array should + * contain the indexes of the list. Therefore, such permutation would not contain indexes of range {@code (0, from)} + */ + protected final void nextPermutation(int from, int to, int[] perm) { + changeBuilder.nextPermutation(from, to, perm); + } + + /** + * Adds a new add operation to the change. + * There's no need to provide the list of added items as they can be found directly in the list + * under the specified indexes. + *

Note: needs to be called inside {@code beginChange()} / {@code endChange()} block. + *

Note: needs to reflect the current state of the list. + * @param from marks the beginning (inclusive) of the range that was added + * @param to marks the end (exclusive) of the range that was added + */ + protected final void nextAdd(int from, int to) { + changeBuilder.nextAdd(from, to); + } + + /** + * Begins a change block. + * + * Must be called before any of the {@code next*} methods is called. + * For every {@code beginChange()}, there must be a corresponding {@link #endChange() } call. + *

{@code beginChange()} calls can be nested in a {@code beginChange()}/{@code endChange()} block. + * + * @see #endChange() + */ + protected final void beginChange() { + changeBuilder.beginChange(); + } + + /** + * Ends the change block. + * + * If the block is the outer-most block for the {@code ObservableList}, the + * {@code Change} is constructed and all listeners are notified. + *

Ending a nested block doesn't fire a notification. + * + * @see #beginChange() + */ + protected final void endChange() { + changeBuilder.endChange(); + } + + @Override + public final void addListener(InvalidationListener listener) { + listenerHelper = ListListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public final void removeListener(InvalidationListener listener) { + listenerHelper = ListListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public final void addListener(ListChangeListener listener) { + listenerHelper = ListListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public final void removeListener(ListChangeListener listener) { + listenerHelper = ListListenerHelper.removeListener(listenerHelper, listener); + } + + /** + * Notifies all listeners of a change + * @param change an object representing the change that was done + */ + protected final void fireChange(ListChangeListener.Change change) { + ListListenerHelper.fireValueChangedEvent(listenerHelper, change); + } + + /** + * Returns true if there are some listeners registered for this list. + * @return true if there is a listener for this list + */ + protected final boolean hasListeners() { + return ListListenerHelper.hasListeners(listenerHelper); + } + + @Override + public boolean addAll(E... elements) { + return addAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(E... elements) { + return setAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + return removeAll(Arrays.asList(elements)); + } + + @Override + public boolean retainAll(E... elements) { + return retainAll(Arrays.asList(elements)); + } + + @Override + public void remove(int from, int to) { + removeRange(from, to); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListWrapper.java new file mode 100644 index 00000000..1a7b95ec --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableListWrapper.java @@ -0,0 +1,208 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.util.Callback; + +import java.util.BitSet; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.RandomAccess; + +/** + * A List wrapper class that implements observability. + * + */ +public class ObservableListWrapper extends ModifiableObservableListBase implements + ObservableList, SortableList, RandomAccess { + + private final List backingList; + + private final ElementObserver elementObserver; + + public ObservableListWrapper(List list) { + backingList = list; + elementObserver = null; + } + + public ObservableListWrapper(List list, Callback extractor) { + backingList = list; + this.elementObserver = new ElementObserver(extractor, new Callback() { + + @Override + public InvalidationListener call(final E e) { + return new InvalidationListener() { + + @Override + public void invalidated(Observable observable) { + beginChange(); + int i = 0; + final int size = size(); + for (; i < size; ++i) { + if (get(i) == e) { + nextUpdate(i); + } + } + endChange(); + } + }; + } + }, this); + final int sz = backingList.size(); + for (int i = 0; i < sz; ++i) { + elementObserver.attachListener(backingList.get(i)); + } + } + + + @Override + public E get(int index) { + return backingList.get(index); + } + + @Override + public int size() { + return backingList.size(); + } + + @Override + protected void doAdd(int index, E element) { + if (elementObserver != null) + elementObserver.attachListener(element); + backingList.add(index, element); + } + + @Override + protected E doSet(int index, E element) { + E removed = backingList.set(index, element); + if (elementObserver != null) { + elementObserver.detachListener(removed); + elementObserver.attachListener(element); + } + return removed; + } + + @Override + protected E doRemove(int index) { + E removed = backingList.remove(index); + if (elementObserver != null) + elementObserver.detachListener(removed); + return removed; + } + + @Override + public int indexOf(Object o) { + return backingList.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return backingList.lastIndexOf(o); + } + + @Override + public boolean contains(Object o) { + return backingList.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return backingList.containsAll(c); + } + + @Override + public void clear() { + if (elementObserver != null) { + final int sz = size(); + for (int i = 0; i < sz; ++i) { + elementObserver.detachListener(get(i)); + } + } + if (hasListeners()) { + beginChange(); + nextRemove(0, this); + } + backingList.clear(); + ++modCount; + if (hasListeners()) { + endChange(); + } + } + + @Override + public void remove(int fromIndex, int toIndex) { + beginChange(); + for (int i = fromIndex; i < toIndex; ++i) { + remove(fromIndex); + } + endChange(); + } + + @Override + public boolean removeAll(Collection c) { + beginChange(); + BitSet bs = new BitSet(c.size()); + for (int i = 0; i < size(); ++i) { + if (c.contains(get(i))) { + bs.set(i); + } + } + if (!bs.isEmpty()) { + int cur = size(); + while ((cur = bs.previousSetBit(cur - 1)) >= 0) { + remove(cur); + } + } + endChange(); + return !bs.isEmpty(); + } + + @Override + public boolean retainAll(Collection c) { + beginChange(); + BitSet bs = new BitSet(c.size()); + for (int i = 0; i < size(); ++i) { + if (!c.contains(get(i))) { + bs.set(i); + } + } + if (!bs.isEmpty()) { + int cur = size(); + while ((cur = bs.previousSetBit(cur - 1)) >= 0) { + remove(cur); + } + } + endChange(); + return !bs.isEmpty(); + } + + private SortHelper helper; + + @Override + @SuppressWarnings("unchecked") + public void sort() { + if (backingList.isEmpty()) { + return; + } + int[] perm = getSortHelper().sort((List)backingList); + fireChange(new NonIterableChange.SimplePermutationChange(0, size(), perm, this)); + } + + @Override + public void sort(Comparator comparator) { + if (backingList.isEmpty()) { + return; + } + int[] perm = getSortHelper().sort(backingList, comparator); + fireChange(new NonIterableChange.SimplePermutationChange(0, size(), perm, this)); + } + + private SortHelper getSortHelper() { + if (helper == null) { + helper = new SortHelper(); + } + return helper; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMap.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMap.java new file mode 100644 index 00000000..cf9436d2 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMap.java @@ -0,0 +1,19 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.Observable; + +import java.util.Map; + +public interface ObservableMap extends Map, Observable { + /** + * Add a listener to this observable map. + * @param listener the listener for listening to the list changes + */ + public void addListener(MapChangeListener listener); + /** + * Tries to removed a listener from this observable map. If the listener is not + * attached to this map, nothing happens. + * @param listener a listener to remove + */ + public void removeListener(MapChangeListener listener); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMapWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMapWrapper.java new file mode 100644 index 00000000..2543d68e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableMapWrapper.java @@ -0,0 +1,668 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * A Map wrapper class that implements observability. + * + */ +public class ObservableMapWrapper implements ObservableMap{ + private ObservableEntrySet entrySet; + private ObservableKeySet keySet; + private ObservableValues values; + + private MapListenerHelper listenerHelper; + private final Map backingMap; + + public ObservableMapWrapper(Map map) { + this.backingMap = map; + } + + private class SimpleChange extends MapChangeListener.Change { + + private final K key; + private final V old; + private final V added; + private final boolean wasAdded; + private final boolean wasRemoved; + + public SimpleChange(K key, V old, V added, boolean wasAdded, boolean wasRemoved) { + super(ObservableMapWrapper.this); + assert(wasAdded || wasRemoved); + this.key = key; + this.old = old; + this.added = added; + this.wasAdded = wasAdded; + this.wasRemoved = wasRemoved; + } + + @Override + public boolean wasAdded() { + return wasAdded; + } + + @Override + public boolean wasRemoved() { + return wasRemoved; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValueAdded() { + return added; + } + + @Override + public V getValueRemoved() { + return old; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (wasAdded) { + if (wasRemoved) { + builder.append(old).append(" replaced by ").append(added); + } else { + builder.append(added).append(" added"); + } + } else { + builder.append(old).append(" removed"); + } + builder.append(" at key ").append(key); + return builder.toString(); + } + + } + + protected void callObservers(MapChangeListener.Change change) { + MapListenerHelper.fireValueChangedEvent(listenerHelper, change); + } + + @Override + public void addListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public void addListener(MapChangeListener observer) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, observer); + } + + @Override + public void removeListener(MapChangeListener observer) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, observer); + } + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return backingMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return backingMap.containsValue(value); + } + + @Override + public V get(Object key) { + return backingMap.get(key); + } + + @Override + public V put(K key, V value) { + V ret; + if (backingMap.containsKey(key)) { + ret = backingMap.put(key, value); + if (ret == null && value != null || ret != null && !ret.equals(value)) { + callObservers(new SimpleChange(key, ret, value, true, true)); + } + } else { + ret = backingMap.put(key, value); + callObservers(new SimpleChange(key, ret, value, true, false)); + } + return ret; + } + + @Override + @SuppressWarnings("unchecked") + public V remove(Object key) { + if (!backingMap.containsKey(key)) { + return null; + } + V ret = backingMap.remove(key); + callObservers(new SimpleChange((K)key, ret, null, false, true)); + return ret; + } + + @Override + public void putAll(Map m) { + for (Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public void clear() { + for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext(); ) { + Entry e = i.next(); + K key = e.getKey(); + V val = e.getValue(); + i.remove(); + callObservers(new SimpleChange(key, val, null, false, true)); + } + } + + @Override + public Set keySet() { + if (keySet == null) { + keySet = new ObservableKeySet(); + } + return keySet; + } + + @Override + public Collection values() { + if (values == null) { + values = new ObservableValues(); + } + return values; + } + + @Override + public Set> entrySet() { + if (entrySet == null) { + entrySet = new ObservableEntrySet(); + } + return entrySet; + } + + @Override + public String toString() { + return backingMap.toString(); + } + + @Override + public boolean equals(Object obj) { + return backingMap.equals(obj); + } + + @Override + public int hashCode() { + return backingMap.hashCode(); + } + + private class ObservableKeySet implements Set{ + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingMap.keySet().contains(o); + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private Iterator> entryIt = backingMap.entrySet().iterator(); + private K lastKey; + private V lastValue; + @Override + public boolean hasNext() { + return entryIt.hasNext(); + } + + @Override + public K next() { + Entry last = entryIt.next(); + lastKey = last.getKey(); + lastValue = last.getValue(); + return last.getKey(); + } + + @Override + public void remove() { + entryIt.remove(); + callObservers(new SimpleChange(lastKey, lastValue, null, false, true)); + } + + }; + } + + @Override + public Object[] toArray() { + return backingMap.keySet().toArray(); + } + + @Override + public T[] toArray(T[] a) { + return backingMap.keySet().toArray(a); + } + + @Override + public boolean add(K e) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean remove(Object o) { + return ObservableMapWrapper.this.remove(o) != null; + } + + @Override + public boolean containsAll(Collection c) { + return backingMap.keySet().containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean retainAll(Collection c) { + return removeRetain(c, false); + } + + private boolean removeRetain(Collection c, boolean remove) { + boolean removed = false; + for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { + Entry e = i.next(); + if (remove == c.contains(e.getKey())) { + removed = true; + K key = e.getKey(); + V value = e.getValue(); + i.remove(); + callObservers(new SimpleChange(key, value, null, false, true)); + } + } + return removed; + } + + @Override + public boolean removeAll(Collection c) { + return removeRetain(c, true); + } + + @Override + public void clear() { + ObservableMapWrapper.this.clear(); + } + + @Override + public String toString() { + return backingMap.keySet().toString(); + } + + @Override + public boolean equals(Object obj) { + return backingMap.keySet().equals(obj); + } + + @Override + public int hashCode() { + return backingMap.keySet().hashCode(); + } + + } + + private class ObservableValues implements Collection { + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingMap.values().contains(o); + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private Iterator> entryIt = backingMap.entrySet().iterator(); + private K lastKey; + private V lastValue; + @Override + public boolean hasNext() { + return entryIt.hasNext(); + } + + @Override + public V next() { + Entry last = entryIt.next(); + lastKey = last.getKey(); + lastValue = last.getValue(); + return lastValue; + } + + @Override + public void remove() { + entryIt.remove(); + callObservers(new SimpleChange(lastKey, lastValue, null, false, true)); + } + + }; + } + + @Override + public Object[] toArray() { + return backingMap.values().toArray(); + } + + @Override + public T[] toArray(T[] a) { + return backingMap.values().toArray(a); + } + + @Override + public boolean add(V e) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean remove(Object o) { + for(Iterator i = iterator(); i.hasNext();) { + if (i.next().equals(o)) { + i.remove(); + return true; + } + } + return false; + } + + @Override + public boolean containsAll(Collection c) { + return backingMap.values().containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean removeAll(Collection c) { + return removeRetain(c, true); + } + + private boolean removeRetain(Collection c, boolean remove) { + boolean removed = false; + for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { + Entry e = i.next(); + if (remove == c.contains(e.getValue())) { + removed = true; + K key = e.getKey(); + V value = e.getValue(); + i.remove(); + callObservers(new SimpleChange(key, value, null, false, true)); + } + } + return removed; + } + + @Override + public boolean retainAll(Collection c) { + return removeRetain(c, false); + } + + @Override + public void clear() { + ObservableMapWrapper.this.clear(); + } + + @Override + public String toString() { + return backingMap.values().toString(); + } + + @Override + public boolean equals(Object obj) { + return backingMap.values().equals(obj); + } + + @Override + public int hashCode() { + return backingMap.values().hashCode(); + } + + + + + } + + private class ObservableEntry implements Entry { + + private final Entry backingEntry; + + public ObservableEntry(Entry backingEntry) { + this.backingEntry = backingEntry; + } + + @Override + public K getKey() { + return backingEntry.getKey(); + } + + @Override + public V getValue() { + return backingEntry.getValue(); + } + + @Override + public V setValue(V value) { + V oldValue = backingEntry.setValue(value); + callObservers(new SimpleChange(getKey(), oldValue, value, true, true)); + return oldValue; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Entry e = (Entry) o; + Object k1 = getKey(); + Object k2 = e.getKey(); + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = e.getValue(); + if (v1 == v2 || (v1 != null && v1.equals(v2))) { + return true; + } + } + return false; + } + + @Override + public final int hashCode() { + return (getKey() == null ? 0 : getKey().hashCode()) + ^ (getValue() == null ? 0 : getValue().hashCode()); + } + + @Override + public final String toString() { + return getKey() + "=" + getValue(); + } + + } + + private class ObservableEntrySet implements Set>{ + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingMap.entrySet().contains(o); + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + + private Iterator> backingIt = backingMap.entrySet().iterator(); + private K lastKey; + private V lastValue; + @Override + public boolean hasNext() { + return backingIt.hasNext(); + } + + @Override + public Entry next() { + Entry last = backingIt.next(); + lastKey = last.getKey(); + lastValue = last.getValue(); + return new ObservableEntry(last); + } + + @Override + public void remove() { + backingIt.remove(); + callObservers(new SimpleChange(lastKey, lastValue, null, false, true)); + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public Object[] toArray() { + Object[] array = backingMap.entrySet().toArray(); + for (int i = 0; i < array.length; ++i) { + array[i] = new ObservableEntry((Entry)array[i]); + } + return array; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + T[] array = backingMap.entrySet().toArray(a); + for (int i = 0; i < array.length; ++i) { + array[i] = (T) new ObservableEntry((Entry)array[i]); + } + return array; + } + + @Override + public boolean add(Entry e) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + boolean ret = backingMap.entrySet().remove(o); + if (ret) { + Entry entry = (Entry) o; + callObservers(new SimpleChange(entry.getKey(), entry.getValue(), null, false, true)); + } + return ret; + } + + @Override + public boolean containsAll(Collection c) { + return backingMap.entrySet().containsAll(c); + } + + @Override + public boolean addAll(Collection> c) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean retainAll(Collection c) { + return removeRetain(c, false); + } + + private boolean removeRetain(Collection c, boolean remove) { + boolean removed = false; + for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { + Entry e = i.next(); + if (remove == c.contains(e)) { + removed = true; + K key = e.getKey(); + V value = e.getValue(); + i.remove(); + callObservers(new SimpleChange(key, value, null, false, true)); + } + } + return removed; + } + + @Override + public boolean removeAll(Collection c) { + return removeRetain(c, true); + } + + @Override + public void clear() { + ObservableMapWrapper.this.clear(); + } + + @Override + public String toString() { + return backingMap.entrySet().toString(); + } + + @Override + public boolean equals(Object obj) { + return backingMap.entrySet().equals(obj); + } + + @Override + public int hashCode() { + return backingMap.entrySet().hashCode(); + } + + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSequentialListWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSequentialListWrapper.java new file mode 100644 index 00000000..b3f69841 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSequentialListWrapper.java @@ -0,0 +1,234 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.util.Callback; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +public final class ObservableSequentialListWrapper extends ModifiableObservableListBase implements ObservableList, SortableList{ + private final List backingList; + private final ElementObserver elementObserver; + private SortHelper helper; + + public ObservableSequentialListWrapper(List list) { + backingList = list; + elementObserver = null; + } + + public ObservableSequentialListWrapper(List list, Callback extractor) { + backingList = list; + this.elementObserver = new ElementObserver(extractor, new Callback() { + + @Override + public InvalidationListener call(final E e) { + return new InvalidationListener() { + + @Override + public void invalidated(Observable observable) { + beginChange(); + int i = 0; + for (Iterator it = backingList.iterator(); it.hasNext();) { + if (it.next() == e) { + nextUpdate(i); + } + ++i; + } + endChange(); + } + }; + } + }, this); + for (E e : backingList) { + elementObserver.attachListener(e); + } + } + + @Override + public boolean contains(Object o) { + return backingList.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return backingList.containsAll(c); + } + + @Override + public int indexOf(Object o) { + return backingList.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return backingList.lastIndexOf(o); + } + + @Override + public ListIterator listIterator(final int index) { + return new ListIterator() { + + private final ListIterator backingIt = backingList.listIterator(index); + private E lastReturned; + + @Override + public boolean hasNext() { + return backingIt.hasNext(); + } + + @Override + public E next() { + return lastReturned = backingIt.next(); + } + + @Override + public boolean hasPrevious() { + return backingIt.hasPrevious(); + } + + @Override + public E previous() { + return lastReturned = backingIt.previous(); + } + + @Override + public int nextIndex() { + return backingIt.nextIndex(); + } + + @Override + public int previousIndex() { + return backingIt.previousIndex(); + } + + @Override + public void remove() { + beginChange(); + int idx = previousIndex(); + backingIt.remove(); + nextRemove(idx, lastReturned); + endChange(); + } + + @Override + public void set(E e) { + beginChange(); + int idx = previousIndex(); + backingIt.set(e); + nextSet(idx, lastReturned); + endChange(); + } + + @Override + public void add(E e) { + beginChange(); + int idx = nextIndex(); + backingIt.add(e); + nextAdd(idx, idx + 1); + endChange(); + } + }; + } + + @Override + public Iterator iterator() { + return listIterator(); + } + + @Override + public E get(int index) { + try { + return backingList.listIterator(index).next(); + } catch (NoSuchElementException exc) { + throw new IndexOutOfBoundsException("Index: "+index); + } + } + + @Override + public boolean addAll(int index, Collection c) { + try { + beginChange(); + boolean modified = false; + ListIterator e1 = listIterator(index); + Iterator e2 = c.iterator(); + while (e2.hasNext()) { + e1.add(e2.next()); + modified = true; + } + endChange(); + return modified; + } catch (NoSuchElementException exc) { + throw new IndexOutOfBoundsException("Index: "+index); + } + } + + @Override + public int size() { + return backingList.size(); + } + + @Override + protected void doAdd(int index, E element) { + try { + backingList.listIterator(index).add(element); + } catch (NoSuchElementException exc) { + throw new IndexOutOfBoundsException("Index: "+index); + } + } + + @Override + protected E doSet(int index, E element) { + try { + ListIterator e = backingList.listIterator(index); + E oldVal = e.next(); + e.set(element); + return oldVal; + } catch (NoSuchElementException exc) { + throw new IndexOutOfBoundsException("Index: "+index); + } + } + + @Override + protected E doRemove(int index) { + try { + ListIterator e = backingList.listIterator(index); + E outCast = e.next(); + e.remove(); + return outCast; + } catch (NoSuchElementException exc) { + throw new IndexOutOfBoundsException("Index: "+index); + } + } + + @Override + @SuppressWarnings("unchecked") + public void sort() { + if (backingList.isEmpty()) { + return; + } + int[] perm = getSortHelper().sort((List)backingList); + fireChange(new NonIterableChange.SimplePermutationChange(0, size(), perm, this)); + } + + @Override + public void sort(Comparator comparator) { + if (backingList.isEmpty()) { + return; + } + int[] perm = getSortHelper().sort(backingList, comparator); + fireChange(new NonIterableChange.SimplePermutationChange(0, size(), perm, this)); + } + + private SortHelper getSortHelper() { + if (helper == null) { + helper = new SortHelper(); + } + return helper; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSet.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSet.java new file mode 100644 index 00000000..c141107f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSet.java @@ -0,0 +1,19 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.Observable; + +import java.util.Set; + +public interface ObservableSet extends Set, Observable { + /** + * Add a listener to this observable set. + * @param listener the listener for listening to the set changes + */ + public void addListener(SetChangeListener listener); + /** + * Tries to removed a listener from this observable set. If the listener is not + * attached to this list, nothing happens. + * @param listener a listener to remove + */ + public void removeListener(SetChangeListener listener); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSetWrapper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSetWrapper.java new file mode 100644 index 00000000..15ce6a44 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/ObservableSetWrapper.java @@ -0,0 +1,381 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +/** + * A Set wrapper class that implements observability. + */ +public class ObservableSetWrapper implements ObservableSet { + + private final Set backingSet; + + private SetListenerHelper listenerHelper; + + /** + * Creates new instance of ObservableSet that wraps + * the particular set specified by the parameter set. + * + * @param set the set being wrapped + */ + public ObservableSetWrapper(Set set) { + this.backingSet = set; + } + + private class SimpleAddChange extends SetChangeListener.Change { + + private final E added; + + public SimpleAddChange(E added) { + super(ObservableSetWrapper.this); + this.added = added; + } + + @Override + public boolean wasAdded() { + return true; + } + + @Override + public boolean wasRemoved() { + return false; + } + + @Override + public E getElementAdded() { + return added; + } + + @Override + public E getElementRemoved() { + return null; + } + + @Override + public String toString() { + return "added " + added; + } + + } + + private class SimpleRemoveChange extends SetChangeListener.Change { + + private final E removed; + + public SimpleRemoveChange(E removed) { + super(ObservableSetWrapper.this); + this.removed = removed; + } + + @Override + public boolean wasAdded() { + return false; + } + + @Override + public boolean wasRemoved() { + return true; + } + + @Override + public E getElementAdded() { + return null; + } + + @Override + public E getElementRemoved() { + return removed; + } + + @Override + public String toString() { + return "removed " + removed; + } + + } + + private void callObservers(SetChangeListener.Change change) { + SetListenerHelper.fireValueChangedEvent(listenerHelper, change); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(InvalidationListener listener) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public void addListener(SetChangeListener observer) { + listenerHelper = SetListenerHelper.addListener(listenerHelper, observer); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeListener(SetChangeListener observer) { + listenerHelper = SetListenerHelper.removeListener(listenerHelper, observer); + } + + /** + * Returns number of elements contained in this set. + * + * @see Set in JDK API documentation + * @return number of elements contained in the set + */ + @Override + public int size() { + return backingSet.size(); + } + + /** + * Returns true if this set contains no elements. + * + * @see Set in JDK API documentation + * @return true if this set contains no elements + */ + @Override + public boolean isEmpty() { + return backingSet.isEmpty(); + } + + /** + * Returns true if this set contains specified element. + * + * @see Set in JDK API documentation + * @param o an element that is being looked for + * @return true if this set contains specified element + */ + @Override + public boolean contains(Object o) { + return backingSet.contains(o); + } + + /** + * Returns an iterator over the elements in this set. + * If the iterator's remove() method is called then the + * registered observers are called as well. + * + * @see Set in JDK API documentation + * @return an iterator over the elements in this set + */ + @Override + public Iterator iterator() { + return new Iterator() { + + private final Iterator backingIt = backingSet.iterator(); + private E lastElement; + + @Override + public boolean hasNext() { + return backingIt.hasNext(); + } + + @Override + public E next() { + lastElement = backingIt.next(); + return lastElement; + } + + @Override + public void remove() { + backingIt.remove(); + callObservers(new SimpleRemoveChange(lastElement)); + } + }; + } + + /** + * Returns an array containing all of the elements in this set. + * + * @see Set in JDK API documentation + * @return an array containing all of the elements in this set + */ + @Override + public Object[] toArray() { + return backingSet.toArray(); + } + + /** + * Returns an array containing all of the elements in this set. + * The runtime type of the returned array is that of the specified array. + * + * @see Set in JDK API documentation + * @param a the array into which the elements of this set are to be stored, if it is big enough; + * otherwise, a new array of the same runtime type is allocated + * @return an array containing all of the elements in this set + */ + @Override + public T[] toArray(T[] a) { + return backingSet.toArray(a); + } + + /** + * Adds the specific element into this set and call all the + * registered observers unless the set already contains the element. + * Returns true in the case the element was added to the set. + * + * @see Set in JDK API documentation + * @param o the element to be added to the set + * @return true if the element was added + */ + @Override + public boolean add(E o) { + boolean ret = backingSet.add(o); + if (ret) { + callObservers(new SimpleAddChange(o)); + } + return ret; + } + + /** + * Removes the specific element from this set and call all the + * registered observers if the set contained the element. + * Returns true in the case the element was removed from the set. + * + * @see Set in JDK API documentation + * @param o the element to be removed from the set + * @return true if the element was removed + */ + @Override + public boolean remove(Object o) { + boolean ret = backingSet.remove(o); + if (ret) { + callObservers(new SimpleRemoveChange((E)o)); + } + return ret; + } + + /** + * Test this set if it contains all the elements in the specified collection. + * In such case returns true. + * + * @see Set in JDK API documentation + * @param c collection to be checked for containment in this set + * @return true if the set contains all the elements in the specified collection + */ + @Override + public boolean containsAll(Collection c) { + return backingSet.containsAll(c); + } + + /** + * Adds the elements from the specified collection. + * Observers are called for each elements that was not already + * present in the set. + * + * @see Set in JDK API documentation + * @param c collection containing elements to be added to this set + * @return true if this set changed as a result of the call + */ + @Override + public boolean addAll(Collection c) { + boolean ret = false; + for (E element : c) { + ret |= add(element); + } + return ret; + } + + /** + * Keeps only elements that are included in the specified collection. + * All other elements are removed. For each removed element all the + * observers are called. + * + * @see Set + * @param c collection containing elements to be kept in this set + * @return true if this set changed as a result of the call + */ + @Override + public boolean retainAll(Collection c) { + return removeRetain(c, false); + } + + /** + * Removes all the elements that are contained in the specified + * collection. Observers are called for each removed element. + * + * @see Set in JDK API documentation + * @param c collection containing elements to be removed from this set + * @return true if this set changed as a result of the call + */ + @Override + public boolean removeAll(Collection c) { + return removeRetain(c, true); + } + + private boolean removeRetain(Collection c, boolean remove) { + boolean removed = false; + for (Iterator i = backingSet.iterator(); i.hasNext();) { + E element = i.next(); + if (remove == c.contains(element)) { + removed = true; + i.remove(); + callObservers(new SimpleRemoveChange(element)); + } + } + return removed; + } + + /** + * Removes all the elements from this set. Observers are called + * for each element. + * @see Set in JDK API documentation + */ + @Override + public void clear() { + for (Iterator i = backingSet.iterator(); i.hasNext(); ) { + E element = i.next(); + i.remove(); + callObservers(new SimpleRemoveChange(element)); + } + } + + /** + * Returns the String representation of the wrapped set. + * @see Object in JDK API documentation + * @return the String representation of the wrapped set + */ + @Override + public String toString() { + return backingSet.toString(); + } + + /** + * Indicates whether some other object is "equal to" the wrapped set. + * @see Object in JDK API documentation + * @param obj the reference object with which to compare + * @return true if the wrapped is equal to the obj argument + */ + @Override + public boolean equals(Object obj) { + return backingSet.equals(obj); + } + + /** + * Returns the hash code for the wrapped set. + * @see Object in JDK API documentation + * @return the hash code for the wrapped set + */ + @Override + public int hashCode() { + return backingSet.hashCode(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetAdapterChange.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetAdapterChange.java new file mode 100644 index 00000000..1026d88b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetAdapterChange.java @@ -0,0 +1,38 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.collections.SetChangeListener.Change; + +public class SetAdapterChange extends SetChangeListener.Change { + private final Change change; + + public SetAdapterChange(ObservableSet set, Change change) { + super(set); + this.change = change; + } + + @Override + public String toString() { + return change.toString(); + } + + @Override + public boolean wasAdded() { + return change.wasAdded(); + } + + @Override + public boolean wasRemoved() { + return change.wasRemoved(); + } + + @Override + public E getElementAdded() { + return change.getElementAdded(); + } + + @Override + public E getElementRemoved() { + return change.getElementRemoved(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetChangeListener.java new file mode 100644 index 00000000..9448ce47 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetChangeListener.java @@ -0,0 +1,75 @@ +package com.tungsten.fclcore.fakefx.collections; + +/** + * Interface that receives notifications of changes to an ObservableSet. + * @param the element type + * @since JavaFX 2.1 + */ +@FunctionalInterface +public interface SetChangeListener { + + /** + * An elementary change done to an ObservableSet. + * Change contains information about an add or remove operation. + * Note that adding element that is already in the set does not + * modify the set and hence no change will be generated. + * + * @param element type + * @since JavaFX 2.1 + */ + public static abstract class Change { + + private ObservableSet set; + + /** + * Constructs a change associated with a set. + * @param set the source of the change + */ + public Change(ObservableSet set) { + this.set = set; + } + + /** + * An observable set that is associated with the change. + * @return the source set + */ + public ObservableSet getSet() { + return set; + } + + /** + * If this change is a result of add operation. + * @return true if a new element was added to the set + */ + public abstract boolean wasAdded(); + + /** + * If this change is a result of removal operation. + * @return true if an old element was removed from the set + */ + public abstract boolean wasRemoved(); + + /** + * Get the new element. Return null if this is a removal. + * @return the element that was just added + */ + public abstract E getElementAdded(); + + /** + * Get the old element. Return null if this is an addition. + * @return the element that was just removed + */ + public abstract E getElementRemoved(); + + } + + /** + * Called after a change has been made to an ObservableSet. + * This method is called on every elementary change (add/remove) once. + * This means, complex changes like removeAll(Collection) or clear() + * may result in more than one call of onChanged method. + * + * @param change the change that was made + */ + void onChanged(Change change); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetListenerHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetListenerHelper.java new file mode 100644 index 00000000..dbf32afb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SetListenerHelper.java @@ -0,0 +1,311 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.binding.ExpressionHelperBase; + +import java.util.Arrays; + +/** + */ +public abstract class SetListenerHelper extends ExpressionHelperBase { + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Static methods + + public static SetListenerHelper addListener(SetListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleInvalidation(listener) : helper.addListener(listener); + } + + public static SetListenerHelper removeListener(SetListenerHelper helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static SetListenerHelper addListener(SetListenerHelper helper, SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? new SingleChange(listener) : helper.addListener(listener); + } + + public static SetListenerHelper removeListener(SetListenerHelper helper, SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + return (helper == null)? null : helper.removeListener(listener); + } + + public static void fireValueChangedEvent(SetListenerHelper helper, SetChangeListener.Change change) { + if (helper != null) { + helper.fireValueChangedEvent(change); + } + } + + public static boolean hasListeners(SetListenerHelper helper) { + return helper != null; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Common implementations + + protected abstract SetListenerHelper addListener(InvalidationListener listener); + protected abstract SetListenerHelper removeListener(InvalidationListener listener); + + protected abstract SetListenerHelper addListener(SetChangeListener listener); + protected abstract SetListenerHelper removeListener(SetChangeListener listener); + + protected abstract void fireValueChangedEvent(SetChangeListener.Change change); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementations + + private static class SingleInvalidation extends SetListenerHelper { + + private final InvalidationListener listener; + + private SingleInvalidation(InvalidationListener listener) { + this.listener = listener; + } + + @Override + protected SetListenerHelper addListener(InvalidationListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected SetListenerHelper removeListener(InvalidationListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected SetListenerHelper addListener(SetChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected SetListenerHelper removeListener(SetChangeListener listener) { + return this; + } + + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + try { + listener.invalidated(change.getSet()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class SingleChange extends SetListenerHelper { + + private final SetChangeListener listener; + + private SingleChange(SetChangeListener listener) { + this.listener = listener; + } + + @Override + protected SetListenerHelper addListener(InvalidationListener listener) { + return new Generic(listener, this.listener); + } + + @Override + protected SetListenerHelper removeListener(InvalidationListener listener) { + return this; + } + + @Override + protected SetListenerHelper addListener(SetChangeListener listener) { + return new Generic(this.listener, listener); + } + + @Override + protected SetListenerHelper removeListener(SetChangeListener listener) { + return (listener.equals(this.listener))? null : this; + } + + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + try { + listener.onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static class Generic extends SetListenerHelper { + + private InvalidationListener[] invalidationListeners; + private SetChangeListener[] changeListeners; + private int invalidationSize; + private int changeSize; + private boolean locked; + + private Generic(InvalidationListener listener0, InvalidationListener listener1) { + this.invalidationListeners = new InvalidationListener[] {listener0, listener1}; + this.invalidationSize = 2; + } + + private Generic(SetChangeListener listener0, SetChangeListener listener1) { + this.changeListeners = new SetChangeListener[] {listener0, listener1}; + this.changeSize = 2; + } + + private Generic(InvalidationListener invalidationListener, SetChangeListener changeListener) { + this.invalidationListeners = new InvalidationListener[] {invalidationListener}; + this.invalidationSize = 1; + this.changeListeners = new SetChangeListener[] {changeListener}; + this.changeSize = 1; + } + + @Override + protected Generic addListener(InvalidationListener listener) { + if (invalidationListeners == null) { + invalidationListeners = new InvalidationListener[] {listener}; + invalidationSize = 1; + } else { + final int oldCapacity = invalidationListeners.length; + if (locked) { + final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } else if (invalidationSize == oldCapacity) { + invalidationSize = trim(invalidationSize, invalidationListeners); + if (invalidationSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); + } + } + invalidationListeners[invalidationSize++] = listener; + } + return this; + } + + @Override + protected SetListenerHelper removeListener(InvalidationListener listener) { + if (invalidationListeners != null) { + for (int index = 0; index < invalidationSize; index++) { + if (listener.equals(invalidationListeners[index])) { + if (invalidationSize == 1) { + if (changeSize == 1) { + return new SingleChange(changeListeners[0]); + } + invalidationListeners = null; + invalidationSize = 0; + } else if ((invalidationSize == 2) && (changeSize == 0)) { + return new SingleInvalidation(invalidationListeners[1-index]); + } else { + final int numMoved = invalidationSize - index - 1; + final InvalidationListener[] oldListeners = invalidationListeners; + if (locked) { + invalidationListeners = new InvalidationListener[invalidationListeners.length]; + System.arraycopy(oldListeners, 0, invalidationListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved); + } + invalidationSize--; + if (!locked) { + invalidationListeners[invalidationSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected SetListenerHelper addListener(SetChangeListener listener) { + if (changeListeners == null) { + changeListeners = new SetChangeListener[] {listener}; + changeSize = 1; + } else { + final int oldCapacity = changeListeners.length; + if (locked) { + final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } else if (changeSize == oldCapacity) { + changeSize = trim(changeSize, changeListeners); + if (changeSize == oldCapacity) { + final int newCapacity = (oldCapacity * 3)/2 + 1; + changeListeners = Arrays.copyOf(changeListeners, newCapacity); + } + } + changeListeners[changeSize++] = listener; + } + return this; + } + + @Override + protected SetListenerHelper removeListener(SetChangeListener listener) { + if (changeListeners != null) { + for (int index = 0; index < changeSize; index++) { + if (listener.equals(changeListeners[index])) { + if (changeSize == 1) { + if (invalidationSize == 1) { + return new SingleInvalidation(invalidationListeners[0]); + } + changeListeners = null; + changeSize = 0; + } else if ((changeSize == 2) && (invalidationSize == 0)) { + return new SingleChange(changeListeners[1-index]); + } else { + final int numMoved = changeSize - index - 1; + final SetChangeListener[] oldListeners = changeListeners; + if (locked) { + changeListeners = new SetChangeListener[changeListeners.length]; + System.arraycopy(oldListeners, 0, changeListeners, 0, index); + } + if (numMoved > 0) { + System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved); + } + changeSize--; + if (!locked) { + changeListeners[changeSize] = null; // Let gc do its work + } + } + break; + } + } + } + return this; + } + + @Override + protected void fireValueChangedEvent(SetChangeListener.Change change) { + final InvalidationListener[] curInvalidationList = invalidationListeners; + final int curInvalidationSize = invalidationSize; + final SetChangeListener[] curChangeList = changeListeners; + final int curChangeSize = changeSize; + + try { + locked = true; + for (int i = 0; i < curInvalidationSize; i++) { + try { + curInvalidationList[i].invalidated(change.getSet()); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + for (int i = 0; i < curChangeSize; i++) { + try { + curChangeList[i].onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } finally { + locked = false; + } + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortHelper.java new file mode 100644 index 00000000..1a54f6db --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortHelper.java @@ -0,0 +1,302 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; + +/** + * Helper class that contains algorithms taken from JDK that additionally + * tracks the permutation that's created thorough the process + */ +public class SortHelper { + private int[] permutation; + private int[] reversePermutation; + + private static final int INSERTIONSORT_THRESHOLD = 7; + + public > int[] sort(List list) { + T[] a = (T[]) Array.newInstance(Comparable.class, list.size()); + try { + a = list.toArray(a); + } catch (ArrayStoreException e) { + // this means this is not comparable (used without generics) + throw new ClassCastException(); + } + int[] result = sort(a); + ListIterator i = list.listIterator(); + for (int j=0; j int[] sort(List list, Comparator c) { + Object[] a = list.toArray(); + int[] result = sort(a, (Comparator)c); + ListIterator i = list.listIterator(); + for (int j=0; j> int[] sort(T[] a) { + return sort(a, null); + } + + public int[] sort(T[] a, Comparator c) { + T[] aux = (T[]) a.clone(); + int[] result = initPermutation(a.length); + if (c==null) + mergeSort(aux, a, 0, a.length, 0); + else + mergeSort(aux, a, 0, a.length, 0, c); + reversePermutation = null; + permutation = null; + return result; + } + + public int[] sort(T[] a, int fromIndex, int toIndex, + Comparator c) { + rangeCheck(a.length, fromIndex, toIndex); + T[] aux = (T[])copyOfRange(a, fromIndex, toIndex); + int[] result = initPermutation(a.length); + if (c==null) + mergeSort(aux, a, fromIndex, toIndex, -fromIndex); + else + mergeSort(aux, a, fromIndex, toIndex, -fromIndex, c); + reversePermutation = null; + permutation = null; + return Arrays.copyOfRange(result, fromIndex, toIndex); + } + + public int[] sort(int[] a, int fromIndex, int toIndex) { + rangeCheck(a.length, fromIndex, toIndex); + int[] aux = (int[])copyOfRange(a, fromIndex, toIndex); + int[] result = initPermutation(a.length); + mergeSort(aux, a, fromIndex, toIndex, -fromIndex); + reversePermutation = null; + permutation = null; + return Arrays.copyOfRange(result, fromIndex, toIndex); + } + + private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) { + if (fromIndex > toIndex) + throw new IllegalArgumentException("fromIndex(" + fromIndex + + ") > toIndex(" + toIndex+")"); + if (fromIndex < 0) + throw new ArrayIndexOutOfBoundsException(fromIndex); + if (toIndex > arrayLen) + throw new ArrayIndexOutOfBoundsException(toIndex); + } + + + private static int[] copyOfRange(int[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) + throw new IllegalArgumentException(from + " > " + to); + int[] copy = new int[newLength]; + System.arraycopy(original, from, copy, 0, + Math.min(original.length - from, newLength)); + return copy; + } + + private static T[] copyOfRange(T[] original, int from, int to) { + return copyOfRange(original, from, to, (Class) original.getClass()); + } + + private static T[] copyOfRange(U[] original, int from, int to, Class newType) { + int newLength = to - from; + if (newLength < 0) + throw new IllegalArgumentException(from + " > " + to); + T[] copy = ((Object)newType == (Object)Object[].class) + ? (T[]) new Object[newLength] + : (T[]) Array.newInstance(newType.getComponentType(), newLength); + System.arraycopy(original, from, copy, 0, + Math.min(original.length - from, newLength)); + return copy; + } + + /** + * Merge sort from Oracle JDK 6 + */ + private void mergeSort(int[] src, + int[] dest, + int low, + int high, + int off) { + int length = high - low; + + // Insertion sort on smallest arrays + if (length < INSERTIONSORT_THRESHOLD) { + for (int i=low; ilow && + ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--) + swap(dest, j, j-1); + return; + } + + // Recursively sort halves of dest into src + int destLow = low; + int destHigh = high; + low += off; + high += off; + int mid = (low + high) >>> 1; + mergeSort(dest, src, low, mid, -off); + mergeSort(dest, src, mid, high, -off); + + // If list is already sorted, just copy from src to dest. This is an + // optimization that results in faster sorts for nearly ordered lists. + if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) { + System.arraycopy(src, low, dest, destLow, length); + return; + } + + // Merge sorted halves (now in src) into dest + for(int i = destLow, p = low, q = mid; i < destHigh; i++) { + if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0) { + dest[i] = src[p]; + permutation[reversePermutation[p++]] = i; + } else { + dest[i] = src[q]; + permutation[reversePermutation[q++]] = i; + } + } + + for (int i = destLow; i < destHigh; ++i) { + reversePermutation[permutation[i]] = i; + } + } + + /** + * Merge sort from Oracle JDK 6 + */ + private void mergeSort(Object[] src, + Object[] dest, + int low, + int high, + int off) { + int length = high - low; + + // Insertion sort on smallest arrays + if (length < INSERTIONSORT_THRESHOLD) { + for (int i=low; ilow && + ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--) + swap(dest, j, j-1); + return; + } + + // Recursively sort halves of dest into src + int destLow = low; + int destHigh = high; + low += off; + high += off; + int mid = (low + high) >>> 1; + mergeSort(dest, src, low, mid, -off); + mergeSort(dest, src, mid, high, -off); + + // If list is already sorted, just copy from src to dest. This is an + // optimization that results in faster sorts for nearly ordered lists. + if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) { + System.arraycopy(src, low, dest, destLow, length); + return; + } + + // Merge sorted halves (now in src) into dest + for(int i = destLow, p = low, q = mid; i < destHigh; i++) { + if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0) { + dest[i] = src[p]; + permutation[reversePermutation[p++]] = i; + } else { + dest[i] = src[q]; + permutation[reversePermutation[q++]] = i; + } + } + + for (int i = destLow; i < destHigh; ++i) { + reversePermutation[permutation[i]] = i; + } + } + + private void mergeSort(Object[] src, + Object[] dest, + int low, int high, int off, + Comparator c) { + int length = high - low; + + // Insertion sort on smallest arrays + if (length < INSERTIONSORT_THRESHOLD) { + for (int i=low; ilow && c.compare(dest[j-1], dest[j])>0; j--) + swap(dest, j, j-1); + return; + } + + // Recursively sort halves of dest into src + int destLow = low; + int destHigh = high; + low += off; + high += off; + int mid = (low + high) >>> 1; + mergeSort(dest, src, low, mid, -off, c); + mergeSort(dest, src, mid, high, -off, c); + + // If list is already sorted, just copy from src to dest. This is an + // optimization that results in faster sorts for nearly ordered lists. + if (c.compare(src[mid-1], src[mid]) <= 0) { + System.arraycopy(src, low, dest, destLow, length); + return; + } + + // Merge sorted halves (now in src) into dest + for(int i = destLow, p = low, q = mid; i < destHigh; i++) { + if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0) { + dest[i] = src[p]; + permutation[reversePermutation[p++]] = i; + } else { + dest[i] = src[q]; + permutation[reversePermutation[q++]] = i; + } + } + + for (int i = destLow; i < destHigh; ++i) { + reversePermutation[permutation[i]] = i; + } + } + + private void swap(int[] x, int a, int b) { + int t = x[a]; + x[a] = x[b]; + x[b] = t; + permutation[reversePermutation[a]] = b; + permutation[reversePermutation[b]] = a; + int tp = reversePermutation[a]; + reversePermutation[a] = reversePermutation[b]; + reversePermutation[b] = tp; + } + + private void swap(Object[] x, int a, int b) { + Object t = x[a]; + x[a] = x[b]; + x[b] = t; + permutation[reversePermutation[a]] = b; + permutation[reversePermutation[b]] = a; + int tp = reversePermutation[a]; + reversePermutation[a] = reversePermutation[b]; + reversePermutation[b] = tp; + } + + private int[] initPermutation(int length) { + permutation = new int[length]; + reversePermutation = new int[length]; + for (int i = 0; i < length; ++i) { + permutation[i] = reversePermutation[i] = i; + } + return permutation; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortableList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortableList.java new file mode 100644 index 00000000..0c34593a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SortableList.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.tungsten.fclcore.fakefx.collections; + +import java.util.Comparator; +import java.util.List; + +/** + * SortableList is a list that can sort itself in an efficient way, in contrast to the + * Collections.sort() method which threat all lists the same way. + * E.g. ObservableList can sort and fire only one notification. + * @param + */ +public interface SortableList extends List { + + /** + * Sort using default comparator + * @throws ClassCastException if some of the elements cannot be cast to Comparable + * @throws UnsupportedOperationException if list's iterator doesn't support set + */ + public void sort(); + + /** + * Sort using comparator + * @param comparator the comparator to use + * @throws ClassCastException if the list contains elements that are not + * mutually comparable using the specified comparator. + * @throws UnsupportedOperationException if the specified list's + * list-iterator does not support the set operation. + */ + public void sort(Comparator comparator); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SourceAdapterChange.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SourceAdapterChange.java new file mode 100644 index 00000000..aa24c173 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/SourceAdapterChange.java @@ -0,0 +1,68 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.List; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; + +public class SourceAdapterChange extends ListChangeListener.Change { + private final Change change; + private int[] perm; + + public SourceAdapterChange(ObservableList list, Change change) { + super(list); + this.change = change; + } + + @Override + public boolean next() { + perm = null; + return change.next(); + } + + @Override + public void reset() { + change.reset(); + } + + @Override + public int getTo() { + return change.getTo(); + } + + @Override + public List getRemoved() { + return (List) change.getRemoved(); + } + + @Override + public int getFrom() { + return change.getFrom(); + } + + @Override + public boolean wasUpdated() { + return change.wasUpdated(); + } + + @Override + protected int[] getPermutation() { + if (perm == null) { + if (change.wasPermutated()) { + final int from = change.getFrom(); + final int n = change.getTo() - from; + perm = new int[n]; + for (int i=0; i extends ObservableListWrapper{ + + public TrackableObservableList(List list) { + super(list); + } + + public TrackableObservableList() { + super(new ArrayList()); + addListener((Change c) -> { + TrackableObservableList.this.onChanged((Change)c); + }); + } + + protected abstract void onChanged(Change c); + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableListSet.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableListSet.java new file mode 100644 index 00000000..23454fb9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableListSet.java @@ -0,0 +1,51 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.List; + +/** + * A special unmodifiable implementation of Set which wraps a List. + * It does not check for uniqueness! There are + * several places in our implementation (Node.lookupAll and + * ObservableSetWrapper are two such places) where we want to use + * a List for speed of insertion and will be in a position to ensure + * that the List is unique without having the overhead of hashing, + * but want to present an unmodifiable Set in the public API. + */ +public final class UnmodifiableListSet extends AbstractSet { + private List backingList; + + public UnmodifiableListSet(List backingList) { + if (backingList == null) throw new NullPointerException(); + this.backingList = backingList; + } + + /** + * Required implementation that returns an iterator. Note that I + * don't just return backingList.iterator() because doing so would + * open up a whole through which developers could remove items from + * this supposedly unmodifiable set. So the iterator is wrapped + * such that it throws an exception on remove. + */ + @Override public Iterator iterator() { + final Iterator itr = backingList.iterator(); + return new Iterator() { + @Override public boolean hasNext() { + return itr.hasNext(); + } + + @Override public E next() { + return itr.next(); + } + + @Override public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override public int size() { + return backingList.size(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableObservableMap.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableObservableMap.java new file mode 100644 index 00000000..c3036247 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/UnmodifiableObservableMap.java @@ -0,0 +1,102 @@ +package com.tungsten.fclcore.fakefx.collections; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.collections.MapChangeListener.Change; + +/** + * ObservableMap wrapper that does not allow changes to the underlying container. + */ +public class UnmodifiableObservableMap extends AbstractMap + implements ObservableMap +{ + private MapListenerHelper listenerHelper; + private final ObservableMap backingMap; + private final MapChangeListener listener; + + private Set keyset; + private Collection values; + private Set> entryset; + + public UnmodifiableObservableMap(ObservableMap map) { + this.backingMap = map; + listener = c -> { + callObservers(new MapAdapterChange(UnmodifiableObservableMap.this, c)); + }; + this.backingMap.addListener(new WeakMapChangeListener(listener)); + } + + private void callObservers(Change c) { + MapListenerHelper.fireValueChangedEvent(listenerHelper, c); + } + + @Override + public void addListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, listener); + } + + @Override + public void addListener(MapChangeListener observer) { + listenerHelper = MapListenerHelper.addListener(listenerHelper, observer); + } + + @Override + public void removeListener(MapChangeListener observer) { + listenerHelper = MapListenerHelper.removeListener(listenerHelper, observer); + } + + @Override + public int size() { + return backingMap.size(); + } + + @Override + public boolean isEmpty() { + return backingMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return backingMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return backingMap.containsValue(value); + } + + @Override + public V get(Object key) { + return backingMap.get(key); + } + + public Set keySet() { + if (keyset == null) { + keyset = Collections.unmodifiableSet(backingMap.keySet()); + } + return keyset; + } + + public Collection values() { + if (values == null) { + values = Collections.unmodifiableCollection(backingMap.values()); + } + return values; + } + + public Set> entrySet() { + if (entryset == null) { + entryset = Collections.unmodifiableMap(backingMap).entrySet(); + } + return entryset; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/VetoableListDecorator.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/VetoableListDecorator.java new file mode 100644 index 00000000..fee72427 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/VetoableListDecorator.java @@ -0,0 +1,780 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +public abstract class VetoableListDecorator implements ObservableList { + + private final ObservableList list; + private int modCount; + private ListListenerHelper helper; + + private static interface ModCountAccessor { + public int get(); + public int incrementAndGet(); + public int decrementAndGet(); + } + + /** + * The type of the change can be observed from the combination of arguments. + *

    + *
  • If something is going to be added toBeAdded is non-empty + * and indexes contain two indexes that are pointing to the position, e.g. {2, 2} + *
  • If something is going to be removed, the indexes are paired by two: + * from(inclusive)-to(exclusive) and are pointing to the current list. + *
    E.g. if we remove 2,3,5 from list {0,1,2,3,4,5}, the indexes + * will be {2, 4, 5, 6}. If there's more than one pair of indexes, toBeAdded is always empty. + *
  • for set toBeAdded contains 1 element and indexes are like with removal: {index, index + 1} + *
  • for setAll, toBeAdded contains all new elements and indexes looks like this: {0, size()} + *
+ * + * Note that it's always safe to iterate over toBeAdded and use indexes as pairs of + * from-to, as there's always at least one pair. + * + * @param toBeAdded the list to be added + * @throws IllegalArgumentException when the change is vetoed + */ + protected abstract void onProposedChange(List toBeAdded, int... indexes); + + public VetoableListDecorator(ObservableList decorated) { + this.list = decorated; + this.list.addListener((ListChangeListener.Change c) -> { + ListListenerHelper.fireValueChangedEvent(helper, + new SourceAdapterChange(VetoableListDecorator.this, c)); + }); + } + + @Override + public void addListener(ListChangeListener listener) { + helper = ListListenerHelper.addListener(helper, listener); + } + + @Override + public void removeListener(ListChangeListener listener) { + helper = ListListenerHelper.removeListener(helper, listener); + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ListListenerHelper.addListener(helper, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ListListenerHelper.removeListener(helper, listener); + } + + @Override + public boolean addAll(E... elements) { + return addAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(E... elements) { + return setAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(Collection col) { + onProposedChange(Collections.unmodifiableList(new ArrayList(col)), 0, size()); + try { + modCount++; + return list.setAll(col); + } catch(Exception e) { + modCount--; + throw e; + } + } + + private void removeFromList(List backingList, int offset, Collection col, boolean complement) { + int[] toBeRemoved = new int[2]; + int pointer = -1; + for (int i = 0; i < backingList.size(); ++i) { + final E el = backingList.get(i); + if (col.contains(el) ^ complement) { + if (pointer == -1) { + toBeRemoved[pointer + 1] = offset + i; + toBeRemoved[pointer + 2] = offset + i + 1; + pointer += 2; + } else { + if (toBeRemoved[pointer - 1] == offset + i) { + toBeRemoved[pointer - 1] = offset + i + 1; + } else { + int[] tmp = new int[toBeRemoved.length + 2]; + System.arraycopy(toBeRemoved, 0, tmp, 0, toBeRemoved.length); + toBeRemoved = tmp; + toBeRemoved[pointer + 1] = offset + i; + toBeRemoved[pointer + 2] = offset + i + 1; + pointer += 2; + } + } + } + } + if (pointer != -1) { + onProposedChange(Collections.emptyList(), toBeRemoved); + } + } + + @Override + public boolean removeAll(E... elements) { + return removeAll(Arrays.asList(elements)); + } + + @Override + public boolean retainAll(E... elements) { + return retainAll(Arrays.asList(elements)); + } + + @Override + public void remove(int from, int to) { + onProposedChange(Collections.emptyList(), from, to); + try { + modCount++; + list.remove(from, to); + } catch (Exception e) { + modCount--; + } + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return new VetoableIteratorDecorator(new ModCountAccessorImpl(),list.iterator(), 0); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + onProposedChange(Collections.singletonList(e), size(), size()); + try { + modCount++; + list.add(e); + return true; + } catch (Exception ex) { + modCount--; + throw ex; + } + } + + @Override + public boolean remove(Object o) { + int i = list.indexOf(o); + if (i != - 1) { + remove(i); + return true; + } + return false; + } + + @Override + public boolean containsAll(Collection c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + onProposedChange(Collections.unmodifiableList(new ArrayList(c)), size(), size()); + try { + modCount++; + boolean ret = list.addAll(c); + if (!ret) + modCount--; + return ret; + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public boolean addAll(int index, Collection c) { + onProposedChange(Collections.unmodifiableList(new ArrayList(c)), index, index); + try { + modCount++; + boolean ret = list.addAll(index, c); + if (!ret) + modCount--; + return ret; + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public boolean removeAll(Collection c) { + removeFromList(this, 0, c, false); + try { + modCount++; + boolean ret = list.removeAll(c); + if (!ret) + modCount--; + return ret; + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public boolean retainAll(Collection c) { + removeFromList(this, 0, c, true); + try { + modCount++; + boolean ret = list.retainAll(c); + if (!ret) + modCount--; + return ret; + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public void clear() { + onProposedChange(Collections.emptyList(), 0, size()); + try { + modCount++; + list.clear(); + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + onProposedChange(Collections.singletonList(element), index, index + 1); + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + onProposedChange(Collections.singletonList(element), index, index); + try { + modCount++; + list.add(index, element); + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public E remove(int index) { + onProposedChange(Collections.emptyList(), index, index + 1); + try { + modCount++; + E ret = list.remove(index); + return ret; + } catch (Exception e) { + modCount--; + throw e; + } + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new VetoableListIteratorDecorator(new ModCountAccessorImpl(), list.listIterator(), 0); + } + + @Override + public ListIterator listIterator(int index) { + return new VetoableListIteratorDecorator(new ModCountAccessorImpl(), list.listIterator(index), index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return new VetoableSubListDecorator(new ModCountAccessorImpl(), list.subList(fromIndex, toIndex), fromIndex); + } + + @Override + public String toString() { + return list.toString(); + } + + @Override + public boolean equals(Object obj) { + return list.equals(obj); + } + + @Override + public int hashCode() { + return list.hashCode(); + } + + private class VetoableSubListDecorator implements List { + + private final List subList; + private final int offset; + private final ModCountAccessor modCountAccessor; + private int modCount; + + public VetoableSubListDecorator(ModCountAccessor modCountAccessor, List subList, int offset) { + this.modCountAccessor = modCountAccessor; + this.modCount = modCountAccessor.get(); + this.subList = subList; + this.offset = offset; + } + + + @Override + public int size() { + checkForComodification(); + return subList.size(); + } + + @Override + public boolean isEmpty() { + checkForComodification(); + return subList.isEmpty(); + } + + @Override + public boolean contains(Object o) { + checkForComodification(); + return subList.contains(o); + } + + @Override + public Iterator iterator() { + checkForComodification(); + return new VetoableIteratorDecorator(new ModCountAccessorImplSub(), subList.iterator(), offset); + } + + @Override + public Object[] toArray() { + checkForComodification(); + return subList.toArray(); + } + + @Override + public T[] toArray(T[] a) { + checkForComodification(); + return subList.toArray(a); + } + + @Override + public boolean add(E e) { + checkForComodification(); + onProposedChange(Collections.singletonList(e), offset + size(), offset + size()); + try { + incrementModCount(); + subList.add(e); + } catch (Exception ex) { + decrementModCount(); + throw ex; + } + return true; + } + + @Override + public boolean remove(Object o) { + checkForComodification(); + int i = indexOf(o); + if (i != -1) { + remove(i); + return true; + } + return false; + } + + @Override + public boolean containsAll(Collection c) { + checkForComodification(); + return subList.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + checkForComodification(); + onProposedChange(Collections.unmodifiableList(new ArrayList(c)), offset + size(), offset + size()); + try { + incrementModCount(); + boolean res = subList.addAll(c); + if (!res) + decrementModCount(); + return res; + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public boolean addAll(int index, Collection c) { + checkForComodification(); + onProposedChange(Collections.unmodifiableList(new ArrayList(c)), offset + index, offset + index); + try { + incrementModCount(); + boolean res = subList.addAll(index, c); + if (!res) + decrementModCount(); + return res; + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public boolean removeAll(Collection c) { + checkForComodification(); + removeFromList(this, offset, c, false); + try { + incrementModCount(); + boolean res = subList.removeAll(c); + if (!res) + decrementModCount(); + return res; + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public boolean retainAll(Collection c) { + checkForComodification(); + removeFromList(this, offset, c, true); + try { + incrementModCount(); + boolean res = subList.retainAll(c); + if (!res) + decrementModCount(); + return res; + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public void clear() { + checkForComodification(); + onProposedChange(Collections.emptyList(), offset, offset + size()); + try { + incrementModCount(); + subList.clear(); + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public E get(int index) { + checkForComodification(); + return subList.get(index); + } + + @Override + public E set(int index, E element) { + checkForComodification(); + onProposedChange(Collections.singletonList(element), offset + index, offset + index + 1); + return subList.set(index, element); + } + + @Override + public void add(int index, E element) { + checkForComodification(); + onProposedChange(Collections.singletonList(element), offset + index, offset + index); + try { + incrementModCount(); + subList.add(index, element); + } catch (Exception e) { + decrementModCount(); + throw e; + } + } + + @Override + public E remove(int index) { + checkForComodification(); + onProposedChange(Collections.emptyList(), offset + index, offset + index + 1); + try { + incrementModCount(); + E res = subList.remove(index); + return res; + } catch (Exception e) { + decrementModCount(); + throw e; + } + + } + + @Override + public int indexOf(Object o) { + checkForComodification(); + return subList.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + checkForComodification(); + return subList.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + checkForComodification(); + return new VetoableListIteratorDecorator(new ModCountAccessorImplSub(), + subList.listIterator(), offset); + } + + @Override + public ListIterator listIterator(int index) { + checkForComodification(); + return new VetoableListIteratorDecorator(new ModCountAccessorImplSub(), + subList.listIterator(index), offset + index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + checkForComodification(); + return new VetoableSubListDecorator(new ModCountAccessorImplSub(), + subList.subList(fromIndex, toIndex), offset + fromIndex); + } + + @Override + public String toString() { + checkForComodification(); + return subList.toString(); + } + + @Override + public boolean equals(Object obj) { + checkForComodification(); + return subList.equals(obj); + } + + @Override + public int hashCode() { + checkForComodification(); + return subList.hashCode(); + } + + private void checkForComodification() { + if (modCount != modCountAccessor.get()) { + throw new ConcurrentModificationException(); + } + } + + private void incrementModCount() { + modCount = modCountAccessor.incrementAndGet(); + } + + private void decrementModCount() { + modCount = modCountAccessor.decrementAndGet(); + } + + private class ModCountAccessorImplSub implements ModCountAccessor{ + + @Override + public int get() { + return modCount; + } + + @Override + public int incrementAndGet() { + return modCount = modCountAccessor.incrementAndGet(); + } + + @Override + public int decrementAndGet() { + return modCount = modCountAccessor.decrementAndGet(); + } + } + } + + private class VetoableIteratorDecorator implements Iterator { + + private final Iterator it; + private final ModCountAccessor modCountAccessor; + private int modCount; + protected final int offset; + protected int cursor; + protected int lastReturned; + + public VetoableIteratorDecorator(ModCountAccessor modCountAccessor, Iterator it, int offset) { + this.modCountAccessor = modCountAccessor; + this.modCount = modCountAccessor.get(); + this.it = it; + this.offset = offset; + } + + @Override + public boolean hasNext() { + checkForComodification(); + return it.hasNext(); + } + + @Override + public E next() { + checkForComodification(); + E e = it.next(); + lastReturned = cursor++; + return e; + } + + @Override + public void remove() { + checkForComodification(); + if (lastReturned == -1) { + throw new IllegalStateException(); + } + onProposedChange(Collections.emptyList(), offset + lastReturned, offset + lastReturned + 1); + try { + incrementModCount(); + it.remove(); + } catch (Exception e) { + decrementModCount(); + throw e; + } + lastReturned = -1; + --cursor; + } + + protected void checkForComodification() { + if (modCount != modCountAccessor.get()) { + throw new ConcurrentModificationException(); + } + } + + protected void incrementModCount() { + modCount = modCountAccessor.incrementAndGet(); + } + + protected void decrementModCount() { + modCount = modCountAccessor.decrementAndGet(); + } + } + + private class VetoableListIteratorDecorator extends VetoableIteratorDecorator implements ListIterator { + + private final ListIterator lit; + + public VetoableListIteratorDecorator(ModCountAccessor modCountAccessor, ListIterator it, int offset) { + super(modCountAccessor, it, offset); + this.lit = it; + } + + @Override + public boolean hasPrevious() { + checkForComodification(); + return lit.hasPrevious(); + } + + @Override + public E previous() { + checkForComodification(); + E e = lit.previous(); + lastReturned = --cursor; + return e; + } + + @Override + public int nextIndex() { + checkForComodification(); + return lit.nextIndex(); + } + + @Override + public int previousIndex() { + checkForComodification(); + return lit.previousIndex(); + } + + @Override + public void set(E e) { + checkForComodification(); + if (lastReturned == -1) { + throw new IllegalStateException(); + } + onProposedChange(Collections.singletonList(e), offset + lastReturned, offset + lastReturned + 1); + lit.set(e); + } + + @Override + public void add(E e) { + checkForComodification(); + onProposedChange(Collections.singletonList(e), offset + cursor, offset + cursor); + try { + incrementModCount(); + lit.add(e); + } catch (Exception ex) { + decrementModCount(); + throw ex; + } + ++cursor; + } + } + + private class ModCountAccessorImpl implements ModCountAccessor { + + public ModCountAccessorImpl() { + } + + @Override + public int get() { + return modCount; + } + + @Override + public int incrementAndGet() { + return ++modCount; + } + + @Override + public int decrementAndGet() { + return --modCount; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakListChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakListChangeListener.java new file mode 100644 index 00000000..ceb962ff --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakListChangeListener.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.WeakListener; + +import java.lang.ref.WeakReference; + +public final class WeakListChangeListener implements ListChangeListener, WeakListener { + + private final WeakReference> ref; + + /** + * The constructor of {@code WeakListChangeListener}. + * + * @param listener + * The original listener that should be notified + */ + public WeakListChangeListener(@NamedArg("listener") ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference>(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean wasGarbageCollected() { + return (ref.get() == null); + } + + /** + * {@inheritDoc} + */ + @Override + public void onChanged(Change change) { + final ListChangeListener listener = ref.get(); + if (listener != null) { + listener.onChanged(change); + } else { + // The weakly reference listener has been garbage collected, + // so this WeakListener will now unhook itself from the + // source bean + change.getList().removeListener(this); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakMapChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakMapChangeListener.java new file mode 100644 index 00000000..df3f1c40 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakMapChangeListener.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.WeakListener; + +import java.lang.ref.WeakReference; + +public final class WeakMapChangeListener implements MapChangeListener, WeakListener { + + private final WeakReference> ref; + + /** + * The constructor of {@code WeakMapChangeListener}. + * + * @param listener + * The original listener that should be notified + */ + public WeakMapChangeListener(@NamedArg("listener") MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference>(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean wasGarbageCollected() { + return (ref.get() == null); + } + + /** + * {@inheritDoc} + */ + @Override + public void onChanged(Change change) { + final MapChangeListener listener = ref.get(); + if (listener != null) { + listener.onChanged(change); + } else { + // The weakly reference listener has been garbage collected, + // so this WeakListener will now unhook itself from the + // source bean + change.getMap().removeListener(this); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakSetChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakSetChangeListener.java new file mode 100644 index 00000000..e747107b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/WeakSetChangeListener.java @@ -0,0 +1,48 @@ +package com.tungsten.fclcore.fakefx.collections; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.WeakListener; + +import java.lang.ref.WeakReference; + +public final class WeakSetChangeListener implements SetChangeListener, WeakListener { + + private final WeakReference> ref; + + /** + * The constructor of {@code WeakSetChangeListener}. + * + * @param listener + * The original listener that should be notified + */ + public WeakSetChangeListener(@NamedArg("listener") SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference>(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean wasGarbageCollected() { + return (ref.get() == null); + } + + /** + * {@inheritDoc} + */ + @Override + public void onChanged(Change change) { + final SetChangeListener listener = ref.get(); + if (listener != null) { + listener.onChanged(change); + } else { + // The weakly reference listener has been garbage collected, + // so this WeakListener will now unhook itself from the + // source bean + change.getSet().removeListener(this); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/FilteredList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/FilteredList.java new file mode 100644 index 00000000..3e3b8a27 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/FilteredList.java @@ -0,0 +1,318 @@ +package com.tungsten.fclcore.fakefx.collections.transformation; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty; +import com.tungsten.fclcore.fakefx.beans.property.ObjectPropertyBase; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.NonIterableChange; +import com.tungsten.fclcore.fakefx.collections.ObservableList; +import com.tungsten.fclcore.fakefx.collections.SortHelper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Predicate; + +/** + * Wraps an ObservableList and filters its content using the provided Predicate. + * All changes in the ObservableList are propagated immediately + * to the FilteredList. + * + * @see TransformationList + * @since JavaFX 8.0 + */ +public final class FilteredList extends TransformationList{ + + private int[] filtered; + private int size; + + private SortHelper helper; + private static final Predicate ALWAYS_TRUE = t -> true; + + /** + * Constructs a new FilteredList wrapper around the source list. + * The provided predicate will match the elements in the source list that will be visible. + * If the predicate is null, all elements will be matched and the list is equal to the source list. + * @param source the source list + * @param predicate the predicate to match the elements or null to match all elements. + */ + public FilteredList(@NamedArg("source") ObservableList source, @NamedArg("predicate") Predicate predicate) { + super(source); + filtered = new int[source.size() * 3 / 2 + 1]; + if (predicate != null) { + setPredicate(predicate); + } else { + for (size = 0; size < source.size(); size++) { + filtered[size] = size; + } + } + } + + /** + * Constructs a new FilteredList wrapper around the source list. + * This list has an "always true" predicate, containing all the elements + * of the source list. + *

+ * This constructor might be useful if you want to bind {@link #predicateProperty()} + * of this list. + * @param source the source list + */ + public FilteredList(@NamedArg("source") ObservableList source) { + this(source, null); + } + + /** + * The predicate that will match the elements that will be in this FilteredList. + * Elements not matching the predicate will be filtered-out. + * Null predicate means "always true" predicate, all elements will be matched. + */ + private ObjectProperty> predicate; + + public final ObjectProperty> predicateProperty() { + if (predicate == null) { + predicate = new ObjectPropertyBase>() { + @Override + protected void invalidated() { + refilter(); + } + + @Override + public Object getBean() { + return FilteredList.this; + } + + @Override + public String getName() { + return "predicate"; + } + + }; + } + return predicate; + } + + public final Predicate getPredicate() { + return predicate == null ? null : predicate.get(); + } + + public final void setPredicate(Predicate predicate) { + predicateProperty().set(predicate); + } + + private Predicate getPredicateImpl() { + if (getPredicate() != null) { + return getPredicate(); + } + return ALWAYS_TRUE; + } + + @Override + protected void sourceChanged(ListChangeListener.Change c) { + beginChange(); + while (c.next()) { + if (c.wasPermutated()) { + permutate(c); + } else if (c.wasUpdated()) { + update(c); + } else { + addRemove(c); + } + } + endChange(); + } + + /** + * Returns the number of elements in this list. + * + * @return the number of elements in this list + */ + @Override + public int size() { + return size; + } + + /** + * Returns the element at the specified position in this list. + * + * @param index index of the element to return + * @return the element at the specified position in this list + * @throws IndexOutOfBoundsException {@inheritDoc} + */ + @Override + public E get(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException(); + } + return getSource().get(filtered[index]); + } + + @Override + public int getSourceIndex(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException(); + } + return filtered[index]; + } + + @Override + public int getViewIndex(int index) { + return Arrays.binarySearch(filtered, 0, size, index); + } + + private SortHelper getSortHelper() { + if (helper == null) { + helper = new SortHelper(); + } + return helper; + } + + private int findPosition(int p) { + if (filtered.length == 0) { + return 0; + } + if (p == 0) { + return 0; + } + int pos = Arrays.binarySearch(filtered, 0, size, p); + if (pos < 0 ) { + pos = ~pos; + } + return pos; + } + + + @SuppressWarnings("unchecked") + private void ensureSize(int size) { + if (filtered.length < size) { + int[] replacement = new int[size * 3/2 + 1]; + System.arraycopy(filtered, 0, replacement, 0, this.size); + filtered = replacement; + } + } + + private void updateIndexes(int from, int delta) { + for (int i = from; i < size; ++i) { + filtered[i] += delta; + } + } + + private void permutate(ListChangeListener.Change c) { + int from = findPosition(c.getFrom()); + int to = findPosition(c.getTo()); + + if (to > from) { + for (int i = from; i < to; ++i) { + filtered[i] = c.getPermutation(filtered[i]); + } + + int[] perm = getSortHelper().sort(filtered, from, to); + nextPermutation(from, to, perm); + } + } + + private void addRemove(ListChangeListener.Change c) { + Predicate pred = getPredicateImpl(); + ensureSize(getSource().size()); + final int from = findPosition(c.getFrom()); + final int to = findPosition(c.getFrom() + c.getRemovedSize()); + + // Mark the nodes that are going to be removed + for (int i = from; i < to; ++i) { + nextRemove(from, c.getRemoved().get(filtered[i] - c.getFrom())); + } + + // Update indexes of the sublist following the last element that was removed + updateIndexes(to, c.getAddedSize() - c.getRemovedSize()); + + // Replace as many removed elements as possible + int fpos = from; + int pos = c.getFrom(); + + ListIterator it = getSource().listIterator(pos); + for (; fpos < to && it.nextIndex() < c.getTo();) { + if (pred.test(it.next())) { + filtered[fpos] = it.previousIndex(); + nextAdd(fpos, fpos + 1); + ++fpos; + } + } + + if (fpos < to) { + // If there were more removed elements than added + System.arraycopy(filtered, to, filtered, fpos, size - to); + size -= to - fpos; + } else { + // Add the remaining elements + while (it.nextIndex() < c.getTo()) { + if (pred.test(it.next())) { + System.arraycopy(filtered, fpos, filtered, fpos + 1, size - fpos); + filtered[fpos] = it.previousIndex(); + nextAdd(fpos, fpos + 1); + ++fpos; + ++size; + } + ++pos; + } + } + } + + private void update(ListChangeListener.Change c) { + Predicate pred = getPredicateImpl(); + ensureSize(getSource().size()); + int sourceFrom = c.getFrom(); + int sourceTo = c.getTo(); + int filterFrom = findPosition(sourceFrom); + int filterTo = findPosition(sourceTo); + ListIterator it = getSource().listIterator(sourceFrom); + int pos = filterFrom; + while (pos < filterTo || sourceFrom < sourceTo) { + E el = it.next(); + if (pos < size && filtered[pos] == sourceFrom) { + if (!pred.test(el)) { + nextRemove(pos, el); + System.arraycopy(filtered, pos + 1, filtered, pos, size - pos - 1); + --size; + --filterTo; + } else { + nextUpdate(pos); + ++pos; + } + } else { + if (pred.test(el)) { + nextAdd(pos, pos + 1); + System.arraycopy(filtered, pos, filtered, pos + 1, size - pos); + filtered[pos] = sourceFrom; + ++size; + ++pos; + ++filterTo; + } + } + sourceFrom++; + } + } + + @SuppressWarnings("unchecked") + private void refilter() { + ensureSize(getSource().size()); + List removed = null; + if (hasListeners()) { + removed = new ArrayList<>(this); + } + size = 0; + int i = 0; + Predicate pred = getPredicateImpl(); + for (Iterator it = getSource().iterator();it.hasNext(); ) { + final E next = it.next(); + if (pred.test(next)) { + filtered[size++] = i; + } + ++i; + } + if (hasListeners()) { + fireChange(new NonIterableChange.GenericAddRemoveChange<>(0, size, removed, this)); + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/SortedList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/SortedList.java new file mode 100644 index 00000000..9403e68c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/SortedList.java @@ -0,0 +1,386 @@ +package com.tungsten.fclcore.fakefx.collections.transformation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty; +import com.tungsten.fclcore.fakefx.beans.property.ObjectPropertyBase; +import com.tungsten.fclcore.fakefx.collections.ListChangeListener.Change; +import com.tungsten.fclcore.fakefx.collections.NonIterableChange; +import com.tungsten.fclcore.fakefx.collections.ObservableList; +import com.tungsten.fclcore.fakefx.collections.SortHelper; +import com.tungsten.fclcore.fakefx.collections.SourceAdapterChange; + +/** + * Wraps an ObservableList and sorts its content. + * All changes in the ObservableList are propagated immediately + * to the SortedList. + * + * Note: invalid SortedList (as a result of broken comparison) doesn't send any notification to listeners on becoming + * valid again. + * + * @see TransformationList + * @since JavaFX 8.0 + */ +public final class SortedList extends TransformationList{ + + private Comparator> elementComparator; + private Element[] sorted; + private int[] perm; + private int size; + + private final SortHelper helper = new SortHelper(); + + private final Element tempElement = new Element<>(null, -1); + + + /** + * Creates a new SortedList wrapped around the source list. + * The source list will be sorted using the comparator provided. If null is provided, the list + * stays unordered and is equal to the source list. + * @param source a list to wrap + * @param comparator a comparator to use or null for unordered List + */ + @SuppressWarnings("unchecked") + public SortedList(@NamedArg("source") ObservableList source, @NamedArg("comparator") Comparator comparator) { + super(source); + sorted = (Element[]) new Element[source.size() *3/2 + 1]; + perm = new int[sorted.length]; + size = source.size(); + for (int i = 0; i < size; ++i) { + sorted[i] = new Element(source.get(i), i); + perm[i] = i; + } + if (comparator != null) { + setComparator(comparator); + } + + } + + /** + * Constructs a new unordered SortedList wrapper around the source list. + * @param source the source list + * @see #SortedList(ObservableList, Comparator) + */ + public SortedList(@NamedArg("source") ObservableList source) { + this(source, (Comparator)null); + } + + @Override + protected void sourceChanged(Change c) { + if (elementComparator != null) { + beginChange(); + while (c.next()) { + if (c.wasPermutated()) { + updatePermutationIndexes(c); + } else if (c.wasUpdated()) { + update(c); + } else { + addRemove(c); + } + } + endChange(); + } else { + updateUnsorted(c); + fireChange(new SourceAdapterChange<>(this, c)); + } + }; + + /** + * The comparator that denotes the order of this SortedList. + * Null for unordered SortedList. + */ + private ObjectProperty> comparator; + + public final ObjectProperty> comparatorProperty() { + if (comparator == null) { + comparator = new ObjectPropertyBase>() { + + @Override + protected void invalidated() { + Comparator current = get(); + elementComparator = current != null ? new ElementComparator<>(current) : null; + doSortWithPermutationChange(); + } + + @Override + public Object getBean() { + return SortedList.this; + } + + @Override + public String getName() { + return "comparator"; + } + + }; + } + return comparator; + } + + public final Comparator getComparator() { + return comparator == null ? null : comparator.get(); + } + + public final void setComparator(Comparator comparator) { + comparatorProperty().set(comparator); + } + + /** + * Returns the element at the specified position in this list. + * + * @param index index of the element to return + * @return the element at the specified position in this list + * @throws IndexOutOfBoundsException {@inheritDoc} + */ + @Override + public E get(int index) { + if (index >= size) { + throw new IndexOutOfBoundsException(); + } + return sorted[index].e; + } + + /** + * Returns the number of elements in this list. + * + * @return the number of elements in this list + */ + @Override + public int size() { + return size; + } + + private void doSortWithPermutationChange() { + if (elementComparator != null) { + int[] perm = helper.sort(sorted, 0, size, elementComparator); + for (int i = 0; i < size; i++) { + this.perm[sorted[i].index] = i; + } + fireChange(new NonIterableChange.SimplePermutationChange<>(0, size, perm, this)); + } else { + int[] perm = new int[size]; + int[] rperm = new int[size]; + for (int i = 0; i < size; ++i) { + perm[i] = rperm[i] = i; + } + boolean changed = false; + int idx = 0; + while (idx < size) { + final int otherIdx = sorted[idx].index; + if (otherIdx == idx) { + ++idx; + continue; + } + Element other = sorted[otherIdx]; + sorted[otherIdx] = sorted[idx]; + sorted[idx] = other; + this.perm[idx] = idx; + this.perm[otherIdx] = otherIdx; + perm[rperm[idx]] = otherIdx; + perm[rperm[otherIdx]] = idx; + int tp = rperm[idx]; + rperm[idx] = rperm[otherIdx]; + rperm[otherIdx] = tp; + changed = true; + } + if (changed) { + fireChange(new NonIterableChange.SimplePermutationChange<>(0, size, perm, this)); + } + } + } + + @Override + public int getSourceIndex(int index) { + return sorted[index].index; + } + + @Override + public int getViewIndex(int index) { + return perm[index]; + } + + private void updatePermutationIndexes(Change change) { + for (int i = 0; i < size; ++i) { + int p = change.getPermutation(sorted[i].index); + sorted[i].index = p; + perm[p] = i; + } + } + + private void updateUnsorted(Change c) { + while (c.next()) { + if (c.wasPermutated()) { + Element[] sortedTmp = new Element[sorted.length]; + for (int i = 0; i < size; ++i) { + if (i >= c.getFrom() && i < c.getTo()) { + int p = c.getPermutation(i); + sortedTmp[p] = sorted[i]; + sortedTmp[p].index = p; + perm[i] = i; + } else { + sortedTmp[i] = sorted[i]; + } + } + sorted = sortedTmp; + } + if (c.wasRemoved()) { + final int removedTo = c.getFrom() + c.getRemovedSize(); + System.arraycopy(sorted, removedTo, sorted, c.getFrom(), size - removedTo); + System.arraycopy(perm, removedTo, perm, c.getFrom(), size - removedTo); + size -= c.getRemovedSize(); + updateIndices(removedTo, removedTo, -c.getRemovedSize()); + } + if (c.wasAdded()) { + ensureSize(size + c.getAddedSize()); + updateIndices(c.getFrom(), c.getFrom(), c.getAddedSize()); + System.arraycopy(sorted, c.getFrom(), sorted, c.getTo(), size - c.getFrom()); + System.arraycopy(perm, c.getFrom(), perm, c.getTo(), size - c.getFrom()); + size += c.getAddedSize(); + for (int i = c.getFrom(); i < c.getTo(); ++i) { + sorted[i] = new Element(c.getList().get(i), i); + perm[i] = i; + } + } + } + } + + private static class Element { + + public Element(E e, int index) { + this.e = e; + this.index = index; + } + + private E e; + private int index; + } + + private static class ElementComparator implements Comparator> { + + private final Comparator comparator; + + public ElementComparator(Comparator comparator) { + this.comparator = comparator; + } + + @Override + @SuppressWarnings("unchecked") + public int compare(Element o1, Element o2) { + return comparator.compare(o1.e, o2.e); + } + + } + + private void ensureSize(int size) { + if (sorted.length < size) { + Element[] replacement = new Element[size * 3/2 + 1]; + System.arraycopy(sorted, 0, replacement, 0, this.size); + sorted = replacement; + int[] replacementPerm = new int[size * 3/2 + 1]; + System.arraycopy(perm, 0, replacementPerm, 0, this.size); + perm = replacementPerm; + } + } + + private void updateIndices(int from, int viewFrom, int difference) { + for (int i = 0 ; i < size; ++i) { + if (sorted[i].index >= from) { + sorted[i].index += difference; + } + if (perm[i] >= viewFrom) { + perm[i] += difference; + } + } + } + + private int findPosition(E e) { + if (sorted.length == 0) { + return 0; + } + tempElement.e = e; + int pos = Arrays.binarySearch(sorted, 0, size, tempElement, elementComparator); + return pos; + } + + private void insertToMapping(E e, int idx) { + int pos = findPosition(e); + if (pos < 0) { + pos = ~pos; + } + ensureSize(size + 1); + updateIndices(idx, pos, 1); + System.arraycopy(sorted, pos, sorted, pos + 1, size - pos); + sorted[pos] = new Element<>(e, idx); + System.arraycopy(perm, idx, perm, idx + 1, size - idx); + perm[idx] = pos; + ++size; + nextAdd(pos, pos + 1); + + } + + private void setAllToMapping(List list, int to) { + ensureSize(to); + size = to; + for (int i = 0; i < to; ++i) { + sorted[i] = new Element(list.get(i), i); + } + int[] perm = helper.sort(sorted, 0, size, elementComparator); + System.arraycopy(perm, 0, this.perm, 0, size); + nextAdd(0, size); + } + + private void removeFromMapping(int idx, E e) { + int pos = perm[idx]; + System.arraycopy(sorted, pos + 1, sorted, pos, size - pos - 1); + System.arraycopy(perm, idx + 1, perm, idx, size - idx - 1); + --size; + sorted[size] = null; + updateIndices(idx + 1, pos, - 1); + + nextRemove(pos, e); + } + + private void removeAllFromMapping() { + List removed = new ArrayList(this); + for (int i = 0; i < size; ++i) { + sorted[i] = null; + } + size = 0; + nextRemove(0, removed); + } + + private void update(Change c) { + int[] perm = helper.sort(sorted, 0, size, elementComparator); + for (int i = 0; i < size; i++) { + this.perm[sorted[i].index] = i; + } + nextPermutation(0, size, perm); + for (int i = c.getFrom(), to = c.getTo(); i < to; ++i) { + nextUpdate(this.perm[i]); + } + } + + private void addRemove(Change c) { + if (c.getFrom() == 0 && c.getRemovedSize() == size) { + removeAllFromMapping(); + } else { + for (int i = 0, sz = c.getRemovedSize(); i < sz; ++i) { + removeFromMapping(c.getFrom(), c.getRemoved().get(i)); + } + } + if (size == 0) { + setAllToMapping(c.getList(), c.getTo()); // This is basically equivalent to getAddedSubList + // as size is 0, only valid "from" is also 0 + } else { + for (int i = c.getFrom(), to = c.getTo(); i < to; ++i) { + insertToMapping(c.getList().get(i), i); + } + } + } + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/TransformationList.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/TransformationList.java new file mode 100644 index 00000000..5fe85509 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/collections/transformation/TransformationList.java @@ -0,0 +1,127 @@ +package com.tungsten.fclcore.fakefx.collections.transformation; + +import com.tungsten.fclcore.fakefx.collections.ListChangeListener; +import com.tungsten.fclcore.fakefx.collections.ObservableList; +import com.tungsten.fclcore.fakefx.collections.ObservableListBase; +import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener; + +import java.util.List; + +public abstract class TransformationList extends ObservableListBase implements ObservableList { + + /** + * Contains the source list of this transformation list. + * This is never null and should be used to directly access source list content + */ + private ObservableList source; + /** + * This field contains the result of expression "source instanceof {@link ObservableList}". + * If this is true, it is possible to do transforms online. + */ + private ListChangeListener sourceListener; + + /** + * Creates a new Transformation list wrapped around the source list. + * @param source the wrapped list + */ + @SuppressWarnings("unchecked") + protected TransformationList(ObservableList source) { + if (source == null) { + throw new NullPointerException(); + } + this.source = source; + source.addListener(new WeakListChangeListener<>(getListener())); + } + + /** + * The source list specified in the constructor of this transformation list. + * @return The List that is directly wrapped by this TransformationList + */ + public final ObservableList getSource() { + return source; + } + + /** + * Checks whether the provided list is in the chain under this + * {@code TransformationList}. + * + * This means the list is either the direct source as returned by + * {@link #getSource()} or the direct source is a {@code TransformationList}, + * and the list is in it's transformation chain. + * @param list the list to check + * @return true if the list is in the transformation chain as specified above. + */ + public final boolean isInTransformationChain(ObservableList list) { + if (source == list) { + return true; + } + List currentSource = source; + while(currentSource instanceof TransformationList) { + currentSource = ((TransformationList)currentSource).source; + if (currentSource == list) { + return true; + } + } + return false; + } + + private ListChangeListener getListener() { + if (sourceListener == null) { + sourceListener = c -> { + TransformationList.this.sourceChanged(c); + }; + } + return sourceListener; + } + + /** + * Called when a change from the source is triggered. + * @param c the change + */ + protected abstract void sourceChanged(ListChangeListener.Change c); + + /** + * Maps the index of this list's element to an index in the direct source list. + * @param index the index in this list + * @return the index of the element's origin in the source list + * @see #getSource() + */ + public abstract int getSourceIndex(int index); + + /** + * Maps the index of this list's element to an index of the provided {@code list}. + * + * The {@code list} must be in the transformation chain. + * + * @param list a list from the transformation chain + * @param index the index of an element in this list + * @return the index of the element's origin in the provided list + * @see #isInTransformationChain(ObservableList) + */ + public final int getSourceIndexFor(ObservableList list, int index) { + if (!isInTransformationChain(list)) { + throw new IllegalArgumentException("Provided list is not in the transformation chain of this" + + "transformation list"); + } + List currentSource = source; + int idx = getSourceIndex(index); + while(currentSource != list && currentSource instanceof TransformationList) { + final TransformationList tSource = (TransformationList)currentSource; + idx = tSource.getSourceIndex(idx); + currentSource = tSource.source; + } + return idx; + } + + /** + * Maps the index of the direct source list's element to an index in this list. + * @param index the index in the source list + * @return the index of the element in this list if it is contained + * in this list or negative value otherwise + * @see #getSource() + * @see #getSourceIndex(int) + * + * @since 9 + */ + public abstract int getViewIndex(int index); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/ActionEvent.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/ActionEvent.java new file mode 100644 index 00000000..ee250fea --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/ActionEvent.java @@ -0,0 +1,51 @@ +package com.tungsten.fclcore.fakefx.event; + +public class ActionEvent extends Event { + + private static final long serialVersionUID = 20121107L; + /** + * The only valid EventType for the ActionEvent. + */ + public static final EventType ACTION = + new EventType(Event.ANY, "ACTION"); + + /** + * Common supertype for all action event types. + * @since JavaFX 8.0 + */ + public static final EventType ANY = ACTION; + + /** + * Creates a new {@code ActionEvent} with an event type of {@code ACTION}. + * The source and target of the event is set to {@code NULL_SOURCE_TARGET}. + */ + public ActionEvent() { + super(ACTION); + } + + /** + * Construct a new {@code ActionEvent} with the specified event source and target. + * If the source or target is set to {@code null}, it is replaced by the + * {@code NULL_SOURCE_TARGET} value. All ActionEvents have their type set to + * {@code ACTION}. + * + * @param source the event source which sent the event + * @param target the event target to associate with the event + */ + public ActionEvent(Object source, EventTarget target) { + super(source, target, ACTION); + } + + @Override + public ActionEvent copyFor(Object newSource, EventTarget newTarget) { + return (ActionEvent) super.copyFor(newSource, newTarget); + } + + @Override + public EventType getEventType() { + return (EventType) super.getEventType(); + } + + + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/BasicEventDispatcher.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/BasicEventDispatcher.java new file mode 100644 index 00000000..d991d368 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/BasicEventDispatcher.java @@ -0,0 +1,63 @@ +package com.tungsten.fclcore.fakefx.event; + +/** + * Event dispatcher which introduces event dispatch phase specific methods - + * {@code dispatchCapturingEvent} and {@code dispatchBubblingEvent}. These + * are used in the {@code BasicEventDispatcher.dispatchEvent} implementation, + * but because they are public they can be called directly as well. Their + * default implementation does nothing and is expected to be overridden in + * subclasses. The {@code BasicEventDispatcher} also adds possibility to chain + * event dispatchers. This is used together with the direct access to the phase + * specific dispatch methods to implement {@code CompositeEventDispatcher}. + *

+ * An event dispatcher derived from {@code BasicEventDispatcher} can act as + * a standalone event dispatcher or can be used to form a dispatch chain in + * {@code CompositeEventDispatcher}. + */ +public abstract class BasicEventDispatcher implements EventDispatcher { + private BasicEventDispatcher previousDispatcher; + private BasicEventDispatcher nextDispatcher; + + @Override + public Event dispatchEvent(Event event, final EventDispatchChain tail) { + event = dispatchCapturingEvent(event); + if (event.isConsumed()) { + return null; + } + event = tail.dispatchEvent(event); + if (event != null) { + event = dispatchBubblingEvent(event); + if (event.isConsumed()) { + return null; + } + } + + return event; + } + + public Event dispatchCapturingEvent(Event event) { + return event; + } + + public Event dispatchBubblingEvent(Event event) { + return event; + } + + public final BasicEventDispatcher getPreviousDispatcher() { + return previousDispatcher; + } + + public final BasicEventDispatcher getNextDispatcher() { + return nextDispatcher; + } + + public final void insertNextDispatcher( + final BasicEventDispatcher newDispatcher) { + if (nextDispatcher != null) { + nextDispatcher.previousDispatcher = newDispatcher; + } + newDispatcher.nextDispatcher = nextDispatcher; + newDispatcher.previousDispatcher = this; + nextDispatcher = newDispatcher; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventDispatcher.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventDispatcher.java new file mode 100644 index 00000000..cf2a6953 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventDispatcher.java @@ -0,0 +1,42 @@ +package com.tungsten.fclcore.fakefx.event; + +/** + * An {@code EventDispatcher} which represents a chain of event dispatchers, but + * can still be set or replaced as a single entity. + */ +public abstract class CompositeEventDispatcher extends BasicEventDispatcher { + public abstract BasicEventDispatcher getFirstDispatcher(); + + public abstract BasicEventDispatcher getLastDispatcher(); + + @Override + public final Event dispatchCapturingEvent(Event event) { + BasicEventDispatcher childDispatcher = getFirstDispatcher(); + while (childDispatcher != null) { + event = childDispatcher.dispatchCapturingEvent(event); + if (event.isConsumed()) { + break; + } + + childDispatcher = childDispatcher.getNextDispatcher(); + } + + return event; + } + + @Override + public final Event dispatchBubblingEvent(Event event) { + // need to dispatch in reversed direction + BasicEventDispatcher childDispatcher = getLastDispatcher(); + while (childDispatcher != null) { + event = childDispatcher.dispatchBubblingEvent(event); + if (event.isConsumed()) { + break; + } + + childDispatcher = childDispatcher.getPreviousDispatcher(); + } + + return event; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventHandler.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventHandler.java new file mode 100644 index 00000000..0b108ab0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventHandler.java @@ -0,0 +1,335 @@ +package com.tungsten.fclcore.fakefx.event; + +public final class CompositeEventHandler { + private EventProcessorRecord firstRecord; + private EventProcessorRecord lastRecord; + + private EventHandler eventHandler; + + public void setEventHandler(final EventHandler eventHandler) { + this.eventHandler = eventHandler; + } + + public EventHandler getEventHandler() { + return eventHandler; + } + + public void addEventHandler(final EventHandler eventHandler) { + if (find(eventHandler, false) == null) { + append(lastRecord, createEventHandlerRecord(eventHandler)); + } + } + + public void removeEventHandler(final EventHandler eventHandler) { + final EventProcessorRecord record = find(eventHandler, false); + if (record != null) { + remove(record); + } + } + + public void addEventFilter(final EventHandler eventFilter) { + if (find(eventFilter, true) == null) { + append(lastRecord, createEventFilterRecord(eventFilter)); + } + } + + public void removeEventFilter(final EventHandler eventFilter) { + final EventProcessorRecord record = find(eventFilter, true); + if (record != null) { + remove(record); + } + } + + public void dispatchBubblingEvent(final Event event) { + final T specificEvent = (T) event; + + EventProcessorRecord record = firstRecord; + while (record != null) { + if (record.isDisconnected()) { + remove(record); + } else { + record.handleBubblingEvent(specificEvent); + } + record = record.nextRecord; + } + + if (eventHandler != null) { + eventHandler.handle(specificEvent); + } + } + + public void dispatchCapturingEvent(final Event event) { + final T specificEvent = (T) event; + + EventProcessorRecord record = firstRecord; + while (record != null) { + if (record.isDisconnected()) { + remove(record); + } else { + record.handleCapturingEvent(specificEvent); + } + record = record.nextRecord; + } + } + + public boolean hasFilter() { + return find(true); + } + + public boolean hasHandler() { + if (getEventHandler() != null) return true; + return find(false); + } + + /* Used for testing. */ + boolean containsHandler(final EventHandler eventHandler) { + return find(eventHandler, false) != null; + } + + /* Used for testing. */ + boolean containsFilter(final EventHandler eventFilter) { + return find(eventFilter, true) != null; + } + + private EventProcessorRecord createEventHandlerRecord( + final EventHandler eventHandler) { + return (eventHandler instanceof WeakEventHandler) + ? new WeakEventHandlerRecord( + (WeakEventHandler) eventHandler) + : new NormalEventHandlerRecord(eventHandler); + } + + private EventProcessorRecord createEventFilterRecord( + final EventHandler eventFilter) { + return (eventFilter instanceof WeakEventHandler) + ? new WeakEventFilterRecord( + (WeakEventHandler) eventFilter) + : new NormalEventFilterRecord(eventFilter); + } + + private void remove(final EventProcessorRecord record) { + final EventProcessorRecord prevRecord = record.prevRecord; + final EventProcessorRecord nextRecord = record.nextRecord; + + if (prevRecord != null) { + prevRecord.nextRecord = nextRecord; + } else { + firstRecord = nextRecord; + } + + if (nextRecord != null) { + nextRecord.prevRecord = prevRecord; + } else { + lastRecord = prevRecord; + } + + // leave record.nextRecord set + } + + private void append(final EventProcessorRecord prevRecord, + final EventProcessorRecord newRecord) { + EventProcessorRecord nextRecord; + if (prevRecord != null) { + nextRecord = prevRecord.nextRecord; + prevRecord.nextRecord = newRecord; + } else { + nextRecord = firstRecord; + firstRecord = newRecord; + } + + if (nextRecord != null) { + nextRecord.prevRecord = newRecord; + } else { + lastRecord = newRecord; + } + + newRecord.prevRecord = prevRecord; + newRecord.nextRecord = nextRecord; + } + + private EventProcessorRecord find( + final EventHandler eventProcessor, + final boolean isFilter) { + EventProcessorRecord record = firstRecord; + while (record != null) { + if (record.isDisconnected()) { + remove(record); + } else if (record.stores(eventProcessor, isFilter)) { + return record; + } + + record = record.nextRecord; + } + + return null; + } + + private boolean find(boolean isFilter) { + EventProcessorRecord record = firstRecord; + while (record != null) { + if (record.isDisconnected()) { + remove(record); + } else if (isFilter == record.isFilter()) { + return true; + } + record = record.nextRecord; + } + return false; + } + + private static abstract class EventProcessorRecord { + private EventProcessorRecord nextRecord; + private EventProcessorRecord prevRecord; + + public abstract boolean stores(EventHandler eventProcessor, + boolean isFilter); + + public abstract boolean isFilter(); + + public abstract void handleBubblingEvent(T event); + + public abstract void handleCapturingEvent(T event); + + public abstract boolean isDisconnected(); + } + + private static final class NormalEventHandlerRecord + extends EventProcessorRecord { + private final EventHandler eventHandler; + + public NormalEventHandlerRecord( + final EventHandler eventHandler) { + this.eventHandler = eventHandler; + } + + @Override + public boolean stores(final EventHandler eventProcessor, + final boolean isFilter) { + return isFilter == isFilter() && (this.eventHandler == eventProcessor); + } + + @Override + public boolean isFilter() { + return false; + } + + @Override + public void handleBubblingEvent(final T event) { + eventHandler.handle(event); + } + + @Override + public void handleCapturingEvent(final T event) { + } + + @Override + public boolean isDisconnected() { + return false; + } + } + + private static final class WeakEventHandlerRecord + extends EventProcessorRecord { + private final WeakEventHandler weakEventHandler; + + public WeakEventHandlerRecord( + final WeakEventHandler weakEventHandler) { + this.weakEventHandler = weakEventHandler; + } + + @Override + public boolean stores(final EventHandler eventProcessor, + final boolean isFilter) { + return isFilter == isFilter() && (weakEventHandler == eventProcessor); + } + + @Override + public boolean isFilter() { + return false; + } + + @Override + public void handleBubblingEvent(final T event) { + weakEventHandler.handle(event); + } + + @Override + public void handleCapturingEvent(final T event) { + } + + @Override + public boolean isDisconnected() { + return weakEventHandler.wasGarbageCollected(); + } + } + + private static final class NormalEventFilterRecord + extends EventProcessorRecord { + private final EventHandler eventFilter; + + public NormalEventFilterRecord( + final EventHandler eventFilter) { + this.eventFilter = eventFilter; + } + + @Override + public boolean stores(final EventHandler eventProcessor, + final boolean isFilter) { + return isFilter == isFilter() && (this.eventFilter == eventProcessor); + } + + @Override + public boolean isFilter() { + return true; + } + + @Override + public void handleBubblingEvent(final T event) { + } + + @Override + public void handleCapturingEvent(final T event) { + eventFilter.handle(event); + } + + @Override + public boolean isDisconnected() { + return false; + } + } + + private static final class WeakEventFilterRecord + extends EventProcessorRecord { + private final WeakEventHandler weakEventFilter; + + public WeakEventFilterRecord( + final WeakEventHandler weakEventFilter) { + this.weakEventFilter = weakEventFilter; + } + + @Override + public boolean stores(final EventHandler eventProcessor, + final boolean isFilter) { + return isFilter == isFilter() && (weakEventFilter == eventProcessor); + } + + @Override + public boolean isFilter() { + return true; + } + + @Override + public void handleBubblingEvent(final T event) { + } + + @Override + public void handleCapturingEvent(final T event) { + weakEventFilter.handle(event); + } + + @Override + public boolean isDisconnected() { + return weakEventFilter.wasGarbageCollected(); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTarget.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTarget.java new file mode 100644 index 00000000..9611c4d3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTarget.java @@ -0,0 +1,9 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.Set; + +public interface CompositeEventTarget extends EventTarget { + Set getTargets(); + + boolean containsTarget(EventTarget target); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTargetImpl.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTargetImpl.java new file mode 100644 index 00000000..ce5f4aa9 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/CompositeEventTargetImpl.java @@ -0,0 +1,44 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class CompositeEventTargetImpl implements CompositeEventTarget { + private final Set eventTargets; + + public CompositeEventTargetImpl(final EventTarget... eventTargets) { + final Set mutableSet = + new HashSet(eventTargets.length); + mutableSet.addAll(Arrays.asList(eventTargets)); + + this.eventTargets = Collections.unmodifiableSet(mutableSet); + } + + @Override + public Set getTargets() { + return eventTargets; + } + + @Override + public boolean containsTarget(EventTarget target) { + return eventTargets.contains(target); + } + + @Override + public EventDispatchChain buildEventDispatchChain( + final EventDispatchChain tail) { + EventDispatchTree eventDispatchTree = (EventDispatchTree) tail; + + for (final EventTarget eventTarget: eventTargets) { + final EventDispatchTree targetDispatchTree = + eventDispatchTree.createTree(); + eventDispatchTree = eventDispatchTree.mergeTree( + (EventDispatchTree) eventTarget.buildEventDispatchChain( + targetDispatchTree)); + } + + return eventDispatchTree; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/DirectEvent.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/DirectEvent.java new file mode 100644 index 00000000..9f4a06a4 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/DirectEvent.java @@ -0,0 +1,30 @@ +package com.tungsten.fclcore.fakefx.event; + +/** + * Used as a wrapper to protect an {@code Event} from being redirected by + * {@code EventRedirector}. The redirector only unwraps such event and sends + * it to the rest of the event chain. + */ +public class DirectEvent extends Event { + private static final long serialVersionUID = 20121107L; + + public static final EventType DIRECT = + new EventType(Event.ANY, "DIRECT"); + + private final Event originalEvent; + + public DirectEvent(final Event originalEvent) { + this(originalEvent, null, null); + } + + public DirectEvent(final Event originalEvent, + final Object source, + final EventTarget target) { + super(source, target, DIRECT); + this.originalEvent = originalEvent; + } + + public Event getOriginalEvent() { + return originalEvent; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/Event.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/Event.java new file mode 100644 index 00000000..a1187b8c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/Event.java @@ -0,0 +1,175 @@ +package com.tungsten.fclcore.fakefx.event; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; + +import java.util.EventObject; + +import java.io.IOException; + +// PENDING_DOC_REVIEW +/** + * Base class for FX events. Each FX event has associated an event source, + * event target and an event type. The event source specifies for an event + * handler the object on which that handler has been registered and which sent + * the event to it. The event target defines the path through which the event + * will travel when posted. The event type provides additional classification + * to events of the same {@code Event} class. + * @since JavaFX 2.0 + */ +public class Event extends EventObject implements Cloneable { + + private static final long serialVersionUID = 20121107L; + /** + * The constant which represents an unknown event source / target. + */ + public static final EventTarget NULL_SOURCE_TARGET = tail -> tail; + + /** + * Common supertype for all event types. + */ + public static final EventType ANY = EventType.ROOT; + + /** + * Type of the event. + */ + protected EventType eventType; + + /** + * Event target that defines the path through which the event + * will travel when posted. + */ + protected transient EventTarget target; + + /** + * Whether this event has been consumed by any filter or handler. + */ + protected boolean consumed; + + /** + * Construct a new {@code Event} with the specified event type. The source + * and target of the event is set to {@code NULL_SOURCE_TARGET}. + * + * @param eventType the event type + */ + public Event(final @NamedArg("eventType") EventType eventType) { + this(null, null, eventType); + } + + /** + * Construct a new {@code Event} with the specified event source, target + * and type. If the source or target is set to {@code null}, it is replaced + * by the {@code NULL_SOURCE_TARGET} value. + * + * @param source the event source which sent the event + * @param target the event target to associate with the event + * @param eventType the event type + */ + public Event(final @NamedArg("source") Object source, + final @NamedArg("target") EventTarget target, + final @NamedArg("eventType") EventType eventType) { + super((source != null) ? source : NULL_SOURCE_TARGET); + this.target = (target != null) ? target : NULL_SOURCE_TARGET; + this.eventType = eventType; + } + + /** + * Returns the event target of this event. The event target specifies + * the path through which the event will travel when posted. + * + * @return the event target + */ + public EventTarget getTarget() { + return target; + } + + /** + * Gets the event type of this event. Objects of the same {@code Event} + * class can have different event types. These event types further specify + * what kind of event occurred. + * + * @return the event type + */ + public EventType getEventType() { + return eventType; + } + + /** + * Creates and returns a copy of this event with the specified event source + * and target. If the source or target is set to {@code null}, it is + * replaced by the {@code NULL_SOURCE_TARGET} value. + * + * @param newSource the new source of the copied event + * @param newTarget the new target of the copied event + * @return the event copy with the new source and target + */ + public Event copyFor(final Object newSource, final EventTarget newTarget) { + final Event newEvent = (Event) clone(); + + newEvent.source = (newSource != null) ? newSource : NULL_SOURCE_TARGET; + newEvent.target = (newTarget != null) ? newTarget : NULL_SOURCE_TARGET; + newEvent.consumed = false; + + return newEvent; + } + + /** + * Indicates whether this {@code Event} has been consumed by any filter or + * handler. + * + * @return {@code true} if this {@code Event} has been consumed, + * {@code false} otherwise + */ + public boolean isConsumed() { + return consumed; + } + + /** + * Marks this {@code Event} as consumed. This stops its further propagation. + */ + public void consume() { + consumed = true; + } + + /** + * Creates and returns a copy of this {@code Event}. + * @return a new instance of {@code Event} with all values copied from + * this {@code Event}. + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException e) { + // we implement Cloneable, this shouldn't happen + throw new RuntimeException("Can't clone Event"); + } + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + source = NULL_SOURCE_TARGET; + target = NULL_SOURCE_TARGET; + } + + + // PENDING_DOC_REVIEW + /** + * Fires the specified event. The given event target specifies the path + * through which the event will travel. + * + * @param eventTarget the target for the event + * @param event the event to fire + * @throws NullPointerException if eventTarget or event is null + */ + public static void fireEvent(EventTarget eventTarget, Event event) { + if (eventTarget == null) { + throw new NullPointerException("Event target must not be null!"); + } + + if (event == null) { + throw new NullPointerException("Event must not be null!"); + } + + EventUtil.fireEvent(eventTarget, event); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChain.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChain.java new file mode 100644 index 00000000..751f75d3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChain.java @@ -0,0 +1,65 @@ +package com.tungsten.fclcore.fakefx.event; + +// PENDING_DOC_REVIEW +/** + * Represents a chain of {@code EventDispatcher} objects, which can dispatch + * an {@code Event}. The event is dispatched by passing it from one + * {@code EventDispatcher} to the next in the chain until the end of chain is + * reached. Each {@code EventDispatcher} in the chain can influence the event + * path and the event itself. The chain is usually formed by following some + * parent - child hierarchy from the root to the event target and appending + * all {@code EventDispatcher} objects encountered to the chain. + * @since JavaFX 2.0 + */ +public interface EventDispatchChain { + /** + * Appends the specified {@code EventDispatcher} to this chain. Returns a + * reference to the chain with the appended element. + *

+ * The caller shouldn't assume that this {@code EventDispatchChain} remains + * unchanged nor that the returned value will reference a different chain + * after the call. All this depends on the {@code EventDispatchChain} + * implementation. + *

+ * So the call should be always done in the following form: + * {@code chain = chain.append(eventDispatcher);} + * + * @param eventDispatcher the {@code EventDispatcher} to append to the + * chain + * @return the chain with the appended event dispatcher + */ + EventDispatchChain append(EventDispatcher eventDispatcher); + + /** + * Prepends the specified {@code EventDispatcher} to this chain. Returns a + * reference to the chain with the prepended element. + *

+ * The caller shouldn't assume that this {@code EventDispatchChain} remains + * unchanged nor that the returned value will reference a different chain + * after the call. All this depends on the {@code EventDispatchChain} + * implementation. + *

+ * So the call should be always done in the following form: + * {@code chain = chain.prepend(eventDispatcher);} + * + * @param eventDispatcher the {@code EventDispatcher} to prepend to the + * chain + * @return the chain with the prepended event dispatcher + */ + EventDispatchChain prepend(EventDispatcher eventDispatcher); + + /** + * Dispatches the specified event through this {@code EventDispatchChain}. + * The return value represents the event after processing done by the chain. + * If further processing is to be done after the call the event referenced + * by the return value should be used instead of the original event. In the + * case the event is fully handled / consumed in the chain the returned + * value is {@code null} and no further processing should be done with that + * event. + * + * @param event the event to dispatch + * @return the processed event or {@code null} if the event had been fully + * handled / consumed + */ + Event dispatchEvent(Event event); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChainImpl.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChainImpl.java new file mode 100644 index 00000000..503e2e6b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchChainImpl.java @@ -0,0 +1,127 @@ +package com.tungsten.fclcore.fakefx.event; + +public class EventDispatchChainImpl implements EventDispatchChain { + /** Must be a power of two. */ + private static final int CAPACITY_GROWTH_FACTOR = 8; + + private EventDispatcher[] dispatchers; + + private int[] nextLinks; + + private int reservedCount; + private int activeCount; + private int headIndex; + private int tailIndex; + + public EventDispatchChainImpl() { + } + + public void reset() { + // shrink? + for (int i = 0; i < reservedCount; ++i) { + dispatchers[i] = null; + } + + reservedCount = 0; + activeCount = 0; + headIndex = 0; + tailIndex = 0; + } + + @Override + public EventDispatchChain append(final EventDispatcher eventDispatcher) { + ensureCapacity(reservedCount + 1); + + if (activeCount == 0) { + insertFirst(eventDispatcher); + return this; + } + + dispatchers[reservedCount] = eventDispatcher; + nextLinks[tailIndex] = reservedCount; + tailIndex = reservedCount; + + ++activeCount; + ++reservedCount; + + return this; + } + + @Override + public EventDispatchChain prepend(final EventDispatcher eventDispatcher) { + ensureCapacity(reservedCount + 1); + + if (activeCount == 0) { + insertFirst(eventDispatcher); + return this; + } + + dispatchers[reservedCount] = eventDispatcher; + nextLinks[reservedCount] = headIndex; + headIndex = reservedCount; + + ++activeCount; + ++reservedCount; + + return this; + } + + @Override + public Event dispatchEvent(final Event event) { + if (activeCount == 0) { + return event; + } + + // push current state + final int savedHeadIndex = headIndex; + final int savedTailIndex = tailIndex; + final int savedActiveCount = activeCount; + final int savedReservedCount = reservedCount; + + final EventDispatcher nextEventDispatcher = dispatchers[headIndex]; + headIndex = nextLinks[headIndex]; + --activeCount; + final Event returnEvent = + nextEventDispatcher.dispatchEvent(event, this); + + // pop saved state + headIndex = savedHeadIndex; + tailIndex = savedTailIndex; + activeCount = savedActiveCount; + reservedCount = savedReservedCount; + + return returnEvent; + } + + private void insertFirst(final EventDispatcher eventDispatcher) { + dispatchers[reservedCount] = eventDispatcher; + headIndex = reservedCount; + tailIndex = reservedCount; + + activeCount = 1; + ++reservedCount; + } + + private void ensureCapacity(final int size) { + final int newCapacity = (size + CAPACITY_GROWTH_FACTOR - 1) + & ~(CAPACITY_GROWTH_FACTOR - 1); + if (newCapacity == 0) { + return; + } + + if ((dispatchers == null) || (dispatchers.length < newCapacity)) { + final EventDispatcher[] newDispatchers = + new EventDispatcher[newCapacity]; + final int[] newLinks = new int[newCapacity]; + + if (reservedCount > 0) { + System.arraycopy(dispatchers, 0, newDispatchers, 0, + reservedCount); + System.arraycopy(nextLinks, 0, newLinks, 0, reservedCount); + } + + dispatchers = newDispatchers; + nextLinks = newLinks; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTree.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTree.java new file mode 100644 index 00000000..53d600c1 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTree.java @@ -0,0 +1,13 @@ +package com.tungsten.fclcore.fakefx.event; + +public interface EventDispatchTree extends EventDispatchChain { + EventDispatchTree createTree(); + + EventDispatchTree mergeTree(EventDispatchTree tree); + + @Override + EventDispatchTree append(EventDispatcher eventDispatcher); + + @Override + EventDispatchTree prepend(EventDispatcher eventDispatcher); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTreeImpl.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTreeImpl.java new file mode 100644 index 00000000..9421ce09 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatchTreeImpl.java @@ -0,0 +1,343 @@ +package com.tungsten.fclcore.fakefx.event; + +public final class EventDispatchTreeImpl implements EventDispatchTree { + /** Must be a power of two. */ + private static final int CAPACITY_GROWTH_FACTOR = 8; + + private static final int NULL_INDEX = -1; + + private EventDispatcher[] dispatchers; + + private int[] nextChildren; + private int[] nextSiblings; + + private int reservedCount; + private int rootIndex; + private int tailFirstIndex; + private int tailLastIndex; + + public EventDispatchTreeImpl() { + rootIndex = NULL_INDEX; + tailFirstIndex = NULL_INDEX; + tailLastIndex = NULL_INDEX; + } + + public void reset() { + // shrink? + for (int i = 0; i < reservedCount; ++i) { + dispatchers[i] = null; + } + + reservedCount = 0; + rootIndex = NULL_INDEX; + tailFirstIndex = NULL_INDEX; + tailLastIndex = NULL_INDEX; + } + + @Override + public EventDispatchTree createTree() { + return new EventDispatchTreeImpl(); + } + + private boolean expandTailFirstPath; + + @Override + public EventDispatchTree mergeTree(final EventDispatchTree tree) { + if (tailFirstIndex != NULL_INDEX) { + if (rootIndex != NULL_INDEX) { + expandTailFirstPath = true; + expandTail(rootIndex); + } else { + rootIndex = tailFirstIndex; + } + + tailFirstIndex = NULL_INDEX; + tailLastIndex = NULL_INDEX; + } + + final EventDispatchTreeImpl treeImpl = (EventDispatchTreeImpl) tree; + int srcLevelIndex = (treeImpl.rootIndex != NULL_INDEX) + ? treeImpl.rootIndex + : treeImpl.tailFirstIndex; + + if (rootIndex == NULL_INDEX) { + rootIndex = copyTreeLevel(treeImpl, srcLevelIndex); + } else { + mergeTreeLevel(treeImpl, rootIndex, srcLevelIndex); + } + + return this; + } + + @Override + public EventDispatchTree append(final EventDispatcher eventDispatcher) { + ensureCapacity(reservedCount + 1); + + dispatchers[reservedCount] = eventDispatcher; + nextSiblings[reservedCount] = NULL_INDEX; + nextChildren[reservedCount] = NULL_INDEX; + if (tailFirstIndex == NULL_INDEX) { + tailFirstIndex = reservedCount; + } else { + nextChildren[tailLastIndex] = reservedCount; + } + + tailLastIndex = reservedCount; + ++reservedCount; + + return this; + } + + @Override + public EventDispatchTree prepend(final EventDispatcher eventDispatcher) { + ensureCapacity(reservedCount + 1); + + dispatchers[reservedCount] = eventDispatcher; + nextSiblings[reservedCount] = NULL_INDEX; + nextChildren[reservedCount] = rootIndex; + + rootIndex = reservedCount; + ++reservedCount; + + return this; + } + + @Override + public Event dispatchEvent(final Event event) { + if (rootIndex == NULL_INDEX) { + if (tailFirstIndex == NULL_INDEX) { + return event; + } + + rootIndex = tailFirstIndex; + tailFirstIndex = NULL_INDEX; + tailLastIndex = NULL_INDEX; + } + + // push current state + final int savedReservedCount = reservedCount; + final int savedRootIndex = rootIndex; + final int savedTailFirstIndex = tailFirstIndex; + final int savedTailLastIndex = tailLastIndex; + + Event returnEvent = null; + int index = rootIndex; + do { + rootIndex = nextChildren[index]; + final Event branchReturnEvent = + dispatchers[index].dispatchEvent(event, this); + if (branchReturnEvent != null) { + returnEvent = (returnEvent != null) ? event + : branchReturnEvent; + } + + index = nextSiblings[index]; + } while (index != NULL_INDEX); + + // pop saved state + reservedCount = savedReservedCount; + rootIndex = savedRootIndex; + tailFirstIndex = savedTailFirstIndex; + tailLastIndex = savedTailLastIndex; + + return returnEvent; + } + + @Override + public String toString() { + int levelIndex = (rootIndex != NULL_INDEX) ? rootIndex : tailFirstIndex; + if (levelIndex == NULL_INDEX) { + return "()"; + } + + final StringBuilder sb = new StringBuilder(); + appendTreeLevel(sb, levelIndex); + + return sb.toString(); + } + + private void ensureCapacity(final int size) { + final int newCapacity = (size + CAPACITY_GROWTH_FACTOR - 1) + & ~(CAPACITY_GROWTH_FACTOR - 1); + if (newCapacity == 0) { + return; + } + + if ((dispatchers == null) || (dispatchers.length < newCapacity)) { + final EventDispatcher[] newDispatchers = + new EventDispatcher[newCapacity]; + final int[] newNextChildren = new int[newCapacity]; + final int[] newNextSiblings = new int[newCapacity]; + + if (reservedCount > 0) { + System.arraycopy(dispatchers, 0, newDispatchers, 0, + reservedCount); + System.arraycopy(nextChildren, 0, newNextChildren, 0, + reservedCount); + System.arraycopy(nextSiblings, 0, newNextSiblings, 0, + reservedCount); + } + + dispatchers = newDispatchers; + nextChildren = newNextChildren; + nextSiblings = newNextSiblings; + } + } + + private void expandTail(final int levelIndex) { + int index = levelIndex; + while (index != NULL_INDEX) { + if (nextChildren[index] != NULL_INDEX) { + expandTail(nextChildren[index]); + } else { + if (expandTailFirstPath) { + nextChildren[index] = tailFirstIndex; + expandTailFirstPath = false; + } else { + final int childLevelIndex = + copyTreeLevel(this, tailFirstIndex); + nextChildren[index] = childLevelIndex; + } + } + + index = nextSiblings[index]; + } + } + + private void mergeTreeLevel(final EventDispatchTreeImpl srcTree, + final int dstLevelIndex, + final int srcLevelIndex) { + int srcIndex = srcLevelIndex; + while (srcIndex != NULL_INDEX) { + final EventDispatcher srcDispatcher = srcTree.dispatchers[srcIndex]; + int dstIndex = dstLevelIndex; + int lastDstIndex = dstLevelIndex; + + while ((dstIndex != NULL_INDEX) + && (srcDispatcher != dispatchers[dstIndex])) { + lastDstIndex = dstIndex; + dstIndex = nextSiblings[dstIndex]; + } + + if (dstIndex == NULL_INDEX) { + final int siblingIndex = copySubtree(srcTree, srcIndex); + nextSiblings[lastDstIndex] = siblingIndex; + nextSiblings[siblingIndex] = NULL_INDEX; + } else { + int nextDstLevelIndex = nextChildren[dstIndex]; + final int nextSrcLevelIndex = getChildIndex(srcTree, srcIndex); + if (nextDstLevelIndex != NULL_INDEX) { + mergeTreeLevel(srcTree, + nextDstLevelIndex, + nextSrcLevelIndex); + } else { + nextDstLevelIndex = copyTreeLevel(srcTree, + nextSrcLevelIndex); + nextChildren[dstIndex] = nextDstLevelIndex; + } + } + + srcIndex = srcTree.nextSiblings[srcIndex]; + } + } + + private int copyTreeLevel(final EventDispatchTreeImpl srcTree, + final int srcLevelIndex) { + if (srcLevelIndex == NULL_INDEX) { + return NULL_INDEX; + } + + int srcIndex = srcLevelIndex; + final int dstLevelIndex = copySubtree(srcTree, srcIndex); + int lastDstIndex = dstLevelIndex; + + srcIndex = srcTree.nextSiblings[srcIndex]; + while (srcIndex != NULL_INDEX) { + int dstIndex = copySubtree(srcTree, srcIndex); + nextSiblings[lastDstIndex] = dstIndex; + + lastDstIndex = dstIndex; + srcIndex = srcTree.nextSiblings[srcIndex]; + } + + nextSiblings[lastDstIndex] = NULL_INDEX; + return dstLevelIndex; + } + + private int copySubtree(final EventDispatchTreeImpl srcTree, + final int srcIndex) { + ensureCapacity(reservedCount + 1); + final int dstIndex = reservedCount++; + + final int dstChildLevelIndex = + copyTreeLevel(srcTree, getChildIndex(srcTree, srcIndex)); + dispatchers[dstIndex] = srcTree.dispatchers[srcIndex]; + nextChildren[dstIndex] = dstChildLevelIndex; + + return dstIndex; + } + + private void appendTreeLevel(final StringBuilder sb, + final int levelIndex) { + sb.append('('); + + int index = levelIndex; + appendSubtree(sb, index); + + index = nextSiblings[index]; + while (index != NULL_INDEX) { + sb.append(","); + appendSubtree(sb, index); + index = nextSiblings[index]; + } + + sb.append(')'); + } + + private void appendSubtree(final StringBuilder sb, + final int index) { + sb.append(dispatchers[index]); + + final int childIndex = getChildIndex(this, index); + if (childIndex != NULL_INDEX) { + sb.append("->"); + appendTreeLevel(sb, childIndex); + } + } + + private static int getChildIndex(final EventDispatchTreeImpl tree, + final int index) { + int childIndex = tree.nextChildren[index]; + if ((childIndex == NULL_INDEX) + && (index != tree.tailLastIndex)) { + childIndex = tree.tailFirstIndex; + } + + return childIndex; + } + +// void dumpInternalData() { +// System.out.println("reservedCount: " + reservedCount); +// System.out.println("rootIndex: " + rootIndex); +// System.out.println("tailFirstIndex: " + tailFirstIndex); +// System.out.println("tailLastIndex: " + tailLastIndex); +// +// System.out.print("dispatchers:"); +// for (int i = 0; i < reservedCount; ++i) { +// System.out.print(" " + dispatchers[i]); +// } +// System.out.println(); +// +// System.out.print("nextSiblings:"); +// for (int i = 0; i < reservedCount; ++i) { +// System.out.print(" " + nextSiblings[i]); +// } +// System.out.println(); +// +// System.out.print("nextChildren:"); +// for (int i = 0; i < reservedCount; ++i) { +// System.out.print(" " + nextChildren[i]); +// } +// System.out.println(); +// } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatcher.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatcher.java new file mode 100644 index 00000000..da9c49fb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventDispatcher.java @@ -0,0 +1,65 @@ +package com.tungsten.fclcore.fakefx.event; + +// PENDING_DOC_REVIEW +/** + * An {@code EventDispatcher} represents an event dispatching and processing + * entity. It is used when an {@code Event} needs to be dispatched to the + * associated {@code EventTarget} through the {@code EventDispatchChain} + * specified by the target. Each {@code EventDispatcher} in the chain can + * influence the event path and the event itself. One {@code EventDispatcher} + * can appear in multiple chains. + *

+ * The system defines two successive phases of event delivery. The first + * phase is called capturing phase and happens when when an event travels from + * the first element of the {@code EventDispatchChain} associated with the event + * target to its last element. If the event target is part of some hierarchy, + * the direction of the event in this phase usually corresponds with the + * direction from the root element of the hierarchy to the target. The second + * phase is called bubbling phase and happens in the reverse order to the first + * phase. So the event is returning back from the last element of the + * {@code EventDispatchChain} to its first element in this phase. Usually that + * corresponds to the direction from the event target back to the root in the + * event target's hierarchy. + *

+ * Each {@code EventDispatcher} in an {@code EventDispatchChain} is responsible + * for forwarding the event to the rest of the chain during event dispatching. + * This forwarding happens in the {@code dispatchEvent} method and forms a chain + * of nested calls which allows one {@code EventDispatcher} to see the event + * during both dispatching phases in a single {@code dispatchEvent} call. + *

+ * Template for {@code dispatchEvent} implementation. +

+public Event dispatchEvent(Event event, EventDispatchChain tail) {
+    // capturing phase, can handle / modify / substitute / divert the event
+
+    if (notHandledYet) {
+        // forward the event to the rest of the chain
+        event = tail.dispatchEvent(event);
+
+        if (event != null) {
+            // bubbling phase, can handle / modify / substitute / divert
+            // the event
+        }
+    }
+
+    return notHandledYet ? event : null;
+
+} + + * @since JavaFX 2.0 + */ +public interface EventDispatcher { + /** + * Dispatches the specified event by this {@code EventDispatcher}. Does + * any required event processing. Both the event and its further path can + * be modified in this method. If the event is not handled / consumed during + * the capturing phase, it should be dispatched to the rest of the chain + * ({@code event = tail.dispatch(event);}). + * + * @param event the event do dispatch + * @param tail the rest of the chain to dispatch event to + * @return the return event or {@code null} if the event has been handled / + * consumed + */ + Event dispatchEvent(Event event, EventDispatchChain tail); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandler.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandler.java new file mode 100644 index 00000000..12d9d502 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandler.java @@ -0,0 +1,21 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.EventListener; + +// PENDING_DOC_REVIEW +/** + * Handler for events of a specific class / type. + * + * @param the event class this handler can handle + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface EventHandler extends EventListener { + /** + * Invoked when a specific event of the type for which this handler is + * registered happens. + * + * @param event the event which occurred + */ + void handle(T event); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandlerManager.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandlerManager.java new file mode 100644 index 00000000..124acd1d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventHandlerManager.java @@ -0,0 +1,237 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.HashMap; +import java.util.Map; + +/** + * An {@code EventDispatcher} which allows user event handler / filter + * registration and when used in an event dispatch chain it forwards received + * events to the appropriate registered handlers / filters. + */ +public class EventHandlerManager extends BasicEventDispatcher { + private final Map, + CompositeEventHandler> eventHandlerMap; + + private final Object eventSource; + + public EventHandlerManager(final Object eventSource) { + this.eventSource = eventSource; + + eventHandlerMap = + new HashMap, + CompositeEventHandler>(); + } + + /** + * Registers an event handler in {@code EventHandlerManager}. + * + * @param the specific event class of the handler + * @param eventType the type of the events to receive by the handler + * @param eventHandler the handler to register + * @throws NullPointerException if the event type or handler is null + */ + public final void addEventHandler( + final EventType eventType, + final EventHandler eventHandler) { + validateEventType(eventType); + validateEventHandler(eventHandler); + + final CompositeEventHandler compositeEventHandler = + createGetCompositeEventHandler(eventType); + + compositeEventHandler.addEventHandler(eventHandler); + } + + /** + * Unregisters a previously registered event handler. + * + * @param the specific event class of the handler + * @param eventType the event type from which to unregister + * @param eventHandler the handler to unregister + * @throws NullPointerException if the event type or handler is null + */ + public final void removeEventHandler( + final EventType eventType, + final EventHandler eventHandler) { + validateEventType(eventType); + validateEventHandler(eventHandler); + + final CompositeEventHandler compositeEventHandler = + (CompositeEventHandler) eventHandlerMap.get(eventType); + + if (compositeEventHandler != null) { + compositeEventHandler.removeEventHandler(eventHandler); + } + } + + /** + * Registers an event filter in {@code EventHandlerManager}. + * + * @param the specific event class of the filter + * @param eventType the type of the events to receive by the filter + * @param eventFilter the filter to register + * @throws NullPointerException if the event type or filter is null + */ + public final void addEventFilter( + final EventType eventType, + final EventHandler eventFilter) { + validateEventType(eventType); + validateEventFilter(eventFilter); + + final CompositeEventHandler compositeEventHandler = + createGetCompositeEventHandler(eventType); + + compositeEventHandler.addEventFilter(eventFilter); + } + + /** + * Unregisters a previously registered event filter. + * + * @param the specific event class of the filter + * @param eventType the event type from which to unregister + * @param eventFilter the filter to unregister + * @throws NullPointerException if the event type or filter is null + */ + public final void removeEventFilter( + final EventType eventType, + final EventHandler eventFilter) { + validateEventType(eventType); + validateEventFilter(eventFilter); + + final CompositeEventHandler compositeEventHandler = + (CompositeEventHandler) eventHandlerMap.get(eventType); + + if (compositeEventHandler != null) { + compositeEventHandler.removeEventFilter(eventFilter); + } + } + + /** + * Sets the specified singleton handler. There can only be one such handler + * specified at a time. + * + * @param the specific event class of the handler + * @param eventType the event type to associate with the given eventHandler + * @param eventHandler the handler to register, or null to unregister + * @throws NullPointerException if the event type is null + */ + public final void setEventHandler( + final EventType eventType, + final EventHandler eventHandler) { + validateEventType(eventType); + + CompositeEventHandler compositeEventHandler = + (CompositeEventHandler) eventHandlerMap.get(eventType); + + if (compositeEventHandler == null) { + if (eventHandler == null) { + return; + } + compositeEventHandler = new CompositeEventHandler(); + eventHandlerMap.put(eventType, compositeEventHandler); + } + + compositeEventHandler.setEventHandler(eventHandler); + } + + public final EventHandler getEventHandler( + final EventType eventType) { + final CompositeEventHandler compositeEventHandler = + (CompositeEventHandler) eventHandlerMap.get(eventType); + + return (compositeEventHandler != null) + ? compositeEventHandler.getEventHandler() + : null; + } + + @Override + public final Event dispatchCapturingEvent(Event event) { + EventType eventType = event.getEventType(); + do { + event = dispatchCapturingEvent(eventType, event); + eventType = eventType.getSuperType(); + } while (eventType != null); + + return event; + } + + @Override + public final Event dispatchBubblingEvent(Event event) { + EventType eventType = event.getEventType(); + do { + event = dispatchBubblingEvent(eventType, event); + eventType = eventType.getSuperType(); + } while (eventType != null); + + return event; + } + + private CompositeEventHandler + createGetCompositeEventHandler(final EventType eventType) { + CompositeEventHandler compositeEventHandler = + (CompositeEventHandler) eventHandlerMap.get(eventType); + if (compositeEventHandler == null) { + compositeEventHandler = new CompositeEventHandler(); + eventHandlerMap.put(eventType, compositeEventHandler); + } + + return compositeEventHandler; + } + + protected Object getEventSource() { + return eventSource; + } + + private Event dispatchCapturingEvent( + final EventType handlerType, Event event) { + final CompositeEventHandler compositeEventHandler = + eventHandlerMap.get(handlerType); + + if (compositeEventHandler != null && compositeEventHandler.hasFilter()) { + event = fixEventSource(event, eventSource); + compositeEventHandler.dispatchCapturingEvent(event); + } + + return event; + } + + private Event dispatchBubblingEvent( + final EventType handlerType, Event event) { + final CompositeEventHandler compositeEventHandler = + eventHandlerMap.get(handlerType); + + if (compositeEventHandler != null && compositeEventHandler.hasHandler()) { + event = fixEventSource(event, eventSource); + compositeEventHandler.dispatchBubblingEvent(event); + } + + return event; + } + + private static Event fixEventSource(final Event event, + final Object eventSource) { + return (event.getSource() != eventSource) + ? event.copyFor(eventSource, event.getTarget()) + : event; + } + + private static void validateEventType(final EventType eventType) { + if (eventType == null) { + throw new NullPointerException("Event type must not be null"); + } + } + + private static void validateEventHandler( + final EventHandler eventHandler) { + if (eventHandler == null) { + throw new NullPointerException("Event handler must not be null"); + } + } + + private static void validateEventFilter( + final EventHandler eventFilter) { + if (eventFilter == null) { + throw new NullPointerException("Event filter must not be null"); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventQueue.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventQueue.java new file mode 100644 index 00000000..20a187f1 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventQueue.java @@ -0,0 +1,28 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.ArrayDeque; +import java.util.Queue; + +public final class EventQueue { + private Queue queue = new ArrayDeque(); + private boolean inLoop; + + public void postEvent(Event event) { + queue.add(event); + } + + public void fire() { + if (inLoop) { + return; //Let the most outer loop do the job + } + inLoop = true; + try { + while (!queue.isEmpty()) { + Event top = queue.remove(); + Event.fireEvent(top.getTarget(), top); + } + } finally { + inLoop = false; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventRedirector.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventRedirector.java new file mode 100644 index 00000000..8346af04 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventRedirector.java @@ -0,0 +1,100 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This event dispatcher redirects received events to the registered child + * dispatchers before dispatching them to the rest of the dispatch chain. The + * redirected events are wrapped in {@code RedirectedEvent} instances, so they + * can be easily recognized from normal direct events. If an original event + * wrapped in the {@code RedirectedEvent} is consumed by any of the child + * dispatchers, it won't be sent by the {@code EventRedirector} to the rest of + * the original dispatch chain. + *

+ * The child dispatchers can also be instances of {@code EventRedirector} and + * might receive both, the normal events (from other sources) and the redirected + * events from the parent {@code EventRedirector}. If a {@code RedirectedEvent} + * is received, it is forwarded to the child event dispatchers without any + * additional wrapping. + *

+ * For this hierarchical arrangement of {@code EventRedirector} instances the + * class defines the {@code handleRedirectedEvent} method, which is called with + * a received redirected event, after the event has been forwarded to the child + * dispatchers. By default this method is empty, but can be overridden in + * derived classes to define specific handling of these redirected events. + */ +public class EventRedirector extends BasicEventDispatcher { + private final EventDispatchChainImpl eventDispatchChain; + + private final List eventDispatchers; + private final Object eventSource; + + /** + * Constructs a new {@code EventRedirector}. + * + * @param eventSource the object for which to redirect the events + * ({@code RedirectedEvent} event source) + */ + public EventRedirector(final Object eventSource) { + this.eventDispatchers = new CopyOnWriteArrayList(); + this.eventDispatchChain = new EventDispatchChainImpl(); + this.eventSource = eventSource; + } + + /** + * Called when a redirected event is received by this instance. + * + * @param eventSource the object from which the event has been redirected + * @param event the event which has been redirected + */ + protected void handleRedirectedEvent( + final Object eventSource, + final Event event) { + } + + public final void addEventDispatcher( + final EventDispatcher eventDispatcher) { + eventDispatchers.add(eventDispatcher); + } + + public final void removeEventDispatcher( + final EventDispatcher eventDispatcher) { + eventDispatchers.remove(eventDispatcher); + } + + @Override + public final Event dispatchCapturingEvent(Event event) { + final EventType eventType = event.getEventType(); + + if (eventType == DirectEvent.DIRECT) { + // unwrap event, but don't redirect + event = ((DirectEvent) event).getOriginalEvent(); + } else { + redirectEvent(event); + + if (eventType == RedirectedEvent.REDIRECTED) { + handleRedirectedEvent( + event.getSource(), + ((RedirectedEvent) event).getOriginalEvent()); + } + } + + return event; + } + + private void redirectEvent(final Event event) { + if (!eventDispatchers.isEmpty()) { + final RedirectedEvent redirectedEvent = + (event.getEventType() == RedirectedEvent.REDIRECTED) + ? (RedirectedEvent) event + : new RedirectedEvent(event, eventSource, null); + + for (final EventDispatcher eventDispatcher: eventDispatchers) { + eventDispatchChain.reset(); + eventDispatcher.dispatchEvent( + redirectedEvent, eventDispatchChain); + } + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventTarget.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventTarget.java new file mode 100644 index 00000000..382ca127 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventTarget.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.fakefx.event; + +// PENDING_DOC_REVIEW +/** + * Represents an event target. + * @since JavaFX 2.0 + */ +public interface EventTarget { + /** + * Construct an event dispatch chain for this target. The event dispatch + * chain contains event dispatchers which might be interested in processing + * of events targeted at this {@code EventTarget}. This event target is + * not automatically added to the chain, so if it wants to process events, + * it needs to add an {@code EventDispatcher} for itself to the chain. + *

+ * In the case the event target is part of some hierarchy, the chain for it + * is usually built from event dispatchers collected from the root of the + * hierarchy to the event target. + *

+ * The event dispatch chain is constructed by modifications to the provided + * initial event dispatch chain. The returned chain should have the initial + * chain at its end so the dispatchers should be prepended to the initial + * chain. + *

+ * The caller shouldn't assume that the initial chain remains unchanged nor + * that the returned value will reference a different chain. + * + * @param tail the initial chain to build from + * @return the resulting event dispatch chain for this target + */ + EventDispatchChain buildEventDispatchChain(EventDispatchChain tail); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventType.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventType.java new file mode 100644 index 00000000..87725935 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventType.java @@ -0,0 +1,211 @@ +package com.tungsten.fclcore.fakefx.event; + +// PENDING_DOC_REVIEW + +import java.io.InvalidObjectException; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * This class represents a specific event type associated with an {@code Event}. + *

+ * Event types form a hierarchy with the {@link EventType#ROOT} (equals to + * {@link Event#ANY}) as its root. This is useful in event filter / handler + * registration where a single event filter / handler can be registered to a + * super event type and will be receiving its sub type events as well. + * Note that you cannot construct two different EventType objects with the same + * name and parent. + * + *

+ * Note about deserialization: All EventTypes that are going to be deserialized + * (e.g. as part of {@link Event} deserialization), need to exist at the time of + * deserialization. Deserialization of EventType will not create new EventType + * objects. + * + * @param the event class to which this type applies + * @since JavaFX 2.0 + */ +public final class EventType implements Serializable{ + + /** + * The root event type. All other event types are either direct or + * indirect sub types of it. It is also the only event type which + * has its super event type set to {@code null}. + */ + public static final EventType ROOT = + new EventType("EVENT", null); + + private WeakHashMap, Void> subTypes; + + private final EventType superType; + + private final String name; + + /** + * Constructs a new {@code EventType} with the {@code EventType.ROOT} as its + * super type and the name set to {@code null}. + * @deprecated Do not use this constructor, as only one such EventType can exist + */ + @Deprecated + public EventType() { + this(ROOT, null); + } + + /** + * Constructs a new {@code EventType} with the specified name and the + * {@code EventType.ROOT} as its super type. + * + * @param name the name + * @throws IllegalArgumentException if an EventType with the same name and + * {@link EventType#ROOT}/{@link Event#ANY} as parent + */ + public EventType(final String name) { + this(ROOT, name); + } + + /** + * Constructs a new {@code EventType} with the specified super type and + * the name set to {@code null}. + * + * @param superType the event super type + * @throws IllegalArgumentException if an EventType with "null" name and + * under this supertype exists + */ + public EventType(final EventType superType) { + this(superType, null); + } + + /** + * Constructs a new {@code EventType} with the specified super type and + * name. + * + * @param superType the event super type + * @param name the name + * @throws IllegalArgumentException if an EventType with the same name and + * superType exists + */ + public EventType(final EventType superType, + final String name) { + if (superType == null) { + throw new NullPointerException( + "Event super type must not be null!"); + } + + this.superType = superType; + this.name = name; + superType.register(this); + } + + /** + * Internal constructor that skips various checks + */ + EventType(final String name, + final EventType superType) { + this.superType = superType; + this.name = name; + if (superType != null) { + if (superType.subTypes != null) { + for (Iterator i = superType.subTypes.keySet().iterator(); i.hasNext();) { + EventType t = (EventType) i.next(); + if (name == null && t.name == null || (name != null && name.equals(t.name))) { + i.remove(); + } + } + } + superType.register(this); + } + } + + /** + * Gets the super type of this event type. The returned value is + * {@code null} only for the {@code EventType.ROOT}. + * + * @return the super type + */ + public final EventType getSuperType() { + return superType; + } + + /** + * Gets the name of this event type. + * + * @return the name + */ + public final String getName() { + return name; + } + + /** + * Returns a string representation of this {@code EventType} object. + * @return a string representation of this {@code EventType} object. + */ + @Override + public String toString() { + return (name != null) ? name : super.toString(); + } + + private void register(EventType subType) { + if (subTypes == null) { + subTypes = new WeakHashMap, Void>(); + } + for (EventType t : subTypes.keySet()) { + if (((t.name == null && subType.name == null) || (t.name != null && t.name.equals(subType.name)))) { + throw new IllegalArgumentException("EventType \"" + subType + "\"" + + "with parent \"" + subType.getSuperType()+"\" already exists"); + } + } + subTypes.put(subType, null); + } + + private Object writeReplace() throws ObjectStreamException { + Deque path = new LinkedList(); + EventType t = this; + while (t != ROOT) { + path.addFirst(t.name); + t = t.superType; + } + return new EventTypeSerialization(new ArrayList(path)); + } + + static class EventTypeSerialization implements Serializable { + private List path; + + public EventTypeSerialization(List path) { + this.path = path; + } + + private Object readResolve() throws ObjectStreamException { + EventType t = ROOT; + for (int i = 0; i < path.size(); ++i) { + String p = path.get(i); + if (t.subTypes != null) { + EventType s = findSubType(t.subTypes.keySet(), p); + if (s == null) { + throw new InvalidObjectException("Cannot find event type \"" + p + "\" (of " + t + ")"); + } + t = s; + } else { + throw new InvalidObjectException("Cannot find event type \"" + p + "\" (of " + t + ")"); + } + } + return t; + } + + private EventType findSubType(Set subTypes, String name) { + for (EventType t : subTypes) { + if (((t.name == null && name == null) || (t.name != null && t.name.equals(name)))) { + return t; + } + } + return null; + } + + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventUtil.java new file mode 100644 index 00000000..1301d961 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/EventUtil.java @@ -0,0 +1,47 @@ +package com.tungsten.fclcore.fakefx.event; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class EventUtil { + private static final EventDispatchChainImpl eventDispatchChain = + new EventDispatchChainImpl(); + + private static final AtomicBoolean eventDispatchChainInUse = + new AtomicBoolean(); + + public static Event fireEvent(EventTarget eventTarget, Event event) { + if (event.getTarget() != eventTarget) { + event = event.copyFor(event.getSource(), eventTarget); + } + + if (eventDispatchChainInUse.getAndSet(true)) { + // the member event dispatch chain is in use currently, we need to + // create a new instance for this call + return fireEventImpl(new EventDispatchChainImpl(), + eventTarget, event); + } + + try { + return fireEventImpl(eventDispatchChain, eventTarget, event); + } finally { + // need to do reset after use to remove references to event + // dispatchers from the chain + eventDispatchChain.reset(); + eventDispatchChainInUse.set(false); + } + } + + public static Event fireEvent(Event event, EventTarget... eventTargets) { + return fireEventImpl(new EventDispatchTreeImpl(), + new CompositeEventTargetImpl(eventTargets), + event); + } + + private static Event fireEventImpl(EventDispatchChain eventDispatchChain, + EventTarget eventTarget, + Event event) { + final EventDispatchChain targetDispatchChain = + eventTarget.buildEventDispatchChain(eventDispatchChain); + return targetDispatchChain.dispatchEvent(event); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/RedirectedEvent.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/RedirectedEvent.java new file mode 100644 index 00000000..3c17c1f6 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/RedirectedEvent.java @@ -0,0 +1,30 @@ +package com.tungsten.fclcore.fakefx.event; + +/** + * Used as a wrapper in {@code EventRedirector} to distinquish between normal + * "direct" events and the events "redirected" from the parent dispatcher(s). + */ +public class RedirectedEvent extends Event { + + private static final long serialVersionUID = 20121107L; + + public static final EventType REDIRECTED = + new EventType(Event.ANY, "REDIRECTED"); + + private final Event originalEvent; + + public RedirectedEvent(final Event originalEvent) { + this(originalEvent, null, null); + } + + public RedirectedEvent(final Event originalEvent, + final Object source, + final EventTarget target) { + super(source, target, REDIRECTED); + this.originalEvent = originalEvent; + } + + public Event getOriginalEvent() { + return originalEvent; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/WeakEventHandler.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/WeakEventHandler.java new file mode 100644 index 00000000..b24f011e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/event/WeakEventHandler.java @@ -0,0 +1,63 @@ +package com.tungsten.fclcore.fakefx.event; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; + +import java.lang.ref.WeakReference; + +/** + * Used in event handler registration in place of its associated event handler. + * Its sole purpose is to break the otherwise strong reference between an event + * handler container and its associated event handler. While the container still + * holds strong reference to the registered {@code WeakEventHandler} proxy, the + * proxy itself references the original handler only weakly and so doesn't + * prevent it from being garbage collected. Until this weak reference is broken, + * any event notification received by the proxy is forwarded to the original + * handler. + * + * @param the event class this handler can handle + * @since JavaFX 8.0 + */ +public final class WeakEventHandler + implements EventHandler { + private final WeakReference> weakRef; + + /** + * Creates a new instance of {@code WeakEventHandler}. + * + * @param eventHandler the original event handler to which to forward event + * notifications + */ + public WeakEventHandler(final @NamedArg("eventHandler") EventHandler eventHandler) { + weakRef = new WeakReference>(eventHandler); + } + + /** + * Indicates whether the associated event handler has been garbage + * collected. Used by containers to detect when the storage of corresponding + * references to this {@code WeakEventHandler} is no longer necessary. + * + * @return {@code true} if the associated handler has been garbage + * collected, {@code false} otherwise + */ + public boolean wasGarbageCollected() { + return weakRef.get() == null; + } + + /** + * Forwards event notification to the associated event handler. + * + * @param event the event which occurred + */ + @Override + public void handle(final T event) { + final EventHandler eventHandler = weakRef.get(); + if (eventHandler != null) { + eventHandler.handle(event); + } + } + + /* Used for testing. */ + void clear() { + weakRef.clear(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/JavaBeanAccessHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/JavaBeanAccessHelper.java new file mode 100644 index 00000000..7576cd64 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/JavaBeanAccessHelper.java @@ -0,0 +1,53 @@ +package com.tungsten.fclcore.fakefx.property; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyObjectProperty; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public final class JavaBeanAccessHelper { + + private static Method JAVA_BEAN_QUICK_ACCESSOR_CREATE_RO; + + private static boolean initialized; + + private JavaBeanAccessHelper() { + + } + + public static ReadOnlyObjectProperty createReadOnlyJavaBeanProperty(Object bean, String propertyName) throws NoSuchMethodException{ + init(); + if (JAVA_BEAN_QUICK_ACCESSOR_CREATE_RO == null) { + throw new UnsupportedOperationException("Java beans are not supported."); + } + try { + return (ReadOnlyObjectProperty) JAVA_BEAN_QUICK_ACCESSOR_CREATE_RO.invoke(null, bean, propertyName); + } catch (IllegalAccessException ex) { + throw new UnsupportedOperationException("Java beans are not supported."); + } catch (InvocationTargetException ex) { + if (ex.getCause() instanceof NoSuchMethodException) { + throw (NoSuchMethodException)ex.getCause(); + } + throw new UnsupportedOperationException("Java beans are not supported."); + } + } + + private static void init() { + if (!initialized) { + try { + Class accessor = Class.forName( + "com.sun.javafx.property.adapter.JavaBeanQuickAccessor", + true, JavaBeanAccessHelper.class.getClassLoader()); + JAVA_BEAN_QUICK_ACCESSOR_CREATE_RO = + accessor.getDeclaredMethod("createReadOnlyJavaBeanObjectProperty", + Object.class, String.class); + } catch (ClassNotFoundException ex) { + //ignore + } catch (NoSuchMethodException ex) { + //ignore + } + initialized = true; + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/MethodHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/MethodHelper.java new file mode 100644 index 00000000..40877214 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/MethodHelper.java @@ -0,0 +1,31 @@ +package com.tungsten.fclcore.fakefx.property; + +import com.tungsten.fclcore.fakefx.reflect.MethodUtil; +import com.tungsten.fclcore.fakefx.reflect.ReflectUtil; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; + +/** + * Utility class to wrap method invocation. + */ +public class MethodHelper { + @SuppressWarnings("removal") + private static final boolean logAccessErrors + = AccessController.doPrivileged((PrivilegedAction) () + -> Boolean.getBoolean("sun.reflect.debugModuleAccessChecks")); + + public static Object invoke(Method m, Object obj, Object[] params) + throws InvocationTargetException, IllegalAccessException { + + final Class clazz = m.getDeclaringClass(); + return MethodUtil.invoke(m, obj, params); + } + + // Utility class, do not instantiate + private MethodHelper() { + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/PropertyReference.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/PropertyReference.java new file mode 100644 index 00000000..61a0f38b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/PropertyReference.java @@ -0,0 +1,229 @@ +package com.tungsten.fclcore.fakefx.property; + +import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyProperty; +import com.tungsten.fclcore.fakefx.reflect.ReflectUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public final class PropertyReference { + private String name; + private Method getter; + private Method setter; + private Method propertyGetter; + private Class clazz; + private Class type; + private boolean reflected = false; + + public PropertyReference(Class clazz, String name) { + if (name == null) + throw new NullPointerException("Name must be specified"); + if (name.trim().length() == 0) + throw new IllegalArgumentException("Name must be specified"); + if (clazz == null) + throw new NullPointerException("Class must be specified"); + ReflectUtil.checkPackageAccess(clazz); + this.name = name; + this.clazz = clazz; + } + + public boolean isWritable() { + reflect(); + return setter != null; + } + + public boolean isReadable() { + reflect(); + return getter != null; + } + + public boolean hasProperty() { + reflect(); + return propertyGetter != null; + } + + public String getName() { + return name; + } + + public Class getContainingClass() { + return clazz; + } + + public Class getType() { + reflect(); + return type; + } + + public void set(Object bean, T value) { + if (!isWritable()) + throw new IllegalStateException( + "Cannot write to readonly property " + name); + assert setter != null; + try { + MethodHelper.invoke(setter, bean, new Object[] {value}); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("unchecked") + public T get(Object bean) { + if (!isReadable()) + throw new IllegalStateException( + "Cannot read from unreadable property " + name); + assert getter != null; + try { + return (T)MethodHelper.invoke(getter, bean, (Object[])null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("unchecked") + public ReadOnlyProperty getProperty(Object bean) { + if (!hasProperty()) + throw new IllegalStateException("Cannot get property " + name); + assert propertyGetter != null; + try { + return (ReadOnlyProperty)MethodHelper.invoke(propertyGetter, bean, (Object[])null); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return name; + } + + private void reflect() { + // If both the getter and setter are null then we have not reflected + // on this property before + if (!reflected) { + reflected = true; + try { + // Since we use it in several places, construct the + // first-letter-capitalized version of name + final String properName = name.length() == 1 ? + name.substring(0, 1).toUpperCase() : + Character.toUpperCase(name.charAt(0)) + + name.substring(1); + + // Now look for the getter. It will be named either + // "get" + name with the first letter of name + // capitalized, or it will be named "is" + name with + // the first letter of the name capitalized. However it + // is only named with "is" as a prefix if the type is + // boolean. + type = null; + // first we check for getXXX + String getterName = "get" + properName; + try { + final Method m = clazz.getMethod(getterName); + if (Modifier.isPublic(m.getModifiers())) { + getter = m; + } + } catch (NoSuchMethodException ex) { + // This is a legitimate error + } + + // Then if it wasn't found we look for isXXX + if (getter == null) { + getterName = "is" + properName; + try { + final Method m = clazz.getMethod(getterName); + if (Modifier.isPublic(m.getModifiers())) { + getter = m; + } + } catch (NoSuchMethodException ex) { + // This is a legitimate error + } + } + + // Now attempt to look for the setter. It is simply + // "set" + name with the first letter of name + // capitalized. + final String setterName = "set" + properName; + + // If we found the getter, we can get the type + // and the setter easily. + if (getter != null) { + type = getter.getReturnType(); + try { + final Method m = clazz.getMethod(setterName, type); + if (Modifier.isPublic(m.getModifiers())) { + setter = m; + } + } catch (NoSuchMethodException ex) { + // This is a legitimate error + } + } else { // no getter found + final Method[] methods = clazz.getMethods(); + for (final Method m : methods) { + final Class[] parameters = m.getParameterTypes(); + if (setterName.equals(m.getName()) + && (parameters.length == 1) + && Modifier.isPublic(m.getModifiers())) + { + setter = m; + type = parameters[0]; + break; + } + } + } + + // Now attempt to look for the property-getter. + final String propertyGetterName = name + "Property"; + try { + final Method m = clazz.getMethod(propertyGetterName); + if (Modifier.isPublic(m.getModifiers())) { + propertyGetter = m; + } else + propertyGetter = null; + } catch (NoSuchMethodException ex) { + // This is a legitimate error + } + } catch (RuntimeException e) { + System.err.println("Failed to introspect property " + name); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PropertyReference)) { + return false; + } + final PropertyReference other = (PropertyReference) obj; + if (this.name != other.name + && (this.name == null || !this.name.equals(other.name))) { + return false; + } + if (this.clazz != other.clazz + && (this.clazz == null || !this.clazz.equals(other.clazz))) { + return false; + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int hash = 5; + hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0); + hash = 97 * hash + (this.clazz != null ? this.clazz.hashCode() : 0); + return hash; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/Disposer.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/Disposer.java new file mode 100644 index 00000000..8e35a10c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/Disposer.java @@ -0,0 +1,52 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class is used for registering and disposing various + * data associated with java objects. + * + * The object can register itself by calling the addRecord method and + * providing a descendant of the Runnable class with overridden + * run() method. + * + * When the object becomes phantom-reachable, the run() method + * of the associated Runnable object will be called. + */ +public class Disposer implements Runnable { + private static final ReferenceQueue queue = new ReferenceQueue(); + private static final Map records = new ConcurrentHashMap<>(); + private static Disposer disposerInstance; + + static { + disposerInstance = new Disposer(); + } + + /** + * Registers the object and the data for later disposal. + * @param target Object to be registered + * @param rec the associated Runnable object + */ + public static void addRecord(Object target, Runnable rec) { + PhantomReference ref = new PhantomReference<>(target, queue); + records.put(ref, rec); + } + + public void run() { + while (true) { + try { + Object obj = queue.remove(); + ((Reference)obj).clear(); + Runnable rec = (Runnable)records.remove(obj); + rec.run(); + } catch (Exception e) { + System.out.println("Exception while removing reference: " + e); + e.printStackTrace(); + } + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanPropertyBuilderHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanPropertyBuilderHelper.java new file mode 100644 index 00000000..1f37bffc --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanPropertyBuilderHelper.java @@ -0,0 +1,118 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import com.tungsten.fclcore.fakefx.reflect.ReflectUtil; + +import java.lang.reflect.Method; + +/** + */ +public class JavaBeanPropertyBuilderHelper { + + private static final String IS_PREFIX = "is"; + private static final String GET_PREFIX = "get"; + private static final String SET_PREFIX = "set"; + + private String propertyName; + private Class beanClass; + private Object bean; + private String getterName; + private String setterName; + private Method getter; + private Method setter; + private PropertyDescriptor descriptor; + + public void name(String propertyName) { + if ((propertyName == null)? this.propertyName != null : !propertyName.equals(this.propertyName)) { + this.propertyName = propertyName; + this.descriptor = null; + } + } + + public void beanClass(Class beanClass) { + if ((beanClass == null)? this.beanClass != null : !beanClass.equals(this.beanClass)) { + ReflectUtil.checkPackageAccess(beanClass); + this.beanClass = beanClass; + this.descriptor = null; + } + } + + public void bean(Object bean) { + this.bean = bean; + if (bean != null) { + Class newClass = bean.getClass(); + if ((beanClass == null) || !beanClass.isAssignableFrom(newClass)) { + ReflectUtil.checkPackageAccess(newClass); + this.beanClass = newClass; + this.descriptor = null; + } + } + } + + public Object getBean() { + return bean; + } + + public void getterName(String getterName) { + if ((getterName == null)? this.getterName != null : !getterName.equals(this.getterName)) { + this.getterName = getterName; + this.descriptor = null; + } + } + + public void setterName(String setterName) { + if ((setterName == null)? this.setterName != null : !setterName.equals(this.setterName)) { + this.setterName = setterName; + this.descriptor = null; + } + } + + public void getter(Method getter) { + if ((getter == null)? this.getter != null : !getter.equals(this.getter)) { + this.getter = getter; + this.descriptor = null; + } + } + + public void setter(Method setter) { + if ((setter == null)? this.setter != null : !setter.equals(this.setter)) { + this.setter = setter; + this.descriptor = null; + } + } + + public PropertyDescriptor getDescriptor() throws NoSuchMethodException { + if (descriptor == null) { + if (propertyName == null) { + throw new NullPointerException("Property name has to be specified"); + } + if (propertyName.isEmpty()) { + throw new IllegalArgumentException("Property name cannot be empty"); + } + final String capitalizedName = ReadOnlyPropertyDescriptor.capitalizedName(propertyName); + Method getterMethod = getter; + if (getterMethod == null) { + if ((getterName != null) && !getterName.isEmpty()) { + getterMethod = beanClass.getMethod(getterName); + } else { + try { + getterMethod = beanClass.getMethod(IS_PREFIX + capitalizedName); + } catch (NoSuchMethodException e) { + getterMethod = beanClass.getMethod(GET_PREFIX + capitalizedName); + } + } + } + Method setterMethod = setter; + if (setterMethod == null) { + final Class type = getterMethod.getReturnType(); + if ((setterName != null) && !setterName.isEmpty()) { + setterMethod = beanClass.getMethod(setterName, type); + } else { + setterMethod = beanClass.getMethod(SET_PREFIX + capitalizedName, type); + } + } + descriptor = new PropertyDescriptor(propertyName, beanClass, getterMethod, setterMethod); + } + return descriptor; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanQuickAccessor.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanQuickAccessor.java new file mode 100644 index 00000000..56e312c3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/JavaBeanQuickAccessor.java @@ -0,0 +1,15 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.adapter.ReadOnlyJavaBeanObjectProperty; +import com.tungsten.fclcore.fakefx.beans.property.adapter.ReadOnlyJavaBeanObjectPropertyBuilder; + +public final class JavaBeanQuickAccessor { + + private JavaBeanQuickAccessor() { + } + + public static ReadOnlyJavaBeanObjectProperty createReadOnlyJavaBeanObjectProperty(Object bean, String name) throws NoSuchMethodException { + return ReadOnlyJavaBeanObjectPropertyBuilder.create().bean(bean).name(name).build(); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyChangeEvent.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyChangeEvent.java new file mode 100644 index 00000000..32d154d0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyChangeEvent.java @@ -0,0 +1,52 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import java.util.EventObject; + +public class PropertyChangeEvent extends EventObject { + private static final long serialVersionUID = 7042693688939648123L; + private String propertyName; + private Object newValue; + private Object oldValue; + private Object propagationId; + + public PropertyChangeEvent(Object source, String propertyName, Object oldValue, Object newValue) { + super(source); + this.propertyName = propertyName; + this.newValue = newValue; + this.oldValue = oldValue; + } + + public String getPropertyName() { + return this.propertyName; + } + + public Object getNewValue() { + return this.newValue; + } + + public Object getOldValue() { + return this.oldValue; + } + + public void setPropagationId(Object propagationId) { + this.propagationId = propagationId; + } + + public Object getPropagationId() { + return this.propagationId; + } + + public String toString() { + StringBuilder sb = new StringBuilder(this.getClass().getName()); + sb.append("[propertyName=").append(this.getPropertyName()); + this.appendTo(sb); + sb.append("; oldValue=").append(this.getOldValue()); + sb.append("; newValue=").append(this.getNewValue()); + sb.append("; propagationId=").append(this.getPropagationId()); + sb.append("; source=").append(this.getSource()); + return sb.append("]").toString(); + } + + void appendTo(StringBuilder sb) { + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyDescriptor.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyDescriptor.java new file mode 100644 index 00000000..cf8c4eeb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyDescriptor.java @@ -0,0 +1,157 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.fakefx.beans.property.adapter.ReadOnlyJavaBeanProperty; +import com.tungsten.fclcore.fakefx.beans.value.ChangeListener; +import com.tungsten.fclcore.fakefx.beans.value.ObservableValue; +import com.tungsten.fclcore.fakefx.property.MethodHelper; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + */ +public class PropertyDescriptor extends ReadOnlyPropertyDescriptor { + + private static final String ADD_VETOABLE_LISTENER_METHOD_NAME = "addVetoableChangeListener"; + private static final String REMOVE_VETOABLE_LISTENER_METHOD_NAME = "removeVetoableChangeListener"; + private static final String ADD_PREFIX = "add"; + private static final String REMOVE_PREFIX = "remove"; + private static final String SUFFIX = "Listener"; + + private static final int ADD_VETOABLE_LISTENER_TAKES_NAME = 1; + private static final int REMOVE_VETOABLE_LISTENER_TAKES_NAME = 2; + + private final Method setter; + private final Method addVetoListener; + private final Method removeVetoListener; + private final int flags; + + public Method getSetter() {return setter;} + + public PropertyDescriptor(String propertyName, Class beanClass, Method getter, Method setter) { + super(propertyName, beanClass, getter); + this.setter = setter; + + Method tmpAddVetoListener = null; + Method tmpRemoveVetoListener = null; + int tmpFlags = 0; + + // reflect addVetoListenerMethod + final String addMethodName = ADD_PREFIX + capitalizedName(name) + SUFFIX; + try { + tmpAddVetoListener = beanClass.getMethod(addMethodName, VetoableChangeListener.class); + } catch (NoSuchMethodException e) { + try { + tmpAddVetoListener = beanClass.getMethod(ADD_VETOABLE_LISTENER_METHOD_NAME, String.class, VetoableChangeListener.class); + tmpFlags |= ADD_VETOABLE_LISTENER_TAKES_NAME; + } catch (NoSuchMethodException e1) { + try { + tmpAddVetoListener = beanClass.getMethod(ADD_VETOABLE_LISTENER_METHOD_NAME, VetoableChangeListener.class); + } catch (NoSuchMethodException e2) { + // ignore + } + } + } + + // reflect removeVetoListenerMethod + final String removeMethodName = REMOVE_PREFIX + capitalizedName(name) + SUFFIX; + try { + tmpRemoveVetoListener = beanClass.getMethod(removeMethodName, VetoableChangeListener.class); + } catch (NoSuchMethodException e) { + try { + tmpRemoveVetoListener = beanClass.getMethod(REMOVE_VETOABLE_LISTENER_METHOD_NAME, String.class, VetoableChangeListener.class); + tmpFlags |= REMOVE_VETOABLE_LISTENER_TAKES_NAME; + } catch (NoSuchMethodException e1) { + try { + tmpRemoveVetoListener = beanClass.getMethod(REMOVE_VETOABLE_LISTENER_METHOD_NAME, VetoableChangeListener.class); + } catch (NoSuchMethodException e2) { + // ignore + } + } + } + + addVetoListener = tmpAddVetoListener; + removeVetoListener = tmpRemoveVetoListener; + flags = tmpFlags; + } + + @Override + public void addListener(ReadOnlyListener listener) { + super.addListener(listener); + if (addVetoListener != null) { + try { + if ((flags & ADD_VETOABLE_LISTENER_TAKES_NAME) > 0) { + addVetoListener.invoke(listener.getBean(), name, listener); + } else { + addVetoListener.invoke(listener.getBean(), listener); + } + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + } + + + + @Override + public void removeListener(ReadOnlyListener listener) { + super.removeListener(listener); + if (removeVetoListener != null) { + try { + if ((flags & REMOVE_VETOABLE_LISTENER_TAKES_NAME) > 0) { + removeVetoListener.invoke(listener.getBean(), name, listener); + } else { + removeVetoListener.invoke(listener.getBean(), listener); + } + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + } + + public class Listener extends ReadOnlyListener implements ChangeListener, VetoableChangeListener { + + private boolean updating; + + public Listener(Object bean, ReadOnlyJavaBeanProperty property) { + super(bean, property); + } + + @Override + public void changed(ObservableValue observable, T oldValue, T newValue) { + final ReadOnlyJavaBeanProperty property = checkRef(); + if (property == null) { + observable.removeListener(this); + } else if (!updating) { + updating = true; + try { + MethodHelper.invoke(setter, bean, new Object[] {newValue}); + property.fireValueChangedEvent(); + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } finally { + updating = false; + } + } + } + + + @Override + public void vetoableChange(PropertyChangeEvent propertyChangeEvent) throws PropertyVetoException { + if (bean.equals(propertyChangeEvent.getSource()) && name.equals(propertyChangeEvent.getPropertyName())) { + final ReadOnlyJavaBeanProperty property = checkRef(); + if ((property instanceof Property) && (((Property)property).isBound()) && !updating) { + throw new PropertyVetoException("A bound value cannot be set.", propertyChangeEvent); + } + } + } + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyVetoException.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyVetoException.java new file mode 100644 index 00000000..28291fdb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/PropertyVetoException.java @@ -0,0 +1,15 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +public class PropertyVetoException extends Exception { + private static final long serialVersionUID = 129596057694162164L; + private PropertyChangeEvent evt; + + public PropertyVetoException(String mess, PropertyChangeEvent evt) { + super(mess); + this.evt = evt; + } + + public PropertyChangeEvent getPropertyChangeEvent() { + return this.evt; + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyJavaBeanPropertyBuilderHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyJavaBeanPropertyBuilderHelper.java new file mode 100644 index 00000000..35d1fe7b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyJavaBeanPropertyBuilderHelper.java @@ -0,0 +1,91 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import com.tungsten.fclcore.fakefx.reflect.ReflectUtil; + +import java.lang.reflect.Method; + +/** + */ +public class ReadOnlyJavaBeanPropertyBuilderHelper { + + private static final String IS_PREFIX = "is"; + private static final String GET_PREFIX = "get"; + + private String propertyName; + private Class beanClass; + private Object bean; + private String getterName; + private Method getter; + private ReadOnlyPropertyDescriptor descriptor; + + public void name(String propertyName) { + if ((propertyName == null)? this.propertyName != null : !propertyName.equals(this.propertyName)) { + this.propertyName = propertyName; + this.descriptor = null; + } + } + + public void beanClass(Class beanClass) { + if ((beanClass == null)? this.beanClass != null : !beanClass.equals(this.beanClass)) { + ReflectUtil.checkPackageAccess(beanClass); + this.beanClass = beanClass; + this.descriptor = null; + } + } + + public void bean(Object bean) { + this.bean = bean; + if (bean != null) { + Class newClass = bean.getClass(); + if ((beanClass == null) || !beanClass.isAssignableFrom(newClass)) { + ReflectUtil.checkPackageAccess(newClass); + this.beanClass = bean.getClass(); + this.descriptor = null; + } + } + } + + public Object getBean() { + return bean; + } + + public void getterName(String getterName) { + if ((getterName == null)? this.getterName != null : !getterName.equals(this.getterName)) { + this.getterName = getterName; + this.descriptor = null; + } + } + + public void getter(Method getter) { + if ((getter == null)? this.getter != null : !getter.equals(this.getter)) { + this.getter = getter; + this.descriptor = null; + } + } + + public ReadOnlyPropertyDescriptor getDescriptor() throws NoSuchMethodException { + if (descriptor == null) { + if ((propertyName == null) || (bean == null)) { + throw new NullPointerException("Bean and property name have to be specified"); + } + if (propertyName.isEmpty()) { + throw new IllegalArgumentException("Property name cannot be empty"); + } + final String capitalizedName = ReadOnlyPropertyDescriptor.capitalizedName(propertyName); + if (getter == null) { + if ((getterName != null) && !getterName.isEmpty()) { + getter = beanClass.getMethod(getterName); + } else { + try { + getter = beanClass.getMethod(IS_PREFIX + capitalizedName); + } catch (NoSuchMethodException e) { + getter = beanClass.getMethod(GET_PREFIX + capitalizedName); + } + } + } + descriptor = new ReadOnlyPropertyDescriptor(propertyName, beanClass, getter); + } + return descriptor; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyPropertyDescriptor.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyPropertyDescriptor.java new file mode 100644 index 00000000..fea92e6e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/ReadOnlyPropertyDescriptor.java @@ -0,0 +1,161 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static java.util.Locale.ENGLISH; + +import com.tungsten.fclcore.fakefx.beans.WeakListener; +import com.tungsten.fclcore.fakefx.beans.property.adapter.ReadOnlyJavaBeanProperty; +import com.tungsten.fclcore.fakefx.reflect.ReflectUtil; + +/** + */ +public class ReadOnlyPropertyDescriptor { + + private static final String ADD_LISTENER_METHOD_NAME = "addPropertyChangeListener"; + private static final String REMOVE_LISTENER_METHOD_NAME = "removePropertyChangeListener"; + private static final String ADD_PREFIX = "add"; + private static final String REMOVE_PREFIX = "remove"; + private static final String SUFFIX = "Listener"; + + private static final int ADD_LISTENER_TAKES_NAME = 1; + private static final int REMOVE_LISTENER_TAKES_NAME = 2; + + protected final String name; + protected final Class beanClass; + private final Method getter; + private final Class type; + + private final Method addChangeListener; + private final Method removeChangeListener; + private final int flags; + + public String getName() {return name;} + public Method getGetter() {return getter;} + public Class getType() {return type;} + + public ReadOnlyPropertyDescriptor(String propertyName, Class beanClass, Method getter) { + ReflectUtil.checkPackageAccess(beanClass); + + this.name = propertyName; + this.beanClass = beanClass; + this.getter = getter; + this.type = getter.getReturnType(); + + Method tmpAddChangeListener = null; + Method tmpRemoveChangeListener = null; + int tmpFlags = 0; + + try { + final String methodName = ADD_PREFIX + capitalizedName(name) + SUFFIX; + tmpAddChangeListener = beanClass.getMethod(methodName, PropertyChangeListener.class); + } catch (NoSuchMethodException e) { + try { + tmpAddChangeListener = beanClass.getMethod(ADD_LISTENER_METHOD_NAME, String.class, PropertyChangeListener.class); + tmpFlags |= ADD_LISTENER_TAKES_NAME; + } catch (NoSuchMethodException e1) { + try { + tmpAddChangeListener = beanClass.getMethod(ADD_LISTENER_METHOD_NAME, PropertyChangeListener.class); + } catch (NoSuchMethodException e2) { + // ignore + } + } + } + + try { + final String methodName = REMOVE_PREFIX + capitalizedName(name) + SUFFIX; + tmpRemoveChangeListener = beanClass.getMethod(methodName, PropertyChangeListener.class); + } catch (NoSuchMethodException e) { + try { + tmpRemoveChangeListener = beanClass.getMethod(REMOVE_LISTENER_METHOD_NAME, String.class, PropertyChangeListener.class); + tmpFlags |= REMOVE_LISTENER_TAKES_NAME; + } catch (NoSuchMethodException e1) { + try { + tmpRemoveChangeListener = beanClass.getMethod(REMOVE_LISTENER_METHOD_NAME, PropertyChangeListener.class); + } catch (NoSuchMethodException e2) { + // ignore + } + } + } + + addChangeListener = tmpAddChangeListener; + removeChangeListener = tmpRemoveChangeListener; + flags = tmpFlags; + } + + public static String capitalizedName(String name) { + return ((name == null) || (name.length() == 0))? name : name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1); + } + + public void addListener(ReadOnlyListener listener) { + if (addChangeListener != null) { + try { + if ((flags & ADD_LISTENER_TAKES_NAME) > 0) { + addChangeListener.invoke(listener.getBean(), name, listener); + } else { + addChangeListener.invoke(listener.getBean(), listener); + } + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + } + + public void removeListener(ReadOnlyListener listener) { + if (removeChangeListener != null) { + try { + if ((flags & REMOVE_LISTENER_TAKES_NAME) > 0) { + removeChangeListener.invoke(listener.getBean(), name, listener); + } else { + removeChangeListener.invoke(listener.getBean(), listener); + } + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + } + + public class ReadOnlyListener implements PropertyChangeListener, WeakListener { + + protected final Object bean; + private final WeakReference> propertyRef; + + public Object getBean() {return bean;} + + public ReadOnlyListener(Object bean, ReadOnlyJavaBeanProperty property) { + this.bean = bean; + this.propertyRef = new WeakReference>(property); + } + + protected ReadOnlyJavaBeanProperty checkRef() { + final ReadOnlyJavaBeanProperty result = propertyRef.get(); + if (result == null) { + removeListener(this); + } + return result; + } + + @Override + public void propertyChange(PropertyChangeEvent propertyChangeEvent) { + if (bean.equals(propertyChangeEvent.getSource()) && name.equals(propertyChangeEvent.getPropertyName())) { + final ReadOnlyJavaBeanProperty property = checkRef(); + if (property != null) { + property.fireValueChangedEvent(); + } + } + } + + @Override + public boolean wasGarbageCollected() { + return checkRef() == null; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/VetoableChangeListener.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/VetoableChangeListener.java new file mode 100644 index 00000000..7ddf42f4 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/property/adapter/VetoableChangeListener.java @@ -0,0 +1,7 @@ +package com.tungsten.fclcore.fakefx.property.adapter; + +import java.util.EventListener; + +public interface VetoableChangeListener extends EventListener { + void vetoableChange(PropertyChangeEvent var1) throws PropertyVetoException; +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ConstructorUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ConstructorUtil.java new file mode 100644 index 00000000..22a2ec42 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ConstructorUtil.java @@ -0,0 +1,20 @@ +package com.tungsten.fclcore.fakefx.reflect; + +import java.lang.reflect.Constructor; + +public final class ConstructorUtil { + + private ConstructorUtil() { + } + + public static Constructor getConstructor(Class cls, Class[] params) + throws NoSuchMethodException { + ReflectUtil.checkPackageAccess(cls); + return cls.getConstructor(params); + } + + public static Constructor[] getConstructors(Class cls) { + ReflectUtil.checkPackageAccess(cls); + return cls.getConstructors(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/FieldUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/FieldUtil.java new file mode 100644 index 00000000..5aa6d70e --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/FieldUtil.java @@ -0,0 +1,15 @@ +package com.tungsten.fclcore.fakefx.reflect; + +import java.lang.reflect.Field; + +public final class FieldUtil { + + private FieldUtil() { + } + + public static Field getField(Class cls, String name) + throws NoSuchFieldException { + ReflectUtil.checkPackageAccess(cls); + return cls.getField(name); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/MethodUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/MethodUtil.java new file mode 100644 index 00000000..3483a603 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/MethodUtil.java @@ -0,0 +1,303 @@ +package com.tungsten.fclcore.fakefx.reflect; + +import java.io.EOFException; +import java.security.AllPermission; +import java.security.AccessController; +import java.security.PermissionCollection; +import java.security.SecureClassLoader; +import java.security.PrivilegedExceptionAction; +import java.security.CodeSource; +import java.io.InputStream; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +class Trampoline { + static { + if (Trampoline.class.getClassLoader() == null) { + throw new Error( + "Trampoline must not be defined by the bootstrap classloader"); + } + } + + @SuppressWarnings("removal") + private static void ensureInvocableMethod(Method m) + throws InvocationTargetException + { + Class clazz = m.getDeclaringClass(); + if (clazz.equals(AccessController.class) || + clazz.equals(Method.class) || + clazz.getName().startsWith("java.lang.invoke.")) + throw new InvocationTargetException( + new UnsupportedOperationException("invocation not supported")); + } + + private static Object invoke(Method m, Object obj, Object[] params) + throws InvocationTargetException, IllegalAccessException + { + ensureInvocableMethod(m); + return m.invoke(obj, params); + } +} + +/* + * Create a trampoline class. + */ +public final class MethodUtil extends SecureClassLoader { + private static final String MISC_PKG = "com.sun.javafx.reflect."; + private static final String TRAMPOLINE = MISC_PKG + "Trampoline"; + private static final Method bounce = getTrampoline(); + + private MethodUtil() { + super(); + } + + public static Method getMethod(Class cls, String name, Class[] args) + throws NoSuchMethodException { + ReflectUtil.checkPackageAccess(cls); + return cls.getMethod(name, args); + } + + public static Method[] getMethods(Class cls) { + ReflectUtil.checkPackageAccess(cls); + return cls.getMethods(); + } + + /* + * Discover the public methods on public classes + * and interfaces accessible to any caller by calling + * Class.getMethods() and walking towards Object until + * we're done. + */ + /*public*/ + @SuppressWarnings("removal") + static Method[] getPublicMethods(Class cls) { + // compatibility for update release + if (System.getSecurityManager() == null) { + return cls.getMethods(); + } + Map sigs = new HashMap(); + while (cls != null) { + boolean done = getInternalPublicMethods(cls, sigs); + if (done) { + break; + } + getInterfaceMethods(cls, sigs); + cls = cls.getSuperclass(); + } + return sigs.values().toArray(new Method[sigs.size()]); + } + + /* + * Process the immediate interfaces of this class or interface. + */ + private static void getInterfaceMethods(Class cls, + Map sigs) { + Class[] intfs = cls.getInterfaces(); + for (int i=0; i < intfs.length; i++) { + Class intf = intfs[i]; + boolean done = getInternalPublicMethods(intf, sigs); + if (!done) { + getInterfaceMethods(intf, sigs); + } + } + } + + /* + * + * Process the methods in this class or interface + */ + private static boolean getInternalPublicMethods(Class cls, + Map sigs) { + Method[] methods = null; + try { + /* + * This class or interface is non-public so we + * can't use any of it's methods. Go back and + * try again with a superclass or superinterface. + */ + if (!Modifier.isPublic(cls.getModifiers())) { + return false; + } + if (!ReflectUtil.isPackageAccessible(cls)) { + return false; + } + + methods = cls.getMethods(); + } catch (SecurityException se) { + return false; + } + + /* + * Check for inherited methods with non-public + * declaring classes. They might override and hide + * methods from their superclasses or + * superinterfaces. + */ + boolean done = true; + for (int i=0; i < methods.length; i++) { + Class dc = methods[i].getDeclaringClass(); + if (!Modifier.isPublic(dc.getModifiers())) { + done = false; + break; + } + } + + if (done) { + /* + * We're done. Spray all the methods into + * the list and then we're out of here. + */ + for (int i=0; i < methods.length; i++) { + addMethod(sigs, methods[i]); + } + } else { + /* + * Simulate cls.getDeclaredMethods() by + * stripping away inherited methods. + */ + for (int i=0; i < methods.length; i++) { + Class dc = methods[i].getDeclaringClass(); + if (cls.equals(dc)) { + addMethod(sigs, methods[i]); + } + } + } + return done; + } + + private static void addMethod(Map sigs, Method method) { + Signature signature = new Signature(method); + if (!sigs.containsKey(signature)) { + sigs.put(signature, method); + } else if (!method.getDeclaringClass().isInterface()){ + /* + * Superclasses beat interfaces. + */ + Method old = sigs.get(signature); + if (old.getDeclaringClass().isInterface()) { + sigs.put(signature, method); + } + } + } + + /** + * A class that represents the unique elements of a method that will be a + * key in the method cache. + */ + private static class Signature { + private final String methodName; + private final Class[] argClasses; + private final int hashCode; + + Signature(Method m) { + this.methodName = m.getName(); + this.argClasses = m.getParameterTypes(); + this.hashCode = methodName.hashCode() + Arrays.hashCode(argClasses); + } + + @Override public int hashCode() { + return hashCode; + } + + @Override public boolean equals(Object o2) { + if (this == o2) { + return true; + } + Signature that = (Signature)o2; + if (!(methodName.equals(that.methodName))) { + return false; + } + if (argClasses.length != that.argClasses.length) { + return false; + } + for (int i = 0; i < argClasses.length; i++) { + if (!(argClasses[i] == that.argClasses[i])) { + return false; + } + } + return true; + } + } + + /* + * Bounce through the trampoline. + */ + public static Object invoke(Method m, Object obj, Object[] params) + throws InvocationTargetException, IllegalAccessException { + try { + return bounce.invoke(null, new Object[] {m, obj, params}); + } catch (InvocationTargetException ie) { + Throwable t = ie.getCause(); + + if (t instanceof InvocationTargetException) { + throw (InvocationTargetException)t; + } else if (t instanceof IllegalAccessException) { + throw (IllegalAccessException)t; + } else if (t instanceof RuntimeException) { + throw (RuntimeException)t; + } else if (t instanceof Error) { + throw (Error)t; + } else { + throw new Error("Unexpected invocation error", t); + } + } catch (IllegalAccessException iae) { + // this can't happen + throw new Error("Unexpected invocation error", iae); + } + } + + @SuppressWarnings("removal") + private static Method getTrampoline() { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + public Method run() throws Exception { + Class t = getTrampolineClass(); + Class[] types = { + Method.class, Object.class, Object[].class + }; + Method b = t.getDeclaredMethod("invoke", types); + b.setAccessible(true); + return b; + } + }); + } catch (Exception e) { + throw new InternalError("bouncer cannot be found", e); + } + } + + /* + * Define the proxy classes + */ + private Class defineClass(String name, byte[] b) throws IOException { + CodeSource cs = new CodeSource(null, (java.security.cert.Certificate[])null); + if (!name.equals(TRAMPOLINE)) { + throw new IOException("MethodUtil: bad name " + name); + } + return defineClass(name, b, 0, b.length, cs); + } + + protected PermissionCollection getPermissions(CodeSource codesource) + { + PermissionCollection perms = super.getPermissions(codesource); + perms.add(new AllPermission()); + return perms; + } + + private static Class getTrampolineClass() { + try { + return Class.forName(TRAMPOLINE, true, new MethodUtil()); + } catch (ClassNotFoundException e) { + } + return null; + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ReflectUtil.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ReflectUtil.java new file mode 100644 index 00000000..1cb9b194 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/reflect/ReflectUtil.java @@ -0,0 +1,105 @@ +package com.tungsten.fclcore.fakefx.reflect; + +import java.lang.reflect.Proxy; +import java.util.Objects; + +public final class ReflectUtil { + + private ReflectUtil() { + } + + /** + * Checks package access on the given class. + * + * If it is a {@link Proxy#isProxyClass(Class)} that implements + * a non-public interface (i.e. may be in a non-restricted package), + * also check the package access on the proxy interfaces. + */ + public static void checkPackageAccess(Class clazz) { + @SuppressWarnings("removal") + SecurityManager s = System.getSecurityManager(); + if (s != null) { + privateCheckPackageAccess(s, clazz); + } + } + + /** + * NOTE: should only be called if a SecurityManager is installed + */ + private static void privateCheckPackageAccess(@SuppressWarnings("removal") SecurityManager s, Class clazz) { + while (clazz.isArray()) { + clazz = clazz.getComponentType(); + } + + String pkg = Objects.requireNonNull(clazz.getPackage()).getName(); + if (pkg != null && !pkg.isEmpty()) { + s.checkPackageAccess(pkg); + } + + if (isNonPublicProxyClass(clazz)) { + privateCheckProxyPackageAccess(s, clazz); + } + } + + /** + * Checks package access on the given classname. + * This method is typically called when the Class instance is not + * available and the caller attempts to load a class on behalf + * the true caller (application). + */ + public static void checkPackageAccess(String name) { + @SuppressWarnings("removal") + SecurityManager s = System.getSecurityManager(); + if (s != null) { + String cname = name.replace('/', '.'); + if (cname.startsWith("[")) { + int b = cname.lastIndexOf('[') + 2; + if (b > 1 && b < cname.length()) { + cname = cname.substring(b); + } + } + int i = cname.lastIndexOf('.'); + if (i != -1) { + s.checkPackageAccess(cname.substring(0, i)); + } + } + } + + public static boolean isPackageAccessible(Class clazz) { + try { + checkPackageAccess(clazz); + } catch (SecurityException e) { + return false; + } + return true; + } + + /** + * NOTE: should only be called if a SecurityManager is installed + */ + private static void privateCheckProxyPackageAccess(@SuppressWarnings("removal") SecurityManager s, Class clazz) { + // check proxy interfaces if the given class is a proxy class + if (Proxy.isProxyClass(clazz)) { + for (Class intf : clazz.getInterfaces()) { + privateCheckPackageAccess(s, intf); + } + } + } + + // Note that bytecode instrumentation tools may exclude 'sun.*' + // classes but not generated proxy classes and so keep it in com.sun.* + public static final String PROXY_PACKAGE = "com.sun.proxy"; + + /** + * Test if the given class is a proxy class that implements + * non-public interface. Such proxy class may be in a non-restricted + * package that bypasses checkPackageAccess. + */ + public static boolean isNonPublicProxyClass(Class cls) { + if (!Proxy.isProxyClass(cls)) { + return false; + } + String pkg = Objects.requireNonNull(cls.getPackage()).getName(); + return pkg == null || !pkg.startsWith(PROXY_PACKAGE); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Builder.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Builder.java new file mode 100644 index 00000000..411785a5 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Builder.java @@ -0,0 +1,17 @@ +package com.tungsten.fclcore.fakefx.util; + +/** + * Interface representing a builder. Builders are objects that are used to + * construct other objects. + * + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface Builder { + /** + * Builds and returns the object. + * + * @return the object + */ + public T build(); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/BuilderFactory.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/BuilderFactory.java new file mode 100644 index 00000000..b621958b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/BuilderFactory.java @@ -0,0 +1,21 @@ +package com.tungsten.fclcore.fakefx.util; + +/** + * Interface representing a builder factory. Builder factories are used to + * produce builders. + * + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface BuilderFactory { + /** + * Returns a builder suitable for constructing instances of the given type. + * + * @param type the given type or null + * + * @return + * A builder for the given type, or {@code null} if this factory does not + * produce builders for the type. + */ + public Builder getBuilder(Class type); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Callback.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Callback.java new file mode 100644 index 00000000..4313eb36 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Callback.java @@ -0,0 +1,28 @@ +package com.tungsten.fclcore.fakefx.util; + +/** + * The Callback interface is designed to allow for a common, reusable interface + * to exist for defining APIs that requires a call back in certain situations. + *

+ * Callback is defined with two generic parameters: the first parameter + * specifies the type of the object passed in to the call method, + * with the second parameter specifying the return type of the method. + * + * @param

The type of the argument provided to the call method. + * @param The type of the return type of the call method. + * @since JavaFX 2.0 + */ +@FunctionalInterface +public interface Callback { + /** + * The call method is called when required, and is given a + * single argument of type P, with a requirement that an object of type R + * is returned. + * + * @param param The single argument upon which the returned value should be + * determined. + * @return An object of type R that may be determined based on the provided + * parameter value. + */ + public R call(P param); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Duration.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Duration.java new file mode 100644 index 00000000..d241e369 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Duration.java @@ -0,0 +1,413 @@ +package com.tungsten.fclcore.fakefx.util; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; + +import java.io.Serializable; + +/** + *

+ * A class that defines a duration of time. Duration instances are immutable, + * and are therefore replaced rather than modified, similar to {@link java.math.BigDecimal}. + * Durations can be created using the constructor, or one of the static construction + * methods such as {@link #seconds} or {@link #minutes}. + *

+ * @since JavaFX 2.0 + */ +public class Duration implements Comparable, Serializable { + /** + * A Duration of 0 (no time). + */ + public static final Duration ZERO = new Duration(0); + + /** + * A Duration of 1 millisecond. + */ + public static final Duration ONE = new Duration(1); + + /** + * An Infinite Duration. + */ + public static final Duration INDEFINITE = new Duration(Double.POSITIVE_INFINITY); + + /** + * A Duration of some unknown amount of time. + */ + public static final Duration UNKNOWN = new Duration(Double.NaN); + + /** + * Factory method that returns a Duration instance for a specified + * amount of time. The syntax is "[number][ms|s|m|h]". + * + * @param time A non-null string properly formatted. Leading or trailing + * spaces will not parse correctly. Throws a NullPointerException if + * time is null. + * @return a Duration which is represented by the time + */ + public static Duration valueOf(String time) { + int index = -1; + for (int i=0; iDuration.millis(50).negate() returns + * a Duration of -50 milliseconds. + * If the called Duration instance is INDEFINITE, return INDEFINITE. + * This function does not change the value of the called Duration instance. + * + * @return the result of negating this duration. This is + * the same as -millis using double arithmetic + */ + public Duration negate() { + return millis(-millis); + } + + /** + * Gets whether this Duration instance is Indefinite. A Duration is Indefinite + * if it equals Duration.INDEFINITE. + * @return true if this Duration is equivalent to Duration.INDEFINITE or Double.POSITIVE_INFINITY. + */ + public boolean isIndefinite() { + return millis == Double.POSITIVE_INFINITY; + } + + /** + * Gets whether this Duration instance is Unknown. A Duration is Unknown + * if it equals Duration.UNKNOWN. + * @return true if this Duration is equivalent to Duration.UNKNOWN or Double.isNaN(millis) + */ + public boolean isUnknown() { + return Double.isNaN(millis); + } + + /** + * Returns true if the specified duration is less than (<) this instance. + * INDEFINITE is treated as if it were positive infinity. + * + * @param other cannot be null + * @return true if millis < other.millis using double arithmetic + */ + public boolean lessThan(Duration other) { + return millis < other.millis; + } + + /** + * Returns true if the specified duration is less than or equal to (<=) this instance. + * INDEFINITE is treated as if it were positive infinity. + * + * @param other cannot be null + * @return true if millis <= other.millis using double arithmetic + */ + public boolean lessThanOrEqualTo(Duration other) { + return millis <= other.millis; + } + + /** + * Returns true if the specified duration is greater than (>) this instance. + * INDEFINITE is treated as if it were positive infinity. + * + * @param other cannot be null + * @return true if millis > other.millis using double arithmetic + */ + public boolean greaterThan(Duration other) { + return millis > other.millis; + } + + /** + * Returns true if the specified duration is greater than or equal to (>=) this instance. + * INDEFINITE is treated as if it were positive infinity. + * + * @param other cannot be null + * @return true if millis >= other.millis using double arithmetic + */ + public boolean greaterThanOrEqualTo(Duration other) { + return millis >= other.millis; + } + + /** + * Returns a string representation of this {@code Duration} object. + * @return a string representation of this {@code Duration} object. + */ + @Override public String toString() { + return isIndefinite() ? "INDEFINITE" : (isUnknown() ? "UNKNOWN" : millis + " ms"); + } + + /** + * Compares durations represented by this object and the specified object. + * Returns a negative integer, zero, or a positive integer as this duration + * is less than, equal to, or greater than the specified duration. + * @param d the duration to be compared. + * @return a negative integer, zero, or a positive integer as this duration + * is less than, equal to, or greater than the specified duration. + */ + @Override public int compareTo(Duration d) { + // Reuse the Double.compare implementation + return Double.compare(millis, d.millis); + } + + /** + * Indicates whether some other object is "equal to" this one. + * @param obj the reference object with which to compare. + * @return {@code true} if this object is equal to the {@code obj} argument; {@code false} otherwise. + */ + @Override public boolean equals(Object obj) { + // Rely on Java's handling of double == double + return obj == this || obj instanceof Duration && millis == ((Duration) obj).millis; + } + + /** + * Returns a hash code for this {@code Duration} object. + * @return a hash code for this {@code Duration} object. + */ + @Override public int hashCode() { + // Uses the same implementation as Double.hashCode + long bits = Double.doubleToLongBits(millis); + return (int)(bits ^ (bits >>> 32)); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/FXPermission.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/FXPermission.java new file mode 100644 index 00000000..d05d33da --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/FXPermission.java @@ -0,0 +1,24 @@ +package com.tungsten.fclcore.fakefx.util; + +import java.security.BasicPermission; + +public final class FXPermission extends BasicPermission { + + private static final long serialVersionUID = 2890556410764946054L; + + /** + * Creates a new {@code FXPermission} with the specified name. + * The name is the symbolic name of the {@code FXPermission}, + * such as "accessClipboard", "createTransparentWindow ", etc. An asterisk + * may be used to indicate all JavaFX permissions. + * + * @param name the name of the FXPermission + * + * @throws NullPointerException if {@code name} is {@code null}. + * @throws IllegalArgumentException if {@code name} is empty. + */ + public FXPermission(String name) { + super(name); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Pair.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Pair.java new file mode 100644 index 00000000..3abc699b --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/Pair.java @@ -0,0 +1,102 @@ +package com.tungsten.fclcore.fakefx.util; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; + +import java.io.Serializable; + + /** + *

A convenience class to represent name-value pairs.

+ * @since JavaFX 2.0 + */ +public class Pair implements Serializable{ + + /** + * Key of this Pair. + */ + private K key; + + /** + * Gets the key for this pair. + * @return key for this pair + */ + public K getKey() { return key; } + + /** + * Value of this this Pair. + */ + private V value; + + /** + * Gets the value for this pair. + * @return value for this pair + */ + public V getValue() { return value; } + + /** + * Creates a new pair + * @param key The key for this pair + * @param value The value to use for this pair + */ + public Pair(@NamedArg("key") K key, @NamedArg("value") V value) { + this.key = key; + this.value = value; + } + + /** + *

String representation of this + * Pair.

+ * + *

The default name/value delimiter '=' is always used.

+ * + * @return String representation of this Pair + */ + @Override + public String toString() { + return key + "=" + value; + } + + /** + *

Generate a hash code for this Pair.

+ * + *

The hash code is calculated using both the name and + * the value of the Pair.

+ * + * @return hash code for this Pair + */ + @Override + public int hashCode() { + int hash = 7; + hash = 31 * hash + (key != null ? key.hashCode() : 0); + hash = 31 * hash + (value != null ? value.hashCode() : 0); + return hash; + } + + /** + *

Test this Pair for equality with another + * Object.

+ * + *

If the Object to be tested is not a + * Pair or is null, then this method + * returns false.

+ * + *

Two Pairs are considered equal if and only if + * both the names and values are equal.

+ * + * @param o the Object to test for + * equality with this Pair + * @return true if the given Object is + * equal to this Pair else false + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof Pair) { + Pair pair = (Pair) o; + if (key != null ? !key.equals(pair.key) : pair.key != null) return false; + if (value != null ? !value.equals(pair.value) : pair.value != null) return false; + return true; + } + return false; + } + } + diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/StringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/StringConverter.java new file mode 100644 index 00000000..93d5081f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/StringConverter.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.fakefx.util; + +/** + * Converter defines conversion behavior between strings and objects. + * The type of objects and formats of strings are defined by the subclasses + * of Converter. + * @since JavaFX 2.0 + */ +public abstract class StringConverter { + + /** + * Creates a default {@code StringConverter}. + */ + public StringConverter() { + } + + /** + * Converts the object provided into its string form. + * Format of the returned string is defined by the specific converter. + * @param object the object of type {@code T} to convert + * @return a string representation of the object passed in + */ + public abstract String toString(T object); + + /** + * Converts the string provided into an object defined by the specific converter. + * Format of the string and type of the resulting object is defined by the specific converter. + * @param string the {@code String} to convert + * @return an object representation of the string passed in. + */ + public abstract T fromString(String string); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigDecimalStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigDecimalStringConverter.java new file mode 100644 index 00000000..38bf7bcb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigDecimalStringConverter.java @@ -0,0 +1,44 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.math.BigDecimal; + +/** + *

{@link StringConverter} implementation for {@link BigDecimal} values.

+ * @since JavaFX 2.1 + */ +public class BigDecimalStringConverter extends StringConverter { + + /** + * Creates a default {@code BigDecimalStringConverter}. + */ + public BigDecimalStringConverter() { + } + + /** {@inheritDoc} */ + @Override public BigDecimal fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return new BigDecimal(value); + } + + /** {@inheritDoc} */ + @Override public String toString(BigDecimal value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return ((BigDecimal)value).toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigIntegerStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigIntegerStringConverter.java new file mode 100644 index 00000000..17c0f000 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BigIntegerStringConverter.java @@ -0,0 +1,44 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.math.BigInteger; + +/** + *

{@link StringConverter} implementation for {@link BigInteger} values.

+ * @since JavaFX 2.1 + */ +public class BigIntegerStringConverter extends StringConverter { + + /** + * Creates a default {@code BigIntegerStringConverter}. + */ + public BigIntegerStringConverter() { + } + + /** {@inheritDoc} */ + @Override public BigInteger fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return new BigInteger(value); + } + + /** {@inheritDoc} */ + @Override public String toString(BigInteger value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return ((BigInteger)value).toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BooleanStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BooleanStringConverter.java new file mode 100644 index 00000000..493123e0 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/BooleanStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Boolean} + * (and boolean primitive) values.

+ * @since JavaFX 2.1 + */ +public class BooleanStringConverter extends StringConverter { + + /** + * Creates a default {@code BooleanStringConverter}. + */ + public BooleanStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Boolean fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Boolean.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Boolean value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return value.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ByteStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ByteStringConverter.java new file mode 100644 index 00000000..1061441a --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ByteStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Byte} + * (and byte primitive) values.

+ * @since JavaFX 2.1 + */ +public class ByteStringConverter extends StringConverter { + + /** + * Creates a default {@code ByteStringConverter}. + */ + public ByteStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Byte fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Byte.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Byte value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return Byte.toString(value.byteValue()); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CharacterStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CharacterStringConverter.java new file mode 100644 index 00000000..6d63eacb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CharacterStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Character} + * (and char primitive) values.

+ * @since JavaFX 2.1 + */ +public class CharacterStringConverter extends StringConverter { + + /** + * Creates a default {@code CharacterStringConverter}. + */ + public CharacterStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Character fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Character.valueOf(value.charAt(0)); + } + + /** {@inheritDoc} */ + @Override public String toString(Character value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return value.toString(); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CurrencyStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CurrencyStringConverter.java new file mode 100644 index 00000000..59c0b467 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/CurrencyStringConverter.java @@ -0,0 +1,71 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; + +public class CurrencyStringConverter extends NumberStringConverter { + + /** + * Constructs a {@code CurrencyStringConverter} with the default locale and format. + */ + public CurrencyStringConverter() { + this(Locale.getDefault()); + } + + /** + * Constructs a {@code CurrencyStringConverter} with the given locale and the default format. + * + * @param locale the locale used in determining the number format used to format the string + */ + public CurrencyStringConverter(Locale locale) { + this(locale, null); + } + + /** + * Constructs a {@code CurrencyStringConverter} with the default locale and the given decimal format pattern. + * + * @param pattern the string pattern used in determining the number format used to format the string + * + * @see DecimalFormat + */ + public CurrencyStringConverter(String pattern) { + this(Locale.getDefault(), pattern); + } + + /** + * Constructs a {@code CurrencyStringConverter} with the given locale and decimal format pattern. + * + * @param locale the locale used in determining the number format used to format the string + * @param pattern the string pattern used in determining the number format used to format the string + * + * @see DecimalFormat + */ + public CurrencyStringConverter(Locale locale, String pattern) { + super(locale, pattern, null); + } + + /** + * Constructs a {@code CurrencyStringConverter} with the given number format. + * + * @param numberFormat the number format used to format the string + */ + public CurrencyStringConverter(NumberFormat numberFormat) { + super(null, null, numberFormat); + } + + /** {@inheritDoc} */ + @Override protected NumberFormat getNumberFormat() { + Locale _locale = locale == null ? Locale.getDefault() : locale; + + if (numberFormat != null) { + return numberFormat; + } else if (pattern != null) { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(_locale); + return new DecimalFormat(pattern, symbols); + } else { + return NumberFormat.getCurrencyInstance(_locale); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateStringConverter.java new file mode 100644 index 00000000..499f90cb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateStringConverter.java @@ -0,0 +1,62 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class DateStringConverter extends DateTimeStringConverter { + + public DateStringConverter() { + this(null, null, null, DateFormat.DEFAULT); + } + + public DateStringConverter(int dateStyle) { + this(null, null, null, dateStyle); + } + + public DateStringConverter(Locale locale) { + this(locale, null, null, DateFormat.DEFAULT); + } + + public DateStringConverter(Locale locale, int dateStyle) { + this(locale, null, null, dateStyle); + } + + public DateStringConverter(String pattern) { + this(null, pattern, null, DateFormat.DEFAULT); + } + + public DateStringConverter(Locale locale, String pattern) { + this(locale, pattern, null, DateFormat.DEFAULT); + } + + public DateStringConverter(DateFormat dateFormat) { + this(null, null, dateFormat, DateFormat.DEFAULT); + } + + private DateStringConverter(Locale locale, String pattern, DateFormat dateFormat, int dateStyle) { + super(locale, pattern, dateFormat, dateStyle, DateFormat.DEFAULT); + } + + + // --------------------------------------------------------- Private Methods + + /** {@inheritDoc} */ + @SuppressWarnings("removal") + @Override protected DateFormat getDateFormat() { + DateFormat df = null; + + if (dateFormat != null) { + return dateFormat; + } else if (pattern != null) { + df = new SimpleDateFormat(pattern, locale); + } else { + df = DateFormat.getDateInstance(dateStyle, locale); + } + + df.setLenient(false); + + return df; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateTimeStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateTimeStringConverter.java new file mode 100644 index 00000000..41941571 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DateTimeStringConverter.java @@ -0,0 +1,191 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + *

{@link StringConverter} implementation for {@link Date} values that + * represent a date and time.

+ * + * @see DateStringConverter + * @see TimeStringConverter + * @since JavaFX 2.1 + */ +public class DateTimeStringConverter extends StringConverter { + + // ------------------------------------------------------ Private properties + + final Locale locale; + + final String pattern; + + final DateFormat dateFormat; + + final int dateStyle; + + final int timeStyle; + + + // ------------------------------------------------------------ Constructors + + /** + * Create a {@link StringConverter} for {@link Date} values, using + * {@link DateFormat#DEFAULT} styles for date and time. + */ + public DateTimeStringConverter() { + this(null, null, null, DateFormat.DEFAULT, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using specified + * {@link DateFormat} styles for date and time. + * + * @param dateStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "M/d/yy" in the US locale. + * @param timeStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "h:mm a" in the US locale. + * + * @since JavaFX 8u40 + */ + public DateTimeStringConverter(int dateStyle, int timeStyle) { + this(null, null, null, dateStyle, timeStyle); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified locale and {@link DateFormat#DEFAULT} styles for date and time. + * + * @param locale the given locale. + */ + public DateTimeStringConverter(Locale locale) { + this(locale, null, null, DateFormat.DEFAULT, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using specified + * locale and {@link DateFormat} styles for date and time. + * + * @param locale the given locale. + * @param dateStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "M/d/yy" in the US locale. + * @param timeStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "h:mm a" in the US locale. + * + * @since JavaFX 8u40 + */ + public DateTimeStringConverter(Locale locale, int dateStyle, int timeStyle) { + this(locale, null, null, dateStyle, timeStyle); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified pattern. + * + * @param pattern the pattern describing the date and time format. + */ + public DateTimeStringConverter(String pattern) { + this(null, pattern, null, DateFormat.DEFAULT, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified locale and pattern. + * + * @param locale the given locale. + * @param pattern the pattern describing the date and time format. + */ + public DateTimeStringConverter(Locale locale, String pattern) { + this(locale, pattern, null, DateFormat.DEFAULT, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified {@link DateFormat} formatter. + * + * @param dateFormat the {@link DateFormat} to be used for formatting and + * parsing. + */ + public DateTimeStringConverter(DateFormat dateFormat) { + this(null, null, dateFormat, DateFormat.DEFAULT, DateFormat.DEFAULT); + } + + DateTimeStringConverter(Locale locale, String pattern, DateFormat dateFormat, + int dateStyle, int timeStyle) { + this.locale = (locale != null) ? locale : Locale.getDefault(Locale.Category.FORMAT); + this.pattern = pattern; + this.dateFormat = dateFormat; + this.dateStyle = dateStyle; + this.timeStyle = timeStyle; + } + + + // ------------------------------------------------------- Converter Methods + + /** {@inheritDoc} */ + @Override public Date fromString(String value) { + try { + // If the specified value is null or zero-length, return null + if (value == null) { + return (null); + } + + value = value.trim(); + + if (value.length() < 1) { + return (null); + } + + // Create and configure the parser to be used + DateFormat parser = getDateFormat(); + + // Perform the requested parsing + return parser.parse(value); + } catch (ParseException ex) { + throw new RuntimeException(ex); + } + } + + /** {@inheritDoc} */ + @Override public String toString(Date value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + // Create and configure the formatter to be used + DateFormat formatter = getDateFormat(); + + // Perform the requested formatting + return formatter.format(value); + } + + // --------------------------------------------------------- Private Methods + + /** + *

Return a DateFormat instance to use for formatting + * and parsing in this {@link StringConverter}.

+ * + * @return a {@code DateFormat} instance for formatting and parsing in this + * {@link StringConverter} + */ + DateFormat getDateFormat() { + DateFormat df = null; + + if (dateFormat != null) { + return dateFormat; + } else if (pattern != null) { + df = new SimpleDateFormat(pattern, locale); + } else { + df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale); + } + + df.setLenient(false); + + return df; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DefaultStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DefaultStringConverter.java new file mode 100644 index 00000000..569c1ea7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DefaultStringConverter.java @@ -0,0 +1,26 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link String} values.

+ * @since JavaFX 2.1 + */ +public class DefaultStringConverter extends StringConverter { + + /** + * Creates a default {@code DefaultStringConverter}. + */ + public DefaultStringConverter() { + } + + /** {@inheritDoc} */ + @Override public String toString(String value) { + return (value != null) ? value : ""; + } + + /** {@inheritDoc} */ + @Override public String fromString(String value) { + return value; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DoubleStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DoubleStringConverter.java new file mode 100644 index 00000000..2a9e1f13 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/DoubleStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Double} + * (and double primitive) values.

+ * @since JavaFX 2.1 + */ +public class DoubleStringConverter extends StringConverter { + + /** + * Creates a default {@code DoubleStringConverter}. + */ + public DoubleStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Double fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Double.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Double value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return Double.toString(value.doubleValue()); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FloatStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FloatStringConverter.java new file mode 100644 index 00000000..c284cb93 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FloatStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Float} + * (and float primitive) values.

+ * @since JavaFX 2.1 + */ +public class FloatStringConverter extends StringConverter { + + /** + * Creates a default {@code FloatStringConverter}. + */ + public FloatStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Float fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Float.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Float value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return Float.toString(((Float)value).floatValue()); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FormatStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FormatStringConverter.java new file mode 100644 index 00000000..a9ac7b0f --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/FormatStringConverter.java @@ -0,0 +1,81 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.beans.NamedArg; +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.text.*; + +/** + *

{@link StringConverter} implementation that can use a {@link Format} + * instance.

+ * + * @since JavaFX 2.2 + */ +public class FormatStringConverter extends StringConverter { + + // ------------------------------------------------------ Private properties + + final Format format; + + // ------------------------------------------------------------ Constructors + + /** + * Creates a {@code FormatStringConverter} for the given {@code Format} instance. + * @param format the {@code Format} instance + */ + public FormatStringConverter(@NamedArg("format") Format format) { + this.format = format; + } + + // ------------------------------------------------------- Converter Methods + + /** {@inheritDoc} */ + @Override public T fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + // Create and configure the parser to be used + Format _format = getFormat(); + + // Perform the requested parsing, and attempt to conver the output + // back to T + final ParsePosition pos = new ParsePosition(0); + T result = (T) _format.parseObject(value, pos); + if (pos.getIndex() != value.length()) { + throw new RuntimeException("Parsed string not according to the format"); + } + return result; + } + + /** {@inheritDoc} */ + @Override public String toString(T value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + // Create and configure the formatter to be used + Format _format = getFormat(); + + // Perform the requested formatting + return _format.format(value); + } + + /** + *

Return a Format instance to use for formatting + * and parsing in this {@link StringConverter}.

+ * + * @return a {@code Format} instance for formatting and parsing in this {@link StringConverter} + */ + protected Format getFormat() { + return format; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/IntegerStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/IntegerStringConverter.java new file mode 100644 index 00000000..235ee088 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/IntegerStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Integer} + * (and int primitive) values.

+ * @since JavaFX 2.1 + */ +public class IntegerStringConverter extends StringConverter { + + /** + * Creates a default {@code IntegerStringConverter}. + */ + public IntegerStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Integer fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Integer.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Integer value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return (Integer.toString(((Integer)value).intValue())); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateStringConverter.java new file mode 100644 index 00000000..3d048284 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateStringConverter.java @@ -0,0 +1,116 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.time.LocalDate; +import java.time.chrono.Chronology; +import java.time.chrono.IsoChronology; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +/** + *

{@link StringConverter} implementation for {@link LocalDate} values.

+ * + * @see LocalTimeStringConverter + * @see LocalDateTimeStringConverter + * @since JavaFX 8u40 + */ +public class LocalDateStringConverter extends StringConverter { + + LocalDateTimeStringConverter.LdtConverter ldtConverter; + + // ------------------------------------------------------------ Constructors + + /** + * Create a {@link StringConverter} for {@link LocalDate} values, using a + * default formatter and parser based on {@link IsoChronology}, + * {@link FormatStyle#SHORT}, and the user's {@link Locale}. + * + *

This converter ensures symmetry between the toString() and + * fromString() methods. Many of the default locale based patterns used by + * {@link DateTimeFormatter} will display only two digits for the year when + * formatting to a string. This would cause a value like 1955 to be + * displayed as 55, which in turn would be parsed back as 2055. This + * converter modifies two-digit year patterns to always use four digits. The + * input parsing is not affected, so two digit year values can still be + * parsed leniently as expected in these locales.

+ */ + public LocalDateStringConverter() { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalDate.class, null, null, + null, null, null, null); + } + + /** + * Create a {@link StringConverter} for {@link LocalDate} values, using a + * default formatter and parser based on {@link IsoChronology}, + * the specified {@link FormatStyle}, and the user's {@link Locale}. + * + * @param dateStyle The {@link FormatStyle} that will be used by the default + * formatter and parser. If null then {@link FormatStyle#SHORT} will be used. + */ + public LocalDateStringConverter(FormatStyle dateStyle) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalDate.class, null, null, + dateStyle, null, null, null); + } + + /** + * Create a {#link StringConverter} for {@link LocalDate} values using the supplied + * formatter and parser. + * + *

For example, to use a fixed pattern for converting both ways:

+ *
+     * String pattern = "yyyy-MM-dd";
+     * DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+     * StringConverter<LocalDate> converter =
+     *     DateTimeStringConverter.getLocalDateStringConverter(formatter, null);
+     * 
+ * + * Note that the formatter and parser can be created to handle non-default + * {@link Locale} and {@link Chronology} as needed. + * + * @param formatter An instance of {@link DateTimeFormatter} that will be + * used for formatting by the toString() method. If null then a default + * formatter will be used. + * @param parser An instance of {@link DateTimeFormatter} that will be used + * for parsing by the fromString() method. This can be identical to + * formatter. If null then formatter will be used, and if that is also null, + * then a default parser will be used. + */ + public LocalDateStringConverter(DateTimeFormatter formatter, DateTimeFormatter parser) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalDate.class, formatter, parser, + null, null, null, null); + } + + + /** + * Create a StringConverter for {@link LocalDate} values using a default + * formatter and parser, which will be based on the supplied + * {@link FormatStyle}, {@link Locale}, and {@link Chronology}. + * + * @param dateStyle The {@link FormatStyle} that will be used by the default + * formatter and parser. If null then {@link FormatStyle#SHORT} will be used. + * @param locale The {@link Locale} that will be used by the default + * formatter and parser. If null then + * {@code Locale.getDefault(Locale.Category.FORMAT)} will be used. + * @param chronology The {@link Chronology} that will be used by the default + * formatter and parser. If null then {@link IsoChronology#INSTANCE} will be used. + */ + public LocalDateStringConverter(FormatStyle dateStyle, Locale locale, Chronology chronology) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalDate.class, null, null, + dateStyle, null, locale, chronology); + } + + // ------------------------------------------------------- Converter Methods + + /** {@inheritDoc} */ + @Override public LocalDate fromString(String value) { + return ldtConverter.fromString(value); + } + + /** {@inheritDoc} */ + @Override public String toString(LocalDate value) { + return ldtConverter.toString(value); + } + +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateTimeStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateTimeStringConverter.java new file mode 100644 index 00000000..0968f964 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalDateTimeStringConverter.java @@ -0,0 +1,259 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.time.chrono.Chronology; +import java.time.chrono.IsoChronology; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DecimalStyle; +import java.time.format.FormatStyle; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +/** + *

{@link StringConverter} implementation for {@link LocalDateTime} values.

+ * + * @see LocalDateStringConverter + * @see LocalTimeStringConverter + * @since JavaFX 8u40 + */ +public class LocalDateTimeStringConverter extends StringConverter { + + LdtConverter ldtConverter; + + + + // ------------------------------------------------------------ Constructors + + /** + * Create a {@link StringConverter} for {@link LocalDateTime} values, using a + * default formatter and parser based on {@link IsoChronology}, + * {@link FormatStyle#SHORT} for both date and time, and the user's + * {@link Locale}. + * + *

This converter ensures symmetry between the toString() and + * fromString() methods. Many of the default locale based patterns used by + * {@link DateTimeFormatter} will display only two digits for the year when + * formatting to a string. This would cause a value like 1955 to be + * displayed as 55, which in turn would be parsed back as 2055. This + * converter modifies two-digit year patterns to always use four digits. The + * input parsing is not affected, so two digit year values can still be + * parsed as expected in these locales.

+ */ + public LocalDateTimeStringConverter() { + ldtConverter = new LdtConverter(LocalDateTime.class, null, null, + null, null, null, null); + } + + /** + * Create a {@link StringConverter} for {@link LocalDateTime} values, using + * a default formatter and parser based on {@link IsoChronology}, the + * specified {@link FormatStyle} values for date and time, and the user's + * {@link Locale}. + * + * @param dateStyle The {@link FormatStyle} that will be used by the default + * formatter and parser for the date. If null then {@link FormatStyle#SHORT} + * will be used. + * @param timeStyle The {@link FormatStyle} that will be used by the default + * formatter and parser for the time. If null then {@link FormatStyle#SHORT} + * will be used. + */ + public LocalDateTimeStringConverter(FormatStyle dateStyle, FormatStyle timeStyle) { + ldtConverter = new LdtConverter(LocalDateTime.class, null, null, + dateStyle, timeStyle, null, null); + } + + /** + * Create a {@link StringConverter} for {@link LocalDateTime} values using + * the supplied formatter and parser. + * + *

For example, to use a fixed pattern for converting both ways:

+ *
+     * String pattern = "yyyy-MM-dd HH:mm";
+     * DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+     * StringConverter<LocalDateTime> converter =
+     *     DateTimeStringConverter.getLocalDateTimeConverter(formatter, null);
+     * 
+ * + * Note that the formatter and parser can be created to handle non-default + * {@link Locale} and {@link Chronology} as needed. + * + * @param formatter An instance of {@link DateTimeFormatter} which will be + * used for formatting by the toString() method. If null then a default + * formatter will be used. + * @param parser An instance of {@link DateTimeFormatter} which will be used + * for parsing by the fromString() method. This can be identical to + * formatter. If null then formatter will be used, and if that is also null, + * then a default parser will be used. + */ + public LocalDateTimeStringConverter(DateTimeFormatter formatter, DateTimeFormatter parser) { + ldtConverter = new LdtConverter(LocalDateTime.class, formatter, parser, + null, null, null, null); + } + + /** + * Create a {@link StringConverter} for {@link LocalDateTime} values using a + * default formatter and parser, which will be based on the supplied + * {@link FormatStyle}s, {@link Locale}, and {@link Chronology}. + * + * @param dateStyle The {@link FormatStyle} that will be used by the default + * formatter and parser for the date. If null then {@link FormatStyle#SHORT} + * will be used. + * @param timeStyle The {@link FormatStyle} that will be used by the default + * formatter and parser for the time. If null then {@link FormatStyle#SHORT} + * will be used. + * @param locale The {@link Locale} that will be used by the + * default formatter and parser. If null then + * {@code Locale.getDefault(Locale.Category.FORMAT)} will be used. + * @param chronology The {@link Chronology} that will be used by the default + * formatter and parser. If null then {@link IsoChronology#INSTANCE} will be + * used. + */ + public LocalDateTimeStringConverter(FormatStyle dateStyle, FormatStyle timeStyle, + Locale locale, Chronology chronology) { + ldtConverter = new LdtConverter(LocalDateTime.class, null, null, + dateStyle, timeStyle, locale, chronology); + } + + + + // ------------------------------------------------------- Converter Methods + + /** {@inheritDoc} */ + @Override public LocalDateTime fromString(String value) { + return ldtConverter.fromString(value); + } + + /** {@inheritDoc} */ + @Override public String toString(LocalDateTime value) { + return ldtConverter.toString(value); + } + + + + static class LdtConverter extends StringConverter { + private Class type; + Locale locale; + Chronology chronology; + DateTimeFormatter formatter; + DateTimeFormatter parser; + FormatStyle dateStyle; + FormatStyle timeStyle; + + LdtConverter(Class type, DateTimeFormatter formatter, DateTimeFormatter parser, + FormatStyle dateStyle, FormatStyle timeStyle, Locale locale, Chronology chronology) { + this.type = type; + this.formatter = formatter; + this.parser = (parser != null) ? parser : formatter; + this.locale = (locale != null) ? locale : Locale.getDefault(Locale.Category.FORMAT); + this.chronology = (chronology != null) ? chronology : IsoChronology.INSTANCE; + + if (type == LocalDate.class || type == LocalDateTime.class) { + this.dateStyle = (dateStyle != null) ? dateStyle : FormatStyle.SHORT; + } + + if (type == LocalTime.class || type == LocalDateTime.class) { + this.timeStyle = (timeStyle != null) ? timeStyle : FormatStyle.SHORT; + } + } + + /** {@inheritDoc} */ + @SuppressWarnings({"unchecked"}) + @Override public T fromString(String text) { + if (text == null || text.isEmpty()) { + return null; + } + + text = text.trim(); + + if (parser == null) { + parser = getDefaultParser(); + } + + TemporalAccessor temporal = parser.parse(text); + + if (type == LocalDate.class) { + return (T) LocalDate.from(temporal); + } else if (type == LocalTime.class) { + return (T) LocalTime.from(temporal); + } else { + return (T) LocalDateTime.from(temporal); + } + } + + + /** {@inheritDoc} */ + @Override public String toString(T value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + if (formatter == null) { + formatter = getDefaultFormatter(); + } + + return formatter.format(value); + } + + + private DateTimeFormatter getDefaultParser() { + String pattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern(dateStyle, timeStyle, + chronology, locale); + return new DateTimeFormatterBuilder().parseLenient() + .appendPattern(pattern) + .toFormatter() + .withChronology(chronology) + .withDecimalStyle(DecimalStyle.of(locale)); + } + + /** + *

Return a default DateTimeFormatter instance to use for formatting + * and parsing in this {@link StringConverter}.

+ */ + private DateTimeFormatter getDefaultFormatter() { + DateTimeFormatter formatter; + + if (dateStyle != null && timeStyle != null) { + formatter = DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); + } else if (dateStyle != null) { + formatter = DateTimeFormatter.ofLocalizedDate(dateStyle); + } else { + formatter = DateTimeFormatter.ofLocalizedTime(timeStyle); + } + + formatter = formatter.withLocale(locale) + .withChronology(chronology) + .withDecimalStyle(DecimalStyle.of(locale)); + + if (dateStyle != null) { + formatter = fixFourDigitYear(formatter, dateStyle, timeStyle, + chronology, locale); + } + + return formatter; + } + + private DateTimeFormatter fixFourDigitYear(DateTimeFormatter formatter, + FormatStyle dateStyle, FormatStyle timeStyle, + Chronology chronology, Locale locale) { + String pattern = + DateTimeFormatterBuilder.getLocalizedDateTimePattern(dateStyle, timeStyle, + chronology, locale); + if (pattern.contains("yy") && !pattern.contains("yyy")) { + // Modify pattern to show four-digit year, including leading zeros. + String newPattern = pattern.replace("yy", "yyyy"); + formatter = DateTimeFormatter.ofPattern(newPattern) + .withDecimalStyle(DecimalStyle.of(locale)); + } + + return formatter; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalTimeStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalTimeStringConverter.java new file mode 100644 index 00000000..1cf0532c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LocalTimeStringConverter.java @@ -0,0 +1,100 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; + +/** + *

{@link StringConverter} implementation for {@link LocalTime} values.

+ * + * @see LocalDateStringConverter + * @see LocalDateTimeStringConverter + * @since JavaFX 8u40 + */ +public class LocalTimeStringConverter extends StringConverter { + + LocalDateTimeStringConverter.LdtConverter ldtConverter; + + // ------------------------------------------------------------ Constructors + + /** + * Create a {@link StringConverter} for {@link LocalTime} values, using a + * default formatter and parser with {@link FormatStyle#SHORT}, and the + * user's {@link Locale}. + */ + public LocalTimeStringConverter() { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalTime.class, null, null, + null, null, null, null); + } + + /** + * Create a {@link StringConverter} for {@link LocalTime} values, using a + * default formatter and parser with the specified {@link FormatStyle} and + * based on the user's {@link Locale}. + * + * @param timeStyle The {@link FormatStyle} that will be used by the default + * formatter and parser. If null then {@link FormatStyle#SHORT} will be used. + */ + public LocalTimeStringConverter(FormatStyle timeStyle) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalTime.class, null, null, + null, timeStyle, null, null); + } + + /** + * Create a StringConverter for {@link LocalTime} values, using a + * default formatter and parser with the specified {@link FormatStyle} + * and {@link Locale}. + * + * @param timeStyle The {@link FormatStyle} that will be used by the default + * formatter and parser. If null then {@link FormatStyle#SHORT} will be used. + * @param locale The {@link Locale} that will be used by the default + * formatter and parser. If null then + * {@code Locale.getDefault(Locale.Category.FORMAT)} will be used. + */ + public LocalTimeStringConverter(FormatStyle timeStyle, Locale locale) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalTime.class, null, null, + null, timeStyle, locale, null); + } + + /** + * Create a StringConverter for {@link LocalTime} values using the + * supplied formatter and parser, which are responsible for + * choosing the desired {@link Locale}. + * + *

For example, a fixed pattern can be used for converting both ways:

+ *
+     * String pattern = "HH:mm:ss";
+     * DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+     * StringConverter<LocalTime> converter =
+     *     DateTimeStringConverter.getLocalTimeConverter(formatter, null);
+     * 
+ * + * @param formatter An instance of {@link DateTimeFormatter} which + * will be used for formatting by the toString() method. If null + * then a default formatter will be used. + * @param parser An instance of {@link DateTimeFormatter} which + * will be used for parsing by the fromString() method. This can + * be identical to formatter. If null, then formatter will be + * used, and if that is also null, then a default parser will be + * used. + */ + public LocalTimeStringConverter(DateTimeFormatter formatter, DateTimeFormatter parser) { + ldtConverter = new LocalDateTimeStringConverter.LdtConverter(LocalTime.class, formatter, parser, + null, null, null, null); + } + + // ------------------------------------------------------- Converter Methods + + /** {@inheritDoc} */ + @Override public LocalTime fromString(String value) { + return ldtConverter.fromString(value); + } + + /** {@inheritDoc} */ + @Override public String toString(LocalTime value) { + return ldtConverter.toString(value); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LongStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LongStringConverter.java new file mode 100644 index 00000000..46f817ed --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/LongStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Long} + * (and long primitive) values.

+ * @since JavaFX 2.1 + */ +public class LongStringConverter extends StringConverter { + + /** + * Creates a default {@code LongStringConverter}. + */ + public LongStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Long fromString(String value) { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + return Long.valueOf(value); + } + + /** {@inheritDoc} */ + @Override public String toString(Long value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + return Long.toString(((Long)value).longValue()); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/NumberStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/NumberStringConverter.java new file mode 100644 index 00000000..87f6a81c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/NumberStringConverter.java @@ -0,0 +1,133 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + +/** + * A {@link StringConverter} implementation for {@link Number} values. Instances of this class are immutable. + * + * @since JavaFX 2.1 + */ +public class NumberStringConverter extends StringConverter { + + final Locale locale; + final String pattern; + final NumberFormat numberFormat; + + /** + * Constructs a {@code NumberStringConverter} with the default locale and format. + */ + public NumberStringConverter() { + this(Locale.getDefault()); + } + + /** + * Constructs a {@code NumberStringConverter} with the given locale and the default format. + * + * @param locale the locale used in determining the number format used to format the string + */ + public NumberStringConverter(Locale locale) { + this(locale, null); + } + + /** + * Constructs a {@code NumberStringConverter} with the default locale and the given decimal format pattern. + * + * @param pattern the string pattern used in determining the number format used to format the string + * + * @see DecimalFormat + */ + public NumberStringConverter(String pattern) { + this(Locale.getDefault(), pattern); + } + + /** + * Constructs a {@code NumberStringConverter} with the given locale and decimal format pattern. + * + * @param locale the locale used in determining the number format used to format the string + * @param pattern the string pattern used in determining the number format used to format the string + * + * @see DecimalFormat + */ + public NumberStringConverter(Locale locale, String pattern) { + this(locale, pattern, null); + } + + /** + * Constructs a {@code NumberStringConverter} with the given number format. + * + * @param numberFormat the number format used to format the string + */ + public NumberStringConverter(NumberFormat numberFormat) { + this(null, null, numberFormat); + } + + NumberStringConverter(Locale locale, String pattern, NumberFormat numberFormat) { + this.locale = locale; + this.pattern = pattern; + this.numberFormat = numberFormat; + } + + /** {@inheritDoc} */ + @Override public Number fromString(String value) { + try { + // If the specified value is null or zero-length, return null + if (value == null) { + return null; + } + + value = value.trim(); + + if (value.length() < 1) { + return null; + } + + // Create and configure the parser to be used + NumberFormat parser = getNumberFormat(); + + // Perform the requested parsing + return parser.parse(value); + } catch (ParseException ex) { + throw new RuntimeException(ex); + } + } + + /** {@inheritDoc} */ + @Override public String toString(Number value) { + // If the specified value is null, return a zero-length String + if (value == null) { + return ""; + } + + // Create and configure the formatter to be used + NumberFormat formatter = getNumberFormat(); + + // Perform the requested formatting + return formatter.format(value); + } + + /** + * Returns a {@code NumberFormat} instance to use for formatting + * and parsing in this {@code StringConverter}. + * + * @return a {@code NumberFormat} instance for formatting and parsing in this + * {@code StringConverter} + */ + protected NumberFormat getNumberFormat() { + Locale _locale = locale == null ? Locale.getDefault() : locale; + + if (numberFormat != null) { + return numberFormat; + } else if (pattern != null) { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(_locale); + return new DecimalFormat(pattern, symbols); + } else { + return NumberFormat.getNumberInstance(_locale); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/PercentageStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/PercentageStringConverter.java new file mode 100644 index 00000000..f823e031 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/PercentageStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import java.text.NumberFormat; +import java.util.Locale; + +public class PercentageStringConverter extends NumberStringConverter { + + /** + * Constructs a {@code PercentageStringConverter} with the default locale and format. + */ + public PercentageStringConverter() { + this(Locale.getDefault()); + } + + /** + * Constructs a {@code PercentageStringConverter} with the given locale and the default format. + * + * @param locale the locale used in determining the number format used to format the string + */ + public PercentageStringConverter(Locale locale) { + super(locale, null, null); + } + + /** + * Constructs a {@code PercentageStringConverter} with the given number format. + * + * @param numberFormat the number format used to format the string + */ + public PercentageStringConverter(NumberFormat numberFormat) { + super(null, null, numberFormat); + } + + /** {@inheritDoc} */ + @Override public NumberFormat getNumberFormat() { + Locale _locale = locale == null ? Locale.getDefault() : locale; + + if (numberFormat != null) { + return numberFormat; + } else { + return NumberFormat.getPercentInstance(_locale); + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ShortStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ShortStringConverter.java new file mode 100644 index 00000000..c4eed7e6 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/ShortStringConverter.java @@ -0,0 +1,43 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Short} values.

+ * @since JavaFX 2.1 + */ +public class ShortStringConverter extends StringConverter { + + /** + * Creates a default {@code ShortStringConverter}. + */ + public ShortStringConverter() { + } + + /** {@inheritDoc} */ + @Override public Short fromString(String text) { + // If the specified value is null or zero-length, return null + if (text == null) { + return null; + } + + text = text.trim(); + + if (text.length() < 1) { + return null; + } + + return Short.valueOf(text); + } + + /** {@inheritDoc} */ + @Override public String toString(Short value) { + // If the specified value is null, return a + // zero-length String + if (value == null) { + return ""; + } + + return Short.toString(((Short)value).shortValue()); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/TimeStringConverter.java b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/TimeStringConverter.java new file mode 100644 index 00000000..007a678d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/fakefx/util/converter/TimeStringConverter.java @@ -0,0 +1,122 @@ +package com.tungsten.fclcore.fakefx.util.converter; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import com.tungsten.fclcore.fakefx.util.StringConverter; + +/** + *

{@link StringConverter} implementation for {@link Date} values that + * represent time.

+ * + * @see DateStringConverter + * @see DateTimeStringConverter + * @since JavaFX 2.1 + */ +public class TimeStringConverter extends DateTimeStringConverter { + + // ------------------------------------------------------------ Constructors + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * {@link DateFormat#DEFAULT} time style. + */ + public TimeStringConverter() { + this(null, null, null, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified {@link DateFormat} time style. + * + * @param timeStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "h:mm a" in the US locale. + * + * @since JavaFX 8u40 + */ + public TimeStringConverter(int timeStyle) { + this(null, null, null, timeStyle); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified locale and the {@link DateFormat#DEFAULT} time style. + * + * @param locale the given locale. + */ + public TimeStringConverter(Locale locale) { + this(locale, null, null, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified locale and {@link DateFormat} time style. + * + * @param locale the given locale. + * @param timeStyle the given formatting style. For example, + * {@link DateFormat#SHORT} for "h:mm a" in the US locale. + * + * @since JavaFX 8u40 + */ + public TimeStringConverter(Locale locale, int timeStyle) { + this(locale, null, null, timeStyle); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified pattern. + * + * @param pattern the pattern describing the time format. + */ + public TimeStringConverter(String pattern) { + this(null, pattern, null, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified locale and pattern. + * + * @param locale the given locale. + * @param pattern the pattern describing the time format. + */ + public TimeStringConverter(Locale locale, String pattern) { + this(locale, pattern, null, DateFormat.DEFAULT); + } + + /** + * Create a {@link StringConverter} for {@link Date} values, using the + * specified {@link DateFormat} formatter. + * + * @param dateFormat the {@link DateFormat} to be used for formatting and + * parsing. + */ + public TimeStringConverter(DateFormat dateFormat) { + this(null, null, dateFormat, DateFormat.DEFAULT); + } + + private TimeStringConverter(Locale locale, String pattern, DateFormat dateFormat, int timeStyle) { + super(locale, pattern, dateFormat, DateFormat.DEFAULT, timeStyle); + } + + + // --------------------------------------------------------- Private Methods + + /** {@inheritDoc} */ + @SuppressWarnings("removal") + @Override protected DateFormat getDateFormat() { + DateFormat df = null; + + if (dateFormat != null) { + return dateFormat; + } else if (pattern != null) { + df = new SimpleDateFormat(pattern, locale); + } else { + df = DateFormat.getTimeInstance(timeStyle, locale); + } + + df.setLenient(false); + + return df; + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/game/GameDirectoryType.java b/FCLCore/src/main/java/com/tungsten/fclcore/game/GameDirectoryType.java index c4703627..cae31468 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/game/GameDirectoryType.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/game/GameDirectoryType.java @@ -11,9 +11,5 @@ public enum GameDirectoryType { /** * .minecraft/versions/<version name> */ - VERSION_FOLDER, - /** - * user customized directory. - */ - CUSTOM + VERSION_FOLDER } diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/game/JavaVersion.java b/FCLCore/src/main/java/com/tungsten/fclcore/game/JavaVersion.java index ffce0de0..645c3814 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/game/JavaVersion.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/game/JavaVersion.java @@ -1,19 +1,35 @@ package com.tungsten.fclcore.game; +import com.tungsten.fclauncher.FCLPath; + public class JavaVersion { - public static final int JAVA_8 = 8; + public static final int JAVA_VERSION_8 = 8; - public static final int JAVA_17 = 17; + public static final int JAVA_VERSION_17 = 17; + private final int id; + private final boolean auto; private final int version; private final String versionName; - private final int architecture; - public JavaVersion(int version, String versionName, int architecture) { + public static final JavaVersion JAVA_AUTO = new JavaVersion(0, true, JAVA_VERSION_8, "Auto"); + public static final JavaVersion JAVA_8 = new JavaVersion(1, false, JAVA_VERSION_8, "1.8"); + public static final JavaVersion JAVA_17 = new JavaVersion(2, false, JAVA_VERSION_17, "17"); + + public JavaVersion(int id, boolean auto, int version, String versionName) { + this.id = id; + this.auto = auto; this.version = version; this.versionName = versionName; - this.architecture = architecture; + } + + public int getId() { + return id; + } + + public boolean isAuto() { + return auto; } public int getVersion() { @@ -24,11 +40,24 @@ public class JavaVersion { return versionName; } - public int getArchitecture() { - return architecture; + public String getJavaPath(Version version) { + JavaVersion javaVersion = auto ? getSuitableJavaVersion(version) : this; + return javaVersion.getVersion() == JAVA_VERSION_8 ? FCLPath.JAVA_8_PATH : FCLPath.JAVA_17_PATH; } - public static int getSuitableJavaVersion(Version version) { + public static JavaVersion getJavaFromId(int id) { + if (id == 0) { + return JAVA_AUTO; + } + else if (id == 1) { + return JAVA_8; + } + else { + return JAVA_17; + } + } + + public static JavaVersion getSuitableJavaVersion(Version version) { if (version.getJavaVersion() == null || version.getJavaVersion().getMajorVersion() == 8) { return JAVA_8; } @@ -37,8 +66,9 @@ public class JavaVersion { public static boolean checkJavaVersion(Version version, JavaVersion javaVersion) { if (version.getJavaVersion() == null || version.getJavaVersion().getMajorVersion() == 8) { - return javaVersion.getVersion() == JAVA_8; + return javaVersion.getVersion() == JAVA_VERSION_8; } - return javaVersion.getVersion() == JAVA_17; + return javaVersion.getVersion() == JAVA_VERSION_17; } + } diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/mod/Datapack.java b/FCLCore/src/main/java/com/tungsten/fclcore/mod/Datapack.java index 1f7bd43a..8c109ccb 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/mod/Datapack.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/mod/Datapack.java @@ -1,10 +1,12 @@ package com.tungsten.fclcore.mod; import com.google.gson.JsonParseException; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableList; import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.StringUtils; -import com.tungsten.fclcore.fakefx.BooleanProperty; -import com.tungsten.fclcore.fakefx.SimpleBooleanProperty; import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.io.CompressingUtils; import com.tungsten.fclcore.util.io.FileUtils; @@ -18,7 +20,7 @@ import java.util.logging.Level; public class Datapack { private boolean isMultiple; private final Path path; - private final ArrayList info = new ArrayList<>(); + private final ObservableList info = FXCollections.observableArrayList(); public Datapack(Path path) { this.path = path; @@ -28,7 +30,7 @@ public class Datapack { return path; } - public ArrayList getInfo() { + public ObservableList getInfo() { return info; } @@ -52,9 +54,12 @@ public class Datapack { if (isMultiple) { new Unzipper(path, worldPath) .setReplaceExistentFile(true) - .setFilter((destPath, isDirectory, zipEntry, entryPath) -> { - // We will merge resources.zip instead of replacement. - return !entryPath.equals("resources.zip"); + .setFilter(new Unzipper.FileFilter() { + @Override + public boolean accept(Path destPath, boolean isDirectory, Path zipEntry, String entryPath) { + // We will merge resources.zip instead of replacement. + return !entryPath.equals("resources.zip"); + } }) .unzip(); @@ -172,7 +177,7 @@ public class Datapack { } } - this.info.addAll(info); + this.info.setAll(info); } public static class Pack { @@ -235,4 +240,4 @@ public class Datapack { private static final String DISABLED_EXT = "disabled"; -} +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/mod/LocalModFile.java b/FCLCore/src/main/java/com/tungsten/fclcore/mod/LocalModFile.java index 22526fbb..5aca7518 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/mod/LocalModFile.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/mod/LocalModFile.java @@ -1,8 +1,8 @@ package com.tungsten.fclcore.mod; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty; import com.tungsten.fclcore.util.Logging; -import com.tungsten.fclcore.fakefx.BooleanProperty; -import com.tungsten.fclcore.fakefx.SimpleBooleanProperty; import com.tungsten.fclcore.util.io.FileUtils; import java.io.IOException; diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/task/Schedulers.java b/FCLCore/src/main/java/com/tungsten/fclcore/task/Schedulers.java index fa0f976d..cba3fb5a 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/task/Schedulers.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/task/Schedulers.java @@ -3,6 +3,8 @@ package com.tungsten.fclcore.task; import static com.tungsten.fclcore.util.Lang.threadPool; import android.app.Activity; +import android.os.Handler; +import android.os.Looper; import com.tungsten.fclcore.util.Logging; @@ -36,8 +38,9 @@ public final class Schedulers { return IO_EXECUTOR; } - public static Executor androidUIThread(Activity activity) { - return activity::runOnUiThread; + public static Executor androidUIThread() { + Handler handler = new Handler(Looper.getMainLooper()); + return handler::post; } public static Executor defaultScheduler() { diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/InvocationDispatcher.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/InvocationDispatcher.java new file mode 100644 index 00000000..3762a483 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/InvocationDispatcher.java @@ -0,0 +1,33 @@ +package com.tungsten.fclcore.util; + +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class InvocationDispatcher implements Consumer { + + public static InvocationDispatcher runOn(Executor executor, Consumer action) { + return new InvocationDispatcher<>(arg -> executor.execute(() -> { + synchronized (action) { + action.accept(arg.get()); + } + })); + } + + private Consumer> handler; + + private AtomicReference> pendingArg = new AtomicReference<>(); + + public InvocationDispatcher(Consumer> handler) { + this.handler = handler; + } + + @Override + public void accept(ARG arg) { + if (pendingArg.getAndSet(Optional.ofNullable(arg)) == null) { + handler.accept(() -> pendingArg.getAndSet(null).orElse(null)); + } + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/BindingMapping.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/BindingMapping.java index c87a7e78..87a7cf82 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/BindingMapping.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/BindingMapping.java @@ -9,10 +9,10 @@ import java.util.function.Supplier; import static java.util.Objects.requireNonNull; -import com.tungsten.fclcore.fakefx.Bindings; -import com.tungsten.fclcore.fakefx.ObjectBinding; -import com.tungsten.fclcore.fakefx.Observable; -import com.tungsten.fclcore.fakefx.ObservableValue; +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.fakefx.beans.value.ObservableValue; public abstract class BindingMapping extends ObjectBinding { @@ -199,4 +199,4 @@ public abstract class BindingMapping extends ObjectBinding { return value; } } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableCache.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableCache.java index 4b75ce45..e8c45d55 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableCache.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableCache.java @@ -1,7 +1,7 @@ package com.tungsten.fclcore.util.fakefx; -import com.tungsten.fclcore.fakefx.Bindings; -import com.tungsten.fclcore.fakefx.ObjectBinding; +import com.tungsten.fclcore.fakefx.beans.binding.Bindings; +import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; import com.tungsten.fclcore.util.function.ExceptionalFunction; import java.util.HashMap; diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableHelper.java index c71765e1..03bb04ae 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableHelper.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableHelper.java @@ -1,7 +1,7 @@ package com.tungsten.fclcore.util.fakefx; -import com.tungsten.fclcore.fakefx.InvalidationListener; -import com.tungsten.fclcore.fakefx.Observable; +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -51,4 +51,4 @@ public class ObservableHelper implements Observable, InvalidationListener { observable.removeListener(this); // remove the previously added listener(if any) observable.addListener(this); } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableOptionalCache.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableOptionalCache.java index 2e922125..4d23e6ae 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableOptionalCache.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/ObservableOptionalCache.java @@ -1,6 +1,6 @@ package com.tungsten.fclcore.util.fakefx; -import com.tungsten.fclcore.fakefx.ObjectBinding; +import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; import com.tungsten.fclcore.util.function.ExceptionalFunction; import java.util.Optional; @@ -42,4 +42,4 @@ public class ObservableOptionalCache { public void invalidate(K key) { backed.invalidate(key); } -} \ No newline at end of file +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/PropertyUtils.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/PropertyUtils.java new file mode 100644 index 00000000..a20ad973 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/fakefx/PropertyUtils.java @@ -0,0 +1,150 @@ +package com.tungsten.fclcore.util.fakefx; + +import com.tungsten.fclcore.fakefx.beans.InvalidationListener; +import com.tungsten.fclcore.fakefx.beans.Observable; +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.fakefx.beans.value.WritableValue; +import com.tungsten.fclcore.fakefx.collections.ObservableList; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +public final class PropertyUtils { + private PropertyUtils() { + } + + public static class PropertyHandle { + public final WritableValue accessor; + public final Observable observable; + + public PropertyHandle(WritableValue accessor, Observable observable) { + this.accessor = accessor; + this.observable = observable; + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static Map> getPropertyHandleFactories(Class type) { + Map collectionGetMethods = new LinkedHashMap<>(); + Map propertyMethods = new LinkedHashMap<>(); + for (Method method : type.getMethods()) { + Class returnType = method.getReturnType(); + if (method.getParameterCount() == 0 + && !returnType.equals(void.class)) { + String name = method.getName(); + if (name.endsWith("Property")) { + String propertyName = name.substring(0, name.length() - "Property".length()); + if (!propertyName.isEmpty() && Property.class.isAssignableFrom(returnType)) { + propertyMethods.put(propertyName, method); + } + } else if (name.startsWith("get")) { + String propertyName = name.substring("get".length()); + if (!propertyName.isEmpty() && + (ObservableList.class.isAssignableFrom(returnType) + || ObservableSet.class.isAssignableFrom(returnType) + || ObservableMap.class.isAssignableFrom(returnType))) { + propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1); + collectionGetMethods.put(propertyName, method); + } + } + } + } + propertyMethods.keySet().forEach(collectionGetMethods::remove); + + Map> result = new LinkedHashMap<>(); + propertyMethods.forEach((propertyName, method) -> { + result.put(propertyName, instance -> { + Property returnValue; + try { + returnValue = (Property) method.invoke(instance); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + return new PropertyHandle(returnValue, returnValue); + }); + }); + + collectionGetMethods.forEach((propertyName, method) -> { + result.put(propertyName, instance -> { + Object returnValue; + try { + returnValue = method.invoke(instance); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + WritableValue accessor; + if (returnValue instanceof ObservableList) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ((ObservableList) returnValue).setAll((List) value); + } + }; + } else if (returnValue instanceof ObservableSet) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ObservableSet target = (ObservableSet) returnValue; + target.clear(); + target.addAll((Set) value); + } + }; + } else if (returnValue instanceof ObservableMap) { + accessor = new WritableValue() { + @Override + public Object getValue() { + return returnValue; + } + + @Override + public void setValue(Object value) { + ObservableMap target = (ObservableMap) returnValue; + target.clear(); + target.putAll((Map) value); + } + }; + } else { + throw new IllegalStateException(); + } + return new PropertyHandle(accessor, (Observable) returnValue); + }); + }); + return result; + } + + public static void copyProperties(Object from, Object to) { + Class type = from.getClass(); + while (!type.isInstance(to)) + type = type.getSuperclass(); + + getPropertyHandleFactories(type) + .forEach((name, factory) -> { + PropertyHandle src = factory.apply(from); + PropertyHandle target = factory.apply(to); + target.accessor.setValue(src.accessor.getValue()); + }); + } + + public static void attachListener(Object instance, InvalidationListener listener) { + getPropertyHandleFactories(instance.getClass()) + .forEach((name, factory) -> { + factory.apply(instance).observable.addListener(listener); + }); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGson.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGson.java new file mode 100644 index 00000000..98dcc723 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGson.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.util.gson.fakefx; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.jetbrains.annotations.NotNull; + +public final class FxGson { + + private FxGson() throws InstantiationException { + throw new InstantiationException("Instances of this type are forbidden."); + } + + @NotNull + public static Gson create() { + return new FxGsonBuilder().create(); + } + + @NotNull + public static GsonBuilder coreBuilder() { + return new FxGsonBuilder().builder(); + } + + @NotNull + public static GsonBuilder fullBuilder() { + return new FxGsonBuilder().builder(); + } + + @NotNull + public static GsonBuilder addFxSupport(@NotNull GsonBuilder builder) { + return new FxGsonBuilder(builder).builder(); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGsonBuilder.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGsonBuilder.java new file mode 100644 index 00000000..9aa22887 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/FxGsonBuilder.java @@ -0,0 +1,55 @@ +package com.tungsten.fclcore.util.gson.fakefx; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +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.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; + +public class FxGsonBuilder { + + private final GsonBuilder builder; + + private boolean strictProperties = true; + + private boolean strictPrimitives = true; + + private boolean includeExtras = false; + + public FxGsonBuilder() { + this(new GsonBuilder()); + } + + public FxGsonBuilder(GsonBuilder sourceBuilder) { + this.builder = sourceBuilder; + } + + public GsonBuilder builder() { + // serialization of nulls is necessary to have properties with null values deserialized properly + builder.serializeNulls() + .registerTypeAdapter(ObservableList.class, new ObservableListCreator()) + .registerTypeAdapter(ObservableSet.class, new ObservableSetCreator()) + .registerTypeAdapter(ObservableMap.class, new ObservableMapCreator()) + .registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(strictProperties, strictPrimitives)); + return builder; + } + + public Gson create() { + return builder().create(); + } + + public FxGsonBuilder acceptNullProperties() { + strictProperties = false; + return this; + } + + public FxGsonBuilder acceptNullPrimitives() { + strictPrimitives = false; + return this; + } + +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableListCreator.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableListCreator.java new file mode 100644 index 00000000..02966bff --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableListCreator.java @@ -0,0 +1,15 @@ +package com.tungsten.fclcore.util.gson.fakefx.creators; + +import com.google.gson.InstanceCreator; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import java.lang.reflect.Type; + +public class ObservableListCreator implements InstanceCreator> { + + public ObservableList createInstance(Type type) { + // No need to use a parametrized list since the actual instance will have the raw type anyway. + return FXCollections.observableArrayList(); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableMapCreator.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableMapCreator.java new file mode 100644 index 00000000..ec874339 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableMapCreator.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.util.gson.fakefx.creators; + +import java.lang.reflect.Type; + +import com.google.gson.InstanceCreator; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +/** + * An {@link InstanceCreator} for observable maps using {@link FXCollections}. + */ +public class ObservableMapCreator implements InstanceCreator> { + + public ObservableMap createInstance(Type type) { + // No need to use a parametrized map since the actual instance will have the raw type anyway. + return FXCollections.observableHashMap(); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableSetCreator.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableSetCreator.java new file mode 100644 index 00000000..a6b6e09c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/creators/ObservableSetCreator.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.util.gson.fakefx.creators; + +import java.lang.reflect.Type; + +import com.google.gson.InstanceCreator; +import com.tungsten.fclcore.fakefx.collections.FXCollections; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +/** + * An {@link InstanceCreator} for observable sets using {@link FXCollections}. + */ +public class ObservableSetCreator implements InstanceCreator> { + + public ObservableSet createInstance(Type type) { + // No need to use a parametrized set since the actual instance will have the raw type anyway. + return FXCollections.observableSet(); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/JavaFxPropertyTypeAdapterFactory.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/JavaFxPropertyTypeAdapterFactory.java new file mode 100644 index 00000000..4e97f224 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/JavaFxPropertyTypeAdapterFactory.java @@ -0,0 +1,114 @@ +package com.tungsten.fclcore.util.gson.fakefx.factories; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty; +import com.tungsten.fclcore.fakefx.beans.property.FloatProperty; +import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty; +import com.tungsten.fclcore.fakefx.beans.property.ListProperty; +import com.tungsten.fclcore.fakefx.beans.property.LongProperty; +import com.tungsten.fclcore.fakefx.beans.property.MapProperty; +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.fakefx.beans.property.SetProperty; +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.gson.fakefx.properties.ListPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.MapPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.ObjectPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.SetPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.StringPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.primitives.BooleanPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.primitives.DoublePropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.primitives.FloatPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.primitives.IntegerPropertyTypeAdapter; +import com.tungsten.fclcore.util.gson.fakefx.properties.primitives.LongPropertyTypeAdapter; + +public class JavaFxPropertyTypeAdapterFactory implements TypeAdapterFactory { + + private final boolean strictProperties; + + private final boolean strictPrimitives; + + /** + * Creates a new JavaFxPropertyTypeAdapterFactory. This default factory forbids null properties and null values for + * primitive properties. + * + * @see #JavaFxPropertyTypeAdapterFactory(boolean, boolean) + */ + public JavaFxPropertyTypeAdapterFactory() { + this(true, true); + } + + public JavaFxPropertyTypeAdapterFactory(boolean throwOnNullProperties, boolean throwOnNullPrimitives) { + this.strictProperties = throwOnNullProperties; + this.strictPrimitives = throwOnNullPrimitives; + } + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class clazz = type.getRawType(); + + // this factory only handles JavaFX property types + if (!Property.class.isAssignableFrom(clazz)) { + return null; + } + + // simple property types + + if (BooleanProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new BooleanPropertyTypeAdapter(gson.getAdapter(boolean.class), strictProperties, + strictPrimitives); + } + if (IntegerProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new IntegerPropertyTypeAdapter(gson.getAdapter(int.class), strictProperties, + strictPrimitives); + } + if (LongProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new LongPropertyTypeAdapter(gson.getAdapter(long.class), strictProperties, + strictPrimitives); + } + if (FloatProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new FloatPropertyTypeAdapter(gson.getAdapter(float.class), strictProperties, + strictPrimitives); + } + if (DoubleProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new DoublePropertyTypeAdapter(gson.getAdapter(double.class), strictProperties, + strictPrimitives); + } + if (StringProperty.class.isAssignableFrom(clazz)) { + return (TypeAdapter) new StringPropertyTypeAdapter(gson.getAdapter(String.class), strictProperties); + } + + // collection property types + + if (ListProperty.class.isAssignableFrom(clazz)) { + TypeAdapter delegate = gson.getAdapter(TypeHelper.withRawType(type, ObservableList.class)); + return new ListPropertyTypeAdapter(delegate, strictProperties); + } + if (SetProperty.class.isAssignableFrom(clazz)) { + TypeAdapter delegate = gson.getAdapter(TypeHelper.withRawType(type, ObservableSet.class)); + return new SetPropertyTypeAdapter(delegate, strictProperties); + } + if (MapProperty.class.isAssignableFrom(clazz)) { + TypeAdapter delegate = gson.getAdapter(TypeHelper.withRawType(type, ObservableMap.class)); + return new MapPropertyTypeAdapter(delegate, strictProperties); + } + + // generic Property type + + Type[] typeParams = ((ParameterizedType) type.getType()).getActualTypeArguments(); + Type param = typeParams[0]; + // null factory skipPast because the nested type argument might also be a Property + TypeAdapter delegate = gson.getAdapter(TypeToken.get(param)); + return (TypeAdapter) new ObjectPropertyTypeAdapter<>(delegate, strictProperties); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/TypeHelper.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/TypeHelper.java new file mode 100644 index 00000000..5a52b4b8 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/factories/TypeHelper.java @@ -0,0 +1,70 @@ +package com.tungsten.fclcore.util.gson.fakefx.factories; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import com.google.gson.reflect.TypeToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A helper to handle {@link Type} and {@link TypeToken} objects creation. + */ +final class TypeHelper { + + private TypeHelper() throws InstantiationException { + throw new InstantiationException("Instances of this type are forbidden."); + } + + /** + * Gets a {@link TypeToken} equivalent to the given one, but with the given raw type instead of the original one. + * + * @param sourceTypeToken + * the initial type token to get the type parameters from + * @param newRawType + * the new raw type to use + * @return a new type token with the given raw type and the type parameters of the given type token + */ + @NotNull + static TypeToken withRawType(@NotNull TypeToken sourceTypeToken, @NotNull Type newRawType) { + ParameterizedType sourceType = (ParameterizedType) sourceTypeToken.getType(); + Type[] typeParams = sourceType.getActualTypeArguments(); + Type targetType = newParametrizedType(newRawType, typeParams); + return TypeToken.get(targetType); + } + + @NotNull + private static ParameterizedType newParametrizedType(@NotNull Type rawType, @NotNull Type... typeArguments) { + return new CustomParameterizedType(rawType, null, typeArguments); + } + + private static class CustomParameterizedType implements ParameterizedType { + + private Type rawType; + + private Type ownerType; + + private Type[] typeArguments; + + private CustomParameterizedType(@NotNull Type rawType, @Nullable Type ownerType, @NotNull Type... typeArgs) { + this.rawType = rawType; + this.ownerType = ownerType; + this.typeArguments = typeArgs; + } + + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return ownerType; + } + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ListPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ListPropertyTypeAdapter.java new file mode 100644 index 00000000..ce474c50 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ListPropertyTypeAdapter.java @@ -0,0 +1,25 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.ListProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleListProperty; +import com.tungsten.fclcore.fakefx.collections.ObservableList; + +import org.jetbrains.annotations.NotNull; + +/** + * A basic {@link TypeAdapter} for JavaFX {@link ListProperty}. It serializes the list inside the property instead of + * the property itself. + */ +public class ListPropertyTypeAdapter extends PropertyTypeAdapter, ListProperty> { + + public ListPropertyTypeAdapter(TypeAdapter> delegate, boolean throwOnNullProperty) { + super(delegate, throwOnNullProperty); + } + + @NotNull + @Override + protected ListProperty createProperty(ObservableList deserializedValue) { + return new SimpleListProperty<>(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/MapPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/MapPropertyTypeAdapter.java new file mode 100644 index 00000000..05c3eeeb --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/MapPropertyTypeAdapter.java @@ -0,0 +1,25 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.MapProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleMapProperty; +import com.tungsten.fclcore.fakefx.collections.ObservableMap; + +import org.jetbrains.annotations.NotNull; + +/** + * A basic {@link TypeAdapter} for JavaFX {@link MapProperty}. It serializes the map inside the property instead of the + * property itself. + */ +public class MapPropertyTypeAdapter extends PropertyTypeAdapter, MapProperty> { + + public MapPropertyTypeAdapter(TypeAdapter> delegate, boolean throwOnNullProperty) { + super(delegate, throwOnNullProperty); + } + + @NotNull + @Override + protected MapProperty createProperty(ObservableMap deserializedValue) { + return new SimpleMapProperty<>(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/NullPropertyException.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/NullPropertyException.java new file mode 100644 index 00000000..e1f37e78 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/NullPropertyException.java @@ -0,0 +1,8 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +public class NullPropertyException extends RuntimeException { + + public NullPropertyException() { + super("Null properties are forbidden"); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ObjectPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ObjectPropertyTypeAdapter.java new file mode 100644 index 00000000..cb4ef853 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/ObjectPropertyTypeAdapter.java @@ -0,0 +1,33 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty; + +import org.jetbrains.annotations.NotNull; + +/** + * A basic {@link TypeAdapter} for JavaFX {@link Property}. It serializes the object inside the property instead of the + * property itself. + */ +public class ObjectPropertyTypeAdapter extends PropertyTypeAdapter> { + + /** + * Creates a new ObjectPropertyTypeAdapter. + * + * @param delegate + * a delegate adapter to use for the inner object value of the property + * @param throwOnNullProperty + * if true, this adapter will throw {@link NullPropertyException} when given a null {@link Property} to + * serialize + */ + public ObjectPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty) { + super(delegate, throwOnNullProperty); + } + + @NotNull + @Override + protected Property createProperty(T deserializedValue) { + return new SimpleObjectProperty<>(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/PropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/PropertyTypeAdapter.java new file mode 100644 index 00000000..2e1e8b70 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/PropertyTypeAdapter.java @@ -0,0 +1,69 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import java.io.IOException; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.tungsten.fclcore.fakefx.beans.property.Property; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A base implementation of {@link TypeAdapter} for JavaFX properties. It serializes the value inside the property + * instead of the property itself. + * + * @param + * the inner value type of the property + * @param

+ * the property type that this adapter can serialize/deserialize, containing a value of type I + */ +public abstract class PropertyTypeAdapter> extends TypeAdapter

{ + + private final TypeAdapter delegate; + + private final boolean throwOnNullProperty; + + /** + * Creates a new PropertyTypeAdapter. + * + * @param innerValueTypeAdapter + * a delegate adapter to use for the inner value of the property + * @param throwOnNullProperty + * if true, this adapter will throw {@link NullPropertyException} when given a null {@link Property} to + * serialize + */ + PropertyTypeAdapter(TypeAdapter innerValueTypeAdapter, boolean throwOnNullProperty) { + this.delegate = innerValueTypeAdapter; + this.throwOnNullProperty = throwOnNullProperty; + } + + @Override + public void write(JsonWriter out, P property) throws IOException { + if (property == null) { + if (throwOnNullProperty) { + throw new NullPropertyException(); + } + out.nullValue(); + return; + } + delegate.write(out, property.getValue()); + } + + @Override + public P read(JsonReader in) throws IOException { + return createProperty(delegate.read(in)); + } + + /** + * Creates a new property object with the given initial value. + * + * @param deserializedValue + * the deserialized value for the property to create. May be null. + * + * @return a new property object containing the given value + */ + @NotNull + protected abstract P createProperty(@Nullable I deserializedValue); +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/SetPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/SetPropertyTypeAdapter.java new file mode 100644 index 00000000..156e9190 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/SetPropertyTypeAdapter.java @@ -0,0 +1,25 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.SetProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleSetProperty; +import com.tungsten.fclcore.fakefx.collections.ObservableSet; + +import org.jetbrains.annotations.NotNull; + +/** + * A basic {@link TypeAdapter} for JavaFX {@link SetProperty}. It serializes the set inside the property instead of the + * property itself. + */ +public class SetPropertyTypeAdapter extends PropertyTypeAdapter, SetProperty> { + + public SetPropertyTypeAdapter(TypeAdapter> delegate, boolean throwOnNullProperty) { + super(delegate, throwOnNullProperty); + } + + @NotNull + @Override + protected SetProperty createProperty(ObservableSet deserializedValue) { + return new SimpleSetProperty<>(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/StringPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/StringPropertyTypeAdapter.java new file mode 100644 index 00000000..f1688391 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/StringPropertyTypeAdapter.java @@ -0,0 +1,24 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty; +import com.tungsten.fclcore.fakefx.beans.property.StringProperty; + +import org.jetbrains.annotations.NotNull; + +/** + * A basic {@link TypeAdapter} for JavaFX {@link StringProperty}. It serializes the string inside the property instead + * of the property itself. + */ +public class StringPropertyTypeAdapter extends PropertyTypeAdapter { + + public StringPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty) { + super(delegate, throwOnNullProperty); + } + + @NotNull + @Override + protected StringProperty createProperty(String deserializedValue) { + return new SimpleStringProperty(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/BooleanPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/BooleanPropertyTypeAdapter.java new file mode 100644 index 00000000..62ce8bb2 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/BooleanPropertyTypeAdapter.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty; + +/** + * An implementation of {@link PrimitivePropertyTypeAdapter} for JavaFX {@link BooleanProperty}. It serializes the + * boolean value of the property instead of the property itself. + */ +public class BooleanPropertyTypeAdapter extends PrimitivePropertyTypeAdapter { + + public BooleanPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty, + boolean crashOnNullValue) { + super(delegate, throwOnNullProperty, crashOnNullValue); + } + + @Override + protected Boolean extractPrimitiveValue(BooleanProperty property) { + return property.get(); + } + + @Override + protected BooleanProperty createDefaultProperty() { + return new SimpleBooleanProperty(); + } + + @Override + protected BooleanProperty wrapNonNullPrimitiveValue(Boolean deserializedValue) { + return new SimpleBooleanProperty(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/DoublePropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/DoublePropertyTypeAdapter.java new file mode 100644 index 00000000..ff4d138d --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/DoublePropertyTypeAdapter.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleDoubleProperty; + +/** + * An implementation of {@link PrimitivePropertyTypeAdapter} for JavaFX {@link DoubleProperty}. It serializes the double + * value of the property instead of the property itself. + */ +public class DoublePropertyTypeAdapter extends PrimitivePropertyTypeAdapter { + + public DoublePropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty, + boolean crashOnNullValue) { + super(delegate, throwOnNullProperty, crashOnNullValue); + } + + @Override + protected Double extractPrimitiveValue(DoubleProperty property) { + return property.get(); + } + + @Override + protected DoubleProperty createDefaultProperty() { + return new SimpleDoubleProperty(); + } + + @Override + protected DoubleProperty wrapNonNullPrimitiveValue(Double deserializedValue) { + return new SimpleDoubleProperty(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/FloatPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/FloatPropertyTypeAdapter.java new file mode 100644 index 00000000..ede474a2 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/FloatPropertyTypeAdapter.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.FloatProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleFloatProperty; + +/** + * An implementation of {@link PrimitivePropertyTypeAdapter} for JavaFX {@link FloatProperty}. It serializes the float + * value of the property instead of the property itself. + */ +public class FloatPropertyTypeAdapter extends PrimitivePropertyTypeAdapter { + + public FloatPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty, + boolean crashOnNullValue) { + super(delegate, throwOnNullProperty, crashOnNullValue); + } + + @Override + protected Float extractPrimitiveValue(FloatProperty property) { + return property.get(); + } + + @Override + protected FloatProperty createDefaultProperty() { + return new SimpleFloatProperty(); + } + + @Override + protected FloatProperty wrapNonNullPrimitiveValue(Float deserializedValue) { + return new SimpleFloatProperty(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/IntegerPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/IntegerPropertyTypeAdapter.java new file mode 100644 index 00000000..9c431c0c --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/IntegerPropertyTypeAdapter.java @@ -0,0 +1,32 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty; + +/** + * An implementation of {@link PrimitivePropertyTypeAdapter} for JavaFX {@link IntegerProperty}. It serializes the int + * value of the property instead of the property itself. + */ +public class IntegerPropertyTypeAdapter extends PrimitivePropertyTypeAdapter { + + public IntegerPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty, + boolean crashOnNullValue) { + super(delegate, throwOnNullProperty, crashOnNullValue); + } + + @Override + protected Integer extractPrimitiveValue(IntegerProperty property) { + return property.getValue(); + } + + @Override + protected IntegerProperty createDefaultProperty() { + return new SimpleIntegerProperty(); + } + + @Override + protected IntegerProperty wrapNonNullPrimitiveValue(Integer deserializedValue) { + return new SimpleIntegerProperty(deserializedValue); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/LongPropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/LongPropertyTypeAdapter.java new file mode 100644 index 00000000..00896bc3 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/LongPropertyTypeAdapter.java @@ -0,0 +1,31 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import com.google.gson.TypeAdapter; +import com.tungsten.fclcore.fakefx.beans.property.LongProperty; +import com.tungsten.fclcore.fakefx.beans.property.SimpleLongProperty; + +/** + * An implementation of {@link PrimitivePropertyTypeAdapter} for JavaFX {@link LongProperty}. It serializes the long + * value of the property instead of the property itself. + */ +public class LongPropertyTypeAdapter extends PrimitivePropertyTypeAdapter { + + public LongPropertyTypeAdapter(TypeAdapter delegate, boolean throwOnNullProperty, boolean crashOnNullValue) { + super(delegate, throwOnNullProperty, crashOnNullValue); + } + + @Override + protected Long extractPrimitiveValue(LongProperty property) { + return property.get(); + } + + @Override + protected LongProperty createDefaultProperty() { + return new SimpleLongProperty(); + } + + @Override + protected LongProperty wrapNonNullPrimitiveValue(Long deserializedValue) { + return new SimpleLongProperty(deserializedValue); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/NullPrimitiveException.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/NullPrimitiveException.java new file mode 100644 index 00000000..7145c102 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/NullPrimitiveException.java @@ -0,0 +1,8 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +public class NullPrimitiveException extends RuntimeException { + + public NullPrimitiveException(String pathInJson) { + super("Illegal null value for a primitive type at path " + pathInJson); + } +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/PrimitivePropertyTypeAdapter.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/PrimitivePropertyTypeAdapter.java new file mode 100644 index 00000000..5575f5bd --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/gson/fakefx/properties/primitives/PrimitivePropertyTypeAdapter.java @@ -0,0 +1,103 @@ +package com.tungsten.fclcore.util.gson.fakefx.properties.primitives; + +import java.io.IOException; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.tungsten.fclcore.fakefx.beans.property.Property; +import com.tungsten.fclcore.util.gson.fakefx.properties.NullPropertyException; + +/** + * An abstract base for {@link TypeAdapter}s of primitive values. By default, it throws {@link NullPrimitiveException} + * when used to deserialize a null JSON value (which is illegal for a primitive). It can be configured to instantiate a + * default value instead in this case. + * + * @param + * the primitive type inside the property + * @param

+ * the type that this adapter can serialize/deserialize + */ +public abstract class PrimitivePropertyTypeAdapter> extends TypeAdapter

{ + + private final TypeAdapter delegate; + + private final boolean throwOnNullProperty; + + private final boolean crashOnNullValue; + + /** + * Creates a new PrimitivePropertyTypeAdapter. + * + * @param innerValueTypeAdapter + * a delegate adapter to use for the inner value of the property + * @param throwOnNullProperty + * if true, this adapter will throw {@link NullPropertyException} when given a null {@link Property} to + * serialize + * @param crashOnNullValue + * if true, this adapter will throw {@link NullPrimitiveException} when reading a null value. If false, this + * adapter will call {@link #createDefaultProperty()} instead. + */ + public PrimitivePropertyTypeAdapter(TypeAdapter innerValueTypeAdapter, boolean throwOnNullProperty, + boolean crashOnNullValue) { + this.delegate = innerValueTypeAdapter; + this.throwOnNullProperty = throwOnNullProperty; + this.crashOnNullValue = crashOnNullValue; + } + + @Override + public void write(JsonWriter out, P property) throws IOException { + if (property == null) { + if (throwOnNullProperty) { + throw new NullPropertyException(); + } + out.nullValue(); + return; + } + delegate.write(out, extractPrimitiveValue(property)); + } + + @Override + public P read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + if (crashOnNullValue) { + throw new NullPrimitiveException(in.getPath()); + } else { + return createDefaultProperty(); + } + } else { + return wrapNonNullPrimitiveValue(delegate.read(in)); + } + } + + /** + * Gets the current value of the given property. + * + * @param property + * the property to get the value for + * + * @return the current value of the given property + */ + protected abstract I extractPrimitiveValue(P property); + + /** + * Creates a default property object. This is used when this adapter deserializes a null value from the input JSON, + * but only if this adapter is set not to crash (see + * {@link #PrimitivePropertyTypeAdapter(TypeAdapter, boolean, boolean)}). + * + * @return a default value to use when null is found + */ + protected abstract P createDefaultProperty(); + + /** + * Wraps the deserialized primitive value in a Property object of the right type. + * + * @param deserializedValue + * the deserialized inner primitive value of the property, may not be null + * + * @return a new property object containing the given value + */ + protected abstract P wrapNonNullPrimitiveValue(I deserializedValue); +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/MemoryUtils.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/MemoryUtils.java new file mode 100644 index 00000000..b7fee551 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/MemoryUtils.java @@ -0,0 +1,35 @@ +package com.tungsten.fclcore.util.platform; + +import android.app.ActivityManager; +import android.content.Context; + +public class MemoryUtils { + + public static int getTotalDeviceMemory(Context context) { + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memInfo); + return (int) (memInfo.totalMem / 1048576L); + } + + public static int getFreeDeviceMemory(Context context) { + ActivityManager actManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + actManager.getMemoryInfo(memInfo); + return (int) (memInfo.availMem / 1048576L); + } + + public static int findBestRAMAllocation(Context context) { + int totalDeviceMemory = getTotalDeviceMemory(context); + if (totalDeviceMemory < 1024) { + return 512; + } else if (totalDeviceMemory < 2048) { + return 1024; + } else if (totalDeviceMemory < 4096) { + return 2048; + } else { + return 4096; + } + } + +} \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/OperatingSystem.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/OperatingSystem.java index e65a734d..d9cc893b 100644 --- a/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/OperatingSystem.java +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/platform/OperatingSystem.java @@ -59,4 +59,17 @@ public enum OperatingSystem { NATIVE_CHARSET = nativeCharset; } + public static boolean isNameValid(String name) { + // empty filename is not allowed + if (name.isEmpty()) + return false; + // . and .. have special meaning on all platforms + if (name.equals(".")) + return false; + // \0 and / are forbidden on all platforms + if (name.indexOf('/') != -1 || name.indexOf('\0') != -1) + return false; + + return true; + } } \ No newline at end of file diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/InvalidSkinException.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/InvalidSkinException.java new file mode 100644 index 00000000..3e1045e7 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/InvalidSkinException.java @@ -0,0 +1,18 @@ +package com.tungsten.fclcore.util.skin; + +public class InvalidSkinException extends Exception { + + public InvalidSkinException() {} + + public InvalidSkinException(String message) { + super(message); + } + + public InvalidSkinException(Throwable cause) { + super(cause); + } + + public InvalidSkinException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/NormalizedSkin.java b/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/NormalizedSkin.java new file mode 100644 index 00000000..d9a0dd41 --- /dev/null +++ b/FCLCore/src/main/java/com/tungsten/fclcore/util/skin/NormalizedSkin.java @@ -0,0 +1,133 @@ +package com.tungsten.fclcore.util.skin; + +import android.graphics.Bitmap; + +/** + * Describes a Minecraft 1.8+ skin (64x64). + * Old format skins are converted to the new format. + */ +public class NormalizedSkin { + + private static void copyImage(Bitmap src, Bitmap dst, int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int pixel = src.getPixel(sx + x, sy + y); + dst.setPixel(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel); + } + } + } + + private final Bitmap texture; + private final Bitmap normalizedTexture; + private final int scale; + private final boolean oldFormat; + + public NormalizedSkin(Bitmap texture) throws InvalidSkinException { + this.texture = texture; + + // check format + int w = texture.getWidth(); + int h = texture.getHeight(); + if (w % 64 != 0) { + throw new InvalidSkinException("Invalid size " + w + "x" + h); + } + if (w == h) { + oldFormat = false; + } else if (w == h * 2) { + oldFormat = true; + } else { + throw new InvalidSkinException("Invalid size " + w + "x" + h); + } + + // compute scale + scale = w / 64; + + normalizedTexture = Bitmap.createBitmap(w, w, Bitmap.Config.ARGB_8888); + copyImage(texture, normalizedTexture, 0, 0, 0, 0, w, h, false); + if (oldFormat) { + convertOldSkin(); + } + } + + private void convertOldSkin() { + copyImageRelative(4, 16, 20, 48, 4, 4, true); // Top Leg + copyImageRelative(8, 16, 24, 48, 4, 4, true); // Bottom Leg + copyImageRelative(0, 20, 24, 52, 4, 12, true); // Outer Leg + copyImageRelative(4, 20, 20, 52, 4, 12, true); // Front Leg + copyImageRelative(8, 20, 16, 52, 4, 12, true); // Inner Leg + copyImageRelative(12, 20, 28, 52, 4, 12, true); // Back Leg + copyImageRelative(44, 16, 36, 48, 4, 4, true); // Top Arm + copyImageRelative(48, 16, 40, 48, 4, 4, true); // Bottom Arm + copyImageRelative(40, 20, 40, 52, 4, 12, true); // Outer Arm + copyImageRelative(44, 20, 36, 52, 4, 12, true); // Front Arm + copyImageRelative(48, 20, 32, 52, 4, 12, true); // Inner Arm + copyImageRelative(52, 20, 44, 52, 4, 12, true); // Back Arm + } + + private void copyImageRelative(int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) { + copyImage(normalizedTexture, normalizedTexture, sx * scale, sy * scale, dx * scale, dy * scale, w * scale, h * scale, flipHorizontal); + } + + public Bitmap getOriginalTexture() { + return texture; + } + + public Bitmap getNormalizedTexture() { + return normalizedTexture; + } + + public int getScale() { + return scale; + } + + public boolean isOldFormat() { + return oldFormat; + } + + /** + * Tests whether the skin is slim. + * Note that this method doesn't guarantee the result is correct. + */ + public boolean isSlim() { + return (hasTransparencyRelative(50, 16, 2, 4) || + hasTransparencyRelative(54, 20, 2, 12) || + hasTransparencyRelative(42, 48, 2, 4) || + hasTransparencyRelative(46, 52, 2, 12)) || + (isAreaBlackRelative(50, 16, 2, 4) && + isAreaBlackRelative(54, 20, 2, 12) && + isAreaBlackRelative(42, 48, 2, 4) && + isAreaBlackRelative(46, 52, 2, 12)); + } + + private boolean hasTransparencyRelative(int x0, int y0, int w, int h) { + x0 *= scale; + y0 *= scale; + w *= scale; + h *= scale; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int pixel = normalizedTexture.getPixel(x0 + x, y0 + y); + if (pixel >>> 24 != 0xff) { + return true; + } + } + } + return false; + } + + private boolean isAreaBlackRelative(int x0, int y0, int w, int h) { + x0 *= scale; + y0 *= scale; + w *= scale; + h *= scale; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int pixel = normalizedTexture.getPixel(x0 + x, y0 + y); + if (pixel != 0xff000000) { + return false; + } + } + } + return true; + } +} diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java index 5e153dd9..4a692160 100644 --- a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java +++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java @@ -24,10 +24,6 @@ public class Theme { private BitmapDrawable backgroundLt; private BitmapDrawable backgroundDk; - public Theme() { - this(Color.parseColor("#9EFF4A"), false, null, null); - } - public Theme(int color, boolean fullscreen, BitmapDrawable backgroundLt, BitmapDrawable backgroundDk) { float[] ltHsv = new float[3]; Color.colorToHSV(color, ltHsv); @@ -109,7 +105,7 @@ public class Theme { public static Theme getTheme(Context context) { SharedPreferences sharedPreferences; sharedPreferences = context.getSharedPreferences("theme", MODE_PRIVATE); - int color = sharedPreferences.getInt("theme_color", Color.parseColor("#9EFF4A")); + int color = sharedPreferences.getInt("theme_color", Color.parseColor("#7797CF")); boolean fullscreen = sharedPreferences.getBoolean("fullscreen", false); Bitmap lt = ConvertUtils.stringToBitmap(sharedPreferences.getString("background_light", null)) == null ? ConvertUtils.getBitmapFromRes(context, R.drawable.background_light) : ConvertUtils.stringToBitmap(sharedPreferences.getString("background_light", null)); BitmapDrawable backgroundLt = new BitmapDrawable(lt); diff --git a/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java b/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java index 7b2120db..8c64e6bc 100644 --- a/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java +++ b/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java @@ -1,6 +1,7 @@ package com.tungsten.fclauncher; import android.content.Context; +import android.os.Environment; public class FCLPath { @@ -19,6 +20,13 @@ public class FCLPath { public static String CACIOCAVALLO_8_DIR; public static String CACIOCAVALLO_17_DIR; + public static String FILES_DIR; + + public static String AUTHLIB_INJECTOR_PATH; + + public static String PRIVATE_COMMON_DIR; + public static String SHARED_COMMON_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + "/FCL/.minecraft"; + public static void loadPaths(Context context) { CONTEXT = context; @@ -34,6 +42,11 @@ public class FCLPath { LWJGL3_DIR = RUNTIME_DIR + "/lwjgl3"; CACIOCAVALLO_8_DIR = RUNTIME_DIR + "/caciocavallo"; CACIOCAVALLO_17_DIR = RUNTIME_DIR + "/caciocavallo17"; + + FILES_DIR = context.getFilesDir().getAbsolutePath(); + AUTHLIB_INJECTOR_PATH = FILES_DIR + "/plugins/authlib-injector.jar"; + + SHARED_COMMON_DIR = context.getExternalFilesDir(".minecraft").getAbsolutePath(); } }