diff --git a/java-sample/build.gradle.kts b/java-sample/build.gradle.kts index 7b38625..16ae52e 100644 --- a/java-sample/build.gradle.kts +++ b/java-sample/build.gradle.kts @@ -13,10 +13,10 @@ repositories { } dependencies { - implementation("io.github.simonscholz:qr-code-with-logo:0.1.0") + implementation("io.github.simonscholz:qr-code-with-logo:0.2.0-SNAPSHOT") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.1") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.1") } tasks.test { diff --git a/qr-code-app/build.gradle.kts b/qr-code-app/build.gradle.kts index 9936fae..6a9d6d5 100644 --- a/qr-code-app/build.gradle.kts +++ b/qr-code-app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("io.gitlab.arturbosch.detekt") id("org.jlleitschuh.gradle.ktlint") + id("com.github.ben-manes.versions") id("org.graalvm.buildtools.native") version "0.9.28" id("org.beryx.runtime") version "1.12.7" @@ -19,6 +20,8 @@ distributions { runtime { addOptions("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages") + imageDir = file("${layout.buildDirectory.asFile.get().name}/qr-code-with-logo-app") + imageZip = file("${layout.buildDirectory.asFile.get().name}/qr-code-with-logo-app.zip") launcher { noConsole = true @@ -30,14 +33,6 @@ runtime { } } -tasks.register("jpackageImageZip") { - dependsOn("jpackageImage") - archiveFileName = "qr-code-app.zip" - destinationDirectory = layout.buildDirectory.dir("dist") - - from(layout.buildDirectory.dir("jpackage/qr-code-app/")) -} - repositories { mavenCentral() } @@ -51,6 +46,7 @@ dependencies { } implementation("com.github.lgooddatepicker:LGoodDatePicker:11.2.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.0") + implementation("com.squareup:kotlinpoet-javapoet:1.15.1") // implementation("io.quarkus:quarkus-awt-deployment:3.5.0") diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/Main.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/Main.kt index debb418..88e1c90 100644 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/Main.kt +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/Main.kt @@ -2,8 +2,10 @@ package io.github.simonscholz import io.github.simonscholz.model.QrCodeConfigViewModel import io.github.simonscholz.observables.SwingRealm +import io.github.simonscholz.service.CodeGeneratorService import io.github.simonscholz.service.ConfigService import io.github.simonscholz.service.ImageService +import io.github.simonscholz.ui.FileUI import io.github.simonscholz.ui.ImageUI import io.github.simonscholz.ui.MainMenu import io.github.simonscholz.ui.MainUI @@ -13,17 +15,16 @@ import org.eclipse.core.databinding.observable.value.IObservableValue import java.awt.event.WindowAdapter import java.awt.event.WindowEvent import java.awt.image.BufferedImage -import java.io.File import javax.swing.JFrame import javax.swing.JPanel import javax.swing.SwingUtilities fun main() { // GraalVM Fix - if (System.getProperty("java.home") == null) { - println("No Java Home set, assuming that we are running from GraalVM. Fixing...") - System.setProperty("java.home", File(".").absolutePath) - } +// if (System.getProperty("java.home") == null) { +// println("No Java Home set, assuming that we are running from GraalVM. Fixing...") +// System.setProperty("java.home", File(".").absolutePath) +// } SwingUtilities.invokeLater { val frame = JFrame("QR Code AWT/Swing UI") @@ -42,10 +43,11 @@ fun main() { var alreadyAppliedOnce = false val alreadyAppliedOnceDelegate = { alreadyAppliedOnce } - val imageService = ImageService(qrCodeConfigViewModel, alreadyAppliedOnceDelegate) - MainMenu.createFrameMenu(frame, qrCodeConfigViewModel.qrCodeContent, imageService) + val imageService = ImageService(qrCodeConfigViewModel) + val fileUi = FileUI(CodeGeneratorService(qrCodeConfigViewModel), configService, imageService, alreadyAppliedOnceDelegate) + MainMenu.createFrameMenu(frame, qrCodeConfigViewModel.qrCodeContent, fileUi, configService) - val (imagePanel, setImage) = ImageUI.createImagePanel(imageService) + val (imagePanel, setImage) = ImageUI.createImagePanel(imageService, fileUi) val (propertiesPanel, applyOnChange) = PropertiesUI.createPropertiesUI(qrCodeConfigViewModel, dataBindingContext) { onPropertyApply(qrCodeConfigViewModel.qrCodeContent, imageService, setImage, imagePanel) alreadyAppliedOnce = true @@ -54,9 +56,6 @@ fun main() { val mainPanel = MainUI.createMainPanel(imagePanel, propertiesPanel) frame.add(mainPanel) - frame.pack() - frame.isVisible = true - dataBindingContext.bindings.forEach { it.model.addChangeListener { if (applyOnChange()) { @@ -66,6 +65,9 @@ fun main() { } } configService.loadConfig() + + frame.pack() + frame.isVisible = true } } diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/CodeGeneratorService.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/CodeGeneratorService.kt new file mode 100644 index 0000000..9917111 --- /dev/null +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/CodeGeneratorService.kt @@ -0,0 +1,211 @@ +package io.github.simonscholz.service + +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeSpec +import io.github.simonscholz.model.QrCodeConfigViewModel +import io.github.simonscholz.qrcode.QrPositionalSquaresConfig +import java.awt.Color +import java.awt.Image +import java.awt.image.BufferedImage +import java.io.IOException +import javax.imageio.ImageIO +import javax.lang.model.element.Modifier +import com.squareup.javapoet.ClassName as JavaClassName +import com.squareup.javapoet.TypeSpec as JavaTypeSpec + +class CodeGeneratorService(private val qrCodeConfigViewModel: QrCodeConfigViewModel) { + + fun generateKotlinCode(): String { + val file = FileSpec.builder("io.github.simonscholz", "QrCodeGenerator") + + file.addImport("io.github.simonscholz.qrcode", "LogoShape", "QrCodeConfig", "QrCodeFactory", "QrPositionalSquaresConfig") + file.addImport("java.awt", "Color", "image.BufferedImage") + file.addImport("java.io", "File") + file.addImport("javax.imageio", "ImageIO") + + file.addFunction( + FunSpec.builder("toFile") + .receiver(ClassName("java.awt.image", "BufferedImage")) + .addParameter("file", ClassName("java.io", "File")) + .addStatement("ImageIO.write(this, \"png\", file)") + .build(), + ) + + file.addFunction( + FunSpec.builder("main") + .addStatement( + """ + |val logo = ImageIO.read( + | QrCodeGenerator::class.java + | .classLoader + | .getResource("avatar-60x.png") + |) + """.trimMargin(), + ) + .addStatement("val qrCodeGenerator = %T()", ClassName("io.github.simonscholz", "QrCodeGenerator")) + .addStatement("%N.generateQrCode(logo).toFile(File(\"qr-code.png\"))", "qrCodeGenerator") + .build(), + ) + + val generateQrCodeFunction = FunSpec.builder("generateQrCode") + .addParameter("logo", ClassName("java.awt", "Image")) + .addStatement( + """ + |val qrPositionalSquaresConfig = %T.Builder() + | .circleShaped(${qrCodeConfigViewModel.positionalSquareIsCircleShaped.value}) + | .relativeSquareBorderRound(${qrCodeConfigViewModel.positionalSquareRelativeBorderRound.value}) + | .centerColor(${colorInstanceStringKotlin(qrCodeConfigViewModel.positionalSquareCenterColor.value)}) + | .innerSquareColor(${colorInstanceStringKotlin(qrCodeConfigViewModel.positionalSquareInnerSquareColor.value)}) + | .outerSquareColor(${colorInstanceStringKotlin(qrCodeConfigViewModel.positionalSquareOuterSquareColor.value)}) + | .outerBorderColor(${colorInstanceStringKotlin(qrCodeConfigViewModel.positionalSquareOuterBorderColor.value)}) + | .build() + """.trimMargin(), + ClassName("io.github.simonscholz.qrcode", "QrPositionalSquaresConfig"), + ClassName("java.awt", "Color"), + ClassName("java.awt", "Color"), + ClassName("java.awt", "Color"), + ClassName("java.awt", "Color"), + ) + .addStatement( + """ + |val qrCodeConfig = %T.Builder("${qrCodeConfigViewModel.qrCodeContent.value}") + | .qrCodeSize(${qrCodeConfigViewModel.size.value}) + | .qrCodeColorConfig( + | bgColor = ${colorInstanceStringKotlin(qrCodeConfigViewModel.backgroundColor.value)}, + | fillColor = ${colorInstanceStringKotlin(qrCodeConfigViewModel.foregroundColor.value)}, + | ) + | .qrLogoConfig( + | logo = logo, + | relativeSize = ${qrCodeConfigViewModel.logoRelativeSize.value}, + | bgColor = ${colorInstanceStringKotlin(qrCodeConfigViewModel.logoBackgroundColor.value)}, + | shape = %T.${qrCodeConfigViewModel.logoShape.value}, + | ) + | .qrBorderConfig( + | color = ${colorInstanceStringKotlin(qrCodeConfigViewModel.borderColor.value)}, + | relativeSize = ${qrCodeConfigViewModel.relativeBorderSize.value}, + | relativeBorderRound = ${qrCodeConfigViewModel.borderRadius.value}, + | ) + | .qrPositionalSquaresConfig(qrPositionalSquaresConfig) + | .build() + """.trimMargin(), + ClassName("io.github.simonscholz.qrcode", "QrCodeConfig"), + ClassName("java.awt", "Color"), + ClassName("java.awt", "Color"), + ClassName("java.awt", "Color"), + ClassName("io.github.simonscholz.qrcode", "LogoShape"), + ClassName("java.awt", "Color"), + ) + .addStatement( + """ + |return %T.createQrCodeApi().createQrCodeImage(qrCodeConfig) + """.trimMargin(), + ClassName("io.github.simonscholz.qrcode", "QrCodeFactory"), + ) + .returns(BufferedImage::class) + .build() + + file.addType( + TypeSpec.classBuilder("QrCodeGenerator") + .addFunction(generateQrCodeFunction) + .build(), + ) + + return StringBuilder().apply { + file.build().writeTo(this) + }.toString() + } + + fun generateJavaCode(): String { + val qrCodeGenerator = JavaTypeSpec.classBuilder("QrCodeGenerator") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + + MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Array::class.java, "args", Modifier.FINAL) + .addException(IOException::class.java) + .addStatement("final var qrCodeGenerator = new QrCodeGenerator()") + .addStatement( + """ + |final var qrCodeImage = qrCodeGenerator.generateQrCode( + | $T.read(QrCodeGenerator.class.getClassLoader() + | .getResource("your-logo.png")) + |) + """.trimMargin(), + ImageIO::class.java, + ) + .addStatement( + "ImageIO.write(qrCodeImage, \"png\", new $T(\"qr-code.png\"))", + JavaClassName.get("java.io", "File"), + ) + .build() + .let(qrCodeGenerator::addMethod) + + val generateQrCodeMethod = MethodSpec.methodBuilder("generateQrCode") + .addModifiers(Modifier.PUBLIC) + .addException(IOException::class.java) + .addParameter(Image::class.java, "logo") + .addStatement( + """ + |final var qrPositionalSquaresConfig = new $T.Builder() + | .circleShaped(${qrCodeConfigViewModel.positionalSquareIsCircleShaped.value}) + | .relativeSquareBorderRound(${qrCodeConfigViewModel.positionalSquareRelativeBorderRound.value}) + | .centerColor(${colorInstanceStringJava(qrCodeConfigViewModel.positionalSquareCenterColor.value)}) + | .innerSquareColor(${colorInstanceStringJava(qrCodeConfigViewModel.positionalSquareInnerSquareColor.value)}) + | .outerSquareColor(${colorInstanceStringJava(qrCodeConfigViewModel.positionalSquareOuterSquareColor.value)}) + | .outerBorderColor(${colorInstanceStringJava(qrCodeConfigViewModel.positionalSquareOuterBorderColor.value)}) + | .build() + """.trimMargin(), + QrPositionalSquaresConfig::class.java, + Color::class.java, + Color::class.java, + Color::class.java, + Color::class.java, + ) + .addStatement( + """ + |final var qrCodeConfig = new $T.Builder("${qrCodeConfigViewModel.qrCodeContent.value}}") + | .qrCodeSize(${qrCodeConfigViewModel.size.value}) + | .qrCodeColorConfig(${colorInstanceStringJava(qrCodeConfigViewModel.backgroundColor.value)}, ${colorInstanceStringJava(qrCodeConfigViewModel.foregroundColor.value)}) + | .qrLogoConfig(logo, ${qrCodeConfigViewModel.logoRelativeSize.value}, ${colorInstanceStringJava(qrCodeConfigViewModel.logoBackgroundColor.value)}, $T.${qrCodeConfigViewModel.logoShape.value}) + | .qrBorderConfig(${colorInstanceStringJava(qrCodeConfigViewModel.borderColor.value)}, ${qrCodeConfigViewModel.relativeBorderSize.value}, ${qrCodeConfigViewModel.borderRadius.value}) + | .qrPositionalSquaresConfig(qrPositionalSquaresConfig) + | .build() + """.trimMargin(), + JavaClassName.get("io.github.simonscholz.qrcode", "QrCodeConfig"), + JavaClassName.get("java.awt", "Color"), + JavaClassName.get("java.awt", "Color"), + JavaClassName.get("java.awt", "Color"), + JavaClassName.get("io.github.simonscholz.qrcode", "LogoShape"), + JavaClassName.get("java.awt", "Color"), + ) + .addStatement( + "return $T.createQrCodeApi().createQrCodeImage(qrCodeConfig)", + JavaClassName.get("io.github.simonscholz.qrcode", "QrCodeFactory"), + ) + .returns(BufferedImage::class.java) + .build() + + qrCodeGenerator.addMethod(generateQrCodeMethod) + + return StringBuilder().apply { + JavaFile.builder("io.github.simonscholz", qrCodeGenerator.build()).build().writeTo(this) + }.toString() + } + + private fun colorInstanceStringKotlin(color: Color): String { + return "%T(${color.red}, ${color.green}, ${color.blue})" + } + + private fun colorInstanceStringJava(color: Color): String { + return "new $T(${color.red}, ${color.green}, ${color.blue})" + } + + companion object { + // https://github.com/square/javapoet/issues/831#issuecomment-817238209 + private const val T = "\$T" + } +} diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ConfigService.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ConfigService.kt index efad5bc..c3e59dd 100644 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ConfigService.kt +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ConfigService.kt @@ -6,33 +6,39 @@ import io.github.simonscholz.model.Mapper import io.github.simonscholz.model.QrCodeConfig import io.github.simonscholz.model.QrCodeConfigViewModel import java.io.File -import java.nio.file.Files -import java.nio.file.Paths +import java.util.prefs.Preferences class ConfigService( private val qrCodeConfigViewModel: QrCodeConfigViewModel, ) { - private val objectMapper = ObjectMapper().registerKotlinModule() + private val preferences = Preferences.userRoot().node("qr-code-app") - fun saveConfig(fileName: String = "config.json") { - val path = Paths.get(System.getProperty("user.home"), ".qr-code-app") - Files.createDirectories(path) - val qrCodeDir = path.toAbsolutePath().toString() + fun saveConfig() { + val config = Mapper.fromViewModel(qrCodeConfigViewModel) + val configJson = objectMapper.writeValueAsString(config) + preferences.put("config", configJson) + } + fun saveConfigFile(filePath: String) { val config = Mapper.fromViewModel(qrCodeConfigViewModel) val configJson = objectMapper.writeValueAsString(config) - val configJsonFile = File(qrCodeDir, fileName) + val finalFilePath = if (filePath.endsWith(".json")) filePath else "$filePath.json" + val configJsonFile = File(finalFilePath) configJsonFile.writeText(configJson) } fun loadConfig() { - val path = Paths.get(System.getProperty("user.home"), ".qr-code-app") - val configJsonFile = File(path.toAbsolutePath().toString(), "config.json") - if (configJsonFile.exists()) { - val configJson = configJsonFile.readText() - val config = objectMapper.readValue(configJson, QrCodeConfig::class.java) + preferences.get("config", null)?.let { + val config = objectMapper.readValue(it, QrCodeConfig::class.java) Mapper.applyViewModel(config, qrCodeConfigViewModel) } } + + fun loadConfigFile(filePath: String) { + val configJsonFile = File(filePath) + val configJson = configJsonFile.readText() + val config = objectMapper.readValue(configJson, QrCodeConfig::class.java) + Mapper.applyViewModel(config, qrCodeConfigViewModel) + } } diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ImageService.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ImageService.kt index cc6a239..b6836df 100644 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ImageService.kt +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/ImageService.kt @@ -10,11 +10,9 @@ import java.awt.Image import java.awt.image.BufferedImage import java.io.File import javax.imageio.ImageIO -import javax.swing.JFileChooser import javax.swing.JOptionPane -import javax.swing.filechooser.FileNameExtensionFilter -class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel, private val alreadyAppliedOnceDelegate: () -> Boolean) { +class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel) { fun renderImage(): BufferedImage { val builder = QrCodeConfig.Builder(qrCodeConfigViewModel.qrCodeContent.value) .qrCodeSize(qrCodeConfigViewModel.size.value) @@ -72,26 +70,6 @@ class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel, pri return logo.getScaledInstance((maxLogoSize * ratio).toInt(), maxLogoSize, Image.SCALE_SMOOTH) } - fun saveFile() { - val fileChooser = JFileChooser() - fileChooser.fileFilter = FileNameExtensionFilter("Png Image Files (*.png)", "png") - val result = fileChooser.showSaveDialog(null) - - if (result == JFileChooser.APPROVE_OPTION) { - if (fileChooser.selectedFile.extension.endsWith("png")) { - val selectedFile = fileChooser.selectedFile - val qrCodeImage = if (alreadyAppliedOnceDelegate()) { - renderImage() - } else { - renderInitialImage() - } - ImageIO.write(qrCodeImage, "png", selectedFile) - } else { - JOptionPane.showMessageDialog(null, "The file to be saved must have the png extension", "Image Saving Error", JOptionPane.ERROR_MESSAGE) - } - } - } - fun renderInitialImage(): BufferedImage { val resource = ImageUI::class.java.getClassLoader().getResource("avatar-60x.png") val logo = ImageIO.read(resource) diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/RenderImageService.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/service/RenderImageService.kt deleted file mode 100644 index cb0bb7d..0000000 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/service/RenderImageService.kt +++ /dev/null @@ -1,76 +0,0 @@ -package io.github.simonscholz.service - -import io.github.simonscholz.model.QrCodeConfigViewModel -import io.github.simonscholz.qrcode.QrCodeConfig -import io.github.simonscholz.qrcode.QrCodeFactory -import io.github.simonscholz.qrcode.QrPositionalSquaresConfig -import io.github.simonscholz.ui.ImageUI -import java.awt.Color -import java.awt.Component -import java.awt.Image -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO -import javax.swing.JOptionPane - -object RenderImageService { - fun renderImage(qrCodeConfigViewModel: QrCodeConfigViewModel, component: Component): BufferedImage { - val builder = QrCodeConfig.Builder(qrCodeConfigViewModel.qrCodeContent.value) - .qrCodeSize(qrCodeConfigViewModel.size.value) - .qrCodeColorConfig( - bgColor = qrCodeConfigViewModel.backgroundColor.value, - fillColor = qrCodeConfigViewModel.foregroundColor.value, - ) - .qrBorderConfig( - color = qrCodeConfigViewModel.borderColor.value, - relativeSize = qrCodeConfigViewModel.relativeBorderSize.value, - relativeBorderRound = qrCodeConfigViewModel.borderRadius.value, - ) - .qrPositionalSquaresConfig( - qrPositionalSquaresConfig = QrPositionalSquaresConfig( - isCircleShaped = qrCodeConfigViewModel.positionalSquareIsCircleShaped.value, - relativeSquareBorderRound = qrCodeConfigViewModel.positionalSquareRelativeBorderRound.value, - centerColor = qrCodeConfigViewModel.positionalSquareCenterColor.value, - innerSquareColor = qrCodeConfigViewModel.positionalSquareInnerSquareColor.value, - outerSquareColor = qrCodeConfigViewModel.positionalSquareOuterSquareColor.value, - outerBorderColor = qrCodeConfigViewModel.positionalSquareOuterBorderColor.value, - ), - ) - if (qrCodeConfigViewModel.logo.value.isNotBlank() && File(qrCodeConfigViewModel.logo.value).exists()) { - runCatching { - ImageIO.read(File(qrCodeConfigViewModel.logo.value)).let { - val logoSize = (qrCodeConfigViewModel.size.value * qrCodeConfigViewModel.logoRelativeSize.value).toInt() - - val scaledLogo = it.getScaledInstance(logoSize, logoSize, Image.SCALE_SMOOTH) - - builder.qrLogoConfig( - logo = scaledLogo, - relativeSize = qrCodeConfigViewModel.logoRelativeSize.value, - bgColor = qrCodeConfigViewModel.logoBackgroundColor.value, - ) - } - }.onFailure { _ -> - JOptionPane.showMessageDialog(component, "You did not select a proper image", "Image Loading Error", JOptionPane.ERROR_MESSAGE) - } - } - val qrCodeConfig = builder.build() - return QrCodeFactory.createQrCodeApi().createQrCodeImage(qrCodeConfig) - } - - fun renderInitialImage(): BufferedImage { - val resource = ImageUI::class.java.getClassLoader().getResource("avatar-60x.png") - val logo = ImageIO.read(resource) - val qrCodeConfig = QrCodeConfig.Builder("https://simonscholz.github.io/") - .qrBorderConfig(Color.BLACK) - .qrLogoConfig(logo) - .qrPositionalSquaresConfig( - QrPositionalSquaresConfig( - isCircleShaped = true, - relativeSquareBorderRound = .2, - centerColor = Color.RED, - ), - ) - .build() - return QrCodeFactory.createQrCodeApi().createQrCodeImage(qrCodeConfig) - } -} diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/FileUI.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/FileUI.kt new file mode 100644 index 0000000..c3694b0 --- /dev/null +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/FileUI.kt @@ -0,0 +1,100 @@ +package io.github.simonscholz.ui + +import io.github.simonscholz.service.CodeGeneratorService +import io.github.simonscholz.service.ConfigService +import io.github.simonscholz.service.ImageService +import java.awt.Toolkit +import java.awt.datatransfer.Clipboard +import java.awt.datatransfer.StringSelection +import java.io.File +import javax.imageio.ImageIO +import javax.swing.JFileChooser +import javax.swing.JOptionPane +import javax.swing.filechooser.FileNameExtensionFilter + +class FileUI( + private val codeGeneratorService: CodeGeneratorService, + private val configService: ConfigService, + private val imageService: ImageService, + private val alreadyAppliedOnceDelegate: () -> Boolean, +) { + + fun copyJavaCodeToClipboard() { + val generateKotlinCode = codeGeneratorService.generateJavaCode() + val clipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard + val copyString = StringSelection(generateKotlinCode) + clipboard.setContents(copyString, null) + } + + fun copyKotlinCodeToClipboard() { + val generateKotlinCode = codeGeneratorService.generateKotlinCode() + val clipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard + val copyString = StringSelection(generateKotlinCode) + clipboard.setContents(copyString, null) + } + + fun saveQrCodeImageFile() { + val fileChooser = JFileChooser() + fileChooser.fileFilter = FileNameExtensionFilter("Png Image Files (*.png)", "png") + val result = fileChooser.showSaveDialog(null) + + if (result == JFileChooser.APPROVE_OPTION) { + val fileToSave = if (fileChooser.selectedFile.name.endsWith(".png")) { + fileChooser.selectedFile + } else { + File("${fileChooser.selectedFile.absolutePath}.png") + } + + val qrCodeImage = if (alreadyAppliedOnceDelegate()) { + imageService.renderImage() + } else { + imageService.renderInitialImage() + } + ImageIO.write(qrCodeImage, "png", fileToSave) + } + } + + fun saveConfig() { + val fileChooser = JFileChooser() + fileChooser.fileFilter = FileNameExtensionFilter("Json Files (*.json)", "json") + val result = fileChooser.showSaveDialog(null) + + if (result == JFileChooser.APPROVE_OPTION) { + val fileToSave = if (fileChooser.selectedFile.name.endsWith(".json")) { + fileChooser.selectedFile + } else { + File("${fileChooser.selectedFile.absolutePath}.json") + } + + runCatching { + configService.saveConfigFile(fileToSave.absolutePath) + }.onFailure { + JOptionPane.showMessageDialog( + null, + "The file to be saved must be a valid json config file.\n Fehler: ${it.message}", + "Config Saving Error", + JOptionPane.ERROR_MESSAGE, + ) + } + } + } + + fun loadConfig() { + val fileChooser = JFileChooser() + fileChooser.fileFilter = FileNameExtensionFilter("Json Files (*.json)", "json") + val result = fileChooser.showOpenDialog(null) + + if (result == JFileChooser.APPROVE_OPTION) { + runCatching { + configService.loadConfigFile(fileChooser.selectedFile.absolutePath) + }.onFailure { + JOptionPane.showMessageDialog( + null, + "The file to be loaded must be a valid json config file.\n Fehler: ${it.message}", + "Config Loading Error", + JOptionPane.ERROR_MESSAGE, + ) + } + } + } +} diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/ImageUI.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/ImageUI.kt index e2c93e5..ed6d1f1 100644 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/ImageUI.kt +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/ImageUI.kt @@ -1,7 +1,6 @@ package io.github.simonscholz.ui import io.github.simonscholz.service.ImageService -import io.github.simonscholz.service.RenderImageService.renderInitialImage import net.miginfocom.swing.MigLayout import java.awt.Color import java.awt.Dimension @@ -18,7 +17,7 @@ import javax.swing.JPopupMenu import javax.swing.KeyStroke object ImageUI { - fun createImagePanel(imageService: ImageService): Pair Unit> { + fun createImagePanel(imageService: ImageService, fileUI: FileUI): Pair Unit> { val imageContainer = JPanel(MigLayout("", "[center]")) imageContainer.background = Color.WHITE @@ -28,7 +27,7 @@ object ImageUI { setImage(image) } imageContainer.add(imageDrawPanel, "wrap") - createPopupMenu(imageService, imageDrawPanel) + createPopupMenu(fileUI, imageDrawPanel) val setImage = (imageDrawPanel::setImage as (BufferedImage) -> Unit) @@ -38,16 +37,12 @@ object ImageUI { return Pair(imageContainer, setImage) } - private fun createPopupMenu(imageService: ImageService, imagePanel: ImagePanel) { - val saveImageMenuItem = JMenuItem("Save Qr Code Image") - // Add the keybinding for Save (Ctrl + S) - saveImageMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK) - saveImageMenuItem.addActionListener { - imageService.saveFile() - } - + private fun createPopupMenu(fileUI: FileUI, imagePanel: ImagePanel) { val contextMenu = JPopupMenu() - contextMenu.add(saveImageMenuItem) + + contextMenu.add(saveQrCodeImageMenuItem(fileUI)) + contextMenu.add(saveKotlinCodeMenuItem(fileUI)) + contextMenu.add(saveJavaCodeMenuItem(fileUI)) imagePanel.addMouseListener( object : MouseAdapter() { @@ -68,6 +63,36 @@ object ImageUI { ) } + private fun saveQrCodeImageMenuItem(fileUI: FileUI): JMenuItem { + val saveImageMenuItem = JMenuItem("Save Qr Code Image") + // Add the keybinding for Save (Ctrl + S) + saveImageMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK) + saveImageMenuItem.addActionListener { + fileUI.saveQrCodeImageFile() + } + return saveImageMenuItem + } + + private fun saveKotlinCodeMenuItem(fileUI: FileUI): JMenuItem { + val saveImageMenuItem = JMenuItem("Copy Kotlin code to clipboard") + // Add the keybinding for Save (Ctrl + K) + saveImageMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK) + saveImageMenuItem.addActionListener { + fileUI.copyKotlinCodeToClipboard() + } + return saveImageMenuItem + } + + private fun saveJavaCodeMenuItem(fileUI: FileUI): JMenuItem { + val saveImageMenuItem = JMenuItem("Copy Java code to clipboard") + // Add the keybinding for Save (Ctrl + J) + saveImageMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_J, InputEvent.CTRL_DOWN_MASK) + saveImageMenuItem.addActionListener { + fileUI.copyJavaCodeToClipboard() + } + return saveImageMenuItem + } + internal class ImagePanel : JPanel(true) { private var image: BufferedImage? = null diff --git a/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/MainMenu.kt b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/MainMenu.kt index 693d324..a24b84a 100644 --- a/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/MainMenu.kt +++ b/qr-code-app/src/main/kotlin/io/github/simonscholz/ui/MainMenu.kt @@ -3,7 +3,7 @@ package io.github.simonscholz.ui import io.github.simonscholz.qrcode.types.SimpleTypes import io.github.simonscholz.qrcode.types.VCard import io.github.simonscholz.qrcode.types.VEvent -import io.github.simonscholz.service.ImageService +import io.github.simonscholz.service.ConfigService import io.github.simonscholz.ui.dialogs.InputDialogs import org.eclipse.core.databinding.observable.value.IObservableValue import java.awt.Desktop @@ -21,23 +21,61 @@ import javax.swing.KeyStroke import kotlin.system.exitProcess object MainMenu { - fun createFrameMenu(frame: JFrame, qrCodeContentObservable: IObservableValue, imageService: ImageService) { - // Create and set the menu bar + fun createFrameMenu( + frame: JFrame, + qrCodeContentObservable: IObservableValue, + fileUI: FileUI, + configService: ConfigService, + ) { val menuBar = JMenuBar() frame.jMenuBar = menuBar - createFileMenu(menuBar, frame, imageService) + createFileMenu(menuBar, frame, fileUI, configService) createSpecialContentMenu(menuBar, frame, qrCodeContentObservable) + createGenerateCodeMenu(menuBar, frame, fileUI) + createHelpMenu(menuBar, frame) } - private fun createFileMenu(menuBar: JMenuBar, frame: JFrame, imageService: ImageService) { + private fun createGenerateCodeMenu(menuBar: JMenuBar, frame: JFrame, fileUI: FileUI) { + val generateCodeMenu = JMenu("Generate Code") + menuBar.add(generateCodeMenu) + + val copyJavaCodeMenuItem = JMenuItem("Copy Java Code") + copyJavaCodeMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_J, InputEvent.CTRL_DOWN_MASK) + frame.rootPane.actionMap.put( + "CopyJavaCodeAction", + object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + fileUI.copyJavaCodeToClipboard() + } + }, + ) + + val copyKotlinCodeMenuItem = JMenuItem("Copy Kotlin Code") + copyKotlinCodeMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK) + frame.rootPane.actionMap.put( + "CopyKotlinCodeAction", + object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + fileUI.copyKotlinCodeToClipboard() + } + }, + ) + + copyJavaCodeMenuItem.addActionListener { fileUI.copyJavaCodeToClipboard() } + copyKotlinCodeMenuItem.addActionListener { fileUI.copyKotlinCodeToClipboard() } + + generateCodeMenu.add(copyJavaCodeMenuItem) + generateCodeMenu.add(copyKotlinCodeMenuItem) + } + + private fun createFileMenu(menuBar: JMenuBar, frame: JFrame, fileUI: FileUI, configService: ConfigService) { val fileMenu = JMenu("File") menuBar.add(fileMenu) - // Create menu items for File menu val saveMenuItem = JMenuItem("Save Qr Code Image") // Add the keybinding for Save (Ctrl + S) saveMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK) @@ -45,27 +83,54 @@ object MainMenu { "SaveAction", object : AbstractAction() { override fun actionPerformed(e: ActionEvent) { - imageService.saveFile() + fileUI.saveQrCodeImageFile() + } + }, + ) + + val importConfigMenuItem = JMenuItem("Import Config") + // Add the keybinding for Save (Ctrl + I) + importConfigMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK) + frame.rootPane.actionMap.put( + "ImportAction", + object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + fileUI.loadConfig() } }, ) + + val exportConfigMenuItem = JMenuItem("Export Config") + // Add the keybinding for Save (Ctrl + E) + exportConfigMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_E, InputEvent.CTRL_DOWN_MASK) + frame.rootPane.actionMap.put( + "ExportAction", + object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + fileUI.saveConfig() + } + }, + ) + val exitMenuItem = JMenuItem("Exit") exitMenuItem.accelerator = KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.ALT_DOWN_MASK) frame.rootPane.actionMap.put( "ExitAction", object : AbstractAction() { override fun actionPerformed(e: ActionEvent) { - exitApplication() + exitApplication(configService) } }, ) - // Add ActionListeners to the menu items - saveMenuItem.addActionListener { imageService.saveFile() } - exitMenuItem.addActionListener { exitApplication() } + saveMenuItem.addActionListener { fileUI.saveQrCodeImageFile() } + importConfigMenuItem.addActionListener { fileUI.loadConfig() } + exportConfigMenuItem.addActionListener { fileUI.saveConfig() } + exitMenuItem.addActionListener { exitApplication(configService) } - // Add menu items to the File menu fileMenu.add(saveMenuItem) + fileMenu.add(importConfigMenuItem) + fileMenu.add(exportConfigMenuItem) fileMenu.add(exitMenuItem) } @@ -222,7 +287,8 @@ object MainMenu { } } - private fun exitApplication() { + private fun exitApplication(configService: ConfigService) { + configService.saveConfig() exitProcess(0) } }