diff --git a/.agent/sdk-live-preview-server-execplan.md b/.agent/sdk-live-preview-server-execplan.md new file mode 100644 index 000000000..b51b7fc97 --- /dev/null +++ b/.agent/sdk-live-preview-server-execplan.md @@ -0,0 +1,155 @@ + + +# Histórico e extração do servidor de Live Preview do SDK + +Este ExecPlan é um documento vivo e deve ser mantido de acordo com .agent/PLANS.md. + +## Purpose / Big Picture + +Este plano registrou a prova inicial de `totalcross.preview.PreviewServer` no SDK. Essa fronteira foi substituída pela extração para `totalcross-tooling/live-preview-server`: o SDK agora contém somente o contrato específico `totalcross.preview.PreviewRuntime` e seu `FrameConsumer` aninhado, enquanto o artefato de tooling fornece HTTP, JSON, classloader e reload. + +## Progress + +- [x] (2026-07-17 00:04Z) Identificados PreviewServer, PreviewRunner, LauncherRuntime, classes totalcross.preview e testes candidatos no checkout. +- [x] (2026-07-17 00:20Z) Removidas as exclusões de LauncherRuntime e totalcross.preview dos source sets main e test; compileJava passou. +- [x] (2026-07-17 00:25Z) Adicionado teste de processo PreviewServer e corrigido o ciclo de vida de shutdown; cinco testes totalcross.preview passaram. +- [x] (2026-07-17 00:25Z) Construído totalcross-sdk-7.2.2.jar, confirmadas classes de preview e provados /health, /frame e /shutdown contra fixture temporária. +- [x] (2026-07-17 00:25Z) Atualizado o relatório editorial com a evidência observada; resta somente commitar os arquivos de escopo. +- [x] (2026-07-17 01:26Z) Extraídos servidor, runner, configuração, classloader e superfícies headless para totalcross-tooling no commit bc751b88a. +- [x] (2026-07-17 01:36Z) Consolidado `PreviewSurface` em `PreviewRuntime.FrameConsumer` (c6c93b57b) e completado o contrato para consumo externo sem `LauncherRuntime` (c7f301696). + +## Surprises & Discoveries + +- Observation: sourceSets.main exclui exatamente as novas classes totalcross/preview/** e totalcross/LauncherRuntime.java. + Evidence: TotalCrossSDK/build.gradle linhas 61 a 69. + +- Observation: PreviewRunner, AppletPreviewSurface, HeadlessPreviewSurface e package.html já possuem mudanças locais preparadas, enquanto a maior parte das classes e testes de preview ainda não está versionada. + Evidence: git diff --cached e git status --short restritos a TotalCrossSDK/src/main/java/totalcross/preview e TotalCrossSDK/src/test/java/totalcross/preview. + +- Observation: HttpServer aceita conexões depois de start(), mas a JVM ainda pode terminar se a thread main retornar antes de existir um executor não daemon atendendo requisições. + Evidence: a primeira prova imprimiu TOTALCROSS_PREVIEW_URL mas curl recebeu connection refused. Após bloquear a main thread com CountDownLatch, /health respondeu 200. + +- Observation: parar HttpServer não encerra por si só as threads AWT iniciadas pelo runtime de preview. + Evidence: a primeira versão do teste PreviewServerTest recebeu 200 em /shutdown, mas o processo não saiu em dez segundos. System.exit(0) depois do latch encerrou o processo e o teste passou. + +## Decision Log + +- Decision: substituir a publicação de PreviewServer no jar principal por um contrato SDK e um artefato companion em totalcross-tooling. + Rationale: o primeiro empacotamento provou o protocolo, mas HTTP, JSON, classloading e reload pertencem à ferramenta de IDE. `PreviewRuntime` mantém no SDK apenas a integração necessária com o launcher. + Date/Author: 2026-07-17 / Codex. + +- Decision: validar o serviço com uma fixture Java mínima compilada no diretório temporário e com curl para /health. + Rationale: isso prova tanto a presença das classes no jar quanto a criação real do servidor HTTP sem depender do VS Code. + Date/Author: 2026-07-17 / Codex. + +- Decision: manter a thread main de PreviewServer bloqueada até /shutdown e chamar System.exit(0) após a confirmação. + Rationale: PreviewServer é uma ferramenta de linha de comando com ciclo de vida próprio. O latch impede saída prematura; o exit elimina threads AWT residuais e torna Stop Preview determinístico para a extensão. + Date/Author: 2026-07-17 / Codex. + +## Outcomes & Retrospective + +O empacotamento inicial de PreviewServer no SDK foi provado e serviu de base para a extração. No estado final, o JAR SDK não contém `PreviewServer`, `PreviewRunner`, configuração, classloader ou superfícies headless; c6c93b57b removeu o contrato paralelo `PreviewSurface`, e c7f301696 deixa o novo servidor depender apenas de `PreviewRuntime`. A prova HTTP e a distribuição agora vivem em `totalcross-tooling/live-preview-server`. + +## Editorial Report + +### Editorial Summary + +Este trabalho transformou classes locais de preview em uma superfície distribuível do SDK. Clientes de IDE podem iniciar um serviço HTTP local a partir de totalcross-sdk.jar, verificar sua saúde, obter uma imagem PNG e encerrá-lo de forma determinística. + +### Original Plan versus Actual Outcome + +A execução incluiu mais que a remoção das exclusões: a primeira prova mostrou que o processo saía antes da requisição, e o primeiro teste revelou que AWT mantinha a JVM viva após shutdown. O resultado final bloqueia até /shutdown e encerra o processo explicitamente. + +### What Changed + +TotalCrossSDK/build.gradle inclui o runtime e o pacote preview nos source sets. totalcross.preview.PreviewServer usa CountDownLatch para viver até /shutdown. PreviewRunner, LauncherRuntime, superfícies, classloader descartável e configuração tornam a prévia recarregável. PreviewServerTest e PreviewMainWindow cobrem o contrato por subprocesso. + +### Decisions and Trade-offs + +O jar principal é escolhido para reduzir configuração e incompatibilidades entre artefatos. Isso aumenta seu conteúdo, mas PreviewRunner já pertence ao SDK e o benefício é uma integração de IDE mais simples. + +### Unexpected Problems and Discoveries + +As exclusões de source set impediam a compilação mesmo com arquivos presentes. Além disso, HttpServer sozinho não mantinha a JVM viva e o shutdown inicial não terminava as threads AWT; ambos foram demonstrados e corrigidos. + +### Validation and Measurable Results + +Observado: ./gradlew-agent compileJava passou em 16 segundos; ./gradlew-agent test --tests 'totalcross.preview.*' passou com cinco testes; ./gradlew-agent jar passou. jar tf build/libs/totalcross-sdk-7.2.2.jar encontrou LauncherRuntime, PreviewRunner, DisposableAppClassLoader, PreviewConfig e PreviewServer. A prova manual iniciou o jar com uma MainWindow de 120x180 e observou /health 200, /frame 200 com PNG de 487 bytes, /shutdown 200 e término do processo. + +### Useful Evidence and Examples + +Evidência: agent-logs/20260716-212021-compileJava-agent.log, agent-logs/20260716-212502-test-agent.log e agent-logs/20260716-212525-jar-agent.log; build/libs/totalcross-sdk-7.2.2.jar; e a fixture temporária /tmp/totalcross-preview-e2e.WsbL6M, que não será versionada. + +### Limitations, Remaining Work, and Open Questions + +A prévia continua local e somente de leitura. Este plano não adiciona input remoto, integração VS Code nem publicação Maven; a extensão consome o jar no ExecPlan separado. + +### Possible Article Angles + +Como transformar um protótipo de preview local em contrato distribuível de SDK para ferramentas de IDE. + +### Suggested Narrative + +Mostrar as exclusões de build, a decisão de empacotar o servidor, a prova HTTP contra uma fixture e o contrato consumido pela extensão. + +### Claims Requiring Human Review + +A escolha de expor PreviewServer no jar principal e a versão em que ele será publicado precisam de revisão de release normal. Nenhuma alegação de compatibilidade além de JDK 17 e da prova local foi verificada. + +## Context and Orientation + +TotalCrossSDK/build.gradle configura o jar totalcross-sdk. O source set main exclui totalcross/preview/** e totalcross/LauncherRuntime.java; isso deixa PreviewServer fora do jar mesmo que os arquivos existam. PreviewServer lê totalcross.preview.json, cria PreviewRunner e expõe /health, /frame, /show, /clear, /reload e /shutdown em loopback. PreviewRunner depende de LauncherRuntime e das superfícies em totalcross.preview. + +Os testes de configuração e classloader estão em TotalCrossSDK/src/test/java/totalcross/preview. A execução segura do Gradle deve usar TotalCrossSDK/gradlew-agent, que grava saída completa em agent-logs e fornece um resumo compacto. + +## Plan of Work + +Alterar TotalCrossSDK/build.gradle para deixar LauncherRuntime e totalcross.preview no source set main. Manter exclusões que não pertencem ao preview. Compilar primeiro as classes Java, corrigindo somente incompatibilidades causadas por promover esse código. Adicionar ou ajustar testes de PreviewConfigLoader, DisposableAppClassLoader e PreviewServer conforme a falha observada. Construir jar, confirmar a presença das classes e iniciar o servidor com uma fixture de MainWindow configurada em diretório temporário. Não incluir recursos, logs, artefatos ou mudanças de SDK não necessárias para esse caminho. + +## Concrete Steps + +1. Em /Users/flsobral/repos/totalcross-github, executar: + + git status --short -- TotalCrossSDK/build.gradle TotalCrossSDK/src/main/java/totalcross/PreviewRunner.java TotalCrossSDK/src/main/java/totalcross/LauncherRuntime.java TotalCrossSDK/src/main/java/totalcross/preview TotalCrossSDK/src/test/java/totalcross/preview + cd TotalCrossSDK + ./gradlew-agent compileJava --warning-mode=none --console=plain + + Esperado: a primeira compilação revela todas as dependências que as exclusões escondiam. Preservar o resumo em agent-logs e registrar erros relevantes. + +2. Remover as duas exclusões de preview do source set main e executar novamente compileJava. Corrigir somente erros necessários e então executar: + + ./gradlew-agent test --tests 'totalcross.preview.*' --warning-mode=none --console=plain + ./gradlew-agent jar --warning-mode=none --console=plain + jar tf build/libs/totalcross-sdk-*.jar | rg 'totalcross/(PreviewRunner|LauncherRuntime)\\.class|totalcross/preview/PreviewServer\\.class' + + Esperado: compileJava, testes focados e jar passam; a listagem encontra as classes. + +3. Criar uma cópia de fixture e um totalcross.preview.json em diretório temporário seguro, iniciar o servidor usando o jar recém-gerado e consultar /health. O comando deve usar classpath explícito para jar do SDK, dependências de runtime e classes compiladas da fixture. Desligar por POST /shutdown e registrar URL/HTTP observados. + +4. Atualizar este plano, executar git diff --check, commitar somente os arquivos de preview e build necessários com mensagens convencionais e corpos que expliquem a distribuição e a prova. + +## Validation and Acceptance + +A aceitação exige três observações: compileJava termina com status zero; jar tf encontra PreviewServer, PreviewRunner e LauncherRuntime; e GET /health retorna JSON com ok:true para um PreviewServer iniciado pelo jar. Os testes Preview focados devem passar. A prova deve usar jar de build, não classes do diretório source. + +## Idempotence and Recovery + +compileJava, test e jar são repetíveis. A fixture fica sob diretório temporário criado por mktemp -d e pode ser removida após a prova. Se promoção das classes quebrar o jar, reverter somente o commit de preview ou restaurar o hunk revisado; nunca limpar ou descartar alterações fora dos caminhos deste plano. + +## Artifacts and Notes + +Registrar o caminho do log agent, os nomes de testes, a versão do jar e a resposta /health. Não commitar build/, dist/, agent-logs ou a fixture temporária. + +## Interfaces and Dependencies + +O artefato totalcross-sdk contém totalcross.preview.PreviewServer, totalcross.PreviewRunner, totalcross.LauncherRuntime e suas dependências de preview. PreviewServer aceita: + + --config --host 127.0.0.1 --port + +Ele deve imprimir TOTALCROSS_PREVIEW_URL=http://127.0.0.1: e fornecer GET /health com JSON que contém ok:true. A extensão VS Code usa esse contrato sem embutir classes Java. + +Revision note (2026-07-17): criado para separar a distribuição do servidor SDK da integração TypeScript, preservando validação e commits independentes. Atualizado após compileJava, testes Preview, inspeção do jar e prova HTTP; documenta os defeitos de término prematuro e shutdown AWT encontrados e corrigidos. diff --git a/TotalCrossSDK/build.gradle b/TotalCrossSDK/build.gradle index 914deca93..7835a9335 100644 --- a/TotalCrossSDK/build.gradle +++ b/TotalCrossSDK/build.gradle @@ -5,6 +5,9 @@ apply plugin: 'java' apply plugin: 'maven-publish' +apply plugin: 'me.champeau.gradle.japicmp' + +apply from: file('gradle/artifact-boundaries.gradle') version = '7.2.2' group = 'com.totalcross' @@ -63,25 +66,25 @@ sourceSets { exclude 'tc/RunSSLSocketTestApplication.java' exclude 'tc/SSLSocketTest.java' exclude 'totalcross/LauncherApplet.java' - exclude 'totalcross/LauncherRuntime.java' - exclude 'totalcross/preview/**' + exclude 'tc/SSLSocketTest.java', 'tc/RunSSLSocketTestApplication.java' } } test { java { exclude 'totalcross/LauncherRuntimeTest.java' exclude 'totalcross/TCEventThreadTest.java' - exclude 'totalcross/preview/**' } } } buildscript { repositories { + gradlePluginPortal() mavenCentral() } dependencies { classpath 'net.sf.proguard:proguard-gradle:5.3.3' + classpath 'me.champeau.gradle.japicmp:me.champeau.gradle.japicmp.gradle.plugin:0.4.6' } } @@ -134,13 +137,36 @@ dependencies { implementation 'com.totalcross.annotations:totalcross-annotations:1.0.0' } +def aggregateCompatibilityBaseline = providers.gradleProperty('aggregateCompatibilityBaseline') +tasks.register('aggregateCompatibilityCheck', me.champeau.gradle.japicmp.JapicmpTask) { + dependsOn tasks.named('jar') + onlyIf { + if (!aggregateCompatibilityBaseline.present) { + logger.lifecycle('Skipping aggregateCompatibilityCheck: set -PaggregateCompatibilityBaseline= to compare releases') + return false + } + true + } + oldClasspath.from(files(aggregateCompatibilityBaseline.map { it })) + newClasspath.from(tasks.named('jar')) + onlyModified = true + onlyBinaryIncompatibleModified = true + failOnModification = providers.gradleProperty('aggregateCompatibilityStrict') + .map { it.toBoolean() }.orElse(true).get() + ignoreMissingClasses = true + txtOutputFile = layout.buildDirectory.file('reports/aggregate-compatibility.txt').get().asFile +} + test { useJUnitPlatform () - // Temporarily disabled because it depends on an external endpoint; it will be fixed and re-enabled later. + // Anonymous telemetry is disabled until its external service is replaced. exclude '**/AnonymousUserDataTest.class' testLogging { exceptionFormat = 'full' } + useJUnitPlatform { + excludeTags 'artifact-boundary' + } } jar { @@ -202,6 +228,26 @@ publishing { from components.java } + mavenApi(MavenPublication) { + artifactId = 'totalcross-api' + artifact tasks.named('totalcrossApiJar') + } + mavenRuntimeJava(MavenPublication) { + artifactId = 'totalcross-runtime-java' + artifact tasks.named('totalcrossRuntimeJavaJar') + } + mavenConverter(MavenPublication) { + artifactId = 'totalcross-converter' + artifact tasks.named('totalcrossConverterJar') + } + mavenDeployer(MavenPublication) { + artifactId = 'totalcross-deployer' + artifact tasks.named('totalcrossDeployerJar') + } + mavenPreviewRuntime(MavenPublication) { + artifactId = 'totalcross-preview-runtime' + artifact tasks.named('totalcrossPreviewRuntimeJar') + } } } @@ -256,7 +302,7 @@ task tcbaseutilJar(type: Jar) { task tcbasemiscJar(type: Jar) { archiveBaseName = "tc.base.misc" from(sourceSets.main.output) { - exclude 'jdkcompat/lang/**', 'totalcross/lang/**', 'jdkcompat/util/**', 'totalcross/util/**', '**/*4A.*', 'totalcross/Launcher*', 'totalcross/TCEventThread*', 'totalcross/TotalCrossApplication.*', 'tc/**', 'totalcross/res/**', 'totalcross/ui/**', 'totalcross/sql/sqlite4j/**', 'totalcross/android/**', 'totalcross/zxing/**', 'tc/tools/InvalidateIPA*' + exclude 'jdkcompat/lang/**', 'totalcross/lang/**', 'jdkcompat/util/**', 'totalcross/util/**', '**/*4A.*', 'totalcross/Launcher*', 'totalcross/PreviewRunner*', 'totalcross/preview/**', 'totalcross/TCEventThread*', 'totalcross/TotalCrossApplication.*', 'tc/**', 'totalcross/res/**', 'totalcross/ui/**', 'totalcross/sql/sqlite4j/**', 'totalcross/android/**', 'totalcross/zxing/**', 'tc/tools/InvalidateIPA*' } } diff --git a/TotalCrossSDK/gradle/artifact-boundaries.gradle b/TotalCrossSDK/gradle/artifact-boundaries.gradle new file mode 100644 index 000000000..457e96113 --- /dev/null +++ b/TotalCrossSDK/gradle/artifact-boundaries.gradle @@ -0,0 +1,52 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// SPDX-License-Identifier: LGPL-2.1-only + +def artifactOutput = layout.buildDirectory.dir('libs') + +def registerBoundaryJar = { String taskName, String archiveName, Closure content -> + tasks.register(taskName, Jar) { + archiveBaseName = archiveName + destinationDirectory = artifactOutput + includeEmptyDirs = false + from(sourceSets.main.output, content) + } +} + +registerBoundaryJar('totalcrossApiJar', 'totalcross-api') { + include 'totalcross/lang/**', 'totalcross/sys/**', 'totalcross/util/**', 'totalcross/ui/**' + include 'jdkcompat/**' + exclude 'totalcross/preview/**', 'totalcross/Launcher*', 'totalcross/PreviewRunner*' +} + +registerBoundaryJar('totalcrossRuntimeJavaJar', 'totalcross-runtime-java') { + include 'totalcross/**', 'jdkcompat/**' + exclude 'totalcross/preview/**', 'tc/**', 'totalcross/LauncherApplet*' +} + +registerBoundaryJar('totalcrossConverterJar', 'totalcross-converter') { + include 'tc/tools/converter/**' +} + +registerBoundaryJar('totalcrossDeployerJar', 'totalcross-deployer') { + include 'tc/Deploy.class', 'tc/Deploy$*.class', 'tc/tools/deployer/**' + include 'tc/tools/JarClassPathLoader.class', 'tc/tools/RegisterSDK.class' + exclude 'tc/tools/converter/**' +} + +registerBoundaryJar('totalcrossPreviewRuntimeJar', 'totalcross-preview-runtime') { + include 'totalcross/preview/**' + include 'totalcross/LauncherRuntime.class', 'totalcross/TotalCrossApplication.class' +} + +tasks.register('artifactContentTest', Test) { + group = 'verification' + description = 'Verifies narrow artifact package boundaries.' + dependsOn totalcrossApiJar, totalcrossRuntimeJavaJar, totalcrossConverterJar, + totalcrossDeployerJar, totalcrossPreviewRuntimeJar + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + systemProperty 'totalcross.artifact.dir', artifactOutput.get().asFile.absolutePath + useJUnitPlatform { + includeTags 'artifact-boundary' + } +} diff --git a/TotalCrossSDK/src/main/java/tc/tools/AnonymousUserData.java b/TotalCrossSDK/src/main/java/tc/tools/AnonymousUserData.java index bbcbf454a..77a11f2ad 100644 --- a/TotalCrossSDK/src/main/java/tc/tools/AnonymousUserData.java +++ b/TotalCrossSDK/src/main/java/tc/tools/AnonymousUserData.java @@ -26,6 +26,8 @@ public class AnonymousUserData { + private static final boolean TELEMETRY_ENABLED = false; + private String BASE_URL = "https://statistics.totalcross.com/api/v1"; private static final String GET_UUID = "/users/get-anonymous-uuid"; private static final String POST_LAUNCHER = "/launch"; @@ -48,7 +50,11 @@ public class AnonymousUserData { private AnonymousUserData() throws IOException { sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); responseRequester = new DefaultResponseRequester(); - loadConfiguration(); + if (TELEMETRY_ENABLED) { + loadConfiguration(); + } else { + config = new JSONObject(); + } } public static AnonymousUserData instance() throws IOException { @@ -84,6 +90,10 @@ public static AnonymousUserData instance() throws IOException { * @throws IOException */ public void loadConfiguration() throws IOException { + if (!TELEMETRY_ENABLED) { + config = new JSONObject(); + return; + } config = null; File configDir = new File(configDirPath); configDir.mkdirs(); @@ -108,6 +118,7 @@ private static JSONObject readJsonObject(Stream stream) throws IOException { } public void launcher(String... args) throws JSONException, IOException { + if (!TELEMETRY_ENABLED) return; if (!GraphicsEnvironment.isHeadless() && config.isNull("userAcceptedToProvideAnonymousData")) { Boolean userAcceptedToContribute = responseRequester.ask(); config.put("userAcceptedToProvideAnonymousData", userAcceptedToContribute); @@ -119,6 +130,7 @@ public void launcher(String... args) throws JSONException, IOException { } public void deploy(String... args) throws JSONException, IOException { + if (!TELEMETRY_ENABLED) return; doPost(BASE_URL + POST_DEPLOY, args); } @@ -144,6 +156,7 @@ private void doGetUUID() throws JSONException, IOException { * @throws JSONException */ public boolean checkUUID(String uuid) throws JSONException, IOException { + if (!TELEMETRY_ENABLED) return true; JSONObject ret = new HttpJsonConnection(BASE_URL + CHECK_UUID + "?uuid=" + uuid).doGet().getResponse(); boolean isValid = (boolean) ret.get("isValid"); if (!isValid) { @@ -250,4 +263,4 @@ public HttpJsonRequest doPost(JSONObject data) throws IOException { return new HttpJsonRequest(); } } -} \ No newline at end of file +} diff --git a/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidPackageFiles.java b/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidPackageFiles.java new file mode 100644 index 000000000..631253cc2 --- /dev/null +++ b/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidPackageFiles.java @@ -0,0 +1,102 @@ +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package tc.tools.deployer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.CRC32; + +import de.schlichtherle.truezip.file.TFile; +import de.schlichtherle.truezip.zip.ZipEntry; +import de.schlichtherle.truezip.zip.ZipOutputStream; +import totalcross.sys.Convert; +import totalcross.util.Hashtable; +import totalcross.util.Vector; + +/** Writes the TotalCross files archive embedded in the Android bundle. */ +final class AndroidPackageFiles { + private AndroidPackageFiles() { + } + + static void write(OutputStream output, String targetTCZ, String tcFolder) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(8192); + Hashtable packages = new Hashtable(13); + Utils.processInstallFile("android.pkg", packages); + Vector locals = (Vector) packages.get("[L]"); + if (locals == null) locals = new Vector(); + Vector globals = (Vector) packages.get("[G]"); + if (globals == null) globals = new Vector(); + locals.addElements(DeploySettings.tczs); + if (globals.size() > 0) locals.addElements(globals.toObjectArray()); + for (File file : DeploySettings.getDefaultTczs()) locals.addElement(file.getAbsolutePath()); + Utils.preprocessPKG(locals, true); + writeLocals(bytes, vector2set(locals, new HashSet()), targetTCZ, tcFolder); + bytes.writeTo(output); + } + + static Set vector2set(Vector vector, Set set) { + for (int i = 0, n = vector.size(); i < n; i++) { + @SuppressWarnings("unchecked") E item = (E) vector.items[i]; + set.add(item); + } + return set; + } + + private static void writeLocals(ByteArrayOutputStream bytes, Set locals, + String targetTCZ, String tcFolder) throws IOException { + ZipOutputStream zip = new ZipOutputStream(bytes); + for (String item : locals) { + String[] parts = Convert.tokenizeString(item, ','); + String pathname = parts[0]; + String name = Utils.getFileName(pathname); + if (parts.length > 1) { + name = Convert.appendPath(parts[1], name); + if (name.startsWith("/")) name = name.substring(1); + } + if (tcFolder != null && pathname.equals(DeploySettings.tczFileName)) name = targetTCZ + ".tcz"; + File file = readable(pathname, Convert.appendPath(DeploySettings.currentDir, pathname), Utils.findPath(pathname, true)); + if (file == null) { + DeployLogger.warn("File not found: " + pathname); + continue; + } + try (FileInputStream input = new FileInputStream(file)) { + ByteArrayOutputStream secondary = new ByteArrayOutputStream(input.available()); + org.apache.commons.io.IOUtils.copy(input, secondary); + byte[] content = secondary.toByteArray(); + ZipEntry entry = new ZipEntry(name); + if (name.endsWith(".tcz")) stored(entry, content); + zip.putNextEntry(entry); + zip.write(content); + zip.closeEntry(); + } + } + zip.close(); + } + + private static void stored(ZipEntry entry, byte[] content) { + CRC32 crc = new CRC32(); + crc.update(content); + entry.setCrc(crc.getValue()); + entry.setMethod(ZipEntry.STORED); + entry.setCompressedSize(content.length); + entry.setSize(content.length); + } + + private static File readable(String... paths) { + for (String path : paths) { + if (path != null) { + File file = new File(path); + if (file.exists() && file.isFile() && file.canRead()) return file; + } + } + return null; + } +} diff --git a/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidToolLocator.java b/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidToolLocator.java new file mode 100644 index 000000000..7350d13c2 --- /dev/null +++ b/TotalCrossSDK/src/main/java/tc/tools/deployer/AndroidToolLocator.java @@ -0,0 +1,91 @@ +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package tc.tools.deployer; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** Locates verified shared Android tools and permits a read-only legacy SDK fallback. */ +final class AndroidToolLocator { + private static final String PROTOC_PROPERTY = "totalcross.tooling.android.protoc"; + private static final String BUNDLETOOL_PROPERTY = "totalcross.tooling.android.bundletool"; + private static boolean legacyWarningShown; + + private AndroidToolLocator() { + } + + static String protoc() { + String configured = System.getProperty(PROTOC_PROPERTY); + if (configured != null && !configured.isBlank()) return verified(Path.of(configured), "21.0").toString(); + String executable = DeploySettings.appendDotExe("protoc"); + Path legacy = Path.of(DeploySettings.etcDir, "tools", "android", "protoc", "bin", executable); + if (Files.isRegularFile(legacy) && probe(List.of(legacy.toString(), "--version"), "21.0")) { + warnLegacy(); + return legacy.toString(); + } + throw new RuntimeException("No verified shared protoc was supplied and the SDK-local fallback is unavailable"); + } + + static String bundletool() { + String configured = System.getProperty(BUNDLETOOL_PROPERTY); + if (configured != null && !configured.isBlank()) { + Path path = Path.of(configured); + if (!Files.isRegularFile(path) + || !probe(List.of(javaExecutable(), "-jar", path.toString(), "version"), null)) { + throw new RuntimeException("External Android bundletool failed its version probe: " + path); + } + return path.toString(); + } + File root = new File(DeploySettings.etcDir, "tools/android"); + File[] candidates = root.listFiles((dir, name) -> name.startsWith("bundletool-all-") && name.endsWith(".jar")); + if (candidates != null) { + for (File candidate : candidates) { + if (probe(List.of(javaExecutable(), "-jar", candidate.getAbsolutePath(), "version"), null)) { + warnLegacy(); + return candidate.getAbsolutePath(); + } + } + } + throw new RuntimeException("No verified shared bundletool was supplied and the SDK-local fallback is unavailable"); + } + + private static Path verified(Path path, String expectedOutput) { + if (!Files.isRegularFile(path) || !probe(List.of(path.toString(), "--version"), expectedOutput)) { + throw new RuntimeException("External Android tool failed its version probe: " + path); + } + return path; + } + + private static void warnLegacy() { + if (!legacyWarningShown) { + legacyWarningShown = true; + DeployLogger.warn("Using Android tools from the SDK; this read-only fallback is deprecated. Install the shared tool store."); + } + } + + private static boolean probe(List command, String expectedOutput) { + try { + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + int exit = process.waitFor(); + return exit == 0 && !output.isBlank() && (expectedOutput == null || output.contains(expectedOutput)); + } catch (IOException e) { + throw new RuntimeException("Could not start Android tool version probe", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during Android tool version probe", e); + } + } + + private static String javaExecutable() { + String name = DeploySettings.appendDotExe("java"); + return Utils.searchIn(DeploySettings.path, name); + } +} diff --git a/TotalCrossSDK/src/main/java/tc/tools/deployer/Deployer4Android.java b/TotalCrossSDK/src/main/java/tc/tools/deployer/Deployer4Android.java index 8d3440688..872b8f58b 100644 --- a/TotalCrossSDK/src/main/java/tc/tools/deployer/Deployer4Android.java +++ b/TotalCrossSDK/src/main/java/tc/tools/deployer/Deployer4Android.java @@ -5,7 +5,6 @@ // SPDX-License-Identifier: LGPL-2.1-only package tc.tools.deployer; -import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -14,37 +13,22 @@ import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Properties; -import java.util.Set; import java.util.zip.Adler32; -import java.util.zip.CRC32; -import java.util.zip.ZipInputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import de.schlichtherle.truezip.file.TFile; import de.schlichtherle.truezip.file.TVFS; -import de.schlichtherle.truezip.zip.ZipEntry; -import de.schlichtherle.truezip.zip.ZipOutputStream; import totalcross.sys.Convert; import totalcross.sys.Vm; -import totalcross.util.Hashtable; -import totalcross.util.Vector; /* A launcher for Android is placed in a Android PacKage (APK), which is a @@ -117,176 +101,6 @@ public class Deployer4Android { private final String targetTCZ; private String tcFolder; - abstract static class ProtocExec { - final static String NAME = "protoc"; - final static String VERSION = "21.0"; - final static String BASE_URL = "https://github.com/protocolbuffers/protobuf/releases/download/v"; - - public static String getExecutable() { - String osName = null; - - if (DeploySettings.isWindows()) { - osName = "win64"; - } else if (DeploySettings.isMac()) { - osName = "osx-universal_binary"; - } else { - String osArch = System.getProperty("os.arch"); - if (osArch.equals("aarch64") || osArch.equals("arm64")) { - osArch = "aarch_64"; - } else if (osArch.equals("x86_64") || osArch.equals("amd64")) { - osArch = "x86_64"; - } else { - DeployLogger.warn("Couldn't detect system architecture, trying with x86_64"); - osArch = "x86_64"; - } - osName = "linux-" + osArch; - } - - final String protocString = NAME + '-' + VERSION + '-' + osName; - final File protocBaseFolder = new File(DeploySettings.etcDir, "tools/android/protoc"); - protocBaseFolder.mkdirs(); - - final File protocExecutable = new File(protocBaseFolder, DeploySettings.appendDotExe("bin/protoc")); - if (!protocExecutable.exists()) { - final String downloadUrl = BASE_URL + VERSION + "/" + protocString + ".zip"; - // download and unzip protoc - try { - DeployLogger.normal("Downloading protoc..."); - downloadAndUnzip(downloadUrl, protocBaseFolder.getAbsolutePath()); - } catch (Exception e) { - throw new RuntimeException("Failed to download protoc at: " + downloadUrl - + " ; You may download it yourself and unzip the contents into the folder: " - + protocBaseFolder.getAbsolutePath(), e); - } - - } - - if (DeploySettings.isMac()) { - try { - // The attribute is optional; avoid treating its absence as a deployment warning. - Process query = new ProcessBuilder("/usr/bin/xattr", "-p", "com.apple.quarantine", protocExecutable.getAbsolutePath()).start(); - if (query.waitFor() == 0) { - Process remove = new ProcessBuilder("/usr/bin/xattr", "-d", "com.apple.quarantine", protocExecutable.getAbsolutePath()).start(); - if (remove.waitFor() != 0) { - DeployLogger.warn("Could not remove the macOS quarantine attribute from protoc; continuing with deployment."); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while removing the macOS quarantine attribute from protoc", e); - } catch (IOException e) { - DeployLogger.warn("Could not start xattr to remove the macOS quarantine attribute from protoc; continuing with deployment."); - } - } - if (!DeploySettings.isWindows()) { - try { - // Also repair a protoc executable restored from an existing SDK cache. - Files.setPosixFilePermissions( - Paths.get(protocExecutable.getAbsolutePath()), - PosixFilePermissions.fromString("rwxr-xr-x")); - } catch (IOException e) { - throw new RuntimeException("Failed to set execution permission to: " + protocExecutable.getAbsolutePath()); - } - } - - final String protocExecutablePath = protocExecutable.getAbsolutePath(); - if (!protocExecutable.exists()) { - throw new RuntimeException("Couldn't find protoc at: " + protocExecutablePath); - } - - return protocExecutablePath; - } - - public static void downloadAndUnzip(String fileUrl, String outputDir) throws Exception { - Path tempZip = Files.createTempFile("download", ".zip"); - - downloadFile(fileUrl, tempZip.toFile()); - - Files.createDirectories(Paths.get(outputDir)); - - unzip(tempZip.toString(), outputDir); - } - - static void downloadFile(String fileURL, File outputFile) throws IOException { - URL url = new URL(fileURL); - HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); - httpConn.setRequestProperty("User-Agent", "Mozilla/5.0"); // Prevents some servers from blocking the request - - try (InputStream in = httpConn.getInputStream(); - FileOutputStream out = new FileOutputStream(outputFile)) { - - byte[] buffer = new byte[8192]; - int len; - - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } - - httpConn.disconnect(); - } - - private static void unzip(String zipFilePath, String destDirectory) throws IOException { - try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath))) { - - java.util.zip.ZipEntry entry = zipIn.getNextEntry(); - - while (entry != null) { - String filePath = destDirectory + File.separator + entry.getName(); - - if (!entry.isDirectory()) { - Files.createDirectories(Paths.get(filePath).getParent()); - - try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) { - byte[] bytesIn = new byte[4096]; - int read; - while ((read = zipIn.read(bytesIn)) != -1) { - bos.write(bytesIn, 0, read); - } - } - } else { - Files.createDirectories(Paths.get(filePath)); - } - - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - } - } - } - - abstract static class BundletoolExec { - final static String NAME = "bundletool-all"; - final static String VERSION = "1.10.0"; - final static String FILE_NAME = NAME + "-" + VERSION + ".jar"; - final static String DOWNLOAD_URL = - "https://github.com/google/bundletool/releases/download/" + VERSION + "/" + FILE_NAME; - - public static String getJarPath() { - final File androidToolsFolder = new File(DeploySettings.etcDir, "tools/android"); - androidToolsFolder.mkdirs(); - - final File bundletoolJar = new File(androidToolsFolder, FILE_NAME); - if (!bundletoolJar.exists()) { - try { - DeployLogger.normal("Downloading bundletool..."); - ProtocExec.downloadFile(DOWNLOAD_URL, bundletoolJar); - } catch (Exception e) { - throw new RuntimeException("Failed to download bundletool at: " + DOWNLOAD_URL - + " ; You may download it yourself and place the jar into the folder: " - + androidToolsFolder.getAbsolutePath(), e); - } - } - - final String bundletoolJarPath = bundletoolJar.getAbsolutePath(); - if (!bundletoolJar.exists()) { - throw new RuntimeException("Couldn't find bundletool at: " + bundletoolJarPath); - } - - return bundletoolJarPath; - } - } - public Deployer4Android() throws Exception { try (FileInputStream fis = new FileInputStream(Utils.findPath(DeploySettings.etcDir + "security/android_keystore.properties", false))) { signingConfig.load(fis); @@ -346,7 +160,7 @@ public Deployer4Android() throws Exception { // 5. decode the AndroidManifest // $PROTO_BIN --decode=aapt.pb.XmlNode --proto_path=tools Configuration.proto Resources.proto < $DEST_FOLDER/base/manifest/AndroidManifest.xml > AndroidManifest_temp.xml - final String protocExecutable = ProtocExec.getExecutable(); + final String protocExecutable = AndroidToolLocator.protoc(); String[] decodeCmd = { protocExecutable, "--decode=aapt.pb.XmlNode", "--proto_path=tools", "Configuration.proto", "Resources.proto" }; Process process = Runtime.getRuntime().exec(decodeCmd, null, new File(DeploySettings.etcDir, "tools/android")); @@ -493,7 +307,7 @@ public boolean accept(File dir, String name) { List javaCmdList = new ArrayList<>(); final String javaExe = Utils.searchIn(DeploySettings.path, DeploySettings.appendDotExe("java")); - final String bundletoolJar = BundletoolExec.getJarPath(); + final String bundletoolJar = AndroidToolLocator.bundletool(); javaCmdList.add(javaExe); javaCmdList.add("-Djava.io.tmpdir=" + new File(targetDir).getAbsolutePath()); javaCmdList.add("-jar"); @@ -616,114 +430,7 @@ private void insertIconPng(OutputStream zos, String name) throws Exception { } private void insertTCFilesZip(OutputStream z) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); - - // parse the android.pkg - Hashtable ht = new Hashtable(13); - Utils.processInstallFile("android.pkg", ht); - - Vector vLocals = (Vector) ht.get("[L]"); - if (vLocals == null) { - vLocals = new Vector(); - } - Vector vGlobals = (Vector) ht.get("[G]"); - if (vGlobals == null) { - vGlobals = new Vector(); - } - vLocals.addElements(DeploySettings.tczs); - if (vGlobals.size() > 0) { - vLocals.addElements(vGlobals.toObjectArray()); - } - // tc is always included - // include non-binary files - List defaultTczs = DeploySettings.getDefaultTczs(); - for (File file : defaultTczs) { - vLocals.addElement(file.getAbsolutePath()); - } - - Utils.preprocessPKG(vLocals, true); - writeVlocals(baos, vector2set(vLocals, new HashSet())); - - // add the file UNCOMPRESSED - baos.writeTo(z); - } - - public static Set vector2set(Vector vec, Set set) { - for (int i = 0, n = vec.size(); i < n; i++) { - @SuppressWarnings("unchecked") - E item = (E) vec.items[i]; - set.add(item); - } - return set; - } - - private void writeVlocals(ByteArrayOutputStream baos, Set vLocals) throws IOException { - ZipOutputStream zos = new ZipOutputStream(baos); - for (String item : vLocals) { - String[] pathnames = Convert.tokenizeString(item, ','); - String pathname = pathnames[0]; - String name = Utils.getFileName(pathname); - if (pathnames.length > 1) { - name = Convert.appendPath(pathnames[1], name); - if (name.startsWith("/")) { - name = name.substring(1); - } - } - // tcz's name must match the lowercase sharedid - if (tcFolder != null && pathname.equals(DeploySettings.tczFileName)) { - name = targetTCZ + ".tcz"; - } - - File f = new File(pathname); - if (!f.exists() || !f.isFile() || !f.canRead()) { - f = new File(Convert.appendPath(DeploySettings.currentDir, pathname)); - } - - File file = getFirstReadableFile(pathname, Convert.appendPath(DeploySettings.currentDir, pathname), Utils.findPath(pathname, true)); - if (file == null) { - DeployLogger.warn("File not found: " + pathname); - continue; - } - - try (FileInputStream fis = new FileInputStream(file)){ - int avaiable = fis.available(); - - ByteArrayOutputStream secondary = new ByteArrayOutputStream(avaiable); - IOUtils.copy(fis, secondary); - byte[] bytes = secondary.toByteArray(); - fis.close(); - ZipEntry zze = new ZipEntry(name); - // tcz files will be stored without compression so they can be read directly - if (name.endsWith(".tcz")) { - setEntryAsStored(zze, bytes); - } - zos.putNextEntry(zze); - zos.write(bytes); - zos.closeEntry(); - } - } - zos.close(); - } - - private static void setEntryAsStored(ZipEntry entry, byte[] content) { - CRC32 crc = new CRC32(); - crc.update(content); - entry.setCrc(crc.getValue()); - entry.setMethod(ZipEntry.STORED); - entry.setCompressedSize(content.length); - entry.setSize(content.length); - } - - private static File getFirstReadableFile (String... paths) { - for (String path : paths) { - if (path != null) { - File f = new File(path); - if (f.exists() && f.isFile() && f.canRead()) { - return f; - } - } - } - return null; + AndroidPackageFiles.write(z, targetTCZ, tcFolder); } private static PipedOutputStream outputToTFile(TFile file) throws IOException { diff --git a/TotalCrossSDK/src/main/java/totalcross/Launcher.java b/TotalCrossSDK/src/main/java/totalcross/Launcher.java index 79960a1de..b3a62deaa 100644 --- a/TotalCrossSDK/src/main/java/totalcross/Launcher.java +++ b/TotalCrossSDK/src/main/java/totalcross/Launcher.java @@ -7,449 +7,79 @@ // SPDX-License-Identifier: LGPL-2.1-only package totalcross; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Frame; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.GraphicsEnvironment; -import java.awt.Insets; -import java.awt.Panel; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.Toolkit; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.KeyListener; -import java.awt.event.MouseMotionListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.awt.event.WindowListener; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigDecimal; -import java.net.URL; -import java.net.URLConnection; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.ZipInputStream; -import net.coobird.thumbnailator.Thumbnails; -import tc.tools.AnonymousUserData; -import net.coobird.thumbnailator.Thumbnails.Builder; -import net.coobird.thumbnailator.resizers.configurations.Antialiasing; -import net.coobird.thumbnailator.resizers.configurations.Dithering; -import net.coobird.thumbnailator.resizers.configurations.Rendering; -import net.coobird.thumbnailator.resizers.configurations.ScalingMode; -import tc.tools.JarClassPathLoader; -import tc.tools.deployer.DeploySettings; -import totalcross.io.IOException; -import totalcross.io.RandomAccessStream; -import totalcross.io.Stream; -import totalcross.sys.Settings; -import totalcross.sys.SpecialKeys; -import totalcross.sys.Time; -import totalcross.sys.Vm; -import totalcross.ui.Control; -import totalcross.ui.MainWindow; -import totalcross.ui.UIColors; -import totalcross.ui.Window; -import totalcross.ui.event.KeyEvent; -import totalcross.ui.event.MultiTouchEvent; -import totalcross.ui.event.PenEvent; -import totalcross.util.Hashtable; -import totalcross.util.IntHashtable; -import totalcross.util.zip.TCZ; +import totalcross.preview.PreviewRuntime; +import totalcross.preview.PreviewFrame; +import totalcross.preview.PreviewFrameConsumer; -/* - * Note: Everything that calls TotalCross code in these classes must be - * synchronized with respect to the Applet uiLock object to allow TotalCross - * programs to be single threaded. This is because of the multi-threaded - * nature of Java and because timers use multiple threads. +/** + * Public compatibility facade for the desktop launcher. * - * Because all calls into TotalCross are synchronized and users can't call this code, - * they can't deadlock the program in any way. If we moved the synchronization - * into TotalCross code, we would have the possibility of deadlock. + * The implementation is split into package-private responsibility layers so + * existing callers still construct and use {@code totalcross.Launcher} while + * lifecycle, arguments, events, rendering, I/O, settings, fonts, and streams + * remain independently testable. */ - -/** Represents the applet or application used as a Java Container to make possible run TotalCross at the desktop. */ @SuppressWarnings({"deprecation", "removal"}) -final public class Launcher extends java.applet.Applet implements WindowListener, KeyListener, - java.awt.event.MouseListener, MouseWheelListener, MouseMotionListener, ComponentListener { - public static Launcher instance; - public static boolean isApplication; - public static boolean terminateIfMainClass = true; - public String commandLine = ""; - public int threadCount; - public Hashtable htOpenedAt = new Hashtable(31); // guich@200b4_82 - public IntHashtable keysPressed = new IntHashtable(129); - public MainWindow mainWindow; - public boolean showKeyCodes; - public Hashtable htAttachedFiles = new Hashtable(5); // guich@566_28 - public static int userFontSize = -1; - - private int toBpp = 24; - private int toWidth = -1; - private int toHeight = -1; - - private boolean fullscreen = false; - private String className; - private boolean appletInitialized; // guich@500_1 - private LauncherFrame frame; - private int toUI = -1; // guich@573_6: since now we have 4 styles, select the target one directly. +public final class Launcher extends LauncherStreams { + // Kept on the facade for source-compatible reflective test and tooling access. private double toScale = -1; - private int toX = -1, toY = -1; - private WinTimer winTimer; - private boolean started; // guich@120 - private boolean destroyed; // guich@230_24 - private boolean settingsFilled; - private int[] screenPixels = new int[0]; - private int lookupR[], lookupG[], lookupB[], lookupGray[]; - private int pal685[]; - private Class _class; // used by the openInputStream method. - protected BufferedImage screenImg; - private Builder thumbnailBuilder; - private AlertBox alert; - private String frameTitle; - private String crid4settings; // prevent from having two different crids for loading and storing the settings. - private StringBuffer mmsb = new StringBuffer(32); - private TCEventThread eventThread; - private boolean isMainClass; - private boolean isDemo; - private boolean fastScale; - - private double toScaleValue = -1; - private double toDensityValue = 1; - public totalcross.ui.Insets toInsetsPortrait; - public totalcross.ui.Insets toInsetsLandscape; + private int toBpp = 24; - public Launcher() { - instance = this; - addKeyListener(this); - addMouseListener(this); - addMouseWheelListener(this); - addMouseMotionListener(this); - try { - File libsFile = new File(DeploySettings.distDir, "libs"); - JarClassPathLoader.addJar(libsFile, "jna"); - JarClassPathLoader.addJar(libsFile, "jna-platform"); - JarClassPathLoader.addJar(libsFile, "slf4j-api"); - JarClassPathLoader.addJar(libsFile, "appdirs"); - JarClassPathLoader.addJar(libsFile, "thumbnailator"); - } catch (java.io.IOException e) { - e.printStackTrace(); + public class UserFont extends LauncherFontTypes.UserFont { + protected UserFont(String fontName, String suffix, int size, totalcross.ui.font.Font base) throws Exception { + super(fontName, suffix, size, base); } - } - @Override - public void destroy() { - if (mainWindow == null || destroyed) { - return; + protected UserFont(String fontName, String suffix) throws Exception { + super(fontName, suffix); } - destroyed = true; - eventThread.invokeInEventThread(true, new Runnable() { - @Override - public void run() { - mainWindow.appEnding(); - System.runFinalization(); - storeSettings(); - } - }); - winTimer.stopGracefully(); // timer must be running when appEnding is called } - private void runtimeInstructions() { - System.out.println("Current path: " + System.getProperty("user.dir")); - System.out.println("TotalCross " + Settings.versionStr + "." + Settings.buildNumber); - // print instructions - System.out.println("==================================="); - System.out.println("Device key emulations:"); - System.out.println("F2 : TAKE SCREEN SHOT AND SAVE TO CURRENT FOLDER"); - System.out.println("F6 : MENU"); - System.out.println("F7 : BACK (ESCAPE)"); - System.out.println("F9 : CHANGE ORIENTATION"); - System.out.println("F11: OPEN KEYBOARD"); - System.out.println("==================================="); - } - - @Override - @SuppressWarnings("static-access") - final public void init() { - boolean showInstructionsOnError = true; - appletInitialized = true; // guich@500_1 - totalcross.sys.Settings.showDesktopMessages = true; // guich@500_1: redo the messages. - try { - alert = new AlertBox(); - // NOTE: getParameter() and size() don't work in a - // java applet constructor, so we need to call them here - if (!isApplication) { - String arguments = getParameter("arguments"); - if (arguments == null) { - throw new Exception( - "Error: you must suply an 'arguments' property with all the argments to create the application"); - } - String[] args = tokenizeString(arguments, ' '); - parseArguments(args); - } + private PreviewFrameConsumer previewFrameConsumer; - fillSettings(); - - try { - _class = getClass(); // guich@500_1: we can use ourselves - // if the user pass: tc/samples/ui/image/test/ImageTest.class, change to tc.samples.ui.image.test.ImageTest - if (className.endsWith(".class")) { - className = className.substring(0, className.length() - 6); - } - className = className.replace('/', '.'); - - Class c = Class.forName(className); // guich@200b2: applets dont let you specify the path. it must be set in the codebase param - guich@520_9: changed from Class. to getClass - showInstructionsOnError = false; - isMainClass = checkIfMainClass(c); // guich@tc122_4 - if (!isMainClass) { - runtimeInstructions(); - } - Object o = c.newInstance(); - if (o instanceof MainClass && !(o instanceof MainWindow)) { - ((MainClass) o).appStarting(0); - ((MainClass) o).appEnding(); - if (terminateIfMainClass) { - System.exit(0); // currently we just exit after the constructor is called in a Non-GUI (headless) application - } else { - return; - } - } - mainWindow = (MainWindow) o; - // NOTE: java will call a partially constructed object if show() is called before all the objects are constructed - if (isApplication) { - frame = new LauncherFrame(); - requestFocus(); - } else { - setLayout(new java.awt.BorderLayout()); - } - if (toUI != -1) { - mainWindow.setUIStyle((byte) toUI); - } - } catch (LinkageError le) { - System.out.println("Fatal Error when running applet: there is an error in the constructor of the class " - + className + " and it could not be instantiated. Stack trace: "); - le.printStackTrace(); - exit(0); - } catch (ClassCastException cce) { - System.out.println("Error: class " + className + " does not extend MainClass nor MainWindow!"); - cce.printStackTrace(); - exit(-1); - } catch (ClassNotFoundException cnfe) { - System.out.println("The MainWindow class specified was not found: " + className + "\n\nCommon causes are:"); - System.out - .println(". The name is misspelled: java is case sensitive, so UIGadgets is not the same of uigadgets"); - if (className.indexOf('.') < 0) { - System.out.println( - ". The package name is incorrect: if you declared a class like: \n package com.foo.bar;\n public class " - + className + "\n then you must specify com.foo.bar." + className - + " as the main class; only specifying " + className + " is not enough."); - } - System.out.println( - ". Its location was not added to the classpath: if you're running from the prompt, be sure to add the path where your application is to the CLASSPATH argument. For example, if the class is in the current path, add a . specifying the current path: java -classpath .;tc.jar totalcross.Launcher " - + className); - exit(-1); - } - } catch (Exception ee) { - if (showInstructionsOnError) { - showInstructions(); - } - ee.printStackTrace(); - } // guich@120 + public Launcher() { + super(); + initializeLauncher(); } - private static boolean checkIfMainClass(Class c) { - Class[] interfaces = c.getInterfaces(); - if (interfaces != null) { - for (int i = 0; i < interfaces.length; i++) { - if (interfaces[i].getName().equals("totalcross.MainClass")) { - return true; - } - } - } - return false; + Launcher(PreviewRuntime.FrameConsumer previewSurface, boolean previewMode) { + super(); + this.previewSurface = previewSurface; + this.previewMode = previewMode; + initializeLauncher(); } - private class LauncherFrame extends Frame { - private Insets insets; - - public LauncherFrame() { - if (fullscreen) { - setExtendedState(Frame.MAXIMIZED_BOTH); - setUndecorated(true); - } - setBackground(new java.awt.Color(getScreenColor(mainWindow.getBackColor()))); - setResizable(Settings.resizableWindow); // guich@570_54 - setLayout(null); - add(instance); - addNotify(); // without this, the insets will not be correctly set. - insets = getInsets(); - if (insets == null) { - insets = new Insets(0, 0, 0, 0); - } - setFrameSize(toWidth, toHeight, true); - setLocation(toX, toY); - super.setTitle(frameTitle != null ? frameTitle : mainWindow.getClass().getName()); - setVisible(true); - addWindowListener(instance); - addComponentListener(instance); - } - - @Override - public void update(java.awt.Graphics g) { - } - - public void setFrameSize(int toWidth, int toHeight, boolean set) { - if (set) { - setSize((int) (toWidth * toScale) + insets.left + insets.right, - (int) (toHeight * toScale) + insets.top + insets.bottom); - } - instance.setBounds(insets.left, insets.top, (int) (toWidth * toScale), (int) (toHeight * toScale)); - } - }; - - private class WinTimer extends java.lang.Thread { - private int interval; - private boolean shouldStop; - - @Override - public void run() { - // NOTE: because we have created an official event queue/thread, which now - // resembles the device event queue much more closely, we must be - // sure that all timers and TC threads are run in that event thread. This - // will ensure that such things as blinking cursors will continue to work - // if there is a blocking modal dialog open. This also means that TC JDK - // threads will act much more like the device threads... in that, threads - // will not run unless a message pump is running. - while (!shouldStop) { - boolean doTick = true; - int millis = interval; - if (millis <= 0) { - // NOTE: Netscape navigator doesn't support interrupt() - // so we sleep here less than we would normally need to - // (1 second) if we're not doing anything to check if - // the timer should start in case interrupt didn't work - millis = 1 * 1000; - doTick = false; - } - // guich@200b4_84: implement the simple thread - long first = System.currentTimeMillis(); - while ((System.currentTimeMillis() - first) < millis) { - try { - sleep(millis); - doTick = true; // guich@230_3 - break; // guich@230_3 - } catch (InterruptedException e) { - doTick = false; - break; // guich@230_4 - } - } - if (doTick && eventThread != null) { - eventThread.invokeInEventThread(false, new Runnable() { - @Override - public void run() { - synchronized (instance) // guich@510_2: synchronize the repaint with the timer - { - mainWindow._onTimerTick(true); - } - } - }); - } - } - } + Launcher(PreviewRuntime.FrameConsumer previewSurface, boolean previewMode, ClassLoader appClassLoader) { + super(); + this.previewSurface = previewSurface; + this.previewMode = previewMode; + this.appClassLoader = appClassLoader; + initializeLauncher(); + } - void setInterval(int millis) { - //System.out.println("setInterval "+millis); - interval = millis < 10 ? 10 : millis; // guich@230_3 - interrupt(); + @Override + public void updateScreen() { + if (toScale != -1 || super.toScale == -1) { + super.toScale = toScale; } - - void stopGracefully() { - // NOTE: It's not a good idea to call stop() on threads since - // it can cause the JVM to crash. - shouldStop = true; - interrupt(); + if (toBpp != 24 || super.toBpp == 24) { + super.toBpp = toBpp; } + super.updateScreen(); } - public boolean eventIsAvailable() { - return eventThread.eventAvailable(); - } - - void startApp() { - eventThread = new TCEventThread(mainWindow); - if (!started) // guich@120 - make sure that the component is available for drawing when starting the application. called by paint. - { - try { - eventThread.invokeInEventThread(true, new Runnable() { - @Override - public void run() { - while (mainWindow == null) { - Thread.yield(); - } - mainWindow.appStarting(isDemo ? 80 : -1); - } // guich@200b4_107 - guich@570_3: check if mainWindow is not null to avoid problems when running on Linux. seems that the paint event is being generated before the start one. - }); - } catch (Throwable e) { - e.printStackTrace(); - } - started = true; - } + public void setPreviewFrameConsumer(PreviewFrameConsumer consumer) { + previewFrameConsumer = consumer; } - static void showInstructions() { - System.out.println("Possible Arguments (in any order and case insensitive). Default is marked as *"); - System.out.println(" /scr WIDTHxHEIGHT : sets the width and height resolution."); - System.out.println(" /scr WIDTHxHEIGHTxBPP : sets the width, height and bits per pixel (8, 16, 24 or 32)"); - System.out.println(" /density <0.1 to 4> : sets the screen pixel density"); - System.out.println(" /scr win32 : Windows 32 (same of /scr 240x320x24)"); - System.out.println("* /scr android : Android (same of /scr 360x592x24 /density 2)"); - System.out.println(" /scr iPhone : iPhone 15 resolution (same of /scr 393x852x24 /density 3 /safeAreaPortrait 59,0,34,0 /safeAreaLandscape 0,59,21,59)"); - System.out.println(" /scr iPhoneSE : iPhone SE resolution (same of /scr 375x667x24 /density 2)"); - System.out.println(" /scr ipad : iPad resolution (same of /scr 768x1024x24 /density 2)"); - System.out.println(" /fullscreen : Use full-screen window"); - System.out.println(" /pos x,y : Sets the openning position of the application"); - System.out.println(" /uiStyle Flat : Flat user interface style"); - System.out.println("* /uiStyle Vista : Vista user interface style"); - System.out.println(" /uiStyle Android : Android 4 user interface style"); - System.out.println(" /uiStyle Holo : Android 5 user interface style"); - System.out.println(" /uiStyle Material: Material 6 user interface style"); - System.out.println(" /penlessDevice : acts as a device that has no touchscreen."); - System.out.println(" /fingerTouch : acts as a device that uses a finger instead of a pen."); - System.out.println(" /unmovablesip : acts as a device whose SIP is unmovable (like in Android and iPhone)."); - System.out.println(" /geofocus : enables geographical focus."); - System.out.println(" /virtualKeyboard : shows the virtual keyboard when in an Edit or a MultiEdit"); - System.out.println(" /showmousepos : shows the mouse position."); - System.out.println(" /bpp 8 : emulates 8 bits per pixel screens (256 colors)"); - System.out.println(" /bpp 16 : emulates 16 bits per pixel screens (64K colors)"); - System.out.println(" /bpp 24 : emulates 24 bits per pixel screens (16M colors)"); - System.out.println(" /bpp 32 : emulates 32 bits per pixel screens (16M colors without transparency)"); - System.out.println(" /scale <0.1 to 8>: scales the screen, using by default a method that gives higher priority to image smoothness than scaling speed."); - System.out.println(" /fastscale : combined with scale, changes its default scaling method for one that gives higher priority to scaling speed than smoothness of the scaled image."); - System.out.println(" /dataPath : sets where the PDB and media files are stored"); - System.out.println(" /cmdLine <...> : the rest of arguments-1 are passed as the command line"); - System.out.println(" /fontSize : set the default font size to the one passed as parameter"); - System.out.println(" /safeAreaPortrait top,left,bottom,right : sets a margin from the device borders to simulate devices with notch on portrait"); - System.out.println(" /safeAreaLandscape top,left,bottom,right : sets a margin from the device borders to simulate devices with notch on landscape"); - System.out.println("The class name that extends MainWindow must always be the last argument"); - System.out.println("Please notice that the Launcher automatically scales down the resolution to fit in the display, to disable this behavior you may include the argument scale with the value 1"); + @Override + protected PreviewFrameConsumer getPreviewFrameConsumer() { + return previewFrameConsumer; } - public static void main(String args[]) { + public static void main(String[] args) { if (args.length == 0 || args[0].equals("/help")) { if (args.length == 0) { showInstructions(); @@ -457,2244 +87,6 @@ public static void main(String args[]) { args = new String[] { "/scr", "480x620x32", "/fontsize", "16", "tc.Help" }; } isApplication = true; - Launcher app = new Launcher(); - app.parseArguments(args); - app.init(); - } - - private int toInt(String s) // Convert.toInt can't be used here, otherwise, the settings will be set too early! - { - try { - return Integer.parseInt(s); - } catch (Exception e) { - return 0; - } - } - - private double toDouble(String s) { - try { - return Double.parseDouble(s); - } catch (Exception e) { - return 0; - } - } - - protected void parseArguments(String... args) { - parseArguments(args[args.length - 1], Arrays.copyOf(args, args.length - 1)); - } - - protected void parseArguments(String clazz, String... args) { - // Send statistic data if user agreed - new Thread(() -> { - try { - AnonymousUserData.instance().launcher(args); - } catch (Exception e) { - //TODO: handle exception in a Log level - } - }).start(); - - int n = args.length, i = 0; - String newDataPath = null; - try { - className = clazz; - for (i = 0; i < n; i++) { - if (args[i].equalsIgnoreCase("/fontsize")) { - userFontSize = toInt(args[++i]); - } else if (args[i].equalsIgnoreCase("/dataPath")) { - newDataPath = args[++i]; - System.out.println("Data path is " + newDataPath); - } else if (args[i].equalsIgnoreCase("/scr")) /* /scr 320x320 or /scr 320x320x8 */ - { - String next = args[++i]; - if (next.equalsIgnoreCase("win32")) { - toWidth = 240; - toHeight = 320; - toBpp = 24; - } else if (next.equalsIgnoreCase("iPhone")) { - toWidth = 393; - toHeight = 852; - toBpp = 24; - toDensityValue = 3; - toInsetsPortrait = new totalcross.ui.Insets(59, 0, 34, 0); - toInsetsLandscape = new totalcross.ui.Insets(0, 59, 21, 59); - } else if (next.equalsIgnoreCase("iPhoneSE")) { - toWidth = 375; - toHeight = 667; - toBpp = 24; - toDensityValue = 2; - } else if (next.equalsIgnoreCase("ipad")) { - toWidth = 768; - toHeight = 1024; - toBpp = 24; - toDensityValue = 2; - } else if (next.equalsIgnoreCase("android")) { - toWidth = 360; - toHeight = 592; - toBpp = 24; - toDensityValue = 2; - } else { - String[] scr = tokenizeString(next.toLowerCase(), 'x'); - if (scr.length == 1) { - throw new Exception(); - } - toWidth = toInt(scr[0]); - toHeight = toInt(scr[1]); - if (scr.length == 3) { - toBpp = toInt(scr[2]); - } - } - System.out.println("Screen is " + toWidth + "x" + toHeight + "x" + toBpp); - } else if (args[i].equalsIgnoreCase("/fullscreen")) { - fullscreen = true; - } else if (args[i].equalsIgnoreCase("/safeAreaPortrait")) { - String[] scr = tokenizeString(args[++i].toLowerCase(), ','); - if (scr.length != 4) { - throw new Exception("Argument /safeAreaPortrait expects 4 comma separated values in the following format: top,left,bottom,right"); - } - toInsetsPortrait = new totalcross.ui.Insets(toInt(scr[0]), toInt(scr[1]), toInt(scr[2]), toInt(scr[3])); - } else if (args[i].equalsIgnoreCase("/safeAreaLandscape")) { - String[] scr = tokenizeString(args[++i].toLowerCase(), ','); - if (scr.length != 4) { - throw new Exception("Argument /safeAreaLandscape expects 4 comma separated values in the following format: top,left,bottom,right"); - } - toInsetsLandscape = new totalcross.ui.Insets(toInt(scr[0]), toInt(scr[1]), toInt(scr[2]), toInt(scr[3])); - } else if (args[i].equalsIgnoreCase("/r")) { - ++i; - } else if (args[i].equalsIgnoreCase("/pos")) /* x,y */ - { - String[] scr = tokenizeString(args[++i].toLowerCase(), ','); - if (scr.length == 1) { - throw new Exception(); - } - toX = toInt(scr[0]); - toY = toInt(scr[1]); - } else if (args[i].equalsIgnoreCase("/cmdline")) { - commandLine = ""; - while (++i < n) { - commandLine += args[i] + " "; - } - commandLine = commandLine.trim(); - System.out.println("Command line is '" + commandLine + "'"); - } else if (args[i].equalsIgnoreCase("/uiStyle")) { - String next = args[++i]; - if (next.equalsIgnoreCase("Flat")) { - toUI = Settings.FLAT_UI; - } else if (next.equalsIgnoreCase("Vista")) { - toUI = Settings.VISTA_UI; - } else if (next.equalsIgnoreCase("Android")) { - toUI = Settings.ANDROID_UI; - } else if (next.equalsIgnoreCase("Holo")) { - toUI = Settings.HOLO_UI; - } else if (next.equalsIgnoreCase("Material")) { - toUI = Settings.MATERIAL_UI; - } else { - throw new Exception(); - } - System.out.println("UI style is " + toUI); - } else if (args[i].equalsIgnoreCase("/penlessDevice")) // guich@573_20 - { - Settings.keyboardFocusTraversable = true; - System.out.println("Penless device is on"); - } else if (args[i].equalsIgnoreCase("/fingertouch")) // guich@573_20 - { - Settings.fingerTouch = true; - System.out.println("Finger touch is on"); - } else if (args[i].equalsIgnoreCase("/unmovablesip")) // guich@573_20 - { - Settings.unmovableSIP = true; - System.out.println("Unmovable SIP is on"); - } else if (args[i].equalsIgnoreCase("/geofocus")) // guich@tc114_31 - { - Settings.geographicalFocus = Settings.keyboardFocusTraversable = true; - System.out.println("Geographical focus is on"); - } else if (args[i].equalsIgnoreCase("/virtualKeyboard")) // bruno@tc110 - { - Settings.virtualKeyboard = true; - System.out.println("Virtual keyboard is on"); - } else if (args[i].equalsIgnoreCase("/bpp")) { - toBpp = toInt(args[++i]); - if (toBpp != 8 && toBpp != 16 && toBpp != 24 && toBpp != 32) { - throw new Exception(); - } - System.out.println("Bpp is " + toBpp); - } else if (args[i].equalsIgnoreCase("/scale")) { - final BigDecimal scaleDecimal = new BigDecimal(args[++i]); - if (scaleDecimal.compareTo(BigDecimal.ZERO) < 0 || scaleDecimal.compareTo(BigDecimal.valueOf(8)) > 0) { - throw new Exception(); - } - toScaleValue = scaleDecimal.doubleValue(); - System.out.println("Scale is " + toScaleValue); - } else if (args[i].equalsIgnoreCase("/fastscale")) { - fastScale = true; - } else if (args[i].equalsIgnoreCase("/showmousepos")) { - Settings.showMousePosition = true; - } else if (args[i].equalsIgnoreCase("/demo")) { - isDemo = true; - } else if (args[i].equalsIgnoreCase("/density")) { - final BigDecimal screenDensityDecimal = new BigDecimal(args[++i]); - if (screenDensityDecimal.compareTo(BigDecimal.ZERO) <= 0 - || screenDensityDecimal.compareTo(BigDecimal.valueOf(4)) > 0) { - throw new Exception(); - } - toDensityValue = screenDensityDecimal.doubleValue(); - } else if(args[i].equalsIgnoreCase("/dbginfo")){ - Settings.showDebugMessages=true; - } else { - throw new Exception(); - } - } - } catch (Exception e) { - showInstructions(); - System.err.println("Invalid or incomplete argument at position " + i + ": " + args[i]); - String s = ""; - for (i = 0; i < args.length; i++) { - s += " " + args[i]; - } - System.err.println(e.getMessage()); - System.err.println("Full command line:\n" + s.trim()); - exit(-1); - return; - } - - // verify the parameters - if (toWidth == -1 || toHeight == -1) // if no width specified, use the lowest one - { - if (isApplication) { - toWidth = 320; - toHeight = 568; - } else { - toWidth = getSize().width; - toHeight = getSize().height; - } - } - - Settings.screenDensity = toDensityValue; - toWidth *= toDensityValue; - toHeight *= toDensityValue; - if (toInsetsPortrait != null) { - toInsetsPortrait.top *= toDensityValue; - toInsetsPortrait.left *= toDensityValue; - toInsetsPortrait.bottom *= toDensityValue; - toInsetsPortrait.right *= toDensityValue; - } - if (toInsetsLandscape != null) { - toInsetsLandscape.top *= toDensityValue; - toInsetsLandscape.left *= toDensityValue; - toInsetsLandscape.bottom *= toDensityValue; - toInsetsLandscape.right *= toDensityValue; - } - - /* - * Gets the display resolution and automatically scales down the Launcher to fit - * in the display *only* if a scale value isn't provided on the command line - */ - if (toScaleValue == -1) { - final Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment() - .getDefaultScreenDevice() - .getDefaultConfiguration() - .getBounds(); - - /** - * Arbitrary value based on tests to avoid overlapping a docked task bar. - */ - final double USEABLE_AREA = 0.88; - final int viewportW = (int) (toWidth / toDensityValue); - final int viewportH = (int) (toHeight / toDensityValue); - final double maxRatio = Math.max((double) viewportW / r.width, (double) viewportH / r.height); - if (maxRatio > USEABLE_AREA) { - toScaleValue = USEABLE_AREA / maxRatio; - } - } - toScale = Math.abs(toScaleValue) / toDensityValue; - - Settings.dataPath = newDataPath; - } - - private String[] tokenizeString(String string, char c) { - java.util.StringTokenizer st = new java.util.StringTokenizer(string, "" + c); - String[] ret = new String[st.countTokens()]; - for (int i = 0; i < ret.length; i++) { - ret[i] = st.nextToken(); - } - return ret; - } - - @Override - public void start() { - mainWindow = MainWindow.getMainWindow(); - } - - ///////// guich@200b2: to make the vm easier to port, i removed all methods from the TotalCross classes that uses the jdk classes ///////// - public void registerMainWindow(totalcross.ui.MainWindow main) { - (winTimer = new WinTimer()).start(); // guich@510_2: start the timer only after we had added the others - } - - public void setTimerInterval(int milliseconds) { - winTimer.setInterval(milliseconds); - } - - public void exit(int exitCode) { - destroy(); // guich@230_24 - if (isApplication) { - System.exit(exitCode); - } - } - - public void minimize() { - if (frame != null) { - frame.setExtendedState(Frame.ICONIFIED); - } - } - - public void restore() { - if (frame != null) { - frame.setExtendedState(Frame.NORMAL); - } - } - - @Override - public void print(java.awt.Graphics g) { - } - - @Override - public boolean isFocusTraversable() // guich@512_1: inform that we want to handle tab - { - return true; - } - - private int modifiers; - - private void updateModifiers(java.awt.event.KeyEvent event) { - if (event.isShiftDown()) { - keysPressed.put(SpecialKeys.SHIFT, 1); - modifiers |= SpecialKeys.SHIFT; - } else { - keysPressed.put(SpecialKeys.SHIFT, 0); - modifiers &= ~SpecialKeys.SHIFT; - } - if (event.isControlDown()) { - keysPressed.put(SpecialKeys.CONTROL, 1); - modifiers |= SpecialKeys.CONTROL; - } else { - keysPressed.put(SpecialKeys.CONTROL, 0); - modifiers &= ~SpecialKeys.CONTROL; - } - if (event.isAltDown()) { - keysPressed.put(SpecialKeys.ALT, 1); - modifiers |= SpecialKeys.ALT; - } else { - keysPressed.put(SpecialKeys.ALT, 0); - modifiers &= ~SpecialKeys.ALT; - } - } - - @Override - public void keyPressed(final java.awt.event.KeyEvent event) { - if (event.getKeyChar() == '1' && event.isControlDown()) { - totalcross.ui.Window.onRobotKey(); - } - updateModifiers(event); - if (event.isActionKey()) { - updateModifiers(event); - int key = 0; - - switch (event.getKeyCode()) { - case java.awt.event.KeyEvent.VK_HOME: - key = SpecialKeys.HOME; - break; - case java.awt.event.KeyEvent.VK_END: - key = SpecialKeys.END; - break; - case java.awt.event.KeyEvent.VK_UP: - key = SpecialKeys.UP; - break; - case java.awt.event.KeyEvent.VK_DOWN: - key = SpecialKeys.DOWN; - break; - case java.awt.event.KeyEvent.VK_LEFT: - key = SpecialKeys.LEFT; - break; - case java.awt.event.KeyEvent.VK_RIGHT: - key = SpecialKeys.RIGHT; - break; - case java.awt.event.KeyEvent.VK_INSERT: - key = SpecialKeys.INSERT; - break; - case java.awt.event.KeyEvent.VK_ENTER: - key = SpecialKeys.ENTER; - break; - case java.awt.event.KeyEvent.VK_TAB: - key = SpecialKeys.TAB; - break; - case java.awt.event.KeyEvent.VK_BACK_SPACE: - key = SpecialKeys.BACKSPACE; - break; - case java.awt.event.KeyEvent.VK_ESCAPE: - key = SpecialKeys.ESCAPE; - break; - case java.awt.event.KeyEvent.VK_DELETE: - key = SpecialKeys.DELETE; - break; - case java.awt.event.KeyEvent.VK_PAGE_UP: - key = SpecialKeys.PAGE_UP; - keysPressed.put(key, 1); - keysPressed.put(java.awt.event.KeyEvent.VK_PAGE_DOWN, 0); - break; // don't let down/up simultanealy - case java.awt.event.KeyEvent.VK_PAGE_DOWN: - key = SpecialKeys.PAGE_DOWN; - keysPressed.put(key, 1); - keysPressed.put(java.awt.event.KeyEvent.VK_PAGE_UP, 0); - break; - // guich@120 - emulate more keys - case java.awt.event.KeyEvent.VK_F1: - break; - case java.awt.event.KeyEvent.VK_F2: - takeScreenShot(); - break; - case java.awt.event.KeyEvent.VK_F3: - break; - case java.awt.event.KeyEvent.VK_F4: - break; - case java.awt.event.KeyEvent.VK_F5: - break; - case java.awt.event.KeyEvent.VK_F6: - key = SpecialKeys.MENU; - break; - case java.awt.event.KeyEvent.VK_F7: - key = SpecialKeys.ESCAPE; - break; - case java.awt.event.KeyEvent.VK_F8: - break; - case java.awt.event.KeyEvent.VK_F10: - break; - case java.awt.event.KeyEvent.VK_F11: - key = SpecialKeys.KEYBOARD_ABC; - break; - case java.awt.event.KeyEvent.VK_F12: - break; - case java.awt.event.KeyEvent.VK_F9: - if (isApplication && !Settings.disableScreenRotation && Settings.screenWidth != Settings.screenHeight - && eventThread != null) // guich@tc: changed orientation? - { - int t = toWidth; - toWidth = toHeight; - toHeight = t; - screenResized(Settings.screenHeight, Settings.screenWidth, true); - key = 0; - ignoreNextResize = true; - } - break; - default: - key = 0; - break; - } - if (key != 0 && eventThread != null) // sometimes, when debugging in applet, eventThread can be null - { - eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, key, 0, 0, modifiers, Vm.getTimeStamp()); - } - if (showKeyCodes && eventThread != null) { - final String msg = "Key code: " + (key == 0 ? event.getKeyCode() : key) + ", Modifier: " + modifiers; - new Thread() { - @Override - public void run() { - Vm.alert(msg); - } - }.start(); // must place this in a separate thread, or the vm dies - } - } - } - - private void takeScreenShot() { - try { - totalcross.ui.image.Image img = MainWindow.getScreenShot(); - String name = totalcross.sys.Settings.appPath + new Time().getTimeLong() + ".png"; - totalcross.io.File f = new totalcross.io.File(name, totalcross.io.File.CREATE_EMPTY); - img.createPng(f); - f.close(); - System.out.println("Saved at " + name); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void screenResized(int w, int h, boolean setframe) { - if (screenImg == null || (Settings.screenWidth == w && Settings.screenHeight == h)) { - return; - } - Settings.screenWidth = w; - Settings.screenHeight = h; - frame.setFrameSize(w, h, setframe); - screenImg = null; // force the creation of a new screen image - eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.SCREEN_CHANGE, 0, 0, modifiers, Vm.getTimeStamp()); - } - - @Override - public void transferFocus() // guich@512_1: handle the tab key. - { - super.transferFocus(); - if (eventThread != null) { - eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.TAB, 0, 0, modifiers, Vm.getTimeStamp()); - } - } - - @Override - public void keyReleased(java.awt.event.KeyEvent event) { - updateModifiers(event); - if (event.isActionKey()) { - switch (event.getKeyCode()) { - // case java.awt.event.KeyEvent.VK_F1: keysPressed.put(SpecialKeys.HARD1,0); break; - // case java.awt.event.KeyEvent.VK_F2: keysPressed.put(SpecialKeys.HARD2,0); break; - // case java.awt.event.KeyEvent.VK_F3: keysPressed.put(SpecialKeys.HARD3,0); break; - // case java.awt.event.KeyEvent.VK_F4: keysPressed.put(SpecialKeys.HARD4,0); break; - case java.awt.event.KeyEvent.VK_PAGE_UP: - keysPressed.put(SpecialKeys.PAGE_UP, 0); - break; - case java.awt.event.KeyEvent.VK_PAGE_DOWN: - keysPressed.put(SpecialKeys.PAGE_DOWN, 0); - break; - } - } - } - - @Override - public void keyTyped(java.awt.event.KeyEvent event) { - updateModifiers(event); - if (!event.isActionKey() && eventThread != null) { - int key = event.getKeyChar(), orig = key; - switch (key) { - case 8: - key = SpecialKeys.BACKSPACE; - break; - case 10: - key = SpecialKeys.ENTER; - break; - case 127: - key = SpecialKeys.DELETE; - break; - case 27: - key = SpecialKeys.ESCAPE; - break; // guich@tc110_79 - } - eventThread.pushEvent(orig < 32 ? KeyEvent.SPECIAL_KEY_PRESS : KeyEvent.KEY_PRESS, key, 0, 0, modifiers, - Vm.getTimeStamp()); - } - } - - boolean isRightButton; - int startPY; - - @Override - public void mousePressed(java.awt.event.MouseEvent event) { - int px = (int) (event.getX() / toScale); - int py = (int) (event.getY() / toScale); - if (eventThread != null) { - eventThread.pushEvent(PenEvent.PEN_DOWN, 0, px, py, modifiers, Vm.getTimeStamp()); - } - if (isRightButton = (event.getButton() & 2) != 0) { - eventThread.pushEvent(MultiTouchEvent.SCALE, 1, px, startPY = py, modifiers, Vm.getTimeStamp()); - } - } - - @Override - public void mouseReleased(java.awt.event.MouseEvent event) { - int px = (int) (event.getX() / toScale); - int py = (int) (event.getY() / toScale); - if (eventThread != null) { - eventThread.pushEvent(PenEvent.PEN_UP, 0, px, py, modifiers, Vm.getTimeStamp()); - } - if ((event.getButton() & 2) != 0) { - eventThread.pushEvent(MultiTouchEvent.SCALE, 2, px, py, modifiers, Vm.getTimeStamp()); - } - } - - @Override - public void mouseDragged(java.awt.event.MouseEvent event) { - int px = (int) (event.getX() / toScale); - int py = (int) (event.getY() / toScale); - if (eventThread != null) // sometimes, when debugging in applet, eventThread can be null - { - if ((event.getButton() & 2) != 0 || isRightButton) { - double scale = py < startPY ? 1.05 : 0.95; - long l = Double.doubleToLongBits(scale); - int x = (int) (l >>> 32); - int y = (int) l; - if (!eventThread.hasEvent(MultiTouchEvent.SCALE)) { - eventThread.pushEvent(MultiTouchEvent.SCALE, 0, x, y, modifiers, Vm.getTimeStamp()); - } - } else if (!eventThread.hasEvent(PenEvent.PEN_DRAG)) { - eventThread.pushEvent(PenEvent.PEN_DRAG, 0, px, py, modifiers, Vm.getTimeStamp()); // guich@580_40: changed from 201 to 203; PenEvent.PEN_MOVE is deprecated - } - } - } - - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (eventThread != null) // sometimes, when debugging in applet, eventThread can be null - { - int ev = totalcross.ui.event.MouseEvent.MOUSE_WHEEL; - if (!eventThread.hasEvent(ev)) { - int px = (int) (e.getX() / toScale); - int py = (int) (e.getY() / toScale); - eventThread.pushEvent(ev, - e.getWheelRotation() < 0 ? totalcross.ui.event.DragEvent.UP : totalcross.ui.event.DragEvent.DOWN, px, py, - modifiers, Vm.getTimeStamp()); // guich@580_40: changed from 201 to 203; PenEvent.PEN_MOVE is deprecated - } - } - } - - @Override - public void windowClosing(java.awt.event.WindowEvent event) { - if (Settings.closeButtonType == Settings.NO_BUTTON) { - eventThread.pushEvent(totalcross.ui.event.KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.MENU, 0, 0, 0, - Vm.getTimeStamp()); - } else { - destroy(); - exit(0); - } - } - - @Override - public void mouseEntered(java.awt.event.MouseEvent event) { - if (frame != null && frame.getFocusOwner() != this && !destroyed) { - requestFocus(); // guich@200b4: correct a bug that sometimes key events was not being sent anymore to the canvas. - } - } - - @Override - public void mouseClicked(java.awt.event.MouseEvent event) { - } - - @Override - public void mouseExited(java.awt.event.MouseEvent event) { - } - - @Override - public void windowActivated(java.awt.event.WindowEvent event) { - } - - @Override - public void windowClosed(java.awt.event.WindowEvent event) { - } - - @Override - public void windowDeactivated(java.awt.event.WindowEvent event) { - } - - @Override - public void windowDeiconified(java.awt.event.WindowEvent event) { - if (mainWindow != null) { - mainWindow.onRestore(); - } - } - - @Override - public void windowIconified(java.awt.event.WindowEvent event) { - if (mainWindow != null) { - mainWindow.onMinimize(); - } - } - - @Override - public void windowOpened(java.awt.event.WindowEvent event) { - } - - @Override - public void mouseMoved(java.awt.event.MouseEvent event) { - if (eventThread != null) { - eventThread.pushEvent(totalcross.ui.event.MouseEvent.MOUSE_MOVE, 0, (int) (event.getX() / toScale), - (int) (event.getY() / toScale), modifiers, Vm.getTimeStamp()); - } - if (frame != null && Settings.showMousePosition) // guich@tc115_48 - { - mmsb.setLength(0); - if (frameTitle != null) { - mmsb.append(frameTitle).append(" ("); - } - int xx = (int) (event.getX() / toScale); - int yy = (int) (event.getY() / toScale); - int[] pixels = totalcross.ui.gfx.Graphics.mainWindowPixels; - mmsb.append(xx).append(",").append(yy).append(" ") - .append(totalcross.sys.Convert.unsigned2hex(pixels[yy * Settings.screenWidth + xx], 6)); - if (frameTitle != null) { - mmsb.append(")"); - } - frame.setTitle(mmsb.toString()); - } - } - - @Override - public void paint(java.awt.Graphics g) { - if (!started) { - startApp(); - } else { - eventThread.invokeInEventThread(false, new Runnable() { - @Override - public void run() { - try { - totalcross.ui.Window.repaintActiveWindows(); - } catch (Exception e) { - System.out.println("Exception in Launcher.paint"); - e.printStackTrace(); - } - } - }); - } - } - - public void pumpEvents() { - if (eventThread != null) { - eventThread.pumpEvents(); - } - } - - @Override - public void update(java.awt.Graphics g) { - } - - public void setNewMainWindow(MainWindow newInstance, String args) // called on Vm.exec - { - commandLine = args; // guich@200b3: added command line support for desktop classes. - winTimer.stopGracefully(); // guich@120 - Window.destroyZStack(); - mainWindow = newInstance; - mainWindow.initUI(); // ps: since we are being called from an app, we cannot use the synchronized method - } - - /** Calls System.out.println. TotalCross system debugging uses this method. See also debug(String s). */ - public static void print(String s) { - if (totalcross.sys.Settings.showDesktopMessages) { - System.err.println(s); - } - } - public static void debug(String s){ - if(totalcross.sys.Settings.showDebugMessages){ - System.out.println(s); - } - } - - //// Graphics //////////////////////////////////////////////////////////////////// - - private void createColorPaletteLookupTables() { - int i, r, g, b; - lookupR = new int[256]; - lookupG = new int[256]; - lookupB = new int[256]; - lookupGray = new int[256]; - - for (i = 0; i < 256; i++) { - r = (i + 1) * 6 / 256; - if (r > 0) { - r--; - } - g = (i + 1) * 8 / 256; - if (g > 0) { - g--; - } - b = (i + 1) * 5 / 256; - if (b > 0) { - b--; - } - lookupR[i] = r * 40; - lookupG[i] = g * 5; - lookupB[i] = b + 16; - lookupGray[i] = i / 0x11; - } - pal685 = totalcross.ui.gfx.Graphics.getPalette(); - } - - private int getScreenColor(int p) { - int r = (p >> 16) & 0xFF; - int g = (p >> 8) & 0xFF; - int b = p & 0xFF; - switch (toBpp) { - case 8: - if (lookupR == null) { - createColorPaletteLookupTables(); - } - return pal685[(g == r && g == b) ? lookupGray[r] : (lookupR[r] + lookupG[g] + lookupB[b])]; - case 16: - return (((r) >> 3) << 19) | (((g) >> 2) << 10) | (((b >> 3) << 3)); - default: - return p; - } - } - - public void updateScreen() { - //int ini = totalcross.sys.Vm.getTimeStamp(); - int w = totalcross.sys.Settings.screenWidth; - int h = totalcross.sys.Settings.screenHeight; - int ww = (int) (w * toScale); - int hh = (int) (h * toScale); - - if (screenImg == null) { - screenImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - // We can typecast directly to DataBufferInt because that's the type used for images with 24+ bit color - DataBufferInt dbi = (DataBufferInt) screenImg.getRaster().getDataBuffer(); - int[] pixels = dbi.getData(); - // Copy whatever was drawn before - System.arraycopy(totalcross.ui.gfx.Graphics.mainWindowPixels, 0, pixels, 0, w*h); - // And replace with our bytes, now we no longer need to copy from the mainWindowPixels to our image. Saving a ton of memory. - totalcross.ui.gfx.Graphics.mainWindowPixels = pixels; - if (toScale != 1 && !fastScale) { - // And as a bonus, we can reuse the thumbnailator builder because it always references the same object. - thumbnailBuilder = Thumbnails - .of(screenImg) - .size(ww, hh) - .rendering(Rendering.SPEED) - .scalingMode(ScalingMode.PROGRESSIVE_BILINEAR) - .antialiasing(Antialiasing.OFF) - .dithering(Dithering.DISABLE); - } - } - int[] pixels = totalcross.ui.gfx.Graphics.mainWindowPixels; - int n = Settings.screenWidth * Settings.screenHeight; - if (toBpp >= 24) { - screenPixels = pixels; - } else if (screenPixels.length < n) { - screenPixels = new int[n]; - } - // convert to the target bpp on-the-fly - switch (toBpp) { - case 8: { - if (lookupR == null) { - createColorPaletteLookupTables(); - } - int[] pal = pal685; - int[] toR = lookupR; - int[] toG = lookupG; - int[] toB = lookupB; - int[] toGray = lookupGray; - while (--n >= 0) { - int p = pixels[n]; - int r = (p >> 16) & 0xFF; - int g = (p >> 8) & 0xFF; - int b = p & 0xFF; - screenPixels[n] = pal[(g == r && g == b) ? toGray[r] : (toR[r] + toG[g] + toB[b])]; - } - break; - } - case 16: { - while (--n >= 0) { - screenPixels[n] = pixels[n] & 0xF8FCF8; // guich@tc100b4_2: use a direct and instead of a bunch of shifts. note: using a DirectColorModel(32,0xF80000,0x00FC00,0x0000F8,0) is 5x SLOWER than doing the mapping by ourselves. - } - break; - } - } - Graphics g = getGraphics(); - int shiftY = totalcross.ui.Window.shiftY; - int shiftH = totalcross.ui.Window.shiftH; - if ((shiftY + shiftH) > h) { - totalcross.ui.Window.shiftY = shiftY = h - shiftH; - } - if (shiftY != 0) { - g.setColor(new Color(UIColors.unsafeAreaColor)); - int yy = (int) (shiftH * toScale); - g.fillRect(0, yy, ww, hh - yy); // erase empty area - g.setClip(0, 0, ww, yy); // limit drawing area - g.translate(0, -(int) (shiftY * toScale)); - } - if (toScale != 1) // guich@tc126_74 - guich@tc130 - { - if (fastScale) { - g.drawImage(screenImg, 0, 0, ww, hh, 0, 0, w, h, this); - } else { - try { - g.drawImage(thumbnailBuilder.asBufferedImage(), 0, 0, this); - } catch (java.io.IOException e) { - e.printStackTrace(); - } - } - } else if (g != null) { - g.drawImage(screenImg, 0, 0, ww, hh, 0, 0, w, h, this); // this is faster than use img.getScaledInstance - } - if (shiftY != 0) { - g.translate(0, (int) (shiftY * toScale)); - g.setClip(0, 0, ww, hh); - } - // make the emulator work like OpenGL: erase the screen to instruct the user that everything must be drawn always - //java.util.Arrays.fill(pixels, getScreenColor(UIColors.unsafeAreaColor)); - } - - public static BufferedImage toBufferedImage(java.awt.Image img) { - if (img instanceof BufferedImage) { - return (BufferedImage) img; - } - - BufferedImage bufferedImage = new BufferedImage(img.getWidth(null), img.getHeight(null), - BufferedImage.TYPE_INT_ARGB); - - Graphics2D graphics = bufferedImage.createGraphics(); - graphics.drawImage(img, 0, 0, null); - graphics.dispose(); - - return bufferedImage; - } - - //static int count; - /////////////////////// I/O ///////////////////////////////////// - private File[] getClassPathDirectories() throws Exception { - char dirSeparator = File.pathSeparatorChar; - File[] classPath; - String pathstr = System.getProperty("java.class.path"); - // Count the number of path separators - int i = 0; - int n = 0; - int j = 0; - while ((i = pathstr.indexOf(dirSeparator, i)) != -1) { - n++; - i++; - } - // Build the class path - File[] path = new File[n + 1]; - int len = pathstr.length(); - for (i = n = 0; i < len; i = j + 1) { - if ((j = pathstr.indexOf(dirSeparator, i)) == -1) { - j = len; - } - if (i != j) { - String p = pathstr.substring(i, j); - File file = new File(p); - if (!file.isDirectory()) { - file = new File(getPathOf(p)); // add the parent path of the file - } - if (file.isDirectory()) { - path[n++] = file; - } - } - } - // Trim class path to exact size - classPath = new File[n]; - System.arraycopy(path, 0, classPath, 0, n); - return classPath; - } - - private InputStream readJavaInputStream(java.io.InputStream is) { - if (is == null) { - return null; - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); - byte[] buf = new byte[128]; - int len; - while (true) { - try { - len = is.read(buf); - } catch (java.io.IOException e) { - break; - } - if (len > 0) { - baos.write(buf, 0, len); - } else { - break; - } - } - return new ByteArrayInputStream(baos.toByteArray()); - } - - private String getPathOf(String pathAndFileName) { - char[] chars = pathAndFileName.toCharArray(); - for (int i = chars.length - 1; i >= 0; i--) { - if (chars[i] == '\\' || chars[i] == '/') { - return new String(chars, 0, i); - } - } - return ""; // no path - } - - public String getDataPath() // guich@420_11 - this now is needed because the user may change the datapath anywhere in the program - { - String path = totalcross.sys.Settings.dataPath; - if (path != null) { - path = path.replace('\\', '/'); - if (!path.endsWith("/")) { - path += "/"; - // don't check for folder to keep compatibility with win32 vm - //java.io.File f = new java.io.File(newDataPath); - //if (!f.isDirectory()) - // System.out.println("ERROR: dataPath specified is not a directory or does not exist! "+newDataPath); - } - } - return path; - } - - private String getMainWindowPath() { - if (MainWindow.getMainWindow() == null) { - return null; - } - String main = MainWindow.getMainWindow().getClass().getName().replace('.', '/'); - return getPathOf(main) + "/"; - } - - /** used in some classes so they can correctly open files. now can open jar files. */ - public InputStream openInputStream(String path) { - String sread = "\nopening for read " + path + "\n"; - String dataPath = getDataPath(); - InputStream stream = null; - String mainpath = getMainWindowPath(); - try { - try // guich@tc100: removed the nonGuiApp flag - { - sread += "#0 - the file given: " + path + "\n"; - stream = new FileInputStream(path); // guich@421_72 - } catch (Exception e) { - stream = null; - } - if (stream == null && isApplication) { - // search in the Settings.dataPath - try { - String p = isOk(dataPath) ? (dataPath + path) : path; - sread += "#1 - dataPath: " + p + "\n"; - stream = new FileInputStream(p); - htOpenedAt.put(path, getPathOf(p)); // guich@200b4_82 - jr: i changed getPathOf(path) to getPathOf(p) - } catch (Exception e) { - stream = null; - } - if (stream == null && mainpath != null) { - try { - String p = mainpath + path; - sread += "#2 - MainWindow's path from current folder: " + p + "\n"; - stream = new FileInputStream(p); - htOpenedAt.put(path, getPathOf(p)); // guich@200b4_82 - jr: i changed getPathOf(path) to getPathOf(p) - } catch (Exception e) { - stream = null; - } - } - // search in the classpath - if (stream == null) { - sread += "#3 - classpath\n"; - File[] dirs = getClassPathDirectories(); - File f = null; - for (int i = 0; i < dirs.length; i++) { - try { - f = new File(dirs[i], path); - if (!f.isFile() && mainpath != null) { - f = new File(dirs[i], mainpath + path); // guich@tc100: search in the path of the main window - } - if (f.isFile()) { - String ff = getPathOf(f.getAbsolutePath()); - htOpenedAt.put(path, ff); // guich@200b4_82 - jr: changed dirs[i].getAbsolutePath - guich@tc112_20: using f.getAbsolutePath instead of dirs[i].getAbsolutePath - break; - } else { - f = null; // guich@400_8: fixed problem when file was not found so the #3 can be tried below - } - } catch (Exception e) { - f = null; - } - } - if (f != null) { - stream = new FileInputStream(f); - } - } - if (stream == null && _class != null) // guich@400_6: now the resources can be read from the jar file - { - sread += "#4 - jar file\n"; - try { - InputStream is = (InputStream) _class.getResourceAsStream("/" + path); - if (is != null) { - stream = readJavaInputStream(is); - } - } catch (Throwable tt) { - if (tt.getMessage() != null) { - System.out.println(tt.getMessage()); - } - } - } - String sjar; - if (stream == null && !path.endsWith(".class") - && (sjar = getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).contains(".jar")) // guich@330 - let tc.Help work from inside a jar - { - sread += "#4b - " + sjar.substring(1) + "\n"; - try { - URL url = getClass().getProtectionDomain().getCodeSource().getLocation(); - ZipInputStream zIn = new ZipInputStream(url.openStream()); - String spath = "/" + path; - for (java.util.zip.ZipEntry zEntry = zIn.getNextEntry(); zEntry != null; zEntry = zIn.getNextEntry()) { - if (zEntry.getName().endsWith(spath)) { - stream = readJavaInputStream(zIn); - break; - } - } - zIn.close(); - } catch (Throwable tt) { - if (tt.getMessage() != null) { - System.out.println(tt.getMessage()); - } - } - } - if (stream == null && htAttachedFiles.size() > 0) // guich@tc100: load from attached libraries too - { - sread += "#5 - attached libraries\n"; - totalcross.io.ByteArrayStream bas = (totalcross.io.ByteArrayStream) htAttachedFiles.get(path.toLowerCase()); - if (bas != null) { - stream = new ByteArrayInputStream(bas.getBuffer()); // buffer is the same size of the loaded file. - } - } - } else if (stream == null) { - URL url; - // zero in the jar file (normal way) - InputStream is = null; - try { - is = (InputStream) _class.getResourceAsStream("/" + path); - } catch (Throwable tt) { - if (tt.getMessage() != null) { - System.out.println(tt.getMessage()); - } - } - sread += "#1 - resource: " + is + "\n"; // guich@200b4_59 - if (is != null) { - stream = readJavaInputStream(is); - } - // first in the jar file - // guich@200b4: using this in Internet makes the archive be fetched from the server at each call of this function. - if (stream == null) { - String archive = getParameter("archive"); - sread += "#2 - archive: " + archive + "\n"; - if (isOk(archive) && !archive.equals("null")) { - String[] archives = tokenizeString(archive, ','); // guich@580_39: if there are more than one file, split them - for (int i = 0; i < archives.length; i++) { - archive = archives[i]; - if (archive.startsWith("null")) { - archive = archive.substring(4); - } - URL codeBase = getCodeBase(); - url = new URL(codeBase + "/" + archive); - try { - ZipInputStream zIn = new ZipInputStream(url.openStream()); - java.util.zip.ZipEntry zEntry = zIn.getNextEntry(); - while (!zEntry.getName().equals(path)) { - zEntry = zIn.getNextEntry(); - if (zEntry == null) { - throw new Exception("doh"); - } - } - // guich@200b2: ok. the zIn.available() returns 1 and not the real size of the zip entry. so, here we read all into a byte stream - stream = readJavaInputStream(zIn); - } catch (Exception e) { - if (!e.getMessage().equals("doh")) { - e.printStackTrace();/* doh didn't find it in the jar thing */ - } - } - } - } - } - // second under the codebase - if (stream == null) { - try { - URL codeBase = getCodeBase(); - String cb = codeBase.toString(); - char lastc = cb.charAt(cb.length() - 1); - char firstc = path.charAt(0); - if (lastc != '/' && firstc != '/') { - cb += "/"; - } - sread += "#3 - url: " + cb + path + "\n"; - url = new URL(cb + path); - stream = url.openStream(); - } catch (FileNotFoundException ee) { - } catch (Exception e) { - e.printStackTrace(); - /* neither in the codebase */} - } - // third in the localhost - if (stream == null) { - try { - sread += "#4- url: file://localhost/" + dataPath + path + "\n"; - url = new URL("file://localhost/" + dataPath + path); // guich@120 - stream = url.openStream(); - } catch (Exception e) { - } - } - ; - if (stream == null && htAttachedFiles.size() > 0) // guich@tc100: load from attached libraries too - { - sread += "#5 - attached libraries\n"; - totalcross.io.ByteArrayStream bas = (totalcross.io.ByteArrayStream) htAttachedFiles.get(path.toLowerCase()); - if (bas != null) { - stream = new ByteArrayInputStream(bas.getBuffer()); // buffer is the same size of the loaded file. - } - } - } - if (stream == null) { - debug(sread + "file not found\n"); - } - } catch (FileNotFoundException ee) { - print("file not found"); - } catch (Exception e) // guich@120 - { - if (isOk(e.getMessage())) { - print("error in JavaBridge.openInputStream: " + e.getMessage()); - } - return null; - } - return stream; - } - - private OutputStream openOutputUrl(URL url) { - try { - URLConnection con = url.openConnection(); - con.setUseCaches(false); - con.setDoOutput(true); - con.setDoInput(false); - return con.getOutputStream(); - } catch (Exception u) // try another way - { - try { - String path = url + ""; - return new FileOutputStream(isOk(totalcross.sys.Settings.dataPath) ? (getDataPath() + path) : path); - } catch (Exception ee) { - return null; - } - } - } - - /** used in some classes so they can correctly open files. used internally by readBytes. */ - public OutputStream openOutputStream(String path) { - debug("\nopening for write " + path); - String dataPath = getDataPath(); - OutputStream stream = null; - String readPath = (String) htOpenedAt.get(path); // guich@tc112_20 - try { - try // guich@tc100: removed the nonGuiApp flag - { - String pp = isOk(dataPath) ? (dataPath + path) - : isOk(readPath) ? totalcross.sys.Convert.appendPath(readPath, path) : path; // guich@tc112_20: use readPath if not null - stream = new FileOutputStream(pp); // guich@421_11: added support for dataPath - } catch (Exception e) { - stream = null; - } - - if (stream == null && isApplication) { - // search in the place where it was read - guich@200b4_82 - if (readPath != null) { - try { - debug("#1 - read path"); - stream = new FileOutputStream(new java.io.File(readPath, path)); - debug("found in " + readPath); - } catch (Exception e) { - stream = null; - } - } - if (stream != null) { - return stream; - } - // search in the Settings.dataPath - try { - String p = isOk(dataPath) ? (dataPath + path) : path; - debug("#2 - Settings.dataPath"); - stream = new FileOutputStream(p); - debug("found in " + p); - } catch (Exception e) { - stream = null; - } - // search in the classpath - if (stream == null) { - debug("#3 - classpath"); - File[] dirs = getClassPathDirectories(); - File f = null; - for (int i = 0; i < dirs.length; i++) { - try { - f = new File(dirs[i], path); - if (f.isFile()) { - debug("found in " + dirs[i]); - break; - } - } catch (Exception e) { - f = null; - } - } - if (f == null) { - debug("could not find file in the classpath"); - } else { - stream = new FileOutputStream(f); - } - } - } else if (stream == null) { - URL url; - // first under the codebase - if (stream == null) { - try { - URL codeBase = getCodeBase(); - debug("#1- codeBase: " + codeBase); - String cb = codeBase.toString(); - char lastc = cb.charAt(cb.length() - 1); - char firstc = path.charAt(0); - if (lastc != '/' && firstc != '/') { - cb += "/"; - } - url = new URL(cb + path); - stream = openOutputUrl(url); - debug("found under codebase: " + url); - } catch (Exception e) { - e.printStackTrace(); - /* neither in the codebase */} - } - // third in the localhost - if (stream == null) { - try { - debug("#2- url: file://localhost/" + dataPath + path); - url = new URL("file://localhost/" + dataPath + path); // guich@120 - stream = openOutputUrl(url); - debug("found under localhost: " + url); - } catch (Exception e) { - } - } - ; - } - if (stream == null) { - debug("file not found"); - } - } catch (FileNotFoundException ee) { - debug("file not found"); - } catch (Exception e) { - /*if (!msgShowed) */print("error in Vm.openOutputStream: " + e.getMessage()); - return null; - } // guich@200 - return stream; - } - - /** read the available bytes from the stream getted with openInputStream. - * called by totalcross.ui.image.Image and totalcross.io.PDBFile - */ - public byte[] readBytes(String path) { - byte[] bytes = null; - try { - InputStream is = openInputStream(path); - if (is != null) { - int n = is.available(); - bytes = new byte[n]; - is.read(bytes); - is.close(); - } - } catch (Exception e) { - e.printStackTrace(); - } - return bytes; - } - - /** write the available bytes to the stream getted with openOutputStream. - * called by totalcross.io.PDBFile - */ - public boolean writeBytes(String path, byte[] buf, int len) { - boolean ret = true; - try { - OutputStream os = openOutputStream(path); - if (os != null) { - if (buf != null) { - os.write(buf, 0, len); - os.close(); // pietj@330_1 - } else { - print("ATT: you sent to stream.writeBytes a null buffer!"); - } - } - } catch (Exception e) { - e.printStackTrace(); - ret = false; - } - return ret; - } - - /** return true is the string is valid. called by openInputStream and openOutputStream in this class. */ - private boolean isOk(String s) { - return s != null && s.length() > 0; - } - - String getDefaultCrid(String name) { - if (name == null) { - return null; - } - - if (name.indexOf('.') != -1) { - name = name.substring(name.lastIndexOf('.') + 1); - } - int i; - int n = name.length(); - int hash = 0; - byte[] creat = new byte[4]; - for (i = 0; i < n; i++) { - hash += (byte) name.charAt(i); - } - for (i = 0; i < 4; i++) { - creat[i] = (byte) ((hash % 26) + 'a'); - if ((hash & 64) > 0) { - creat[i] += ('A' - 'a'); - } - hash = hash / 2; - } - return new String(creat); - } - - void storeSettings() { - try { - String crid = crid4settings;//totalcross.sys.Settings.applicationId; - // first verify if the PDBFile is created but the String is null - totalcross.sys.Settings.showDesktopMessages = false; // guich@340_49 - boolean saveSettings = totalcross.sys.Settings.appSettings != null || totalcross.sys.Settings.appSecretKey != null - || totalcross.sys.Settings.appSettingsBin != null; // guich@570_9: also check if appSecretKey is null - - totalcross.io.PDBFile cat; - - if (!saveSettings) { - try { - cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, totalcross.io.PDBFile.READ_WRITE); // guich@241_17: changed READ_ONLY to READ_WRITE to fix "operation invalid" error - cat.delete(); - } catch (totalcross.io.FileNotFoundException e) { - } - } else { - cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, totalcross.io.PDBFile.CREATE); - totalcross.io.ResizeRecord rs = new totalcross.io.ResizeRecord(cat, 256); - totalcross.io.DataStream ds = new totalcross.io.DataStream(rs); - - try { - cat.setRecordPos(1); - cat.deleteRecord(); - } catch (totalcross.io.IOException e) { - } - try { - cat.setRecordPos(0); - cat.deleteRecord(); - } catch (totalcross.io.IOException e) { - } - rs.startRecord(); - // store the appSettings record - ds.writeString(totalcross.sys.Settings.appSettings); - ds.writeString(totalcross.sys.Settings.appSecretKey); - rs.endRecord(); - // guich@573_16: store the bin in another record - if (totalcross.sys.Settings.appSettingsBin != null) { - int len = totalcross.sys.Settings.appSettingsBin.length; - cat.addRecord(len); - cat.writeBytes(totalcross.sys.Settings.appSettingsBin, 0, len); - } - cat.close(); - - } - totalcross.sys.Settings.showDesktopMessages = true; - } catch (Throwable t) { - System.out.println("Settings can't be stored: " + t.toString()); - } - } - - private void getAppSettings() { - String crid = crid4settings = totalcross.sys.Settings.applicationId; - totalcross.sys.Settings.showDesktopMessages = false; // guich@340_49 - try { - totalcross.io.PDBFile cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, - totalcross.io.PDBFile.READ_WRITE); - totalcross.io.DataStream ds = new totalcross.io.DataStream(cat); - cat.setRecordPos(0); - String s; - s = ds.readString(); - if (!"".equals(s)) { - totalcross.sys.Settings.appSettings = s; - } - try { - s = ds.readString(); - if (!"".equals(s)) { - totalcross.sys.Settings.appSecretKey = s; - } - } catch (Throwable t) { - System.out.println("Reading an old settings file; no appSecretKey available."); - } - - if (cat.getRecordCount() > 1) // guich@573_16 - { - cat.setRecordPos(1); - byte[] buf = new byte[cat.getRecordSize()]; - cat.readBytes(buf, 0, buf.length); - totalcross.sys.Settings.appSettingsBin = buf; - } - - cat.close(); - } catch (Throwable t) { - } - totalcross.sys.Settings.showDesktopMessages = true; // guich@340_49 - } - - private char getFirstSymbol(String s) { - char[] c = s.toCharArray(); - for (int i = 0; i < c.length; i++) { - if (c[i] != ' ' && !('0' <= c[i] && c[i] <= '9')) { - return c[i]; - } - } - return ' '; - } - - /** called by totalcross.Launcher.init() */ - public void fillSettings() { - if (settingsFilled) { - return; - } - settingsFilled = true; - java.util.Calendar cal = java.util.Calendar.getInstance(); - // guich@340_34: since java can't provide us good methods to return these values, we use parse the return of some formatting methods - cal.set(2002, 11, 25, 20, 0, 0); // guich@401_32 - java.text.DateFormat df = java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); // guich@401_32: fixed wrong results in some systems - String d = df.format(cal.getTime()); - totalcross.sys.Settings.dateFormat = d.startsWith("25") ? totalcross.sys.Settings.DATE_DMY - : d.startsWith("12") ? totalcross.sys.Settings.DATE_MDY : totalcross.sys.Settings.DATE_YMD; - totalcross.sys.Settings.dateSeparator = getFirstSymbol(d); - df = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT); // guich@401_32 - d = df.format(cal.getTime()); - - totalcross.sys.Settings.is24Hour = d.toLowerCase().indexOf("am") == -1 && d.toLowerCase().indexOf("pm") == -1; - totalcross.sys.Settings.timeSeparator = getFirstSymbol(d); - // - - totalcross.sys.Settings.weekStart = (byte) (cal.getFirstDayOfWeek() - 1); - settingsRefresh(false); - - java.text.DecimalFormatSymbols dfs = new java.text.DecimalFormatSymbols(); - totalcross.sys.Settings.thousandsSeparator = dfs.getGroupingSeparator(); - totalcross.sys.Settings.decimalSeparator = dfs.getDecimalSeparator(); - totalcross.sys.Settings.screenBPP = toBpp; - try { - totalcross.sys.Settings.screenWidthInDPI = totalcross.sys.Settings.screenHeightInDPI = Toolkit.getDefaultToolkit() - .getScreenResolution(); - } catch (Throwable t) { - totalcross.sys.Settings.screenWidthInDPI = 96; - } - totalcross.sys.Settings.romVersion = 0x02000000; - totalcross.sys.Settings.uiStyle = totalcross.sys.Settings.VISTA_UI; - totalcross.sys.Settings.screenWidth = toWidth; - totalcross.sys.Settings.screenHeight = toHeight; - totalcross.sys.Settings.onJavaSE = true; - totalcross.sys.Settings.platform = Settings.JAVA; - totalcross.sys.Settings.applicationId = getDefaultCrid(className); // dhaysmith@420_4 - totalcross.sys.Settings.deviceId = "Desktop"; // guich@568_2 - if (totalcross.sys.Settings.applicationId != null) { - getAppSettings(); // guich@330_47 - } - try { - // Fill all paths - String basePath = System.getProperty("user.dir"); - totalcross.sys.Settings.vmPath = basePath; - totalcross.sys.Settings.appPath = basePath; - // guich@tc112_21: commented - if (totalcross.sys.Settings.dataPath == null) totalcross.sys.Settings.dataPath = basePath; // flsobral@tc100b5_51: Settings.dataPath was being overwritten if set before the Launcher was initialized. - - if (totalcross.sys.Settings.appPath != null) // guich@582_17: make sure that it ends with a slash - { - if (totalcross.sys.Settings.appPath.indexOf('/') >= 0 && !totalcross.sys.Settings.appPath.endsWith("/")) { - totalcross.sys.Settings.appPath += "/"; - } else if (totalcross.sys.Settings.appPath.indexOf('\\') >= 0 - && !totalcross.sys.Settings.appPath.endsWith("\\")) { - totalcross.sys.Settings.appPath += "\\"; - } - } - totalcross.sys.Settings.userName = !isApplication ? null : java.lang.System.getProperty("user.name"); - } catch (SecurityException se) { - totalcross.sys.Settings.userName = null; - } - } - - @SuppressWarnings("deprecation") - public void settingsRefresh(boolean callStoreSettings) // guich@tc115_81 - { - java.util.TimeZone tz = java.util.TimeZone.getDefault(); // guich@340_33 - Settings.daylightSavingsMinutes = tz.getDSTSavings() / 60000; - Settings.daylightSavings = Settings.daylightSavingsMinutes != 0; - Settings.timeZone = tz.getRawOffset() / (60 * 60000); - Settings.timeZoneMinutes = tz.getRawOffset() / 60000; - Settings.timeZoneStr = java.util.TimeZone.getDefault().getID(); - if (callStoreSettings) { - try { - storeSettings(); - } catch (Exception e) { - } - } - } - - //// font and font metrics ////////////////////////////////////////////////////// - static final int AA_NO = 0; - static final int AA_4BPP = 1; - static final int AA_8BPP = 2; - private totalcross.util.Hashtable htLoadedFonts = new totalcross.util.Hashtable(31); - static Hashtable htBaseFonts = new Hashtable(5); - private Map loadedFontsMap = new HashMap<>(); - - static totalcross.ui.font.Font getBaseFont(String name, boolean bold, int size, String suffix) { - String key = name + "|" + bold + "|" + size + "|" + suffix; - totalcross.ui.font.Font f = (totalcross.ui.font.Font) htBaseFonts.get(key); - if (f == null) { - int i; - if (!name.endsWith("noaa")) { - TCZ z = (TCZ) loadedTCZs.get((name + ".tcz").toLowerCase()); - if (z == null) { - return null; - } - FontInfo fi = (FontInfo) z.bag; - for (i = 0; i < fi.sizes.length - 1; i++) { - if (size <= fi.sizes[i]) { - size = fi.sizes[i]; - break; - } - } - } - - int idx = Integer.parseInt(suffix.substring(suffix.indexOf('u') + 1)); - totalcross.ui.font.Font.baseChar = (char) idx; - f = totalcross.ui.font.Font.getFont(name, bold, size); - totalcross.ui.font.Font.baseChar = ' '; - if (f != null) { - f.removeFromCache(); - htBaseFonts.put(key, f); - } - } - - return f; - } - - private UserFont loadUF(String fontName, String suffix) { - boolean hasTriedToLoadBefore = loadedFontsMap.containsKey(fontName.toLowerCase()); - if (hasTriedToLoadBefore && loadedFontsMap.get(fontName.toLowerCase()) == null) { - return null; - } - try { - if (totalcross.ui.font.Font.baseChar == ' ' && !fontName.endsWith("noaa")) // test if there's another 8bpp native font. - base font - { - boolean bold = suffix.charAt(1) == 'b'; - int size = Integer.parseInt(suffix.substring(2, suffix.indexOf('u'))); - totalcross.ui.font.Font base = getBaseFont(fontName, bold, size, suffix); - if (base == null) { - new UserFont(fontName, suffix); // load sizes - base = getBaseFont(fontName, bold, size, suffix); - } - if (base != null) { - return new UserFont(fontName, suffix, size, base); - } - } - return new UserFont(fontName, suffix); - } catch (Exception e) { - String msg = "" + e.getMessage(); - if (!msg.startsWith("name") && !msg.endsWith("not found")) { - if (Settings.onJavaSE) { - e.printStackTrace(); - } - } - } - return null; - } - - public UserFont getFont(totalcross.ui.font.Font f, char c) { - UserFont uf = null; - try { - // verify if its in the cache. - String fontName = f.name; - int size = (int) (Math.max(f.size, totalcross.ui.font.Font.MIN_FONT_SIZE) * Settings.screenDensity); // guich@tc122_15: don't check for the maximum font size here - - char faceType = c < 0x3000 && f.style == 1 ? 'b' : 'p'; - int uIndex = ((int) c >> 8) << 8; - String suffix = "$" + faceType + size + "u" + uIndex; - String key = fontName + suffix; - uf = (UserFont) htLoadedFonts.get(key); - if (uf != null) { - return uf; - } - - boolean hasTriedToLoadBefore = loadedFontsMap.containsKey(fontName.toLowerCase()); - if (fontName.charAt(0) == '$') { - print("Native fonts are not supported on Desktop"); - } else if (!hasTriedToLoadBefore || loadedFontsMap.get(fontName.toLowerCase()) != null) { - // first, try to load the font itself using the current font pattern - uf = loadUF(fontName, suffix); - if (uf == null) { - uf = loadUF(fontName, "$p" + size + "u" + uIndex); // guich@tc122_15: ... check only here - } - if (uf == null && f.size != totalcross.ui.font.Font.NORMAL_SIZE) { - int t = f.size; - while (uf == null && ++t <= 120) { - uf = loadUF(fontName, "$p" + t + "u" + uIndex); - } - t = f.size; - while (uf == null && --t >= 5) { - uf = loadUF(fontName, "$p" + t + "u" + uIndex); - } - } - if (uf == null) { - uf = loadUF(fontName, "$" + faceType + totalcross.ui.font.Font.NORMAL_SIZE + "u" + uIndex); - } - if (uf == null && faceType != 'p') { - uf = loadUF(fontName, "$p" + totalcross.ui.font.Font.NORMAL_SIZE + "u" + uIndex); - } - } - - // at last, use the default font - if (uf == null) { - uf = loadUF(totalcross.ui.font.Font.DEFAULT, suffix); - } - if (uf == null && fontName.charAt(0) != '$') { - for (int i = totalcross.ui.font.Font.MIN_FONT_SIZE; i <= totalcross.ui.font.Font.MAX_FONT_SIZE; i++) { - if ((uf = loadUF(fontName, "$p" + i + "u" + uIndex)) != null) { - break; - } - } - } - if (uf == null) { - for (int i = totalcross.ui.font.Font.MIN_FONT_SIZE; i <= totalcross.ui.font.Font.MAX_FONT_SIZE; i++) { - if ((uf = loadUF(totalcross.ui.font.Font.DEFAULT, "$p" + i + "u" + uIndex)) != null) { - break; - } - } - } - - if (uf != null) { - if (totalcross.ui.font.Font.baseChar == ' ') { - htLoadedFonts.put(key, uf); // note that we will use the original key to avoid entering all exception handlers. - } - f.name = uf.fontName; // update the name, the font may have been replaced. - } else if (htLoadedFonts.size() > 0) { - return c == ' ' ? null : getFont(f, ' '); // probably the index was outside the available ranges at this font - guich@tc110_28: if space, just return null - } else if (appletInitialized) // guich@500_1: when retroguard is loaded, Applet.init is never called, so we just skip here. - { - System.err.println("No fonts found! be sure to place the file " + totalcross.ui.font.Font.DEFAULT - + ".tcz in the same directory from where you're running your application" - + (isApplication ? " or put a reference to TotalCross3/etc folder in the classpath!" - : "or in your applet's codebase or in a jar file!")); - System.exit(2); - } - } catch (Exception e) { - System.out.println("Launcher.getFont: " + e); - } - return uf; - } - - /** Represents the internal font structure, read from a pdb file. used internally. */ - // created by guich@200b2 - public static class CharBits // pgr@402_50 - describe the bitmap for a given character - { - public int rowWIB; // width in bytes - public byte[] charBitmapTable; - public int offset; // offset relative to the bitmap table - public int width; - public int index; - public totalcross.ui.image.Image img; - } - - private static Hashtable loadedTCZs = new Hashtable(31); - - class FontInfo { - totalcross.io.ByteArrayStream chunks[]; - int[] sizes; - } - - public class UserFont { - // 25/120 14/70 4/25 2/15 - public UserFont ubase; - public totalcross.ui.image.Image[] nativeFonts; // stores the system font in some platforms - public int antialiased; // AA_ flags - public int firstChar; // ASCII code of first character - public int lastChar; // ASCII code of last character - public int spaceWidth; // width of the space char - public int maxWidth; // width of font rectangle - unused - public int maxHeight; // height of font rectangle - public int owTLoc; // offset to offset/width table - unused - public int ascent; // ascent - public int descent; // descent - public int rowWords; // row width of bit image / 2 - used only to compute rowWidthInBytes - - private int rowWidthInBytes; - private byte[] bitmapTable; - private int[] bitIndexTable; - private String fontName; - private int numberWidth; - private int minusW; - - private UserFont(String fontName, String sufix, int size, totalcross.ui.font.Font base) throws Exception { - UserFont ubase = (UserFont) base.hv_UserFont; - this.ubase = ubase; - this.maxHeight = size; - this.rowWidthInBytes = ubase.rowWidthInBytes * maxHeight / ubase.maxHeight; - this.bitIndexTable = new int[ubase.bitIndexTable.length]; - for (int i = 0; i < bitIndexTable.length; i++) { - this.bitIndexTable[i] = ubase.bitIndexTable[i] * maxHeight / ubase.maxHeight; - } - this.nativeFonts = new totalcross.ui.image.Image[bitIndexTable.length]; - this.fontName = base.name; - this.firstChar = ubase.firstChar; - this.lastChar = ubase.lastChar; - this.antialiased = ubase.antialiased; - this.descent = ubase.descent * maxHeight / ubase.maxHeight; - this.ascent = size - this.descent; - this.numberWidth = ubase.numberWidth * maxHeight / ubase.maxHeight; - this.spaceWidth = ubase.spaceWidth * maxHeight / ubase.maxHeight; - this.minusW = ubase.minusW; - } - - private UserFont(String fontName, String sufix) throws Exception { - this.fontName = fontName; - String fileName = fontName + ".tcz"; - TCZ z = (TCZ) loadedTCZs.get(fileName.toLowerCase()); - if (z == null) { - InputStream is = openInputStream(fileName); - if (is == null) { - is = openInputStream("vm/" + fileName); // for the release sdk, there's no etc/fonts. the tcfont.tcz is located at dist/vm/tcfont.tcz - if (is == null) { - is = openInputStream("etc/fonts/" + fileName); // if looking for the default font when debugging, use etc/fonts - if (is == null) { - loadedFontsMap.put(fontName.toLowerCase(), null); - throw new Exception("file " + fileName + " " + sufix + " not found"); // loaded = false - } - } - } - z = new TCZ(new IS2S(is)); - FontInfo fi = new FontInfo(); - int n = z.numberOfChunks; - fi.chunks = new totalcross.io.ByteArrayStream[n]; - totalcross.util.IntVector sizes = new totalcross.util.IntVector(n / 2); - for (int i = 0; i < n; i++) { - int s = z.getNextChunkSize(); - fi.chunks[i] = new totalcross.io.ByteArrayStream(s); - z.readNextChunk(fi.chunks[i]); - // compute size - $p20u0 - String name = z.names[i]; - String ss = name.substring(name.lastIndexOf('$') + 2, name.lastIndexOf('u')); - int size = toInt(ss); - if (!sizes.contains(size)) { - sizes.addElement(size); - } - } - sizes.qsort(); - fi.sizes = sizes.toIntArray(); - z.bag = fi; - loadedTCZs.put(fileName.toLowerCase(), z); - loadedFontsMap.put(fontName.toLowerCase(), fileName); - } - fontName += sufix; - int index = z.findNamePosition(fontName.toLowerCase()); - if (index == -1) { - throw new Exception("name " + fontName + " not found"); // loaded = false - } - - totalcross.io.ByteArrayStream bas = ((FontInfo) z.bag).chunks[index]; - bas.reset(); - totalcross.io.DataStreamLE ds = new totalcross.io.DataStreamLE(bas); - antialiased = ds.readUnsignedShort(); - firstChar = ds.readUnsignedShort(); - lastChar = ds.readUnsignedShort(); - spaceWidth = ds.readUnsignedShort(); - maxWidth = ds.readUnsignedShort(); - maxHeight = ds.readUnsignedShort(); - owTLoc = ds.readUnsignedShort(); - ascent = ds.readUnsignedShort(); - descent = ds.readUnsignedShort(); - rowWords = ds.readUnsignedShort(); - - rowWidthInBytes = 2 * rowWords * (antialiased == AA_NO ? 1 : antialiased == AA_4BPP ? 4 : 8); - int bitmapTableSize = (int) rowWidthInBytes * (int) maxHeight; - - bitmapTable = new byte[bitmapTableSize]; - ds.readBytes(bitmapTable); - bitIndexTable = new int[lastChar - firstChar + 1 + 1]; - for (int i = 0; i < bitIndexTable.length; i++) { - bitIndexTable[i] = ds.readUnsignedShort(); - } - // - minusW = antialiased == AA_8BPP && fontName.equals("TCFont") ? 1 : 0; - if (firstChar <= '0' && '0' <= lastChar) { - index = (int) '0' - (int) firstChar; - numberWidth = bitIndexTable[index + 1] - bitIndexTable[index] - minusW; - } - if (antialiased == AA_8BPP) { - nativeFonts = new totalcross.ui.image.Image[bitIndexTable.length]; - } - } - - private totalcross.ui.image.Image getBaseCharImage(int index) throws totalcross.ui.image.ImageException // called only in ubase instances - { - if (bitmapTable == null && ubase != null) { - return ubase.getBaseCharImage(index); - } - int offset = bitIndexTable[index]; - int width = bitIndexTable[index + 1] - offset - minusW; - totalcross.ui.image.Image img = new totalcross.ui.image.Image(width, maxHeight); - int[] pixels = img.getPixels(); - for (int y = 0, idx = 0; y < maxHeight; y++) { - for (int x = 0; x < width; x++, idx++) { - pixels[idx] = bitmapTable[y * rowWidthInBytes + x + offset] << 24; - } - } - return img; - } - - // Get the source x coordinate and width of the character - public void setCharBits(char ch, CharBits bits) { - if (firstChar <= ch && ch <= lastChar) { - int index = (int) ch - (int) firstChar; - bits.index = index; - bits.rowWIB = rowWidthInBytes; - bits.charBitmapTable = bitmapTable; - bits.offset = bitIndexTable[index]; - bits.width = bitIndexTable[index + 1] - bits.offset - minusW; - if (bits.width == 0) { - bits.width += minusW; - } - if (ubase != null && ubase.nativeFonts != null) { - try { - if (ubase.nativeFonts[index] == null) { - ubase.nativeFonts[index] = ubase.getBaseCharImage(index); - } - if (nativeFonts[index] == null) { - nativeFonts[index] = ubase.nativeFonts[index].getHwScaledInstance(bits.width, maxHeight); - } - bits.img = nativeFonts[index]; - bits.rowWIB = bits.width; - } catch (Exception e) { - if (Settings.showDesktopMessages) { - e.printStackTrace(); - } - bits.width = spaceWidth; - bits.offset = -1; - } - } - } else { - bits.width = spaceWidth; - bits.offset = -1; - } - } - } - - public int getCharWidth(totalcross.ui.font.Font f, char ch) // guich@tc122_16: moved to outside UserFont, because each char may be in a different UserFont - { - UserFont font = (UserFont) f.hv_UserFont; - if (ch < font.firstChar || ch > font.lastChar) { - f.hv_UserFont = font = Launcher.instance.getFont(f, ch); - } - if (ch == 160) { - return font.numberWidth; - } - if (ch < ' ') { - return (ch == '\t') ? font.spaceWidth * totalcross.ui.font.Font.TAB_SIZE : 0; // guich@tc100: handle tabs - } - int index = (int) ch - (int) font.firstChar; - return (font.firstChar <= ch && ch <= font.lastChar) - ? font.bitIndexTable[index + 1] - font.bitIndexTable[index] - font.minusW : font.spaceWidth; - } - - private class AlertBox extends Frame implements java.awt.event.ActionListener { - private java.awt.Button ok; - private java.awt.TextArea ta; - - public AlertBox() { - super("Alert"); - setLayout(new BorderLayout()); - add("Center", ta = new java.awt.TextArea()); - Panel p = new Panel(); - p.setLayout(new FlowLayout()); - p.add(ok = new java.awt.Button("Ok")); - ok.addActionListener(this); - add("South", p); - pack(); - Dimension d = getToolkit().getScreenSize(); - setLocation(d.width / 3, d.height / 3); - } - - @Override - public void actionPerformed(java.awt.event.ActionEvent ae) { - if (ae.getSource() == ok) { - setVisible(false); - } - } - - public void setText(String s) { - ta.setText(s); - } - } - - public void alert(String msg) { - if (!started) { - System.out.println("Alert: " + msg); - } else { - alert.setText(msg); - alert.setVisible(true); - while (alert.isVisible()) { - try { - Thread.sleep(10); - } catch (Exception e) { - } - } - } - } - - /** Converts a java.io.InputStream into a totalcross.io.Stream */ - public static class IS2S extends totalcross.io.Stream { - InputStream is; - - public IS2S(InputStream is) { - this.is = is; - } - - @Override - public void close() { - try { - is.close(); - } catch (Exception e) { - } - is = null; - } - - @Override - public int readBytes(byte[] buf, int start, int count) { - try { - return is.read(buf, start, count); - } catch (Exception e) { - return -1; - } - } - - @Override - public int writeBytes(byte[] buf, int start, int count) { - return 0; // not supported - } - } - - public static class S2FIS extends java.io.FilterInputStream { - private RandomAccessStream s; - private int pos = -1; - private int readLimit = -1; - - public S2FIS(RandomAccessStream s) { - this(s, -1, true); - } - - public S2FIS(RandomAccessStream s, int max) { - this(s, max, true); - } - - public S2FIS(RandomAccessStream s, int max, boolean closeUnderlying) { - super(new S2IS(s, max, closeUnderlying)); - this.s = s; - } - - @Override - public synchronized void mark(int readlimit) { - try { - this.pos = s.getPos(); - this.readLimit = readlimit; - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public synchronized void reset() throws java.io.IOException { - if (this.pos == -1) { - throw new java.io.IOException("the stream has not been marked"); - } - if (s.getPos() - this.pos > readLimit) { - throw new java.io.IOException("the mark has been invalidated"); - } - s.setPos(this.pos); - } - - @Override - public boolean markSupported() { - return true; - } - } - - public static class S2IS extends java.io.InputStream { - private Stream s; - private byte[] oneByte = new byte[1]; - private int left; - private boolean closeUnderlying; - - public S2IS(Stream s) { - this(s, -1, true); - } - - public S2IS(Stream s, int max) { - this(s, max, true); - } - - public S2IS(Stream s, int max, boolean closeUnderlying) { - this.s = s; - this.left = max; - this.closeUnderlying = closeUnderlying; - } - - @Override - public int read() throws java.io.IOException { - if (left == 0) { - return -1; - } - - try { - int r = s.readBytes(oneByte, 0, 1); - - if (left != -1 && r == 1) { - left--; - } - - return r > 0 ? ((int) oneByte[0] & 0xFF) : -1; - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - - @Override - public int read(byte[] buf, int off, int len) throws java.io.IOException { - if (left == 0) { - return -1; - } - - try { - if (left != -1 && len > left) { - len = left; - } - - int r = s.readBytes(buf, off, len); - - if (left != -1 && r > 0) { - left -= r; - } - - return r; - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - - @Override - public void close() throws java.io.IOException { - if (closeUnderlying) { - try { - s.close(); - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - } - } - - public static class S2OS extends java.io.OutputStream { - private Stream s; - private byte[] oneByte = new byte[1]; - private int count; - private boolean closeUnderlying; - - public S2OS(Stream s) { - this(s, true); - } - - public S2OS(Stream s, boolean closeUnderlying) { - this.s = s; - this.closeUnderlying = closeUnderlying; - } - - public int count() { - return count; - } - - @Override - public void write(int b) throws java.io.IOException { - try { - oneByte[0] = (byte) (b & 0xFF); - - int c = s.writeBytes(oneByte, 0, 1); - if (c < 0) { - throw new java.io.IOException("Unknown error when writing to stream"); - } - count++; - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - - @Override - public void write(byte[] b, int off, int len) throws java.io.IOException { - try { - int c = s.writeBytes(b, off, len); - if (c < 0) { - throw new java.io.IOException("Unknown error when writing to stream"); - } - count += c; - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - - @Override - public void close() throws java.io.IOException { - if (closeUnderlying) { - try { - s.close(); - } catch (IOException e) { - throw new java.io.IOException(e.getMessage()); - } - } - } - } - - public void setTitle(String title) { - if (isApplication) { - frameTitle = title; - if (frame != null) { - frame.setTitle(title); - } - } - } - - public void vibrate(final int millis) { - if (isApplication && frame != null) { - new Thread() { - @Override - public void run() { - Point p = frame.getLocation(); - int x = p.x, y = p.y; - - int[] xPoints = { x - 3, x, x + 3, x, x + 3, x, x - 3, x }; - int[] yPoints = { y - 3, y, y + 3, y, y - 3, y, y + 3, y }; - int i = 0; - int j = 0; - - int t = Vm.getTimeStamp(); - do { - frame.setLocation(xPoints[i], yPoints[j]); - - i = ++i % xPoints.length; - if (i == 0) { - j = ++j % yPoints.length; - } - - Thread.yield();// give some time for the other threads to execute - } while (Vm.getTimeStamp() - t < millis); - - frame.setLocation(x, y); // restore original location - } - }.start(); - } - } - - public void setSIP(int option, Control edit, boolean secret) { - } - - @Override - public void componentHidden(ComponentEvent arg0) { - } - - @Override - public void componentMoved(ComponentEvent arg0) { - } - - @Override - public void componentShown(ComponentEvent arg0) { - } - - boolean ignoreNextResize; // guich@tc168: ignore when using F9 - - @Override - public void componentResized(ComponentEvent ev) { - if (ignoreNextResize) { - ignoreNextResize = false; - return; - } - int w = frame.getWidth() - frame.insets.left - frame.insets.right; - int h = frame.getHeight() - frame.insets.top - frame.insets.bottom; - w /= toScale; // guich@tc168: consider scale - h /= toScale; - if (w < toWidth || h < toHeight) { - screenResized(w >= toWidth ? w : toWidth, h >= toHeight ? h : toHeight, true); - } else { - screenResized(w, h, false); - } + LauncherRuntime.startApplication(args[args.length - 1], Arrays.copyOf(args, args.length - 1)); } } diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherArgumentParser.java b/TotalCrossSDK/src/main/java/totalcross/LauncherArgumentParser.java new file mode 100644 index 000000000..ca171c647 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherArgumentParser.java @@ -0,0 +1,271 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.math.BigDecimal; + +import totalcross.sys.Settings; + +/** + * Parses legacy desktop launcher arguments into an incremental runtime config. + */ +final class LauncherArgumentParser { + private LauncherArgumentParser() { + } + + static LauncherParsedConfig parse(LauncherConfig config, boolean application, int fallbackWidth, int fallbackHeight) + throws InvalidArgumentException { + String[] args = config.getLauncherArgs(); + LauncherParsedConfig result = new LauncherParsedConfig(config.getMainWindowClass()); + int n = args.length; + int i = 0; + try { + for (i = 0; i < n; i++) { + if (args[i].equalsIgnoreCase("/fontsize")) { + result.userFontSize = toInt(args[++i]); + } else if (args[i].equalsIgnoreCase("/dataPath")) { + result.dataPath = args[++i]; + System.out.println("Data path is " + result.dataPath); + } else if (args[i].equalsIgnoreCase("/scr")) { + parseScreen(result, args[++i]); + System.out.println("Screen is " + result.width + "x" + result.height + "x" + result.bpp); + } else if (args[i].equalsIgnoreCase("/fullscreen")) { + result.fullscreen = true; + } else if (args[i].equalsIgnoreCase("/safeAreaPortrait")) { + result.insetsPortrait = parseInsets(args[++i], + "Argument /safeAreaPortrait expects 4 comma separated values in the following format: top,left,bottom,right"); + } else if (args[i].equalsIgnoreCase("/safeAreaLandscape")) { + result.insetsLandscape = parseInsets(args[++i], + "Argument /safeAreaLandscape expects 4 comma separated values in the following format: top,left,bottom,right"); + } else if (args[i].equalsIgnoreCase("/r")) { + ++i; + } else if (args[i].equalsIgnoreCase("/pos")) { + String[] scr = tokenizeString(args[++i].toLowerCase(), ','); + if (scr.length == 1) { + throw new Exception(); + } + result.x = toInt(scr[0]); + result.y = toInt(scr[1]); + } else if (args[i].equalsIgnoreCase("/cmdline")) { + result.commandLine = ""; + while (++i < n) { + result.commandLine += args[i] + " "; + } + result.commandLine = result.commandLine.trim(); + System.out.println("Command line is '" + result.commandLine + "'"); + } else if (args[i].equalsIgnoreCase("/uiStyle")) { + result.uiStyle = parseUiStyle(args[++i]); + System.out.println("UI style is " + result.uiStyle); + } else if (args[i].equalsIgnoreCase("/penlessDevice")) { + result.keyboardFocusTraversable = true; + System.out.println("Penless device is on"); + } else if (args[i].equalsIgnoreCase("/fingertouch")) { + result.fingerTouch = true; + System.out.println("Finger touch is on"); + } else if (args[i].equalsIgnoreCase("/unmovablesip")) { + result.unmovableSIP = true; + System.out.println("Unmovable SIP is on"); + } else if (args[i].equalsIgnoreCase("/geofocus")) { + result.geographicalFocus = true; + result.keyboardFocusTraversable = true; + System.out.println("Geographical focus is on"); + } else if (args[i].equalsIgnoreCase("/virtualKeyboard")) { + result.virtualKeyboard = true; + System.out.println("Virtual keyboard is on"); + } else if (args[i].equalsIgnoreCase("/bpp")) { + result.bpp = toInt(args[++i]); + if (result.bpp != 8 && result.bpp != 16 && result.bpp != 24 && result.bpp != 32) { + throw new Exception(); + } + System.out.println("Bpp is " + result.bpp); + } else if (args[i].equalsIgnoreCase("/scale")) { + BigDecimal scaleDecimal = new BigDecimal(args[++i]); + if (scaleDecimal.compareTo(BigDecimal.ZERO) < 0 || scaleDecimal.compareTo(BigDecimal.valueOf(8)) > 0) { + throw new Exception(); + } + result.scaleValue = scaleDecimal.doubleValue(); + System.out.println("Scale is " + result.scaleValue); + } else if (args[i].equalsIgnoreCase("/fastscale")) { + result.fastScale = true; + } else if (args[i].equalsIgnoreCase("/showmousepos")) { + result.showMousePosition = true; + } else if (args[i].equalsIgnoreCase("/demo")) { + result.demo = true; + } else if (args[i].equalsIgnoreCase("/density")) { + BigDecimal densityDecimal = new BigDecimal(args[++i]); + if (densityDecimal.compareTo(BigDecimal.ZERO) <= 0 || densityDecimal.compareTo(BigDecimal.valueOf(4)) > 0) { + throw new Exception(); + } + result.densityValue = densityDecimal.doubleValue(); + } else if (args[i].equalsIgnoreCase("/dbginfo")) { + result.showDebugMessages = true; + } else { + throw new Exception(); + } + } + } catch (Exception e) { + throw new InvalidArgumentException(i, argumentAt(args, i), fullCommandLine(args), e); + } + + if (result.width == -1 || result.height == -1) { + if (application) { + result.width = 320; + result.height = 568; + } else { + result.width = fallbackWidth; + result.height = fallbackHeight; + } + } + + result.width *= result.densityValue; + result.height *= result.densityValue; + scaleInsets(result.insetsPortrait, result.densityValue); + scaleInsets(result.insetsLandscape, result.densityValue); + + if (result.scaleValue == -1) { + Rectangle r = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getDefaultScreenDevice() + .getDefaultConfiguration() + .getBounds(); + + double useableArea = 0.88; + int viewportW = (int) (result.width / result.densityValue); + int viewportH = (int) (result.height / result.densityValue); + double maxRatio = Math.max((double) viewportW / r.width, (double) viewportH / r.height); + if (maxRatio > useableArea) { + result.scaleValue = useableArea / maxRatio; + } + } + result.scale = Math.abs(result.scaleValue) / result.densityValue; + return result; + } + + private static void parseScreen(LauncherParsedConfig result, String value) throws Exception { + if (value.equalsIgnoreCase("win32")) { + result.width = 240; + result.height = 320; + result.bpp = 24; + } else if (value.equalsIgnoreCase("iPhone")) { + result.width = 393; + result.height = 852; + result.bpp = 24; + result.densityValue = 3; + result.insetsPortrait = new totalcross.ui.Insets(59, 0, 34, 0); + result.insetsLandscape = new totalcross.ui.Insets(0, 59, 21, 59); + } else if (value.equalsIgnoreCase("iPhoneSE")) { + result.width = 375; + result.height = 667; + result.bpp = 24; + result.densityValue = 2; + } else if (value.equalsIgnoreCase("ipad")) { + result.width = 768; + result.height = 1024; + result.bpp = 24; + result.densityValue = 2; + } else if (value.equalsIgnoreCase("android")) { + result.width = 360; + result.height = 592; + result.bpp = 24; + result.densityValue = 2; + } else { + String[] scr = tokenizeString(value.toLowerCase(), 'x'); + if (scr.length == 1) { + throw new Exception(); + } + result.width = toInt(scr[0]); + result.height = toInt(scr[1]); + if (scr.length == 3) { + result.bpp = toInt(scr[2]); + } + } + } + + private static totalcross.ui.Insets parseInsets(String value, String message) throws Exception { + String[] scr = tokenizeString(value.toLowerCase(), ','); + if (scr.length != 4) { + throw new Exception(message); + } + return new totalcross.ui.Insets(toInt(scr[0]), toInt(scr[1]), toInt(scr[2]), toInt(scr[3])); + } + + private static int parseUiStyle(String value) throws Exception { + if (value.equalsIgnoreCase("Flat")) { + return Settings.FLAT_UI; + } else if (value.equalsIgnoreCase("Vista")) { + return Settings.VISTA_UI; + } else if (value.equalsIgnoreCase("Android")) { + return Settings.ANDROID_UI; + } else if (value.equalsIgnoreCase("Holo")) { + return Settings.HOLO_UI; + } else if (value.equalsIgnoreCase("Material")) { + return Settings.MATERIAL_UI; + } + throw new Exception(); + } + + private static void scaleInsets(totalcross.ui.Insets insets, double density) { + if (insets != null) { + insets.top *= density; + insets.left *= density; + insets.bottom *= density; + insets.right *= density; + } + } + + private static int toInt(String s) { + try { + return Integer.parseInt(s); + } catch (Exception e) { + return 0; + } + } + + private static String[] tokenizeString(String string, char c) { + java.util.StringTokenizer st = new java.util.StringTokenizer(string, "" + c); + String[] ret = new String[st.countTokens()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = st.nextToken(); + } + return ret; + } + + private static String argumentAt(String[] args, int index) { + return index >= 0 && index < args.length ? args[index] : ""; + } + + private static String fullCommandLine(String[] args) { + String s = ""; + for (int i = 0; i < args.length; i++) { + s += " " + args[i]; + } + return s.trim(); + } + + static final class InvalidArgumentException extends Exception { + private final int index; + private final String argument; + private final String fullCommandLine; + + InvalidArgumentException(int index, String argument, String fullCommandLine, Throwable cause) { + super(cause.getMessage(), cause); + this.index = index; + this.argument = argument; + this.fullCommandLine = fullCommandLine; + } + + int getIndex() { + return index; + } + + String getArgument() { + return argument; + } + + String getFullCommandLine() { + return fullCommandLine; + } + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherArguments.java b/TotalCrossSDK/src/main/java/totalcross/LauncherArguments.java new file mode 100644 index 000000000..1f923d29a --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherArguments.java @@ -0,0 +1,263 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherArguments extends LauncherState { + static void showInstructions() { + System.out.println("Possible Arguments (in any order and case insensitive). Default is marked as *"); + System.out.println(" /scr WIDTHxHEIGHT : sets the width and height resolution."); + System.out.println(" /scr WIDTHxHEIGHTxBPP : sets the width, height and bits per pixel (8, 16, 24 or 32)"); + System.out.println(" /density <0.1 to 4> : sets the screen pixel density"); + System.out.println(" /scr win32 : Windows 32 (same of /scr 240x320x24)"); + System.out.println("* /scr android : Android (same of /scr 360x592x24 /density 2)"); + System.out.println(" /scr iPhone : iPhone 15 resolution (same of /scr 393x852x24 /density 3 /safeAreaPortrait 59,0,34,0 /safeAreaLandscape 0,59,21,59)"); + System.out.println(" /scr iPhoneSE : iPhone SE resolution (same of /scr 375x667x24 /density 2)"); + System.out.println(" /scr ipad : iPad resolution (same of /scr 768x1024x24 /density 2)"); + System.out.println(" /fullscreen : Use full-screen window"); + System.out.println(" /pos x,y : Sets the openning position of the application"); + System.out.println(" /uiStyle Flat : Flat user interface style"); + System.out.println("* /uiStyle Vista : Vista user interface style"); + System.out.println(" /uiStyle Android : Android 4 user interface style"); + System.out.println(" /uiStyle Holo : Android 5 user interface style"); + System.out.println(" /uiStyle Material: Material 6 user interface style"); + System.out.println(" /penlessDevice : acts as a device that has no touchscreen."); + System.out.println(" /fingerTouch : acts as a device that uses a finger instead of a pen."); + System.out.println(" /unmovablesip : acts as a device whose SIP is unmovable (like in Android and iPhone)."); + System.out.println(" /geofocus : enables geographical focus."); + System.out.println(" /virtualKeyboard : shows the virtual keyboard when in an Edit or a MultiEdit"); + System.out.println(" /showmousepos : shows the mouse position."); + System.out.println(" /bpp 8 : emulates 8 bits per pixel screens (256 colors)"); + System.out.println(" /bpp 16 : emulates 16 bits per pixel screens (64K colors)"); + System.out.println(" /bpp 24 : emulates 24 bits per pixel screens (16M colors)"); + System.out.println(" /bpp 32 : emulates 32 bits per pixel screens (16M colors without transparency)"); + System.out.println(" /scale <0.1 to 8>: scales the screen, using by default a method that gives higher priority to image smoothness than scaling speed."); + System.out.println(" /fastscale : combined with scale, changes its default scaling method for one that gives higher priority to scaling speed than smoothness of the scaled image."); + System.out.println(" /dataPath : sets where the PDB and media files are stored"); + System.out.println(" /cmdLine <...> : the rest of arguments-1 are passed as the command line"); + System.out.println(" /fontSize : set the default font size to the one passed as parameter"); + System.out.println(" /safeAreaPortrait top,left,bottom,right : sets a margin from the device borders to simulate devices with notch on portrait"); + System.out.println(" /safeAreaLandscape top,left,bottom,right : sets a margin from the device borders to simulate devices with notch on landscape"); + System.out.println("The class name that extends MainWindow must always be the last argument"); + System.out.println("Please notice that the Launcher automatically scales down the resolution to fit in the display, to disable this behavior you may include the argument scale with the value 1"); + } + + public static void main(String args[]) { + if (args.length == 0 || args[0].equals("/help")) { + if (args.length == 0) { + showInstructions(); + } + args = new String[] { "/scr", "480x620x32", "/fontsize", "16", "tc.Help" }; + } + isApplication = true; + LauncherRuntime.startApplication(args[args.length - 1], Arrays.copyOf(args, args.length - 1)); + } + + protected int toInt(String s) // Convert.toInt can't be used here, otherwise, the settings will be set too early! + { + try { + return Integer.parseInt(s); + } catch (Exception e) { + return 0; + } + } + + protected void parseArguments(String... args) { + parseArguments(args[args.length - 1], Arrays.copyOf(args, args.length - 1)); + } + + protected void parseArguments(String clazz, String... args) { + LauncherConfig config = new LauncherConfig(clazz, args); + try { + getRuntime().parseLauncherArguments((Launcher) this, config, isApplication, getSize().width, getSize().height); + } catch (LauncherArgumentParser.InvalidArgumentException e) { + showInstructions(); + System.err.println("Invalid or incomplete argument at position " + e.getIndex() + ": " + e.getArgument()); + System.err.println(e.getMessage()); + System.err.println("Full command line:\n" + e.getFullCommandLine()); + exit(-1); + return; + } + } + + void applyParsedArguments(LauncherParsedConfig result) { + className = result.className; + toWidth = result.width; + toHeight = result.height; + toBpp = result.bpp; + commandLine = result.commandLine; + toUI = result.uiStyle; + toScaleValue = result.scaleValue; + toDensityValue = result.densityValue; + toScale = result.scale; + fastScale = result.fastScale; + isDemo = result.demo; + toInsetsPortrait = result.insetsPortrait; + toInsetsLandscape = result.insetsLandscape; + } + + protected String[] tokenizeString(String string, char c) { + java.util.StringTokenizer st = new java.util.StringTokenizer(string, "" + c); + String[] ret = new String[st.countTokens()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = st.nextToken(); + } + return ret; + } + + public void start() { + mainWindow = MainWindow.getMainWindow(); + } + + void setAppletArguments(String appletArguments) { + this.appletArguments = appletArguments; + } + + void setAppletCodeBase(URL appletCodeBase) { + this.appletCodeBase = appletCodeBase; + } + + void setAppletParameter(String name, String value) { + if (name != null && value != null) { + appletParameters.put(name, value); + } + } + + protected String getLauncherParameter(String name) { + return appletParameters.get(name); + } + + protected URL getLauncherCodeBase() throws java.net.MalformedURLException { + return appletCodeBase == null ? new File(".").toURI().toURL() : appletCodeBase; + } + + void setRuntime(LauncherRuntime runtime) { + this.runtime = runtime; + } + + ///////// guich@200b2: to make the vm easier to port, i removed all methods from the TotalCross classes that uses the jdk classes ///////// + public void registerMainWindow(totalcross.ui.MainWindow main) { + (winTimer = new WinTimer()).start(); // guich@510_2: start the timer only after we had added the others + } + + public void setTimerInterval(int milliseconds) { + winTimer.setInterval(milliseconds); + } + + public void exit(int exitCode) { + destroy(); // guich@230_24 + if (isApplication) { + System.exit(exitCode); + } + } + + public void minimize() { + if (hasWindowBackend()) { + minimizeWindow(); + } + } + + public void restore() { + if (hasWindowBackend()) { + restoreWindow(); + } + } + + @Override + public void print(java.awt.Graphics g) { + } + + @Override + public boolean isFocusTraversable() // guich@512_1: inform that we want to handle tab + { + return true; + } + + protected int modifiers; + + protected void updateModifiers(java.awt.event.KeyEvent event) { + if (event.isShiftDown()) { + keysPressed.put(SpecialKeys.SHIFT, 1); + modifiers |= SpecialKeys.SHIFT; + } else { + keysPressed.put(SpecialKeys.SHIFT, 0); + modifiers &= ~SpecialKeys.SHIFT; + } + if (event.isControlDown()) { + keysPressed.put(SpecialKeys.CONTROL, 1); + modifiers |= SpecialKeys.CONTROL; + } else { + keysPressed.put(SpecialKeys.CONTROL, 0); + modifiers &= ~SpecialKeys.CONTROL; + } + if (event.isAltDown()) { + keysPressed.put(SpecialKeys.ALT, 1); + modifiers |= SpecialKeys.ALT; + } else { + keysPressed.put(SpecialKeys.ALT, 0); + modifiers &= ~SpecialKeys.ALT; + } + } + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherConfig.java b/TotalCrossSDK/src/main/java/totalcross/LauncherConfig.java new file mode 100644 index 000000000..43fb76f00 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherConfig.java @@ -0,0 +1,32 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +/** + * Immutable launcher startup configuration. + *

+ * This is intentionally small for the transition: it captures the entry point + * and raw launcher arguments first, while the legacy Launcher parser still owns + * detailed screen/window settings. + */ +public final class LauncherConfig { + private final String mainWindowClass; + private final String[] launcherArgs; + + public LauncherConfig(String mainWindowClass, String... launcherArgs) { + if (mainWindowClass == null || mainWindowClass.length() == 0) { + throw new IllegalArgumentException("mainWindowClass cannot be empty"); + } + this.mainWindowClass = mainWindowClass; + this.launcherArgs = launcherArgs == null ? new String[0] : launcherArgs.clone(); + } + + public String getMainWindowClass() { + return mainWindowClass; + } + + public String[] getLauncherArgs() { + return launcherArgs.clone(); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherFontTypes.java b/TotalCrossSDK/src/main/java/totalcross/LauncherFontTypes.java new file mode 100644 index 000000000..afb07fd7a --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherFontTypes.java @@ -0,0 +1,297 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherFontTypes extends LauncherStreamTypes { + static final int AA_NO = 0; + static final int AA_4BPP = 1; + static final int AA_8BPP = 2; + protected totalcross.util.Hashtable htLoadedFonts = new totalcross.util.Hashtable(31); + static Hashtable htBaseFonts = new Hashtable(5); + protected Map loadedFontsMap = new HashMap<>(); + public static class CharBits // pgr@402_50 - describe the bitmap for a given character + { + public int rowWIB; // width in bytes + public byte[] charBitmapTable; + public int offset; // offset relative to the bitmap table + public int width; + public int index; + public totalcross.ui.image.Image img; + } + + protected static Hashtable loadedTCZs = new Hashtable(31); + + protected int toInt(String value) { + try { + return Integer.parseInt(value); + } catch (Exception e) { + return 0; + } + } + + protected InputStream openInputStream(String path) { + return null; + } + + class FontInfo { + totalcross.io.ByteArrayStream chunks[]; + int[] sizes; + } + + public class UserFont { + // 25/120 14/70 4/25 2/15 + public UserFont ubase; + public totalcross.ui.image.Image[] nativeFonts; // stores the system font in some platforms + public int antialiased; // AA_ flags + public int firstChar; // ASCII code of first character + public int lastChar; // ASCII code of last character + public int spaceWidth; // width of the space char + public int maxWidth; // width of font rectangle - unused + public int maxHeight; // height of font rectangle + public int owTLoc; // offset to offset/width table - unused + public int ascent; // ascent + public int descent; // descent + public int rowWords; // row width of bit image / 2 - used only to compute rowWidthInBytes + + protected int rowWidthInBytes; + protected byte[] bitmapTable; + protected int[] bitIndexTable; + protected String fontName; + protected int numberWidth; + protected int minusW; + + protected UserFont(String fontName, String sufix, int size, totalcross.ui.font.Font base) throws Exception { + UserFont ubase = (UserFont) base.hv_UserFont; + this.ubase = ubase; + this.maxHeight = size; + this.rowWidthInBytes = ubase.rowWidthInBytes * maxHeight / ubase.maxHeight; + this.bitIndexTable = new int[ubase.bitIndexTable.length]; + for (int i = 0; i < bitIndexTable.length; i++) { + this.bitIndexTable[i] = ubase.bitIndexTable[i] * maxHeight / ubase.maxHeight; + } + this.nativeFonts = new totalcross.ui.image.Image[bitIndexTable.length]; + this.fontName = base.name; + this.firstChar = ubase.firstChar; + this.lastChar = ubase.lastChar; + this.antialiased = ubase.antialiased; + this.descent = ubase.descent * maxHeight / ubase.maxHeight; + this.ascent = size - this.descent; + this.numberWidth = ubase.numberWidth * maxHeight / ubase.maxHeight; + this.spaceWidth = ubase.spaceWidth * maxHeight / ubase.maxHeight; + this.minusW = ubase.minusW; + } + + protected UserFont(String fontName, String sufix) throws Exception { + this.fontName = fontName; + String fileName = fontName + ".tcz"; + TCZ z = (TCZ) loadedTCZs.get(fileName.toLowerCase()); + if (z == null) { + InputStream is = openInputStream(fileName); + if (is == null) { + is = openInputStream("vm/" + fileName); // for the release sdk, there's no etc/fonts. the tcfont.tcz is located at dist/vm/tcfont.tcz + if (is == null) { + is = openInputStream("etc/fonts/" + fileName); // if looking for the default font when debugging, use etc/fonts + if (is == null) { + loadedFontsMap.put(fontName.toLowerCase(), null); + throw new Exception("file " + fileName + " " + sufix + " not found"); // loaded = false + } + } + } + z = new TCZ(new IS2S(is)); + FontInfo fi = new FontInfo(); + int n = z.numberOfChunks; + fi.chunks = new totalcross.io.ByteArrayStream[n]; + totalcross.util.IntVector sizes = new totalcross.util.IntVector(n / 2); + for (int i = 0; i < n; i++) { + int s = z.getNextChunkSize(); + fi.chunks[i] = new totalcross.io.ByteArrayStream(s); + z.readNextChunk(fi.chunks[i]); + // compute size - $p20u0 + String name = z.names[i]; + String ss = name.substring(name.lastIndexOf('$') + 2, name.lastIndexOf('u')); + int size = toInt(ss); + if (!sizes.contains(size)) { + sizes.addElement(size); + } + } + sizes.qsort(); + fi.sizes = sizes.toIntArray(); + z.bag = fi; + loadedTCZs.put(fileName.toLowerCase(), z); + loadedFontsMap.put(fontName.toLowerCase(), fileName); + } + fontName += sufix; + int index = z.findNamePosition(fontName.toLowerCase()); + if (index == -1) { + throw new Exception("name " + fontName + " not found"); // loaded = false + } + + totalcross.io.ByteArrayStream bas = ((FontInfo) z.bag).chunks[index]; + bas.reset(); + totalcross.io.DataStreamLE ds = new totalcross.io.DataStreamLE(bas); + antialiased = ds.readUnsignedShort(); + firstChar = ds.readUnsignedShort(); + lastChar = ds.readUnsignedShort(); + spaceWidth = ds.readUnsignedShort(); + maxWidth = ds.readUnsignedShort(); + maxHeight = ds.readUnsignedShort(); + owTLoc = ds.readUnsignedShort(); + ascent = ds.readUnsignedShort(); + descent = ds.readUnsignedShort(); + rowWords = ds.readUnsignedShort(); + + rowWidthInBytes = 2 * rowWords * (antialiased == AA_NO ? 1 : antialiased == AA_4BPP ? 4 : 8); + int bitmapTableSize = (int) rowWidthInBytes * (int) maxHeight; + + bitmapTable = new byte[bitmapTableSize]; + ds.readBytes(bitmapTable); + bitIndexTable = new int[lastChar - firstChar + 1 + 1]; + for (int i = 0; i < bitIndexTable.length; i++) { + bitIndexTable[i] = ds.readUnsignedShort(); + } + // + minusW = antialiased == AA_8BPP && fontName.equals("TCFont") ? 1 : 0; + if (firstChar <= '0' && '0' <= lastChar) { + index = (int) '0' - (int) firstChar; + numberWidth = bitIndexTable[index + 1] - bitIndexTable[index] - minusW; + } + if (antialiased == AA_8BPP) { + nativeFonts = new totalcross.ui.image.Image[bitIndexTable.length]; + } + } + + protected totalcross.ui.image.Image getBaseCharImage(int index) throws totalcross.ui.image.ImageException // called only in ubase instances + { + if (bitmapTable == null && ubase != null) { + return ubase.getBaseCharImage(index); + } + int offset = bitIndexTable[index]; + int width = bitIndexTable[index + 1] - offset - minusW; + totalcross.ui.image.Image img = new totalcross.ui.image.Image(width, maxHeight); + int[] pixels = img.getPixels(); + for (int y = 0, idx = 0; y < maxHeight; y++) { + for (int x = 0; x < width; x++, idx++) { + pixels[idx] = bitmapTable[y * rowWidthInBytes + x + offset] << 24; + } + } + return img; + } + + // Get the source x coordinate and width of the character + public void setCharBits(char ch, CharBits bits) { + if (firstChar <= ch && ch <= lastChar) { + int index = (int) ch - (int) firstChar; + bits.index = index; + bits.rowWIB = rowWidthInBytes; + bits.charBitmapTable = bitmapTable; + bits.offset = bitIndexTable[index]; + bits.width = bitIndexTable[index + 1] - bits.offset - minusW; + if (bits.width == 0) { + bits.width += minusW; + } + if (ubase != null && ubase.nativeFonts != null) { + try { + if (ubase.nativeFonts[index] == null) { + ubase.nativeFonts[index] = ubase.getBaseCharImage(index); + } + if (nativeFonts[index] == null) { + nativeFonts[index] = ubase.nativeFonts[index].getHwScaledInstance(bits.width, maxHeight); + } + bits.img = nativeFonts[index]; + bits.rowWIB = bits.width; + } catch (Exception e) { + if (Settings.showDesktopMessages) { + e.printStackTrace(); + } + bits.width = spaceWidth; + bits.offset = -1; + } + } + } else { + bits.width = spaceWidth; + bits.offset = -1; + } + } + } + + public int getCharWidth(totalcross.ui.font.Font f, char ch) // guich@tc122_16: moved to outside UserFont, because each char may be in a different UserFont + { + UserFont font = (UserFont) f.hv_UserFont; + if (ch < font.firstChar || ch > font.lastChar) { + f.hv_UserFont = font = Launcher.instance.getFont(f, ch); + } + if (ch == 160) { + return font.numberWidth; + } + if (ch < ' ') { + return (ch == '\t') ? font.spaceWidth * totalcross.ui.font.Font.TAB_SIZE : 0; // guich@tc100: handle tabs + } + int index = (int) ch - (int) font.firstChar; + return (font.firstChar <= ch && ch <= font.lastChar) + ? font.bitIndexTable[index + 1] - font.bitIndexTable[index] - font.minusW : font.spaceWidth; + } + + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherFonts.java b/TotalCrossSDK/src/main/java/totalcross/LauncherFonts.java new file mode 100644 index 000000000..a6c251955 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherFonts.java @@ -0,0 +1,4 @@ +package totalcross; + +abstract class LauncherFonts extends LauncherSettings { +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherInput.java b/TotalCrossSDK/src/main/java/totalcross/LauncherInput.java new file mode 100644 index 000000000..196e6519f --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherInput.java @@ -0,0 +1,523 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherInput extends LauncherArguments { + + void injectPreviewPointer(int x, int y, int button, boolean pressed) { + if (eventThread == null) return; + eventThread.pushEvent(pressed ? PenEvent.PEN_DOWN : PenEvent.PEN_UP, button, x, y, modifiers, + Vm.getTimeStamp()); + } + + void injectPreviewKey(int keyCode, boolean pressed, int modifiers) { + if (eventThread == null || !pressed) return; + eventThread.pushEvent(keyCode < 32 ? KeyEvent.SPECIAL_KEY_PRESS : KeyEvent.KEY_PRESS, keyCode, 0, 0, + modifiers, Vm.getTimeStamp()); + } + + @Override + public void keyPressed(final java.awt.event.KeyEvent event) { + if (event.getKeyChar() == '1' && event.isControlDown()) { + totalcross.ui.Window.onRobotKey(); + } + updateModifiers(event); + if (event.isActionKey()) { + updateModifiers(event); + int key = 0; + + switch (event.getKeyCode()) { + case java.awt.event.KeyEvent.VK_HOME: + key = SpecialKeys.HOME; + break; + case java.awt.event.KeyEvent.VK_END: + key = SpecialKeys.END; + break; + case java.awt.event.KeyEvent.VK_UP: + key = SpecialKeys.UP; + break; + case java.awt.event.KeyEvent.VK_DOWN: + key = SpecialKeys.DOWN; + break; + case java.awt.event.KeyEvent.VK_LEFT: + key = SpecialKeys.LEFT; + break; + case java.awt.event.KeyEvent.VK_RIGHT: + key = SpecialKeys.RIGHT; + break; + case java.awt.event.KeyEvent.VK_INSERT: + key = SpecialKeys.INSERT; + break; + case java.awt.event.KeyEvent.VK_ENTER: + key = SpecialKeys.ENTER; + break; + case java.awt.event.KeyEvent.VK_TAB: + key = SpecialKeys.TAB; + break; + case java.awt.event.KeyEvent.VK_BACK_SPACE: + key = SpecialKeys.BACKSPACE; + break; + case java.awt.event.KeyEvent.VK_ESCAPE: + key = SpecialKeys.ESCAPE; + break; + case java.awt.event.KeyEvent.VK_DELETE: + key = SpecialKeys.DELETE; + break; + case java.awt.event.KeyEvent.VK_PAGE_UP: + key = SpecialKeys.PAGE_UP; + keysPressed.put(key, 1); + keysPressed.put(java.awt.event.KeyEvent.VK_PAGE_DOWN, 0); + break; // don't let down/up simultanealy + case java.awt.event.KeyEvent.VK_PAGE_DOWN: + key = SpecialKeys.PAGE_DOWN; + keysPressed.put(key, 1); + keysPressed.put(java.awt.event.KeyEvent.VK_PAGE_UP, 0); + break; + // guich@120 - emulate more keys + case java.awt.event.KeyEvent.VK_F1: + break; + case java.awt.event.KeyEvent.VK_F2: + takeScreenShot(); + break; + case java.awt.event.KeyEvent.VK_F3: + break; + case java.awt.event.KeyEvent.VK_F4: + break; + case java.awt.event.KeyEvent.VK_F5: + break; + case java.awt.event.KeyEvent.VK_F6: + key = SpecialKeys.MENU; + break; + case java.awt.event.KeyEvent.VK_F7: + key = SpecialKeys.ESCAPE; + break; + case java.awt.event.KeyEvent.VK_F8: + break; + case java.awt.event.KeyEvent.VK_F10: + break; + case java.awt.event.KeyEvent.VK_F11: + key = SpecialKeys.KEYBOARD_ABC; + break; + case java.awt.event.KeyEvent.VK_F12: + break; + case java.awt.event.KeyEvent.VK_F9: + if (isApplication && !Settings.disableScreenRotation && Settings.screenWidth != Settings.screenHeight + && eventThread != null) // guich@tc: changed orientation? + { + int t = toWidth; + toWidth = toHeight; + toHeight = t; + screenResized(Settings.screenHeight, Settings.screenWidth, true); + key = 0; + ignoreNextResize = true; + } + break; + default: + key = 0; + break; + } + if (key != 0 && eventThread != null) // sometimes, when debugging in applet, eventThread can be null + { + eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, key, 0, 0, modifiers, Vm.getTimeStamp()); + } + if (showKeyCodes && eventThread != null) { + final String msg = "Key code: " + (key == 0 ? event.getKeyCode() : key) + ", Modifier: " + modifiers; + new Thread() { + @Override + public void run() { + Vm.alert(msg); + } + }.start(); // must place this in a separate thread, or the vm dies + } + } + } + + protected void takeScreenShot() { + try { + totalcross.ui.image.Image img = MainWindow.getScreenShot(); + String name = totalcross.sys.Settings.appPath + new Time().getTimeLong() + ".png"; + totalcross.io.File f = new totalcross.io.File(name, totalcross.io.File.CREATE_EMPTY); + img.createPng(f); + f.close(); + System.out.println("Saved at " + name); + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected void screenResized(int w, int h, boolean setframe) { + if (screenImg == null || (Settings.screenWidth == w && Settings.screenHeight == h)) { + return; + } + Settings.screenWidth = w; + Settings.screenHeight = h; + setWindowSize(w, h, setframe); + screenImg = null; // force the creation of a new screen image + eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.SCREEN_CHANGE, 0, 0, modifiers, Vm.getTimeStamp()); + } + + @Override + public void transferFocus() // guich@512_1: handle the tab key. + { + super.transferFocus(); + if (eventThread != null) { + eventThread.pushEvent(KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.TAB, 0, 0, modifiers, Vm.getTimeStamp()); + } + } + + @Override + public void keyReleased(java.awt.event.KeyEvent event) { + updateModifiers(event); + if (event.isActionKey()) { + switch (event.getKeyCode()) { + // case java.awt.event.KeyEvent.VK_F1: keysPressed.put(SpecialKeys.HARD1,0); break; + // case java.awt.event.KeyEvent.VK_F2: keysPressed.put(SpecialKeys.HARD2,0); break; + // case java.awt.event.KeyEvent.VK_F3: keysPressed.put(SpecialKeys.HARD3,0); break; + // case java.awt.event.KeyEvent.VK_F4: keysPressed.put(SpecialKeys.HARD4,0); break; + case java.awt.event.KeyEvent.VK_PAGE_UP: + keysPressed.put(SpecialKeys.PAGE_UP, 0); + break; + case java.awt.event.KeyEvent.VK_PAGE_DOWN: + keysPressed.put(SpecialKeys.PAGE_DOWN, 0); + break; + } + } + } + + @Override + public void keyTyped(java.awt.event.KeyEvent event) { + updateModifiers(event); + if (!event.isActionKey() && eventThread != null) { + int key = event.getKeyChar(), orig = key; + switch (key) { + case 8: + key = SpecialKeys.BACKSPACE; + break; + case 10: + key = SpecialKeys.ENTER; + break; + case 127: + key = SpecialKeys.DELETE; + break; + case 27: + key = SpecialKeys.ESCAPE; + break; // guich@tc110_79 + } + eventThread.pushEvent(orig < 32 ? KeyEvent.SPECIAL_KEY_PRESS : KeyEvent.KEY_PRESS, key, 0, 0, modifiers, + Vm.getTimeStamp()); + } + } + + boolean isRightButton; + int startPY; + + @Override + public void mousePressed(java.awt.event.MouseEvent event) { + int px = (int) (event.getX() / toScale); + int py = (int) (event.getY() / toScale); + if (eventThread != null) { + eventThread.pushEvent(PenEvent.PEN_DOWN, 0, px, py, modifiers, Vm.getTimeStamp()); + } + if (isRightButton = (event.getButton() & 2) != 0) { + eventThread.pushEvent(MultiTouchEvent.SCALE, 1, px, startPY = py, modifiers, Vm.getTimeStamp()); + } + } + + @Override + public void mouseReleased(java.awt.event.MouseEvent event) { + int px = (int) (event.getX() / toScale); + int py = (int) (event.getY() / toScale); + if (eventThread != null) { + eventThread.pushEvent(PenEvent.PEN_UP, 0, px, py, modifiers, Vm.getTimeStamp()); + } + if ((event.getButton() & 2) != 0) { + eventThread.pushEvent(MultiTouchEvent.SCALE, 2, px, py, modifiers, Vm.getTimeStamp()); + } + } + + @Override + public void mouseDragged(java.awt.event.MouseEvent event) { + int px = (int) (event.getX() / toScale); + int py = (int) (event.getY() / toScale); + if (eventThread != null) // sometimes, when debugging in applet, eventThread can be null + { + if ((event.getButton() & 2) != 0 || isRightButton) { + double scale = py < startPY ? 1.05 : 0.95; + long l = Double.doubleToLongBits(scale); + int x = (int) (l >>> 32); + int y = (int) l; + if (!eventThread.hasEvent(MultiTouchEvent.SCALE)) { + eventThread.pushEvent(MultiTouchEvent.SCALE, 0, x, y, modifiers, Vm.getTimeStamp()); + } + } else if (!eventThread.hasEvent(PenEvent.PEN_DRAG)) { + eventThread.pushEvent(PenEvent.PEN_DRAG, 0, px, py, modifiers, Vm.getTimeStamp()); // guich@580_40: changed from 201 to 203; PenEvent.PEN_MOVE is deprecated + } + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (eventThread != null) // sometimes, when debugging in applet, eventThread can be null + { + int ev = totalcross.ui.event.MouseEvent.MOUSE_WHEEL; + if (!eventThread.hasEvent(ev)) { + int px = (int) (e.getX() / toScale); + int py = (int) (e.getY() / toScale); + eventThread.pushEvent(ev, + e.getWheelRotation() < 0 ? totalcross.ui.event.DragEvent.UP : totalcross.ui.event.DragEvent.DOWN, px, py, + modifiers, Vm.getTimeStamp()); // guich@580_40: changed from 201 to 203; PenEvent.PEN_MOVE is deprecated + } + } + } + + @Override + public void windowClosing(java.awt.event.WindowEvent event) { + if (Settings.closeButtonType == Settings.NO_BUTTON) { + eventThread.pushEvent(totalcross.ui.event.KeyEvent.SPECIAL_KEY_PRESS, SpecialKeys.MENU, 0, 0, 0, + Vm.getTimeStamp()); + } else { + destroy(); + exit(0); + } + } + + @Override + public void mouseEntered(java.awt.event.MouseEvent event) { + if (hasWindowBackend() && windowFocusOwnerIsNotThis() && !destroyed) { + requestFocus(); // guich@200b4: correct a bug that sometimes key events was not being sent anymore to the canvas. + } + } + + @Override + public void mouseClicked(java.awt.event.MouseEvent event) { + } + + @Override + public void mouseExited(java.awt.event.MouseEvent event) { + } + + @Override + public void windowActivated(java.awt.event.WindowEvent event) { + } + + @Override + public void windowClosed(java.awt.event.WindowEvent event) { + } + + @Override + public void windowDeactivated(java.awt.event.WindowEvent event) { + } + + @Override + public void windowDeiconified(java.awt.event.WindowEvent event) { + if (mainWindow != null) { + mainWindow.onRestore(); + } + } + + @Override + public void windowIconified(java.awt.event.WindowEvent event) { + if (mainWindow != null) { + mainWindow.onMinimize(); + } + } + + @Override + public void windowOpened(java.awt.event.WindowEvent event) { + } + + @Override + public void mouseMoved(java.awt.event.MouseEvent event) { + if (eventThread != null) { + eventThread.pushEvent(totalcross.ui.event.MouseEvent.MOUSE_MOVE, 0, (int) (event.getX() / toScale), + (int) (event.getY() / toScale), modifiers, Vm.getTimeStamp()); + } + if (hasWindowBackend() && Settings.showMousePosition) // guich@tc115_48 + { + mmsb.setLength(0); + if (frameTitle != null) { + mmsb.append(frameTitle).append(" ("); + } + int xx = (int) (event.getX() / toScale); + int yy = (int) (event.getY() / toScale); + int[] pixels = totalcross.ui.gfx.Graphics.mainWindowPixels; + mmsb.append(xx).append(",").append(yy).append(" ") + .append(totalcross.sys.Convert.unsigned2hex(pixels[yy * Settings.screenWidth + xx], 6)); + if (frameTitle != null) { + mmsb.append(")"); + } + setWindowTitle(mmsb.toString()); + } + } + + @Override + public void paint(java.awt.Graphics g) { + if (!started) { + startApp(); + } else { + eventThread.invokeInEventThread(false, new Runnable() { + @Override + public void run() { + try { + totalcross.ui.Window.repaintActiveWindows(); + } catch (Exception e) { + System.out.println("Exception in Launcher.paint"); + e.printStackTrace(); + } + } + }); + } + } + + public void pumpEvents() { + if (eventThread != null) { + eventThread.pumpEvents(); + } + } + + @Override + public void update(java.awt.Graphics g) { + } + + public void setNewMainWindow(MainWindow newInstance, String args) // called on Vm.exec + { + if (runtime != null) { + runtime.setNewMainWindow(newInstance, args); + } else { + replaceMainWindow(newInstance, args); + } + } + + void replaceMainWindow(MainWindow newInstance, String args) + { + commandLine = args; // guich@200b3: added command line support for desktop classes. + if (winTimer != null) { + winTimer.stopGracefully(); // guich@120 + winTimer = null; + } + Window.destroyZStack(); + mainWindow = newInstance; + mainWindow.initUI(); // ps: since we are being called from an app, we cannot use the synchronized method + } + + void preparePreviewMainWindowReload() { + if (winTimer != null) { + winTimer.stopGracefully(); + winTimer = null; + } + MainWindow.resetPreviewState(); + } + + void replacePreviewMainWindow(MainWindow newInstance, String args) { + commandLine = args; + mainWindow = newInstance; + if (eventThread != null) { + eventThread.setMainClass(newInstance); + boolean started = eventThread.invokeInEventThread(true, new Runnable() { + @Override + public void run() { + mainWindow.appStarting(isDemo ? 80 : -1); + } + }, PREVIEW_DESTROY_TIMEOUT_MILLIS); + if (!started) { + System.err.println("Timed out waiting for TotalCross preview appStarting during reload."); + } + } else { + mainWindow.appStarting(isDemo ? 80 : -1); + } + } + + void showPreviewContainer(final Container container) { + runInPreviewEventThread(new Runnable() { + @Override + public void run() { + mainWindow.swap(container); + } + }, "container preview"); + } + + void showPreviewControl(final Control control) { + runInPreviewEventThread(new Runnable() { + @Override + public void run() { + mainWindow.removeAll(); + mainWindow.add(control, Control.CENTER, Control.CENTER, Control.PREFERRED, Control.PREFERRED); + } + }, "control preview"); + } + + protected void runInPreviewEventThread(Runnable runnable, String operation) { + if (eventThread != null) { + boolean completed = eventThread.invokeInEventThread(true, runnable, PREVIEW_DESTROY_TIMEOUT_MILLIS); + if (!completed) { + System.err.println("Timed out waiting for TotalCross " + operation + "."); + } + } else { + runnable.run(); + } + } + + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherParsedConfig.java b/TotalCrossSDK/src/main/java/totalcross/LauncherParsedConfig.java new file mode 100644 index 000000000..8386c69a3 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherParsedConfig.java @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +/** + * Parsed launcher configuration produced from legacy command-line arguments. + */ +final class LauncherParsedConfig { + final String className; + int userFontSize = Launcher.userFontSize; + int width = -1; + int height = -1; + int bpp = 24; + int uiStyle = -1; + int x = -1; + int y = -1; + boolean fullscreen; + boolean demo; + boolean fastScale; + boolean keyboardFocusTraversable; + boolean fingerTouch; + boolean unmovableSIP; + boolean geographicalFocus; + boolean virtualKeyboard; + boolean showMousePosition; + boolean showDebugMessages; + String commandLine = ""; + String dataPath; + double scaleValue = -1; + double densityValue = 1; + double scale; + totalcross.ui.Insets insetsPortrait; + totalcross.ui.Insets insetsLandscape; + + LauncherParsedConfig(String className) { + this.className = className; + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherRendering.java b/TotalCrossSDK/src/main/java/totalcross/LauncherRendering.java new file mode 100644 index 000000000..87c398ba7 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherRendering.java @@ -0,0 +1,212 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; +import totalcross.preview.PreviewFrame; +import totalcross.preview.PreviewFrameConsumer; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherRendering extends LauncherInput { + /** Calls System.out.println. TotalCross system debugging uses this method. See also debug(String s). */ + public static void print(String s) { + if (totalcross.sys.Settings.showDesktopMessages) { + System.err.println(s); + } + } + public static void debug(String s){ + if(totalcross.sys.Settings.showDebugMessages){ + System.out.println(s); + } + } + + //// Graphics //////////////////////////////////////////////////////////////////// + + protected void createColorPaletteLookupTables() { + int i, r, g, b; + lookupR = new int[256]; + lookupG = new int[256]; + lookupB = new int[256]; + lookupGray = new int[256]; + + for (i = 0; i < 256; i++) { + r = (i + 1) * 6 / 256; + if (r > 0) { + r--; + } + g = (i + 1) * 8 / 256; + if (g > 0) { + g--; + } + b = (i + 1) * 5 / 256; + if (b > 0) { + b--; + } + lookupR[i] = r * 40; + lookupG[i] = g * 5; + lookupB[i] = b + 16; + lookupGray[i] = i / 0x11; + } + pal685 = totalcross.ui.gfx.Graphics.getPalette(); + } + + protected int getScreenColor(int p) { + int r = (p >> 16) & 0xFF; + int g = (p >> 8) & 0xFF; + int b = p & 0xFF; + switch (toBpp) { + case 8: + if (lookupR == null) { + createColorPaletteLookupTables(); + } + return pal685[(g == r && g == b) ? lookupGray[r] : (lookupR[r] + lookupG[g] + lookupB[b])]; + case 16: + return (((r) >> 3) << 19) | (((g) >> 2) << 10) | (((b >> 3) << 3)); + default: + return p; + } + } + + public void updateScreen() { + //int ini = totalcross.sys.Vm.getTimeStamp(); + int w = totalcross.sys.Settings.screenWidth; + int h = totalcross.sys.Settings.screenHeight; + + if (screenImg == null) { + screenImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // We can typecast directly to DataBufferInt because that's the type used for images with 24+ bit color + DataBufferInt dbi = (DataBufferInt) screenImg.getRaster().getDataBuffer(); + int[] pixels = dbi.getData(); + // Copy whatever was drawn before + System.arraycopy(totalcross.ui.gfx.Graphics.mainWindowPixels, 0, pixels, 0, w*h); + // And replace with our bytes, now we no longer need to copy from the mainWindowPixels to our image. Saving a ton of memory. + totalcross.ui.gfx.Graphics.mainWindowPixels = pixels; + } + int[] pixels = totalcross.ui.gfx.Graphics.mainWindowPixels; + int n = Settings.screenWidth * Settings.screenHeight; + if (toBpp >= 24) { + screenPixels = pixels; + } else if (screenPixels.length < n) { + screenPixels = new int[n]; + } + // convert to the target bpp on-the-fly + switch (toBpp) { + case 8: { + if (lookupR == null) { + createColorPaletteLookupTables(); + } + int[] pal = pal685; + int[] toR = lookupR; + int[] toG = lookupG; + int[] toB = lookupB; + int[] toGray = lookupGray; + while (--n >= 0) { + int p = pixels[n]; + int r = (p >> 16) & 0xFF; + int g = (p >> 8) & 0xFF; + int b = p & 0xFF; + screenPixels[n] = pal[(g == r && g == b) ? toGray[r] : (toR[r] + toG[g] + toB[b])]; + } + break; + } + case 16: { + while (--n >= 0) { + screenPixels[n] = pixels[n] & 0xF8FCF8; // guich@tc100b4_2: use a direct and instead of a bunch of shifts. note: using a DirectColorModel(32,0xF80000,0x00FC00,0x0000F8,0) is 5x SLOWER than doing the mapping by ourselves. + } + break; + } + } + getPreviewSurface().present(screenImg); + PreviewFrameConsumer frameConsumer = getPreviewFrameConsumer(); + if (frameConsumer != null) { + frameConsumer.present(new PreviewFrame(w, h, w, Settings.screenDensity, + PreviewFrame.PixelFormat.ARGB_8888, screenPixels)); + } + // make the emulator work like OpenGL: erase the screen to instruct the user that everything must be drawn always + //java.util.Arrays.fill(pixels, getScreenColor(UIColors.unsafeAreaColor)); + } + + protected PreviewRuntime.FrameConsumer getPreviewSurface() { + if (previewSurface == null) { + previewSurface = new AppletPreviewSurface(this, toScale, fastScale); + } + return previewSurface; + } + + public static BufferedImage toBufferedImage(java.awt.Image img) { + if (img instanceof BufferedImage) { + return (BufferedImage) img; + } + + BufferedImage bufferedImage = new BufferedImage(img.getWidth(null), img.getHeight(null), + BufferedImage.TYPE_INT_ARGB); + + Graphics2D graphics = bufferedImage.createGraphics(); + graphics.drawImage(img, 0, 0, null); + graphics.dispose(); + + return bufferedImage; + } + + //static int count; + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherRuntime.java b/TotalCrossSDK/src/main/java/totalcross/LauncherRuntime.java new file mode 100644 index 000000000..a169f1e40 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherRuntime.java @@ -0,0 +1,344 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.Color; +import java.awt.event.ComponentListener; +import java.awt.event.WindowListener; + +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.preview.PreviewFrameConsumer; +import totalcross.preview.WindowConfig; +import totalcross.sys.Settings; +import totalcross.ui.Container; +import totalcross.ui.Control; +import totalcross.ui.MainWindow; + +/** + * Owns the Java desktop launcher lifecycle independently of the caller. + *

+ * IDE tooling should prefer this runtime or {@link PreviewRunner} instead of + * constructing {@link Launcher} directly. The current implementation still uses + * the existing launcher internals so desktop simulator behavior remains stable + * while window and rendering responsibilities are extracted incrementally. + */ +public final class LauncherRuntime implements PreviewRuntime { + private LauncherConfig config; + private LauncherParsedConfig parsedConfig; + private PreviewRuntime.FrameConsumer previewSurface; + private PreviewFrameConsumer previewFrameSurface; + private boolean previewMode; + private ClassLoader appClassLoader; + private Launcher launcher; + + public static LauncherRuntime startApplication(String mainWindowClass, String... args) { + LauncherRuntime runtime = new LauncherRuntime(); + runtime.parseArguments(mainWindowClass, args); + runtime.startApplication(); + return runtime; + } + + public static LauncherRuntime startPreview(String mainWindowClass, PreviewRuntime.FrameConsumer surface, ClassLoader appClassLoader, + String... args) { + LauncherRuntime runtime = new LauncherRuntime(); + runtime.setPreviewSurface(surface); + runtime.setAppClassLoader(appClassLoader); + runtime.parseArguments(mainWindowClass, args); + runtime.startPreview(); + return runtime; + } + + public static LauncherRuntime startPreviewFrames(String mainWindowClass, PreviewFrameConsumer surface, + ClassLoader appClassLoader, String... args) { + LauncherRuntime runtime = new LauncherRuntime(); + runtime.setPreviewFrameSurface(surface); + runtime.setAppClassLoader(appClassLoader); + runtime.parseArguments(mainWindowClass, args); + runtime.startPreview(); + return runtime; + } + + public void parseArguments(String mainWindowClass, String... args) { + this.config = new LauncherConfig(mainWindowClass, args); + } + + public void configure(LauncherConfig config) { + if (config == null) { + throw new IllegalArgumentException("config cannot be null"); + } + this.config = config; + this.parsedConfig = null; + } + + public void recordLauncherUsage() { + // Anonymous telemetry is intentionally disabled until its service contract + // is replaced with a maintained, opt-in endpoint. + } + + void parseLauncherArguments(Launcher launcher, LauncherConfig config, boolean application, int fallbackWidth, + int fallbackHeight) throws LauncherArgumentParser.InvalidArgumentException { + configure(config); + applyParsedConfig(launcher, LauncherArgumentParser.parse(config, application, fallbackWidth, fallbackHeight)); + } + + public void setPreviewSurface(PreviewRuntime.FrameConsumer previewSurface) { + this.previewSurface = previewSurface; + } + + public void setPreviewFrameSurface(PreviewFrameConsumer previewFrameSurface) { + this.previewFrameSurface = previewFrameSurface; + } + + public void setAppClassLoader(ClassLoader appClassLoader) { + this.appClassLoader = appClassLoader; + } + + public void startApplication() { + previewMode = false; + start(false); + } + + public void startPreview() { + previewMode = true; + start(true); + } + + public void pumpEvents() { + if (launcher != null) { + launcher.pumpEvents(); + } + } + + /** Applies an IDE resize to the active desktop preview. */ + public void resizePreview(int width, int height, double density) { + if (launcher == null) throw new IllegalStateException("LauncherRuntime has not been started"); + runInPreviewEventThread(() -> { + Settings.screenDensity = density; + Settings.screenWidth = width; + Settings.screenHeight = height; + if (launcher.hasWindowBackend()) launcher.setWindowSize(width, height, true); + launcher.updateScreen(); + }, "preview resize"); + } + + private void runInPreviewEventThread(Runnable command, String operation) { + if (launcher.eventThread == null) { + command.run(); + return; + } + if (!launcher.eventThread.invokeInEventThread(true, command, LauncherState.PREVIEW_DESTROY_TIMEOUT_MILLIS)) { + throw new IllegalStateException("Timed out waiting for TotalCross " + operation + "."); + } + } + + /** Injects a pointer transition from an external preview surface. */ + public void injectPreviewPointer(int x, int y, int button, boolean pressed) { + if (launcher == null) throw new IllegalStateException("LauncherRuntime has not been started"); + launcher.injectPreviewPointer(x, y, button, pressed); + } + + /** Injects a key transition from an external preview surface. */ + public void injectPreviewKey(int keyCode, boolean pressed, int modifiers) { + if (launcher == null) throw new IllegalStateException("LauncherRuntime has not been started"); + launcher.injectPreviewKey(keyCode, pressed, modifiers); + } + + public void stop() { + if (launcher != null) { + launcher.destroy(); + launcher = null; + } + } + + @Override + public void close() { + stop(); + } + + public void setNewMainWindow(MainWindow newInstance, String args) { + if (launcher == null) { + throw new IllegalStateException("LauncherRuntime has not been started"); + } + launcher.replaceMainWindow(newInstance, args); + } + + @Override + public void preparePreviewMainWindowReload() { + if (launcher == null) { + throw new IllegalStateException("LauncherRuntime has not been started"); + } + launcher.preparePreviewMainWindowReload(); + } + + @Override + public void replaceMainWindow(MainWindow newInstance, String args) { + if (launcher == null) { + throw new IllegalStateException("LauncherRuntime has not been started"); + } + launcher.replacePreviewMainWindow(newInstance, args); + } + + @Override + public void showContainer(Container container) { + if (launcher == null) { + throw new IllegalStateException("LauncherRuntime has not been started"); + } + launcher.showPreviewContainer(container); + } + + @Override + public void showControl(Control control) { + if (launcher == null) { + throw new IllegalStateException("LauncherRuntime has not been started"); + } + launcher.showPreviewControl(control); + } + + void initializeSettings(Launcher launcher) { + launcher.fillSettings(); + } + + void setParsedConfig(LauncherParsedConfig parsedConfig) { + this.parsedConfig = parsedConfig; + } + + LauncherParsedConfig getParsedConfig() { + return parsedConfig; + } + + void applyParsedConfig(Launcher launcher, LauncherParsedConfig parsedConfig) { + setParsedConfig(parsedConfig); + applySettings(parsedConfig); + launcher.applyParsedArguments(parsedConfig); + } + + @Override + public MainWindow createMainWindow(String className, ClassLoader classLoader, boolean terminateIfMainClass) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + String normalizedClassName = normalizeMainWindowClassName(className); + Class mainClass = Class.forName(normalizedClassName, true, classLoader); + boolean mainClassOnly = checkIfMainClass(mainClass); + if (!mainClassOnly) { + runtimeInstructions(); + } + Object instance = mainClass.newInstance(); + if (instance instanceof MainClass && !(instance instanceof MainWindow)) { + ((MainClass) instance).appStarting(0); + ((MainClass) instance).appEnding(); + if (terminateIfMainClass) { + System.exit(0); + } + return null; + } + return (MainWindow) instance; + } + + AwtWindowBackend startWindowBackend(Launcher launcher, String title, Color background, + WindowListener windowListener, ComponentListener componentListener) { + if (parsedConfig == null) { + throw new IllegalStateException("Launcher arguments must be parsed before starting the window backend"); + } + WindowConfig config = new WindowConfig(parsedConfig.width, parsedConfig.height, parsedConfig.scale, title, + parsedConfig.x, parsedConfig.y, parsedConfig.fullscreen, Settings.resizableWindow, background, windowListener, + componentListener); + AwtWindowBackend backend = new AwtWindowBackend(launcher); + backend.start(config); + return backend; + } + + Launcher getLauncher() { + return launcher; + } + + boolean isPreviewMode() { + return previewMode; + } + + String getMainWindowClass() { + return config == null ? null : config.getMainWindowClass(); + } + + String[] getLauncherArgs() { + return config == null ? new String[0] : config.getLauncherArgs(); + } + + static String normalizeMainWindowClassName(String className) { + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - 6); + } + return className.replace('/', '.'); + } + + private void start(boolean preview) { + if (config == null) { + throw new IllegalStateException("parseArguments must be called before start"); + } + Launcher.isApplication = true; + recordLauncherUsage(); + launcher = new Launcher(previewSurface, preview, appClassLoader); + launcher.setPreviewFrameConsumer(previewFrameSurface); + launcher.setRuntime(this); + launcher.parseArguments(config.getMainWindowClass(), config.getLauncherArgs()); + launcher.init(); + if (preview) { + launcher.startApp(); + launcher.pumpEvents(); + // A headless consumer has no AWT paint callback to trigger the first frame. + launcher.updateScreen(); + } + } + + private void applySettings(LauncherParsedConfig parsedConfig) { + Launcher.userFontSize = parsedConfig.userFontSize; + if (parsedConfig.keyboardFocusTraversable) { + Settings.keyboardFocusTraversable = true; + } + if (parsedConfig.fingerTouch) { + Settings.fingerTouch = true; + } + if (parsedConfig.unmovableSIP) { + Settings.unmovableSIP = true; + } + if (parsedConfig.geographicalFocus) { + Settings.geographicalFocus = true; + } + if (parsedConfig.virtualKeyboard) { + Settings.virtualKeyboard = true; + } + if (parsedConfig.showMousePosition) { + Settings.showMousePosition = true; + } + if (parsedConfig.showDebugMessages) { + Settings.showDebugMessages = true; + } + Settings.screenDensity = parsedConfig.densityValue; + Settings.dataPath = parsedConfig.dataPath; + } + + private static boolean checkIfMainClass(Class c) { + Class[] interfaces = c.getInterfaces(); + if (interfaces != null) { + for (int i = 0; i < interfaces.length; i++) { + if (interfaces[i].getName().equals("totalcross.MainClass")) { + return true; + } + } + } + return false; + } + + private static void runtimeInstructions() { + System.out.println("Current path: " + System.getProperty("user.dir")); + System.out.println("TotalCross " + Settings.versionStr + "." + Settings.buildNumber); + System.out.println("==================================="); + System.out.println("Device key emulations:"); + System.out.println("F2 : TAKE SCREEN SHOT AND SAVE TO CURRENT FOLDER"); + System.out.println("F6 : MENU"); + System.out.println("F7 : BACK (ESCAPE)"); + System.out.println("F9 : CHANGE ORIENTATION"); + System.out.println("F11: OPEN KEYBOARD"); + System.out.println("==================================="); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherSettings.java b/TotalCrossSDK/src/main/java/totalcross/LauncherSettings.java new file mode 100644 index 000000000..6f975d8cc --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherSettings.java @@ -0,0 +1,423 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherSettings extends LauncherStorage { + String getDefaultCrid(String name) { + if (name == null) { + return null; + } + + if (name.indexOf('.') != -1) { + name = name.substring(name.lastIndexOf('.') + 1); + } + int i; + int n = name.length(); + int hash = 0; + byte[] creat = new byte[4]; + for (i = 0; i < n; i++) { + hash += (byte) name.charAt(i); + } + for (i = 0; i < 4; i++) { + creat[i] = (byte) ((hash % 26) + 'a'); + if ((hash & 64) > 0) { + creat[i] += ('A' - 'a'); + } + hash = hash / 2; + } + return new String(creat); + } + + protected void storeSettings() { + try { + String crid = crid4settings;//totalcross.sys.Settings.applicationId; + // first verify if the PDBFile is created but the String is null + totalcross.sys.Settings.showDesktopMessages = false; // guich@340_49 + boolean saveSettings = totalcross.sys.Settings.appSettings != null || totalcross.sys.Settings.appSecretKey != null + || totalcross.sys.Settings.appSettingsBin != null; // guich@570_9: also check if appSecretKey is null + + totalcross.io.PDBFile cat; + + if (!saveSettings) { + try { + cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, totalcross.io.PDBFile.READ_WRITE); // guich@241_17: changed READ_ONLY to READ_WRITE to fix "operation invalid" error + cat.delete(); + } catch (totalcross.io.FileNotFoundException e) { + } + } else { + cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, totalcross.io.PDBFile.CREATE); + totalcross.io.ResizeRecord rs = new totalcross.io.ResizeRecord(cat, 256); + totalcross.io.DataStream ds = new totalcross.io.DataStream(rs); + + try { + cat.setRecordPos(1); + cat.deleteRecord(); + } catch (totalcross.io.IOException e) { + } + try { + cat.setRecordPos(0); + cat.deleteRecord(); + } catch (totalcross.io.IOException e) { + } + rs.startRecord(); + // store the appSettings record + ds.writeString(totalcross.sys.Settings.appSettings); + ds.writeString(totalcross.sys.Settings.appSecretKey); + rs.endRecord(); + // guich@573_16: store the bin in another record + if (totalcross.sys.Settings.appSettingsBin != null) { + int len = totalcross.sys.Settings.appSettingsBin.length; + cat.addRecord(len); + cat.writeBytes(totalcross.sys.Settings.appSettingsBin, 0, len); + } + cat.close(); + + } + totalcross.sys.Settings.showDesktopMessages = true; + } catch (Throwable t) { + System.out.println("Settings can't be stored: " + t.toString()); + } + } + + protected void getAppSettings() { + String crid = crid4settings = totalcross.sys.Settings.applicationId; + totalcross.sys.Settings.showDesktopMessages = false; // guich@340_49 + try { + totalcross.io.PDBFile cat = new totalcross.io.PDBFile("Settings4" + crid + ".TCVM." + crid, + totalcross.io.PDBFile.READ_WRITE); + totalcross.io.DataStream ds = new totalcross.io.DataStream(cat); + cat.setRecordPos(0); + String s; + s = ds.readString(); + if (!"".equals(s)) { + totalcross.sys.Settings.appSettings = s; + } + try { + s = ds.readString(); + if (!"".equals(s)) { + totalcross.sys.Settings.appSecretKey = s; + } + } catch (Throwable t) { + System.out.println("Reading an old settings file; no appSecretKey available."); + } + + if (cat.getRecordCount() > 1) // guich@573_16 + { + cat.setRecordPos(1); + byte[] buf = new byte[cat.getRecordSize()]; + cat.readBytes(buf, 0, buf.length); + totalcross.sys.Settings.appSettingsBin = buf; + } + + cat.close(); + } catch (Throwable t) { + } + totalcross.sys.Settings.showDesktopMessages = true; // guich@340_49 + } + + protected char getFirstSymbol(String s) { + char[] c = s.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (c[i] != ' ' && !('0' <= c[i] && c[i] <= '9')) { + return c[i]; + } + } + return ' '; + } + + /** called by totalcross.Launcher.init() */ + public void fillSettings() { + if (settingsFilled) { + return; + } + settingsFilled = true; + java.util.Calendar cal = java.util.Calendar.getInstance(); + // guich@340_34: since java can't provide us good methods to return these values, we use parse the return of some formatting methods + cal.set(2002, 11, 25, 20, 0, 0); // guich@401_32 + java.text.DateFormat df = java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); // guich@401_32: fixed wrong results in some systems + String d = df.format(cal.getTime()); + totalcross.sys.Settings.dateFormat = d.startsWith("25") ? totalcross.sys.Settings.DATE_DMY + : d.startsWith("12") ? totalcross.sys.Settings.DATE_MDY : totalcross.sys.Settings.DATE_YMD; + totalcross.sys.Settings.dateSeparator = getFirstSymbol(d); + df = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT); // guich@401_32 + d = df.format(cal.getTime()); + + totalcross.sys.Settings.is24Hour = d.toLowerCase().indexOf("am") == -1 && d.toLowerCase().indexOf("pm") == -1; + totalcross.sys.Settings.timeSeparator = getFirstSymbol(d); + // + + totalcross.sys.Settings.weekStart = (byte) (cal.getFirstDayOfWeek() - 1); + settingsRefresh(false); + + java.text.DecimalFormatSymbols dfs = new java.text.DecimalFormatSymbols(); + totalcross.sys.Settings.thousandsSeparator = dfs.getGroupingSeparator(); + totalcross.sys.Settings.decimalSeparator = dfs.getDecimalSeparator(); + totalcross.sys.Settings.screenBPP = toBpp; + try { + totalcross.sys.Settings.screenWidthInDPI = totalcross.sys.Settings.screenHeightInDPI = Toolkit.getDefaultToolkit() + .getScreenResolution(); + } catch (Throwable t) { + totalcross.sys.Settings.screenWidthInDPI = 96; + } + totalcross.sys.Settings.romVersion = 0x02000000; + totalcross.sys.Settings.uiStyle = totalcross.sys.Settings.VISTA_UI; + totalcross.sys.Settings.screenWidth = toWidth; + totalcross.sys.Settings.screenHeight = toHeight; + totalcross.sys.Settings.onJavaSE = true; + totalcross.sys.Settings.platform = Settings.JAVA; + totalcross.sys.Settings.applicationId = getDefaultCrid(className); // dhaysmith@420_4 + totalcross.sys.Settings.deviceId = "Desktop"; // guich@568_2 + if (totalcross.sys.Settings.applicationId != null) { + getAppSettings(); // guich@330_47 + } + try { + // Fill all paths + String basePath = System.getProperty("user.dir"); + totalcross.sys.Settings.vmPath = basePath; + totalcross.sys.Settings.appPath = basePath; + // guich@tc112_21: commented - if (totalcross.sys.Settings.dataPath == null) totalcross.sys.Settings.dataPath = basePath; // flsobral@tc100b5_51: Settings.dataPath was being overwritten if set before the Launcher was initialized. + + if (totalcross.sys.Settings.appPath != null) // guich@582_17: make sure that it ends with a slash + { + if (totalcross.sys.Settings.appPath.indexOf('/') >= 0 && !totalcross.sys.Settings.appPath.endsWith("/")) { + totalcross.sys.Settings.appPath += "/"; + } else if (totalcross.sys.Settings.appPath.indexOf('\\') >= 0 + && !totalcross.sys.Settings.appPath.endsWith("\\")) { + totalcross.sys.Settings.appPath += "\\"; + } + } + totalcross.sys.Settings.userName = !isApplication ? null : java.lang.System.getProperty("user.name"); + } catch (SecurityException se) { + totalcross.sys.Settings.userName = null; + } + } + + @SuppressWarnings("deprecation") + public void settingsRefresh(boolean callStoreSettings) // guich@tc115_81 + { + java.util.TimeZone tz = java.util.TimeZone.getDefault(); // guich@340_33 + Settings.daylightSavingsMinutes = tz.getDSTSavings() / 60000; + Settings.daylightSavings = Settings.daylightSavingsMinutes != 0; + Settings.timeZone = tz.getRawOffset() / (60 * 60000); + Settings.timeZoneMinutes = tz.getRawOffset() / 60000; + Settings.timeZoneStr = java.util.TimeZone.getDefault().getID(); + if (callStoreSettings) { + try { + storeSettings(); + } catch (Exception e) { + } + } + } + + static totalcross.ui.font.Font getBaseFont(String name, boolean bold, int size, String suffix) { + String key = name + "|" + bold + "|" + size + "|" + suffix; + totalcross.ui.font.Font f = (totalcross.ui.font.Font) htBaseFonts.get(key); + if (f == null) { + int i; + if (!name.endsWith("noaa")) { + TCZ z = (TCZ) loadedTCZs.get((name + ".tcz").toLowerCase()); + if (z == null) { + return null; + } + FontInfo fi = (FontInfo) z.bag; + for (i = 0; i < fi.sizes.length - 1; i++) { + if (size <= fi.sizes[i]) { + size = fi.sizes[i]; + break; + } + } + } + + int idx = Integer.parseInt(suffix.substring(suffix.indexOf('u') + 1)); + totalcross.ui.font.Font.baseChar = (char) idx; + f = totalcross.ui.font.Font.getFont(name, bold, size); + totalcross.ui.font.Font.baseChar = ' '; + if (f != null) { + f.removeFromCache(); + htBaseFonts.put(key, f); + } + } + + return f; + } + + protected Launcher.UserFont loadUF(String fontName, String suffix) { + boolean hasTriedToLoadBefore = loadedFontsMap.containsKey(fontName.toLowerCase()); + if (hasTriedToLoadBefore && loadedFontsMap.get(fontName.toLowerCase()) == null) { + return null; + } + try { + if (totalcross.ui.font.Font.baseChar == ' ' && !fontName.endsWith("noaa")) // test if there's another 8bpp native font. - base font + { + boolean bold = suffix.charAt(1) == 'b'; + int size = Integer.parseInt(suffix.substring(2, suffix.indexOf('u'))); + totalcross.ui.font.Font base = getBaseFont(fontName, bold, size, suffix); + if (base == null) { + ((Launcher) this).new UserFont(fontName, suffix); // load sizes + base = getBaseFont(fontName, bold, size, suffix); + } + if (base != null) { + return ((Launcher) this).new UserFont(fontName, suffix, size, base); + } + } + return ((Launcher) this).new UserFont(fontName, suffix); + } catch (Exception e) { + String msg = "" + e.getMessage(); + if (!msg.startsWith("name") && !msg.endsWith("not found")) { + if (Settings.onJavaSE) { + e.printStackTrace(); + } + } + } + return null; + } + + public Launcher.UserFont getFont(totalcross.ui.font.Font f, char c) { + Launcher.UserFont uf = null; + try { + // verify if its in the cache. + String fontName = f.name; + int size = (int) (Math.max(f.size, totalcross.ui.font.Font.MIN_FONT_SIZE) * Settings.screenDensity); // guich@tc122_15: don't check for the maximum font size here + + char faceType = c < 0x3000 && f.style == 1 ? 'b' : 'p'; + int uIndex = ((int) c >> 8) << 8; + String suffix = "$" + faceType + size + "u" + uIndex; + String key = fontName + suffix; + uf = (Launcher.UserFont) htLoadedFonts.get(key); + if (uf != null) { + return uf; + } + + boolean hasTriedToLoadBefore = loadedFontsMap.containsKey(fontName.toLowerCase()); + if (fontName.charAt(0) == '$') { + print("Native fonts are not supported on Desktop"); + } else if (!hasTriedToLoadBefore || loadedFontsMap.get(fontName.toLowerCase()) != null) { + // first, try to load the font itself using the current font pattern + uf = loadUF(fontName, suffix); + if (uf == null) { + uf = loadUF(fontName, "$p" + size + "u" + uIndex); // guich@tc122_15: ... check only here + } + if (uf == null && f.size != totalcross.ui.font.Font.NORMAL_SIZE) { + int t = f.size; + while (uf == null && ++t <= 120) { + uf = loadUF(fontName, "$p" + t + "u" + uIndex); + } + t = f.size; + while (uf == null && --t >= 5) { + uf = loadUF(fontName, "$p" + t + "u" + uIndex); + } + } + if (uf == null) { + uf = loadUF(fontName, "$" + faceType + totalcross.ui.font.Font.NORMAL_SIZE + "u" + uIndex); + } + if (uf == null && faceType != 'p') { + uf = loadUF(fontName, "$p" + totalcross.ui.font.Font.NORMAL_SIZE + "u" + uIndex); + } + } + + // at last, use the default font + if (uf == null) { + uf = loadUF(totalcross.ui.font.Font.DEFAULT, suffix); + } + if (uf == null && fontName.charAt(0) != '$') { + for (int i = totalcross.ui.font.Font.MIN_FONT_SIZE; i <= totalcross.ui.font.Font.MAX_FONT_SIZE; i++) { + if ((uf = loadUF(fontName, "$p" + i + "u" + uIndex)) != null) { + break; + } + } + } + if (uf == null) { + for (int i = totalcross.ui.font.Font.MIN_FONT_SIZE; i <= totalcross.ui.font.Font.MAX_FONT_SIZE; i++) { + if ((uf = loadUF(totalcross.ui.font.Font.DEFAULT, "$p" + i + "u" + uIndex)) != null) { + break; + } + } + } + + if (uf != null) { + if (totalcross.ui.font.Font.baseChar == ' ') { + htLoadedFonts.put(key, uf); // note that we will use the original key to avoid entering all exception handlers. + } + f.name = uf.fontName; // update the name, the font may have been replaced. + } else if (htLoadedFonts.size() > 0) { + return c == ' ' ? null : getFont(f, ' '); // probably the index was outside the available ranges at this font - guich@tc110_28: if space, just return null + } else if (appletInitialized) // guich@500_1: when retroguard is loaded, Applet.init is never called, so we just skip here. + { + System.err.println("No fonts found! be sure to place the file " + totalcross.ui.font.Font.DEFAULT + + ".tcz in the same directory from where you're running your application" + + (isApplication ? " or put a reference to TotalCross3/etc folder in the classpath!" + : "or in your applet's codebase or in a jar file!")); + System.exit(2); + } + } catch (Exception e) { + System.out.println("Launcher.getFont: " + e); + } + return uf; + } + + /** Represents the internal font structure, read from a pdb file. used internally. */ + // created by guich@200b2 + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherState.java b/TotalCrossSDK/src/main/java/totalcross/LauncherState.java new file mode 100644 index 000000000..aecb74e6d --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherState.java @@ -0,0 +1,320 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherState extends LauncherSupport { + public void destroy() { + if (mainWindow == null || destroyed) { + return; + } + destroyed = true; + boolean ended = eventThread == null || eventThread.invokeInEventThread(true, new Runnable() { + @Override + public void run() { + mainWindow.appEnding(); + System.runFinalization(); + storeSettings(); + } + }, previewMode ? PREVIEW_DESTROY_TIMEOUT_MILLIS : 0); + if (!ended) { + System.err.println("Timed out waiting for TotalCross preview appEnding during reload."); + } + if (winTimer != null) { + winTimer.stopGracefully(); // timer must be running when appEnding is called + } + if (eventThread != null) { + eventThread.stopGracefully(); + eventThread = null; + } + stopWindowBackend(); + } + + @SuppressWarnings("static-access") + final public void init() { + boolean showInstructionsOnError = true; + appletInitialized = true; // guich@500_1 + totalcross.sys.Settings.showDesktopMessages = true; // guich@500_1: redo the messages. + try { + alert = new AlertBox(); + // NOTE: applet parameters are supplied by LauncherApplet after construction, + // so they can only be parsed during init. + if (!isApplication) { + String arguments = appletArguments; + if (arguments == null) { + throw new Exception( + "Error: you must suply an 'arguments' property with all the argments to create the application"); + } + String[] args = tokenizeString(arguments, ' '); + parseArguments(args); + getRuntime().recordLauncherUsage(); + } + + getRuntime().initializeSettings((Launcher) this); + + try { + _class = getClass(); // guich@500_1: we can use ourselves + // if the user pass: tc/samples/ui/image/test/ImageTest.class, change to tc.samples.ui.image.test.ImageTest + className = LauncherRuntime.normalizeMainWindowClassName(className); + mainWindow = getRuntime().createMainWindow(className, getAppClassLoader(), terminateIfMainClass); + showInstructionsOnError = false; + if (mainWindow == null) { + return; + } + // NOTE: java will call a partially constructed object if show() is called before all the objects are constructed + if (isApplication && !previewMode) { + createWindowBackend(); + requestFocus(); + } else if (!previewMode) { + setLayout(new java.awt.BorderLayout()); + } + if (toUI != -1) { + mainWindow.setUIStyle((byte) toUI); + } + } catch (LinkageError le) { + System.out.println("Fatal Error when running applet: there is an error in the constructor of the class " + + className + " and it could not be instantiated. Stack trace: "); + le.printStackTrace(); + exit(0); + } catch (ClassCastException cce) { + System.out.println("Error: class " + className + " does not extend MainClass nor MainWindow!"); + cce.printStackTrace(); + exit(-1); + } catch (ClassNotFoundException cnfe) { + System.out.println("The MainWindow class specified was not found: " + className + "\n\nCommon causes are:"); + System.out + .println(". The name is misspelled: java is case sensitive, so UIGadgets is not the same of uigadgets"); + if (className.indexOf('.') < 0) { + System.out.println( + ". The package name is incorrect: if you declared a class like: \n package com.foo.bar;\n public class " + + className + "\n then you must specify com.foo.bar." + className + + " as the main class; only specifying " + className + " is not enough."); + } + System.out.println( + ". Its location was not added to the classpath: if you're running from the prompt, be sure to add the path where your application is to the CLASSPATH argument. For example, if the class is in the current path, add a . specifying the current path: java -classpath .;tc.jar totalcross.Launcher " + + className); + exit(-1); + } + } catch (Exception ee) { + if (showInstructionsOnError) { + LauncherArguments.showInstructions(); + } + ee.printStackTrace(); + } // guich@120 + } + + protected ClassLoader getAppClassLoader() { + return appClassLoader == null ? getClass().getClassLoader() : appClassLoader; + } + + protected LauncherRuntime getRuntime() { + if (runtime == null) { + runtime = new LauncherRuntime(); + } + return runtime; + } + + protected void createWindowBackend() { + windowBackend = getRuntime().startWindowBackend(instance, + frameTitle != null ? frameTitle : mainWindow.getClass().getName(), + new java.awt.Color(getScreenColor(mainWindow.getBackColor())), instance, instance); + } + + protected boolean hasWindowBackend() { + return windowBackend != null; + } + + protected void stopWindowBackend() { + if (windowBackend != null) { + windowBackend.stop(); + windowBackend = null; + } + } + + protected void setWindowSize(int width, int height, boolean resizeFrame) { + windowBackend.setContentSize(width, height, resizeFrame); + } + + protected void setWindowTitle(String title) { + windowBackend.setTitle(title); + } + + protected void minimizeWindow() { + windowBackend.setExtendedState(Frame.ICONIFIED); + } + + protected void restoreWindow() { + windowBackend.setExtendedState(Frame.NORMAL); + } + + protected boolean windowFocusOwnerIsNotThis() { + return windowBackend.getFocusOwner() != this; + } + + protected Point getWindowLocation() { + return windowBackend.getLocation(); + } + + protected void setWindowLocation(int x, int y) { + windowBackend.setLocation(x, y); + } + + protected int getWindowContentWidth() { + return windowBackend.getContentWidth(); + } + + protected int getWindowContentHeight() { + return windowBackend.getContentHeight(); + } + + protected class WinTimer extends java.lang.Thread { + protected int interval; + protected boolean shouldStop; + + @Override + public void run() { + // NOTE: because we have created an official event queue/thread, which now + // resembles the device event queue much more closely, we must be + // sure that all timers and TC threads are run in that event thread. This + // will ensure that such things as blinking cursors will continue to work + // if there is a blocking modal dialog open. This also means that TC JDK + // threads will act much more like the device threads... in that, threads + // will not run unless a message pump is running. + while (!shouldStop) { + boolean doTick = true; + int millis = interval; + if (millis <= 0) { + // NOTE: Netscape navigator doesn't support interrupt() + // so we sleep here less than we would normally need to + // (1 second) if we're not doing anything to check if + // the timer should start in case interrupt didn't work + millis = 1 * 1000; + doTick = false; + } + // guich@200b4_84: implement the simple thread + long first = System.currentTimeMillis(); + while ((System.currentTimeMillis() - first) < millis) { + try { + sleep(millis); + doTick = true; // guich@230_3 + break; // guich@230_3 + } catch (InterruptedException e) { + doTick = false; + break; // guich@230_4 + } + } + if (doTick && eventThread != null) { + eventThread.invokeInEventThread(false, new Runnable() { + @Override + public void run() { + synchronized (instance) // guich@510_2: synchronize the repaint with the timer + { + mainWindow._onTimerTick(true); + } + } + }); + } + } + } + + void setInterval(int millis) { + //System.out.println("setInterval "+millis); + interval = millis < 10 ? 10 : millis; // guich@230_3 + interrupt(); + } + + void stopGracefully() { + // NOTE: It's not a good idea to call stop() on threads since + // it can cause the JVM to crash. + shouldStop = true; + interrupt(); + } + } + + public boolean eventIsAvailable() { + return eventThread.eventAvailable(); + } + + void startApp() { + eventThread = new TCEventThread(mainWindow); + if (!started) // guich@120 - make sure that the component is available for drawing when starting the application. called by paint. + { + try { + eventThread.invokeInEventThread(true, new Runnable() { + @Override + public void run() { + while (mainWindow == null) { + Thread.yield(); + } + mainWindow.appStarting(isDemo ? 80 : -1); + } // guich@200b4_107 - guich@570_3: check if mainWindow is not null to avoid problems when running on Linux. seems that the paint event is being generated before the start one. + }); + } catch (Throwable e) { + e.printStackTrace(); + } + started = true; + } + } + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherStorage.java b/TotalCrossSDK/src/main/java/totalcross/LauncherStorage.java new file mode 100644 index 000000000..bb9969b2f --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherStorage.java @@ -0,0 +1,535 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherStorage extends LauncherRendering { + /////////////////////// I/O ///////////////////////////////////// + protected File[] getClassPathDirectories() throws Exception { + char dirSeparator = File.pathSeparatorChar; + File[] classPath; + String pathstr = System.getProperty("java.class.path"); + // Count the number of path separators + int i = 0; + int n = 0; + int j = 0; + while ((i = pathstr.indexOf(dirSeparator, i)) != -1) { + n++; + i++; + } + // Build the class path + File[] path = new File[n + 1]; + int len = pathstr.length(); + for (i = n = 0; i < len; i = j + 1) { + if ((j = pathstr.indexOf(dirSeparator, i)) == -1) { + j = len; + } + if (i != j) { + String p = pathstr.substring(i, j); + File file = new File(p); + if (!file.isDirectory()) { + file = new File(getPathOf(p)); // add the parent path of the file + } + if (file.isDirectory()) { + path[n++] = file; + } + } + } + // Trim class path to exact size + classPath = new File[n]; + System.arraycopy(path, 0, classPath, 0, n); + return classPath; + } + + protected InputStream readJavaInputStream(java.io.InputStream is) { + if (is == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + byte[] buf = new byte[128]; + int len; + while (true) { + try { + len = is.read(buf); + } catch (java.io.IOException e) { + break; + } + if (len > 0) { + baos.write(buf, 0, len); + } else { + break; + } + } + return new ByteArrayInputStream(baos.toByteArray()); + } + + protected String getPathOf(String pathAndFileName) { + char[] chars = pathAndFileName.toCharArray(); + for (int i = chars.length - 1; i >= 0; i--) { + if (chars[i] == '\\' || chars[i] == '/') { + return new String(chars, 0, i); + } + } + return ""; // no path + } + + public String getDataPath() // guich@420_11 - this now is needed because the user may change the datapath anywhere in the program + { + String path = totalcross.sys.Settings.dataPath; + if (path != null) { + path = path.replace('\\', '/'); + if (!path.endsWith("/")) { + path += "/"; + // don't check for folder to keep compatibility with win32 vm + //java.io.File f = new java.io.File(newDataPath); + //if (!f.isDirectory()) + // System.out.println("ERROR: dataPath specified is not a directory or does not exist! "+newDataPath); + } + } + return path; + } + + protected String getMainWindowPath() { + if (MainWindow.getMainWindow() == null) { + return null; + } + String main = MainWindow.getMainWindow().getClass().getName().replace('.', '/'); + return getPathOf(main) + "/"; + } + + /** used in some classes so they can correctly open files. now can open jar files. */ + public InputStream openInputStream(String path) { + String sread = "\nopening for read " + path + "\n"; + String dataPath = getDataPath(); + InputStream stream = null; + String mainpath = getMainWindowPath(); + try { + try // guich@tc100: removed the nonGuiApp flag + { + sread += "#0 - the file given: " + path + "\n"; + stream = new FileInputStream(path); // guich@421_72 + } catch (Exception e) { + stream = null; + } + if (stream == null && isApplication) { + // search in the Settings.dataPath + try { + String p = isOk(dataPath) ? (dataPath + path) : path; + sread += "#1 - dataPath: " + p + "\n"; + stream = new FileInputStream(p); + htOpenedAt.put(path, getPathOf(p)); // guich@200b4_82 - jr: i changed getPathOf(path) to getPathOf(p) + } catch (Exception e) { + stream = null; + } + if (stream == null && mainpath != null) { + try { + String p = mainpath + path; + sread += "#2 - MainWindow's path from current folder: " + p + "\n"; + stream = new FileInputStream(p); + htOpenedAt.put(path, getPathOf(p)); // guich@200b4_82 - jr: i changed getPathOf(path) to getPathOf(p) + } catch (Exception e) { + stream = null; + } + } + // search in the classpath + if (stream == null) { + sread += "#3 - classpath\n"; + File[] dirs = getClassPathDirectories(); + File f = null; + for (int i = 0; i < dirs.length; i++) { + try { + f = new File(dirs[i], path); + if (!f.isFile() && mainpath != null) { + f = new File(dirs[i], mainpath + path); // guich@tc100: search in the path of the main window + } + if (f.isFile()) { + String ff = getPathOf(f.getAbsolutePath()); + htOpenedAt.put(path, ff); // guich@200b4_82 - jr: changed dirs[i].getAbsolutePath - guich@tc112_20: using f.getAbsolutePath instead of dirs[i].getAbsolutePath + break; + } else { + f = null; // guich@400_8: fixed problem when file was not found so the #3 can be tried below + } + } catch (Exception e) { + f = null; + } + } + if (f != null) { + stream = new FileInputStream(f); + } + } + if (stream == null && _class != null) // guich@400_6: now the resources can be read from the jar file + { + sread += "#4 - jar file\n"; + try { + InputStream is = (InputStream) _class.getResourceAsStream("/" + path); + if (is != null) { + stream = readJavaInputStream(is); + } + } catch (Throwable tt) { + if (tt.getMessage() != null) { + System.out.println(tt.getMessage()); + } + } + } + String sjar; + if (stream == null && !path.endsWith(".class") + && (sjar = getClass().getProtectionDomain().getCodeSource().getLocation().getPath()).contains(".jar")) // guich@330 - let tc.Help work from inside a jar + { + sread += "#4b - " + sjar.substring(1) + "\n"; + try { + URL url = getClass().getProtectionDomain().getCodeSource().getLocation(); + ZipInputStream zIn = new ZipInputStream(url.openStream()); + String spath = "/" + path; + for (java.util.zip.ZipEntry zEntry = zIn.getNextEntry(); zEntry != null; zEntry = zIn.getNextEntry()) { + if (zEntry.getName().endsWith(spath)) { + stream = readJavaInputStream(zIn); + break; + } + } + zIn.close(); + } catch (Throwable tt) { + if (tt.getMessage() != null) { + System.out.println(tt.getMessage()); + } + } + } + if (stream == null && htAttachedFiles.size() > 0) // guich@tc100: load from attached libraries too + { + sread += "#5 - attached libraries\n"; + totalcross.io.ByteArrayStream bas = (totalcross.io.ByteArrayStream) htAttachedFiles.get(path.toLowerCase()); + if (bas != null) { + stream = new ByteArrayInputStream(bas.getBuffer()); // buffer is the same size of the loaded file. + } + } + } else if (stream == null) { + URL url; + // zero in the jar file (normal way) + InputStream is = null; + try { + is = (InputStream) _class.getResourceAsStream("/" + path); + } catch (Throwable tt) { + if (tt.getMessage() != null) { + System.out.println(tt.getMessage()); + } + } + sread += "#1 - resource: " + is + "\n"; // guich@200b4_59 + if (is != null) { + stream = readJavaInputStream(is); + } + // first in the jar file + // guich@200b4: using this in Internet makes the archive be fetched from the server at each call of this function. + if (stream == null) { + String archive = getLauncherParameter("archive"); + sread += "#2 - archive: " + archive + "\n"; + if (isOk(archive) && !archive.equals("null")) { + String[] archives = tokenizeString(archive, ','); // guich@580_39: if there are more than one file, split them + for (int i = 0; i < archives.length; i++) { + archive = archives[i]; + if (archive.startsWith("null")) { + archive = archive.substring(4); + } + URL codeBase = getLauncherCodeBase(); + url = new URL(codeBase + "/" + archive); + try { + ZipInputStream zIn = new ZipInputStream(url.openStream()); + java.util.zip.ZipEntry zEntry = zIn.getNextEntry(); + while (!zEntry.getName().equals(path)) { + zEntry = zIn.getNextEntry(); + if (zEntry == null) { + throw new Exception("doh"); + } + } + // guich@200b2: ok. the zIn.available() returns 1 and not the real size of the zip entry. so, here we read all into a byte stream + stream = readJavaInputStream(zIn); + } catch (Exception e) { + if (!e.getMessage().equals("doh")) { + e.printStackTrace();/* doh didn't find it in the jar thing */ + } + } + } + } + } + // second under the codebase + if (stream == null) { + try { + URL codeBase = getLauncherCodeBase(); + String cb = codeBase.toString(); + char lastc = cb.charAt(cb.length() - 1); + char firstc = path.charAt(0); + if (lastc != '/' && firstc != '/') { + cb += "/"; + } + sread += "#3 - url: " + cb + path + "\n"; + url = new URL(cb + path); + stream = url.openStream(); + } catch (FileNotFoundException ee) { + } catch (Exception e) { + e.printStackTrace(); + /* neither in the codebase */} + } + // third in the localhost + if (stream == null) { + try { + sread += "#4- url: file://localhost/" + dataPath + path + "\n"; + url = new URL("file://localhost/" + dataPath + path); // guich@120 + stream = url.openStream(); + } catch (Exception e) { + } + } + ; + if (stream == null && htAttachedFiles.size() > 0) // guich@tc100: load from attached libraries too + { + sread += "#5 - attached libraries\n"; + totalcross.io.ByteArrayStream bas = (totalcross.io.ByteArrayStream) htAttachedFiles.get(path.toLowerCase()); + if (bas != null) { + stream = new ByteArrayInputStream(bas.getBuffer()); // buffer is the same size of the loaded file. + } + } + } + if (stream == null) { + debug(sread + "file not found\n"); + } + } catch (FileNotFoundException ee) { + print("file not found"); + } catch (Exception e) // guich@120 + { + if (isOk(e.getMessage())) { + print("error in JavaBridge.openInputStream: " + e.getMessage()); + } + return null; + } + return stream; + } + + protected OutputStream openOutputUrl(URL url) { + try { + URLConnection con = url.openConnection(); + con.setUseCaches(false); + con.setDoOutput(true); + con.setDoInput(false); + return con.getOutputStream(); + } catch (Exception u) // try another way + { + try { + String path = url + ""; + return new FileOutputStream(isOk(totalcross.sys.Settings.dataPath) ? (getDataPath() + path) : path); + } catch (Exception ee) { + return null; + } + } + } + + /** used in some classes so they can correctly open files. used internally by readBytes. */ + public OutputStream openOutputStream(String path) { + debug("\nopening for write " + path); + String dataPath = getDataPath(); + OutputStream stream = null; + String readPath = (String) htOpenedAt.get(path); // guich@tc112_20 + try { + try // guich@tc100: removed the nonGuiApp flag + { + String pp = isOk(dataPath) ? (dataPath + path) + : isOk(readPath) ? totalcross.sys.Convert.appendPath(readPath, path) : path; // guich@tc112_20: use readPath if not null + stream = new FileOutputStream(pp); // guich@421_11: added support for dataPath + } catch (Exception e) { + stream = null; + } + + if (stream == null && isApplication) { + // search in the place where it was read - guich@200b4_82 + if (readPath != null) { + try { + debug("#1 - read path"); + stream = new FileOutputStream(new java.io.File(readPath, path)); + debug("found in " + readPath); + } catch (Exception e) { + stream = null; + } + } + if (stream != null) { + return stream; + } + // search in the Settings.dataPath + try { + String p = isOk(dataPath) ? (dataPath + path) : path; + debug("#2 - Settings.dataPath"); + stream = new FileOutputStream(p); + debug("found in " + p); + } catch (Exception e) { + stream = null; + } + // search in the classpath + if (stream == null) { + debug("#3 - classpath"); + File[] dirs = getClassPathDirectories(); + File f = null; + for (int i = 0; i < dirs.length; i++) { + try { + f = new File(dirs[i], path); + if (f.isFile()) { + debug("found in " + dirs[i]); + break; + } + } catch (Exception e) { + f = null; + } + } + if (f == null) { + debug("could not find file in the classpath"); + } else { + stream = new FileOutputStream(f); + } + } + } else if (stream == null) { + URL url; + // first under the codebase + if (stream == null) { + try { + URL codeBase = getLauncherCodeBase(); + debug("#1- codeBase: " + codeBase); + String cb = codeBase.toString(); + char lastc = cb.charAt(cb.length() - 1); + char firstc = path.charAt(0); + if (lastc != '/' && firstc != '/') { + cb += "/"; + } + url = new URL(cb + path); + stream = openOutputUrl(url); + debug("found under codebase: " + url); + } catch (Exception e) { + e.printStackTrace(); + /* neither in the codebase */} + } + // third in the localhost + if (stream == null) { + try { + debug("#2- url: file://localhost/" + dataPath + path); + url = new URL("file://localhost/" + dataPath + path); // guich@120 + stream = openOutputUrl(url); + debug("found under localhost: " + url); + } catch (Exception e) { + } + } + ; + } + if (stream == null) { + debug("file not found"); + } + } catch (FileNotFoundException ee) { + debug("file not found"); + } catch (Exception e) { + /*if (!msgShowed) */print("error in Vm.openOutputStream: " + e.getMessage()); + return null; + } // guich@200 + return stream; + } + + /** read the available bytes from the stream getted with openInputStream. + * called by totalcross.ui.image.Image and totalcross.io.PDBFile + */ + public byte[] readBytes(String path) { + byte[] bytes = null; + try { + InputStream is = openInputStream(path); + if (is != null) { + int n = is.available(); + bytes = new byte[n]; + is.read(bytes); + is.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return bytes; + } + + /** write the available bytes to the stream getted with openOutputStream. + * called by totalcross.io.PDBFile + */ + public boolean writeBytes(String path, byte[] buf, int len) { + boolean ret = true; + try { + OutputStream os = openOutputStream(path); + if (os != null) { + if (buf != null) { + os.write(buf, 0, len); + os.close(); // pietj@330_1 + } else { + print("ATT: you sent to stream.writeBytes a null buffer!"); + } + } + } catch (Exception e) { + e.printStackTrace(); + ret = false; + } + return ret; + } + + /** return true is the string is valid. called by openInputStream and openOutputStream in this class. */ + protected boolean isOk(String s) { + return s != null && s.length() > 0; + } + + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherStreamTypes.java b/TotalCrossSDK/src/main/java/totalcross/LauncherStreamTypes.java new file mode 100644 index 000000000..0fd8667e1 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherStreamTypes.java @@ -0,0 +1,309 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherStreamTypes extends Panel implements WindowListener, KeyListener, + java.awt.event.MouseListener, MouseWheelListener, MouseMotionListener, ComponentListener { + protected class AlertBox extends Frame implements java.awt.event.ActionListener { + protected java.awt.Button ok; + protected java.awt.TextArea ta; + + public AlertBox() { + super("Alert"); + setLayout(new BorderLayout()); + add("Center", ta = new java.awt.TextArea()); + Panel p = new Panel(); + p.setLayout(new FlowLayout()); + p.add(ok = new java.awt.Button("Ok")); + ok.addActionListener(this); + add("South", p); + pack(); + Dimension d = getToolkit().getScreenSize(); + setLocation(d.width / 3, d.height / 3); + } + + @Override + public void actionPerformed(java.awt.event.ActionEvent ae) { + if (ae.getSource() == ok) { + setVisible(false); + } + } + + public void setText(String s) { + ta.setText(s); + } + } + + public static class IS2S extends totalcross.io.Stream { + InputStream is; + + public IS2S(InputStream is) { + this.is = is; + } + + @Override + public void close() { + try { + is.close(); + } catch (Exception e) { + } + is = null; + } + + @Override + public int readBytes(byte[] buf, int start, int count) { + try { + return is.read(buf, start, count); + } catch (Exception e) { + return -1; + } + } + + @Override + public int writeBytes(byte[] buf, int start, int count) { + return 0; // not supported + } + } + + public static class S2FIS extends java.io.FilterInputStream { + protected RandomAccessStream s; + protected int pos = -1; + protected int readLimit = -1; + + public S2FIS(RandomAccessStream s) { + this(s, -1, true); + } + + public S2FIS(RandomAccessStream s, int max) { + this(s, max, true); + } + + public S2FIS(RandomAccessStream s, int max, boolean closeUnderlying) { + super(new S2IS(s, max, closeUnderlying)); + this.s = s; + } + + @Override + public synchronized void mark(int readlimit) { + try { + this.pos = s.getPos(); + this.readLimit = readlimit; + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public synchronized void reset() throws java.io.IOException { + if (this.pos == -1) { + throw new java.io.IOException("the stream has not been marked"); + } + if (s.getPos() - this.pos > readLimit) { + throw new java.io.IOException("the mark has been invalidated"); + } + s.setPos(this.pos); + } + + @Override + public boolean markSupported() { + return true; + } + } + + public static class S2IS extends java.io.InputStream { + protected Stream s; + protected byte[] oneByte = new byte[1]; + protected int left; + protected boolean closeUnderlying; + + public S2IS(Stream s) { + this(s, -1, true); + } + + public S2IS(Stream s, int max) { + this(s, max, true); + } + + public S2IS(Stream s, int max, boolean closeUnderlying) { + this.s = s; + this.left = max; + this.closeUnderlying = closeUnderlying; + } + + @Override + public int read() throws java.io.IOException { + if (left == 0) { + return -1; + } + + try { + int r = s.readBytes(oneByte, 0, 1); + + if (left != -1 && r == 1) { + left--; + } + + return r > 0 ? ((int) oneByte[0] & 0xFF) : -1; + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + + @Override + public int read(byte[] buf, int off, int len) throws java.io.IOException { + if (left == 0) { + return -1; + } + + try { + if (left != -1 && len > left) { + len = left; + } + + int r = s.readBytes(buf, off, len); + + if (left != -1 && r > 0) { + left -= r; + } + + return r; + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + + @Override + public void close() throws java.io.IOException { + if (closeUnderlying) { + try { + s.close(); + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + } + } + + public static class S2OS extends java.io.OutputStream { + protected Stream s; + protected byte[] oneByte = new byte[1]; + protected int count; + protected boolean closeUnderlying; + + public S2OS(Stream s) { + this(s, true); + } + + public S2OS(Stream s, boolean closeUnderlying) { + this.s = s; + this.closeUnderlying = closeUnderlying; + } + + public int count() { + return count; + } + + @Override + public void write(int b) throws java.io.IOException { + try { + oneByte[0] = (byte) (b & 0xFF); + + int c = s.writeBytes(oneByte, 0, 1); + if (c < 0) { + throw new java.io.IOException("Unknown error when writing to stream"); + } + count++; + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + + @Override + public void write(byte[] b, int off, int len) throws java.io.IOException { + try { + int c = s.writeBytes(b, off, len); + if (c < 0) { + throw new java.io.IOException("Unknown error when writing to stream"); + } + count += c; + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + + @Override + public void close() throws java.io.IOException { + if (closeUnderlying) { + try { + s.close(); + } catch (IOException e) { + throw new java.io.IOException(e.getMessage()); + } + } + } + } + + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherStreams.java b/TotalCrossSDK/src/main/java/totalcross/LauncherStreams.java new file mode 100644 index 000000000..e3effc8e5 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherStreams.java @@ -0,0 +1,160 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +class LauncherStreams extends LauncherFonts { + public void alert(String msg) { + if (!started) { + System.out.println("Alert: " + msg); + } else { + alert.setText(msg); + alert.setVisible(true); + while (alert.isVisible()) { + try { + Thread.sleep(10); + } catch (Exception e) { + } + } + } + } + + /** Converts a java.io.InputStream into a totalcross.io.Stream */ + public void setTitle(String title) { + if (isApplication) { + frameTitle = title; + if (hasWindowBackend()) { + setWindowTitle(title); + } + } + } + + public void vibrate(final int millis) { + if (isApplication && hasWindowBackend()) { + new Thread() { + @Override + public void run() { + Point p = getWindowLocation(); + int x = p.x, y = p.y; + + int[] xPoints = { x - 3, x, x + 3, x, x + 3, x, x - 3, x }; + int[] yPoints = { y - 3, y, y + 3, y, y - 3, y, y + 3, y }; + int i = 0; + int j = 0; + + int t = Vm.getTimeStamp(); + do { + setWindowLocation(xPoints[i], yPoints[j]); + + i = ++i % xPoints.length; + if (i == 0) { + j = ++j % yPoints.length; + } + + Thread.yield();// give some time for the other threads to execute + } while (Vm.getTimeStamp() - t < millis); + + setWindowLocation(x, y); // restore original location + } + }.start(); + } + } + + public void setSIP(int option, Control edit, boolean secret) { + } + + @Override + public void componentHidden(ComponentEvent arg0) { + } + + @Override + public void componentMoved(ComponentEvent arg0) { + } + + @Override + public void componentShown(ComponentEvent arg0) { + } + + boolean ignoreNextResize; // guich@tc168: ignore when using F9 + + @Override + public void componentResized(ComponentEvent ev) { + if (ignoreNextResize) { + ignoreNextResize = false; + return; + } + if (!hasWindowBackend()) { + return; + } + int w = getWindowContentWidth(); + int h = getWindowContentHeight(); + w /= toScale; // guich@tc168: consider scale + h /= toScale; + if (w < toWidth || h < toHeight) { + screenResized(w >= toWidth ? w : toWidth, h >= toHeight ? h : toHeight, true); + } else { + screenResized(w, h, false); + } + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/LauncherSupport.java b/TotalCrossSDK/src/main/java/totalcross/LauncherSupport.java new file mode 100644 index 000000000..20b521833 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/LauncherSupport.java @@ -0,0 +1,151 @@ +// Copyright (C) 1998, 1999 Wabasoft +// Copyright (C) 2000 Dave Slaughter +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; + +import tc.tools.JarClassPathLoader; +import tc.tools.deployer.DeploySettings; +import totalcross.io.IOException; +import totalcross.io.RandomAccessStream; +import totalcross.io.Stream; +import totalcross.preview.AppletPreviewSurface; +import totalcross.preview.AwtWindowBackend; +import totalcross.preview.PreviewRuntime; +import totalcross.preview.PreviewFrameConsumer; +import totalcross.sys.Settings; +import totalcross.sys.SpecialKeys; +import totalcross.sys.Time; +import totalcross.sys.Vm; +import totalcross.ui.Control; +import totalcross.ui.Container; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.event.KeyEvent; +import totalcross.ui.event.MultiTouchEvent; +import totalcross.ui.event.PenEvent; +import totalcross.util.Hashtable; +import totalcross.util.IntHashtable; +import totalcross.util.zip.TCZ; + +@SuppressWarnings({"deprecation", "removal"}) +abstract class LauncherSupport extends LauncherFontTypes { + public static Launcher instance; + public static boolean isApplication; + public static boolean terminateIfMainClass = true; + public String commandLine = ""; + public int threadCount; + public Hashtable htOpenedAt = new Hashtable(31); // guich@200b4_82 + public IntHashtable keysPressed = new IntHashtable(129); + public MainWindow mainWindow; + public boolean showKeyCodes; + public Hashtable htAttachedFiles = new Hashtable(5); // guich@566_28 + public static int userFontSize = -1; + + protected int toBpp = 24; + protected int toWidth = -1; + protected int toHeight = -1; + + protected String className; + protected boolean appletInitialized; // guich@500_1 + protected AwtWindowBackend windowBackend; + protected int toUI = -1; // guich@573_6: since now we have 4 styles, select the target one directly. + protected double toScale = -1; + protected LauncherState.WinTimer winTimer; + protected boolean started; // guich@120 + protected boolean destroyed; // guich@230_24 + protected boolean settingsFilled; + protected int[] screenPixels = new int[0]; + protected int lookupR[], lookupG[], lookupB[], lookupGray[]; + protected int pal685[]; + protected Class _class; // used by the openInputStream method. + protected BufferedImage screenImg; + protected AlertBox alert; + protected String frameTitle; + protected String crid4settings; // prevent from having two different crids for loading and storing the settings. + protected StringBuffer mmsb = new StringBuffer(32); + protected TCEventThread eventThread; + protected static final long PREVIEW_DESTROY_TIMEOUT_MILLIS = 10000; + protected boolean isDemo; + protected boolean fastScale; + protected boolean previewMode; + protected PreviewRuntime.FrameConsumer previewSurface; + protected ClassLoader appClassLoader; + protected LauncherRuntime runtime; + protected String appletArguments; + protected URL appletCodeBase; + protected final Map appletParameters = new HashMap(); + + protected double toScaleValue = -1; + protected double toDensityValue = 1; + public totalcross.ui.Insets toInsetsPortrait; + public totalcross.ui.Insets toInsetsLandscape; + protected LauncherSupport() { + instance = (Launcher) this; + } + + protected void initializeLauncher() { + if (!previewMode) { + addKeyListener(this); + addMouseListener(this); + addMouseWheelListener(this); + addMouseMotionListener(this); + } + try { + File libsFile = new File(DeploySettings.distDir, "libs"); + JarClassPathLoader.addJar(libsFile, "jna"); + JarClassPathLoader.addJar(libsFile, "jna-platform"); + JarClassPathLoader.addJar(libsFile, "slf4j-api"); + JarClassPathLoader.addJar(libsFile, "appdirs"); + JarClassPathLoader.addJar(libsFile, "thumbnailator"); + } catch (java.io.IOException e) { + e.printStackTrace(); + } + } + + protected boolean ignoreNextResize; + protected void parseArguments(String... args) { } + protected void storeSettings() { } + static void showInstructions() { } + protected int getScreenColor(int pixel) { return pixel; } + protected String[] tokenizeString(String value, char separator) { return value.split(java.util.regex.Pattern.quote(String.valueOf(separator))); } + protected PreviewFrameConsumer getPreviewFrameConsumer() { return null; } + public abstract void exit(int exitCode); + +} diff --git a/TotalCrossSDK/src/main/java/totalcross/TCEventThread.java b/TotalCrossSDK/src/main/java/totalcross/TCEventThread.java index 4fb8eb663..a73da107a 100644 --- a/TotalCrossSDK/src/main/java/totalcross/TCEventThread.java +++ b/TotalCrossSDK/src/main/java/totalcross/TCEventThread.java @@ -1,6 +1,7 @@ // Copyright (C) 2000 Dave Slaughter -// Copyright (C) 2000-2010 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -50,17 +51,35 @@ void pumpEvents() { } } + void stopGracefully() { + running = false; + eventQueue.push(new TCEvent(INVOKE_IN_EVENT_THREAD, new Runnable() { + @Override + public void run() { + } + })); + interrupt(); + } + + void setMainClass(MainClass win) { + this.win = win; + } + void privatePumpEvents() { // This gives the system CPU some breathing room when in a tight event // loop and no events are posted. final TCEvent event = popTime <= 0 ? (TCEvent) eventQueue.pop() : (TCEvent) eventQueue.popWait(popTime); if (event != null) { if (event.type == INVOKE_IN_EVENT_THREAD) { - event.r.run(); - // If they are waiting for this, then notify. - if (event.synch != null) { - synchronized (event.synch) { - event.synch.notify(); + try { + event.r.run(); + } finally { + // If they are waiting for this, notify even when the runnable fails. + if (event.synch != null) { + synchronized (event.synch) { + event.completed = true; + event.synch.notify(); + } } } } else { @@ -86,8 +105,13 @@ boolean hasEvent(int type) { } public void invokeInEventThread(boolean wait, Runnable r) { + invokeInEventThread(wait, r, 0); + } + + public boolean invokeInEventThread(boolean wait, Runnable r, long timeoutMillis) { if (!wait) { eventQueue.push(new TCEvent(INVOKE_IN_EVENT_THREAD, r)); + return true; } else { // 9/16/02 - Andy - I have modified the code below so that it now checks // to see if we are currently in the event thread. If we are in the @@ -98,18 +122,32 @@ public void invokeInEventThread(boolean wait, Runnable r) { java.lang.Thread current = java.lang.Thread.currentThread(); if (current.equals(this)) { r.run(); // Execute directly. + return true; } else { // We are not in the event thread, so push to event thread. // We have to create a new Object, in case there is more than one // thread waiting on an invoke. Object synch = new Object(); + TCEvent event = new TCEvent(INVOKE_IN_EVENT_THREAD, r, synch); synchronized (synch) { - eventQueue.push(new TCEvent(INVOKE_IN_EVENT_THREAD, r, synch)); + eventQueue.push(event); try { - synch.wait(); + if (timeoutMillis <= 0) { + while (!event.completed) { + synch.wait(); + } + } else { + long deadline = System.currentTimeMillis() + timeoutMillis; + long remaining = timeoutMillis; + while (!event.completed && remaining > 0) { + synch.wait(remaining); + remaining = deadline - System.currentTimeMillis(); + } + } } catch (Exception ie) { } } + return event.completed; } } } @@ -187,6 +225,7 @@ private class TCEvent { int timestamp; Runnable r; Object synch; + boolean completed; TCEvent(int type, int key, int x, int y, int modifiers, int timestamp) { this.type = type; diff --git a/TotalCrossSDK/src/main/java/totalcross/TotalCrossApplication.java b/TotalCrossSDK/src/main/java/totalcross/TotalCrossApplication.java index 2fd17d3ee..65d16258a 100644 --- a/TotalCrossSDK/src/main/java/totalcross/TotalCrossApplication.java +++ b/TotalCrossSDK/src/main/java/totalcross/TotalCrossApplication.java @@ -1,3 +1,7 @@ +// Copyright (C) 2020-2021 TotalCross Global Mobile Platform Ltda +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only package totalcross; import tc.Help; @@ -43,10 +47,8 @@ public static void run(Class clazz, String... args) { clazz = Help.class; args = new String[] { "/scr", "android", "/fontsize", "20", "/fingertouch" }; } - Launcher.isApplication = true; - - Launcher app = new Launcher(); - app.parseArguments(clazz.getCanonicalName(), args); - app.init(); + LauncherRuntime runtime = new LauncherRuntime(); + runtime.configure(new LauncherConfig(clazz.getCanonicalName(), args)); + runtime.startApplication(); } } diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/AppletPreviewSurface.java b/TotalCrossSDK/src/main/java/totalcross/preview/AppletPreviewSurface.java new file mode 100644 index 000000000..dc1a90392 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/AppletPreviewSurface.java @@ -0,0 +1,17 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.awt.Component; + +/** + * Compatibility wrapper for the existing Applet/AWT launcher presentation path. + * New simulator code should use {@link AwtCanvasSurface} directly. + */ +@Deprecated +public class AppletPreviewSurface extends AwtCanvasSurface { + public AppletPreviewSurface(Component component, double scale, boolean fastScale) { + super(component, scale, fastScale); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/AwtCanvasSurface.java b/TotalCrossSDK/src/main/java/totalcross/preview/AwtCanvasSurface.java new file mode 100644 index 000000000..d3f5daf8b --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/AwtCanvasSurface.java @@ -0,0 +1,134 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +import net.coobird.thumbnailator.Thumbnails; +import net.coobird.thumbnailator.Thumbnails.Builder; +import net.coobird.thumbnailator.resizers.configurations.Antialiasing; +import net.coobird.thumbnailator.resizers.configurations.Dithering; +import net.coobird.thumbnailator.resizers.configurations.Rendering; +import net.coobird.thumbnailator.resizers.configurations.ScalingMode; +import totalcross.ui.UIColors; + +/** + * AWT render surface for desktop simulator and preview windows. + *

+ * It owns the rendered image used by {@link RenderSurface#present(int[], int, int)} + * and also implements the {@link PreviewRuntime.FrameConsumer} image presentation path + * so Launcher.updateScreen() can keep creating and aliasing its BufferedImage + * exactly as before. + */ +public class AwtCanvasSurface extends Canvas implements RenderSurface, PreviewRuntime.FrameConsumer { + private final Component paintTarget; + private final double scale; + private final boolean fastScale; + private BufferedImage image; + private BufferedImage scaledImageSource; + private Builder thumbnailBuilder; + + public AwtCanvasSurface() { + this(null, 1, false); + } + + public AwtCanvasSurface(Component paintTarget, double scale, boolean fastScale) { + this.paintTarget = paintTarget == null ? this : paintTarget; + this.scale = scale; + this.fastScale = fastScale; + } + + @Override + public void resize(int width, int height, int scale) { + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + @Override + public void present(int[] pixels, int width, int height) { + if (image == null || image.getWidth() != width || image.getHeight() != height) { + resize(width, height, 1); + } + int[] target = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + System.arraycopy(pixels, 0, target, 0, Math.min(pixels.length, target.length)); + present(image); + } + + @Override + public void present(BufferedImage image) { + this.image = image; + Graphics graphics = paintTarget.getGraphics(); + if (graphics != null) { + paintImage(graphics, image, paintTarget); + } else { + paintTarget.repaint(); + } + } + + @Override + public void paint(Graphics graphics) { + if (image != null) { + paintImage(graphics, image, this); + } + } + + @Override + public void update(Graphics graphics) { + paint(graphics); + } + + private void paintImage(Graphics graphics, BufferedImage image, Component observer) { + int width = image.getWidth(); + int height = image.getHeight(); + int scaledWidth = (int) (width * scale); + int scaledHeight = (int) (height * scale); + int shiftY = totalcross.ui.Window.shiftY; + int shiftH = totalcross.ui.Window.shiftH; + if ((shiftY + shiftH) > height) { + totalcross.ui.Window.shiftY = shiftY = height - shiftH; + } + if (shiftY != 0) { + graphics.setColor(new Color(UIColors.unsafeAreaColor)); + int yy = (int) (shiftH * scale); + graphics.fillRect(0, yy, scaledWidth, scaledHeight - yy); + graphics.setClip(0, 0, scaledWidth, yy); + graphics.translate(0, -(int) (shiftY * scale)); + } + if (scale != 1) { + if (fastScale) { + graphics.drawImage(image, 0, 0, scaledWidth, scaledHeight, 0, 0, width, height, observer); + } else { + try { + graphics.drawImage(getThumbnailBuilder(image, scaledWidth, scaledHeight).asBufferedImage(), 0, 0, observer); + } catch (java.io.IOException e) { + e.printStackTrace(); + } + } + } else { + graphics.drawImage(image, 0, 0, scaledWidth, scaledHeight, 0, 0, width, height, observer); + } + if (shiftY != 0) { + graphics.translate(0, (int) (shiftY * scale)); + graphics.setClip(0, 0, scaledWidth, scaledHeight); + } + } + + private Builder getThumbnailBuilder(BufferedImage image, int width, int height) { + if (thumbnailBuilder == null || scaledImageSource != image) { + scaledImageSource = image; + thumbnailBuilder = Thumbnails + .of(image) + .size(width, height) + .rendering(Rendering.SPEED) + .scalingMode(ScalingMode.PROGRESSIVE_BILINEAR) + .antialiasing(Antialiasing.OFF) + .dithering(Dithering.DISABLE); + } + return thumbnailBuilder; + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/AwtWindowBackend.java b/TotalCrossSDK/src/main/java/totalcross/preview/AwtWindowBackend.java new file mode 100644 index 000000000..f31d6f32b --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/AwtWindowBackend.java @@ -0,0 +1,128 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.awt.Component; +import java.awt.Frame; +import java.awt.Insets; +import java.awt.Point; + +/** + * AWT window backend for the desktop simulator. + */ +public class AwtWindowBackend implements WindowBackend { + private final Frame frame; + private final Component component; + private final RenderSurface renderSurface; + private Insets insets = new Insets(0, 0, 0, 0); + private double scale = 1; + + public AwtWindowBackend(Component component) { + this(new Frame() { + @Override + public void update(java.awt.Graphics g) { + } + }, component); + } + + public AwtWindowBackend(Frame frame, Component component) { + this(frame, component, component instanceof RenderSurface ? (RenderSurface) component : new AwtCanvasSurface()); + } + + public AwtWindowBackend(Frame frame, Component component, RenderSurface renderSurface) { + this.frame = frame; + this.component = component; + this.renderSurface = renderSurface; + } + + @Override + public void start(WindowConfig config) { + scale = config.scaleFactor; + if (config.fullscreen) { + frame.setExtendedState(Frame.MAXIMIZED_BOTH); + frame.setUndecorated(true); + } + if (config.background != null) { + frame.setBackground(config.background); + } + frame.setResizable(config.resizable); + frame.setLayout(null); + frame.add(component); + frame.addNotify(); + insets = frame.getInsets(); + if (insets == null) { + insets = new Insets(0, 0, 0, 0); + } + setContentSize(config.width, config.height, true); + frame.setLocation(config.x, config.y); + frame.setTitle(config.title); + frame.setVisible(true); + if (config.windowListener != null) { + frame.addWindowListener(config.windowListener); + } + if (config.componentListener != null) { + frame.addComponentListener(config.componentListener); + } + } + + @Override + public void stop() { + frame.dispose(); + } + + @Override + public void setTitle(String title) { + frame.setTitle(title); + } + + @Override + public void requestRepaint() { + component.repaint(); + } + + @Override + public RenderSurface getRenderSurface() { + return renderSurface; + } + + public void setContentSize(int width, int height, boolean resizeFrame) { + if (resizeFrame) { + frame.setSize((int) (width * scale) + insets.left + insets.right, + (int) (height * scale) + insets.top + insets.bottom); + } + component.setBounds(insets.left, insets.top, (int) (width * scale), (int) (height * scale)); + } + + public int getContentWidth() { + return frame.getWidth() - insets.left - insets.right; + } + + public int getContentHeight() { + return frame.getHeight() - insets.top - insets.bottom; + } + + public Insets getInsets() { + return insets; + } + + public Point getLocation() { + return frame.getLocation(); + } + + public void setLocation(int x, int y) { + frame.setLocation(x, y); + } + + public int getExtendedState() { + return frame.getExtendedState(); + } + + public void setExtendedState(int state) { + frame.setExtendedState(state); + } + + public Component getFocusOwner() { + return frame.getFocusOwner(); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/PreviewCommandAdapter.java b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewCommandAdapter.java new file mode 100644 index 000000000..97d5d44bf --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewCommandAdapter.java @@ -0,0 +1,98 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +/** + * Runtime-neutral command boundary for a future IDE/process protocol. + * Implementations may bind the handler to the in-process launcher or to a + * worker transport without exposing AWT or TotalCross UI objects. + */ +public final class PreviewCommandAdapter implements AutoCloseable { + public interface Handler { + void start(String mainWindowClass, String[] args); + + void pump(); + + void resize(int width, int height, double density); + + void pointer(int x, int y, int button, boolean pressed); + + void key(int keyCode, boolean pressed, int modifiers); + + void prepareReload(); + + void replaceMainWindow(String mainWindowClass, String[] args); + + void close(); + } + + public interface LifecycleListener { + void onLifecycleEvent(PreviewLifecycleEvent event); + } + + private final Handler handler; + private final LifecycleListener lifecycle; + + public PreviewCommandAdapter(Handler handler, LifecycleListener lifecycle) { + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + this.handler = handler; + this.lifecycle = lifecycle; + } + + public void start(String mainWindowClass, String... args) { + emit(PreviewLifecycleEvent.of(PreviewLifecycleEvent.Kind.START, mainWindowClass)); + try { + handler.start(mainWindowClass, args == null ? new String[0] : args.clone()); + emit(PreviewLifecycleEvent.of(PreviewLifecycleEvent.Kind.READY, mainWindowClass)); + } catch (RuntimeException e) { + emit(PreviewLifecycleEvent.error(e)); + throw e; + } + } + + public void pump() { + handler.pump(); + } + + public void resize(int width, int height, double density) { + handler.resize(width, height, density); + } + + public void pointer(int x, int y, int button, boolean pressed) { + handler.pointer(x, y, button, pressed); + } + + public void key(int keyCode, boolean pressed, int modifiers) { + handler.key(keyCode, pressed, modifiers); + } + + public void prepareReload() { + handler.prepareReload(); + emit(PreviewLifecycleEvent.of(PreviewLifecycleEvent.Kind.RELOAD_READY, null)); + } + + public void replaceMainWindow(String mainWindowClass, String... args) { + handler.replaceMainWindow(mainWindowClass, args == null ? new String[0] : args.clone()); + } + + public void framePresented(PreviewFrame frame) { + if (frame != null) { + emit(PreviewLifecycleEvent.of(PreviewLifecycleEvent.Kind.FRAME, null)); + } + } + + @Override + public void close() { + handler.close(); + emit(PreviewLifecycleEvent.of(PreviewLifecycleEvent.Kind.CLOSED, null)); + } + + private void emit(PreviewLifecycleEvent event) { + if (lifecycle != null) { + lifecycle.onLifecycleEvent(event); + } + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrame.java b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrame.java new file mode 100644 index 000000000..4996bf9b6 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrame.java @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.util.Arrays; + +/** Immutable, runtime-neutral snapshot of one rendered preview frame. */ +public final class PreviewFrame { + public enum PixelFormat { + ARGB_8888 + } + + private final int width; + private final int height; + private final int stride; + private final double density; + private final PixelFormat pixelFormat; + private final int[] pixels; + + public PreviewFrame(int width, int height, int stride, double density, PixelFormat pixelFormat, int[] pixels) { + if (width < 0 || height < 0 || stride < width) { + throw new IllegalArgumentException("invalid frame dimensions"); + } + if (pixelFormat == null || pixels == null || pixels.length < stride * height) { + throw new IllegalArgumentException("invalid frame payload"); + } + this.width = width; + this.height = height; + this.stride = stride; + this.density = density; + this.pixelFormat = pixelFormat; + this.pixels = Arrays.copyOf(pixels, stride * height); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getStride() { + return stride; + } + + public double getDensity() { + return density; + } + + public PixelFormat getPixelFormat() { + return pixelFormat; + } + + /** Returns a defensive copy so callers cannot mutate a retained frame. */ + public int[] copyPixels() { + return Arrays.copyOf(pixels, pixels.length); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrameConsumer.java b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrameConsumer.java new file mode 100644 index 000000000..2d6c65493 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewFrameConsumer.java @@ -0,0 +1,10 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +/** Receives owned, runtime-neutral preview frames. */ +@FunctionalInterface +public interface PreviewFrameConsumer { + void present(PreviewFrame frame); +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/PreviewLifecycleEvent.java b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewLifecycleEvent.java new file mode 100644 index 000000000..66a06e666 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewLifecycleEvent.java @@ -0,0 +1,46 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +/** Structured lifecycle notification for an IDE preview host. */ +public final class PreviewLifecycleEvent { + public enum Kind { + START, + READY, + FRAME, + RELOAD_READY, + ERROR, + CLOSED + } + + private final Kind kind; + private final String message; + private final Throwable error; + + private PreviewLifecycleEvent(Kind kind, String message, Throwable error) { + this.kind = kind; + this.message = message; + this.error = error; + } + + public static PreviewLifecycleEvent of(Kind kind, String message) { + return new PreviewLifecycleEvent(kind, message, null); + } + + public static PreviewLifecycleEvent error(Throwable error) { + return new PreviewLifecycleEvent(Kind.ERROR, error == null ? null : error.getMessage(), error); + } + + public Kind getKind() { + return kind; + } + + public String getMessage() { + return message; + } + + public Throwable getError() { + return error; + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/PreviewRuntime.java b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewRuntime.java new file mode 100644 index 000000000..f1179a67d --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/PreviewRuntime.java @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.awt.image.BufferedImage; + +import totalcross.LauncherRuntime; +import totalcross.ui.Container; +import totalcross.ui.Control; +import totalcross.ui.MainWindow; + +/** Stable runtime contract consumed by external Live Preview tooling. */ +public interface PreviewRuntime extends AutoCloseable { + /** Receives launcher-owned frames; retained frames must be copied by the consumer. */ + @FunctionalInterface + interface FrameConsumer { + void present(BufferedImage image); + } + + static PreviewRuntime startPreview(String mainWindowClass, FrameConsumer surface, ClassLoader appClassLoader, + String... args) { + return LauncherRuntime.startPreview(mainWindowClass, surface, appClassLoader, args); + } + + void pumpEvents(); + + MainWindow createMainWindow(String className, ClassLoader classLoader, boolean terminateIfMainClass) + throws ClassNotFoundException, InstantiationException, IllegalAccessException; + + void preparePreviewMainWindowReload(); + + void replaceMainWindow(MainWindow mainWindow, String commandLine); + + void showContainer(Container container); + + void showControl(Control control); + + @Override + void close(); +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/RenderSurface.java b/TotalCrossSDK/src/main/java/totalcross/preview/RenderSurface.java new file mode 100644 index 000000000..29a24e048 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/RenderSurface.java @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +/** + * Pixel presentation boundary for future IDE and simulator backends. + */ +public interface RenderSurface { + void resize(int width, int height, int scale); + + void present(int[] pixels, int width, int height); +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/WindowBackend.java b/TotalCrossSDK/src/main/java/totalcross/preview/WindowBackend.java new file mode 100644 index 000000000..b48cee8c5 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/WindowBackend.java @@ -0,0 +1,19 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +/** + * Window lifecycle boundary used by the preview architecture. + */ +public interface WindowBackend { + void start(WindowConfig config); + + void stop(); + + void setTitle(String title); + + void requestRepaint(); + + RenderSurface getRenderSurface(); +} diff --git a/TotalCrossSDK/src/main/java/totalcross/preview/WindowConfig.java b/TotalCrossSDK/src/main/java/totalcross/preview/WindowConfig.java new file mode 100644 index 000000000..8700deb50 --- /dev/null +++ b/TotalCrossSDK/src/main/java/totalcross/preview/WindowConfig.java @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import java.awt.Color; +import java.awt.event.ComponentListener; +import java.awt.event.WindowListener; + +/** + * Immutable AWT preview window configuration. + */ +public class WindowConfig { + public final int width; + public final int height; + public final int scale; + public final String title; + public final double scaleFactor; + public final int x; + public final int y; + public final boolean fullscreen; + public final boolean resizable; + public final Color background; + public final WindowListener windowListener; + public final ComponentListener componentListener; + + public WindowConfig(int width, int height, int scale, String title) { + this(width, height, scale, scale, title, 0, 0, false, true, null, null, null); + } + + public WindowConfig(int width, int height, double scaleFactor, String title, int x, int y, boolean fullscreen, + boolean resizable, Color background, WindowListener windowListener, ComponentListener componentListener) { + this(width, height, (int) scaleFactor, scaleFactor, title, x, y, fullscreen, resizable, background, windowListener, + componentListener); + } + + private WindowConfig(int width, int height, int scale, double scaleFactor, String title, int x, int y, + boolean fullscreen, boolean resizable, Color background, WindowListener windowListener, + ComponentListener componentListener) { + this.width = width; + this.height = height; + this.scale = scale; + this.scaleFactor = scaleFactor; + this.title = title; + this.x = x; + this.y = y; + this.fullscreen = fullscreen; + this.resizable = resizable; + this.background = background; + this.windowListener = windowListener; + this.componentListener = componentListener; + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/Control.java b/TotalCrossSDK/src/main/java/totalcross/ui/Control.java index c13a03d18..bfc456558 100644 --- a/TotalCrossSDK/src/main/java/totalcross/ui/Control.java +++ b/TotalCrossSDK/src/main/java/totalcross/ui/Control.java @@ -1934,7 +1934,7 @@ public static void resetStyle() { /** Internal use only */ public static void uiStyleChanged() { - if (!uiStyleAlreadyChanged) { + if (Settings.onJavaSE || !uiStyleAlreadyChanged) { uiFlat = Settings.uiStyle == Settings.Flat; uiMaterial = Settings.uiStyle == Settings.Material; uiAndroid = Settings.uiStyle == Settings.Android || Settings.uiStyle == Settings.Holo || uiMaterial; diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/MainWindow.java b/TotalCrossSDK/src/main/java/totalcross/ui/MainWindow.java index e59505b8b..34b945b5c 100644 --- a/TotalCrossSDK/src/main/java/totalcross/ui/MainWindow.java +++ b/TotalCrossSDK/src/main/java/totalcross/ui/MainWindow.java @@ -1,6 +1,7 @@ // Copyright (C) 1998, 1999 Wabasoft // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -134,8 +135,10 @@ public MainWindow(String title, byte style) // guich@112 if (bytes != null) { Settings.appProps = new Hashtable(new String(bytes)); } - FirebaseManager.getInstance().registerFirebaseInstanceIdService(initFirebaseInstanceIdService()); - FirebaseManager.getInstance().setMessagingService(initFirebaseMessagingService()); + if (!Settings.onJavaSE) { + FirebaseManager.getInstance().registerFirebaseInstanceIdService(initFirebaseInstanceIdService()); + FirebaseManager.getInstance().setMessagingService(initFirebaseMessagingService()); + } } @Override @@ -194,6 +197,19 @@ void mainWindowCreate() { void mainWindowCreate4D() { } // not needed at device + /** + * Resets singleton window state before a preview reload creates a fresh + * application MainWindow with a disposable classloader. + */ + public static void resetPreviewState() { + Window.destroyZStack(); + topMost = null; + mainWindowInstance = null; + mainThread = null; + lastMinInterval = 0; + timeAvailable = 0; + } + /** Sets the default font used in all controls created. To change the default font, assign it to this member in the MainWindow constructor, * making it the FIRST LINE in the constructor; you'll not be able to use super(title,border): change by setBorderStyle and setTitle, after * the defaultFont assignment. Example: diff --git a/TotalCrossSDK/src/main/java/totalcross/ui/SideMenuContainer.java b/TotalCrossSDK/src/main/java/totalcross/ui/SideMenuContainer.java index 369651ab8..be0f9b519 100644 --- a/TotalCrossSDK/src/main/java/totalcross/ui/SideMenuContainer.java +++ b/TotalCrossSDK/src/main/java/totalcross/ui/SideMenuContainer.java @@ -146,6 +146,14 @@ public void setBarFont(Font f) { bar.setFont(f); } + public void open() { + topMenu.popup(); + } + + public void close() { + topMenu.unpop(); + } + /** * Presents the given SideMenuContainer.Item, swapping the caption and the * contents of this SideMenu. diff --git a/TotalCrossSDK/src/test/java/tc/tools/AnonymousUserDataTest.java b/TotalCrossSDK/src/test/java/tc/tools/AnonymousUserDataTest.java index 49bb752de..2141e32c9 100644 --- a/TotalCrossSDK/src/test/java/tc/tools/AnonymousUserDataTest.java +++ b/TotalCrossSDK/src/test/java/tc/tools/AnonymousUserDataTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import totalcross.json.JSONObject; @@ -17,6 +18,7 @@ import java.io.IOException; import java.nio.file.Paths; +@Disabled("Anonymous telemetry is disabled until its external service is replaced") public class AnonymousUserDataTest { static AnonymousUserData anonymousUserData; static String configDirPath; diff --git a/TotalCrossSDK/src/test/java/tc/tools/ArtifactBoundariesTest.java b/TotalCrossSDK/src/test/java/tc/tools/ArtifactBoundariesTest.java new file mode 100644 index 000000000..97f5d65e9 --- /dev/null +++ b/TotalCrossSDK/src/test/java/tc/tools/ArtifactBoundariesTest.java @@ -0,0 +1,58 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// SPDX-License-Identifier: LGPL-2.1-only + +package tc.tools; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("artifact-boundary") +class ArtifactBoundariesTest { + private static final Path ARTIFACTS = Path.of(System.getProperty("totalcross.artifact.dir")); + + @Test + void apiDoesNotContainToolingOrPreviewImplementation() throws Exception { + Set entries = entries("totalcross-api"); + assertTrue(entries.stream().anyMatch(name -> name.startsWith("totalcross/ui/"))); + assertFalse(entries.stream().anyMatch(name -> name.startsWith("tc/tools/converter/"))); + assertFalse(entries.stream().anyMatch(name -> name.startsWith("tc/tools/deployer/"))); + assertFalse(entries.stream().anyMatch(name -> name.startsWith("totalcross/preview/"))); + } + + @Test + void converterAndDeployerRemainSeparate() throws Exception { + Set converter = entries("totalcross-converter"); + Set deployer = entries("totalcross-deployer"); + assertTrue(converter.stream().anyMatch(name -> name.startsWith("tc/tools/converter/"))); + assertFalse(converter.stream().anyMatch(name -> name.startsWith("tc/tools/deployer/"))); + assertTrue(deployer.contains("tc/Deploy.class")); + assertTrue(deployer.stream().anyMatch(name -> name.startsWith("tc/tools/deployer/"))); + assertFalse(deployer.stream().anyMatch(name -> name.startsWith("tc/tools/converter/"))); + } + + @Test + void previewArtifactContainsOnlyPreviewSurface() throws Exception { + Set preview = entries("totalcross-preview-runtime"); + assertTrue(preview.stream().anyMatch(name -> name.startsWith("totalcross/preview/"))); + assertFalse(preview.stream().anyMatch(name -> name.startsWith("tc/tools/converter/"))); + assertFalse(preview.stream().anyMatch(name -> name.startsWith("tc/tools/deployer/"))); + } + + private static Set entries(String prefix) throws IOException { + Path artifact = Files.list(ARTIFACTS).filter(path -> path.getFileName().toString().startsWith(prefix + "-")) + .findFirst().orElseThrow(); + try (JarFile jar = new JarFile(artifact.toFile())) { + return jar.stream().map(entry -> entry.getName()).collect(Collectors.toSet()); + } + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/LauncherArgumentParserTest.java b/TotalCrossSDK/src/test/java/totalcross/LauncherArgumentParserTest.java new file mode 100644 index 000000000..ad338b010 --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/LauncherArgumentParserTest.java @@ -0,0 +1,58 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class LauncherArgumentParserTest { + @Test + void parsesScreenScaleAndFlags() throws Exception { + LauncherConfig config = new LauncherConfig("com.example.App", "/scr", "320x480x16", "/scale", "1", + "/fastscale", "/demo", "/cmdline", "one", "two"); + + LauncherParsedConfig result = LauncherArgumentParser.parse(config, true, 0, 0); + + assertEquals("com.example.App", result.className); + assertEquals(320, result.width); + assertEquals(480, result.height); + assertEquals(16, result.bpp); + assertEquals(1, result.scaleValue); + assertEquals(1, result.scale); + assertTrue(result.fastScale); + assertTrue(result.demo); + assertEquals("one two", result.commandLine); + } + + @Test + void storesSettingsFlagsWithoutApplyingThem() throws Exception { + LauncherConfig config = new LauncherConfig("com.example.App", "/scr", "320x480x16", "/fingertouch", + "/geofocus", "/virtualKeyboard", "/showmousepos", "/dbginfo"); + + LauncherParsedConfig result = LauncherArgumentParser.parse(config, true, 0, 0); + + assertTrue(result.fingerTouch); + assertTrue(result.geographicalFocus); + assertTrue(result.keyboardFocusTraversable); + assertTrue(result.virtualKeyboard); + assertTrue(result.showMousePosition); + assertTrue(result.showDebugMessages); + } + + @Test + void reportsInvalidArgumentDetails() { + LauncherConfig config = new LauncherConfig("com.example.App", "/bpp", "7"); + + LauncherArgumentParser.InvalidArgumentException error = assertThrows( + LauncherArgumentParser.InvalidArgumentException.class, + () -> LauncherArgumentParser.parse(config, true, 0, 0)); + + assertEquals(1, error.getIndex()); + assertEquals("7", error.getArgument()); + assertEquals("/bpp 7", error.getFullCommandLine()); + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/LauncherPreviewSurfaceTest.java b/TotalCrossSDK/src/test/java/totalcross/LauncherPreviewSurfaceTest.java new file mode 100644 index 000000000..d30241b76 --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/LauncherPreviewSurfaceTest.java @@ -0,0 +1,81 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.lang.reflect.Field; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import totalcross.preview.PreviewRuntime; +import totalcross.preview.PreviewFrame; +import totalcross.sys.Settings; + +class LauncherPreviewSurfaceTest { + @AfterEach + void tearDown() { + totalcross.ui.gfx.Graphics.mainWindowPixels = null; + Launcher.instance = null; + } + + @Test + @SuppressWarnings("deprecation") + void launcherDoesNotExtendApplet() { + assertFalse(java.applet.Applet.class.isAssignableFrom(Launcher.class)); + } + + @Test + void updateScreenPresentsBufferedImageAndKeepsPixelBufferAlias() throws Exception { + RecordingFrameConsumer surface = new RecordingFrameConsumer(); + RecordingPreviewFrameConsumer copiedSurface = new RecordingPreviewFrameConsumer(); + Launcher launcher = new Launcher(surface, true); + launcher.setPreviewFrameConsumer(copiedSurface); + setField(launcher, "toScale", 1D); + setField(launcher, "toBpp", 24); + Settings.screenWidth = 2; + Settings.screenHeight = 2; + int[] originalPixels = new int[] { 0xFF000001, 0xFF000002, 0xFF000003, 0xFF000004 }; + totalcross.ui.gfx.Graphics.mainWindowPixels = originalPixels; + + launcher.updateScreen(); + + assertNotNull(surface.presentedImage); + int[] backingPixels = ((DataBufferInt) surface.presentedImage.getRaster().getDataBuffer()).getData(); + assertSame(backingPixels, totalcross.ui.gfx.Graphics.mainWindowPixels); + assertArrayEquals(originalPixels, backingPixels); + backingPixels[0] = 0; + assertArrayEquals(originalPixels, copiedSurface.presentedFrame.copyPixels()); + } + + private void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static class RecordingFrameConsumer implements PreviewRuntime.FrameConsumer { + private BufferedImage presentedImage; + + @Override + public void present(BufferedImage image) { + presentedImage = image; + } + } + + private static class RecordingPreviewFrameConsumer implements totalcross.preview.PreviewFrameConsumer { + private PreviewFrame presentedFrame; + + @Override + public void present(PreviewFrame frame) { + presentedFrame = frame; + } + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/LauncherRuntimeTest.java b/TotalCrossSDK/src/test/java/totalcross/LauncherRuntimeTest.java new file mode 100644 index 000000000..9ae295343 --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/LauncherRuntimeTest.java @@ -0,0 +1,134 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import totalcross.sys.Settings; + +class LauncherRuntimeTest { + private final boolean keyboardFocusTraversable = Settings.keyboardFocusTraversable; + private final boolean fingerTouch = Settings.fingerTouch; + private final boolean unmovableSIP = Settings.unmovableSIP; + private final boolean geographicalFocus = Settings.geographicalFocus; + private final boolean virtualKeyboard = Settings.virtualKeyboard; + private final boolean showMousePosition = Settings.showMousePosition; + private final boolean showDebugMessages = Settings.showDebugMessages; + private final double screenDensity = Settings.screenDensity; + private final String dataPath = Settings.dataPath; + private final int userFontSize = Launcher.userFontSize; + private final Launcher launcherInstance = Launcher.instance; + + @AfterEach + void restoreSettings() { + Settings.keyboardFocusTraversable = keyboardFocusTraversable; + Settings.fingerTouch = fingerTouch; + Settings.unmovableSIP = unmovableSIP; + Settings.geographicalFocus = geographicalFocus; + Settings.virtualKeyboard = virtualKeyboard; + Settings.showMousePosition = showMousePosition; + Settings.showDebugMessages = showDebugMessages; + Settings.screenDensity = screenDensity; + Settings.dataPath = dataPath; + Launcher.userFontSize = userFontSize; + Launcher.instance = launcherInstance; + } + + @Test + void parseArgumentsKeepsDefensiveCopy() { + LauncherRuntime runtime = new LauncherRuntime(); + String[] args = new String[] { "/scr", "320x480x32" }; + + runtime.parseArguments("com.example.App", args); + args[1] = "changed"; + String[] storedArgs = runtime.getLauncherArgs(); + storedArgs[0] = "changed"; + + assertArrayEquals(new String[] { "/scr", "320x480x32" }, runtime.getLauncherArgs()); + } + + @Test + void configureKeepsImmutableLauncherConfig() { + String[] args = new String[] { "/scr", "320x480x32" }; + LauncherConfig config = new LauncherConfig("com.example.App", args); + args[1] = "changed"; + LauncherRuntime runtime = new LauncherRuntime(); + + runtime.configure(config); + + assertEquals("com.example.App", runtime.getMainWindowClass()); + assertArrayEquals(new String[] { "/scr", "320x480x32" }, runtime.getLauncherArgs()); + } + + @Test + void configureClearsParsedConfig() { + LauncherRuntime runtime = new LauncherRuntime(); + runtime.setParsedConfig(new LauncherParsedConfig("com.example.App")); + + runtime.configure(new LauncherConfig("com.example.Other")); + + assertNull(runtime.getParsedConfig()); + } + + @Test + void startRequiresParsedMainWindowClass() { + LauncherRuntime runtime = new LauncherRuntime(); + + assertThrows(IllegalStateException.class, runtime::startApplication); + } + + @Test + void setNewMainWindowRequiresStartedRuntime() { + LauncherRuntime runtime = new LauncherRuntime(); + + assertThrows(IllegalStateException.class, () -> runtime.setNewMainWindow(null, "")); + } + + @Test + void startWindowBackendRequiresParsedConfig() { + LauncherRuntime runtime = new LauncherRuntime(); + + assertThrows(IllegalStateException.class, () -> runtime.startWindowBackend(null, "title", null, null, null)); + } + + @Test + void normalizesMainWindowClassNames() { + assertEquals("tc.samples.Main", LauncherRuntime.normalizeMainWindowClassName("tc/samples/Main.class")); + } + + @Test + void applyParsedConfigUpdatesRuntimeSettings() { + LauncherRuntime runtime = new LauncherRuntime(); + LauncherParsedConfig parsedConfig = new LauncherParsedConfig("com.example.App"); + parsedConfig.userFontSize = 18; + parsedConfig.keyboardFocusTraversable = true; + parsedConfig.fingerTouch = true; + parsedConfig.unmovableSIP = true; + parsedConfig.geographicalFocus = true; + parsedConfig.virtualKeyboard = true; + parsedConfig.showMousePosition = true; + parsedConfig.showDebugMessages = true; + parsedConfig.densityValue = 2; + parsedConfig.dataPath = "data"; + + runtime.applyParsedConfig(new Launcher(null, true), parsedConfig); + + assertEquals(18, Launcher.userFontSize); + assertEquals(true, Settings.keyboardFocusTraversable); + assertEquals(true, Settings.fingerTouch); + assertEquals(true, Settings.unmovableSIP); + assertEquals(true, Settings.geographicalFocus); + assertEquals(true, Settings.virtualKeyboard); + assertEquals(true, Settings.showMousePosition); + assertEquals(true, Settings.showDebugMessages); + assertEquals(2, Settings.screenDensity); + assertEquals("data", Settings.dataPath); + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/TCEventThreadTest.java b/TotalCrossSDK/src/test/java/totalcross/TCEventThreadTest.java new file mode 100644 index 000000000..269f181e9 --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/TCEventThreadTest.java @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class TCEventThreadTest { + @Test + void invokeInEventThreadNotifiesWaitingCallerWhenRunnableFails() { + TCEventThread eventThread = new TCEventThread(new NoopMainClass()); + try { + boolean completed = eventThread.invokeInEventThread(true, new Runnable() { + @Override + public void run() { + throw new RuntimeException("boom"); + } + }, 5000); + + assertTrue(completed); + } finally { + eventThread.stopGracefully(); + } + } + + private static class NoopMainClass implements MainClass { + @Override + public void _postEvent(int type, int key, int x, int y, int modifiers, int timeStamp) { + } + + @Override + public void appStarting(int timeAvail) { + } + + @Override + public void appEnding() { + } + + @Override + public void _onTimerTick(boolean canUpdate) { + } + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/preview/PreviewCommandAdapterTest.java b/TotalCrossSDK/src/test/java/totalcross/preview/PreviewCommandAdapterTest.java new file mode 100644 index 000000000..45f6e3643 --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/preview/PreviewCommandAdapterTest.java @@ -0,0 +1,51 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class PreviewCommandAdapterTest { + @Test + void forwardsCommandsAndReportsStructuredLifecycle() { + RecordingHandler handler = new RecordingHandler(); + List events = new ArrayList<>(); + PreviewCommandAdapter adapter = new PreviewCommandAdapter(handler, + event -> events.add(event.getKind())); + + adapter.start("example.Main", "one"); + adapter.resize(320, 480, 2); + adapter.pointer(4, 5, 1, true); + adapter.key(65, true, 2); + adapter.prepareReload(); + adapter.replaceMainWindow("example.Other", "two"); + adapter.pump(); + adapter.close(); + + assertEquals(List.of("start", "resize", "pointer", "key", "prepare", "replace", "pump", "close"), + handler.calls); + assertEquals(List.of(PreviewLifecycleEvent.Kind.START, PreviewLifecycleEvent.Kind.READY, + PreviewLifecycleEvent.Kind.RELOAD_READY, PreviewLifecycleEvent.Kind.CLOSED), events); + assertArrayEquals(new String[] { "one" }, handler.startArgs); + } + + private static final class RecordingHandler implements PreviewCommandAdapter.Handler { + final List calls = new ArrayList<>(); + String[] startArgs; + + public void start(String mainWindowClass, String[] args) { calls.add("start"); startArgs = args; } + public void pump() { calls.add("pump"); } + public void resize(int width, int height, double density) { calls.add("resize"); } + public void pointer(int x, int y, int button, boolean pressed) { calls.add("pointer"); } + public void key(int keyCode, boolean pressed, int modifiers) { calls.add("key"); } + public void prepareReload() { calls.add("prepare"); } + public void replaceMainWindow(String mainWindowClass, String[] args) { calls.add("replace"); } + public void close() { calls.add("close"); } + } +} diff --git a/TotalCrossSDK/src/test/java/totalcross/preview/PreviewFrameTest.java b/TotalCrossSDK/src/test/java/totalcross/preview/PreviewFrameTest.java new file mode 100644 index 000000000..a18ab6f5e --- /dev/null +++ b/TotalCrossSDK/src/test/java/totalcross/preview/PreviewFrameTest.java @@ -0,0 +1,26 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only +package totalcross.preview; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class PreviewFrameTest { + @Test + void ownsPixelsAndReportsRuntimeNeutralMetadata() { + int[] source = { 1, 2, 3, 4, 5, 6 }; + PreviewFrame frame = new PreviewFrame(2, 2, 3, 2, PreviewFrame.PixelFormat.ARGB_8888, source); + source[0] = 99; + int[] copy = frame.copyPixels(); + copy[1] = 88; + + assertEquals(2, frame.getWidth()); + assertEquals(2, frame.getHeight()); + assertEquals(3, frame.getStride()); + assertEquals(2, frame.getDensity()); + assertArrayEquals(new int[] { 1, 2, 3, 4, 5, 6 }, frame.copyPixels()); + } +}