Skip to content

Commit

Permalink
Add Base64 logo for UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Scholz committed Nov 26, 2023
1 parent 337adba commit 26eb10c
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 57 deletions.
6 changes: 1 addition & 5 deletions qr-code-app/src/main/kotlin/io/github/simonscholz/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,13 @@ fun main() {
},
)

var alreadyAppliedOnce = false
val alreadyAppliedOnceDelegate = { alreadyAppliedOnce }
val imageService = ImageService(qrCodeConfigViewModel)
val fileUi = FileUI(CodeGeneratorService(qrCodeConfigViewModel), configService, imageService, alreadyAppliedOnceDelegate)
val fileUi = FileUI(CodeGeneratorService(qrCodeConfigViewModel), configService, imageService)
MainMenu.createFrameMenu(frame, qrCodeConfigViewModel.qrCodeContent, fileUi, configService)

val (imagePanel, setImage) = ImageUI.createImagePanel(imageService, fileUi)
val (propertiesPanel, applyOnChange) = PropertiesUI.createPropertiesUI(qrCodeConfigViewModel, dataBindingContext) {
onPropertyApply(qrCodeConfigViewModel.qrCodeContent, imageService, setImage, imagePanel)
alreadyAppliedOnce = true
}

val mainPanel = MainUI.createMainPanel(imagePanel, propertiesPanel)
Expand All @@ -60,7 +57,6 @@ fun main() {
it.model.addChangeListener {
if (applyOnChange()) {
onPropertyApply(qrCodeConfigViewModel.qrCodeContent, imageService, setImage, imagePanel)
alreadyAppliedOnce = true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.simonscholz.extension

import io.github.simonscholz.observables.BackgroundColorObservable
import io.github.simonscholz.observables.CheckboxObservable
import io.github.simonscholz.observables.ButtonSelectedObservable
import io.github.simonscholz.observables.ComponentEnabledObservable
import io.github.simonscholz.observables.ComponentInvertedEnabledObservable
import io.github.simonscholz.observables.DocumentObservable
Expand Down Expand Up @@ -29,6 +29,6 @@ fun Component.toBackgroundColorObservable(): IObservableValue<Color> =
fun Component.toEnabledObservable(): IObservableValue<Boolean> = ComponentEnabledObservable(this)
fun Component.toEnabledInvertedObservable(): IObservableValue<Boolean> = ComponentInvertedEnabledObservable(this)

fun AbstractButton.toCheckboxObservable(): IObservableValue<Boolean> = CheckboxObservable(this)
fun AbstractButton.toButtonSelectedObservable(): IObservableValue<Boolean> = ButtonSelectedObservable(this)

fun <E> JComboBox<E>.toSelectedItemObservable(): IObservableValue<E> = JComboBoxSelectedItemObservable(this)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ object Mapper {
backgroundColor = viewModel.backgroundColor.value.toColorInfo(),
foregroundColor = viewModel.foregroundColor.value.toColorInfo(),
logo = viewModel.logo.value,
logoBase64 = viewModel.logoBase64.value,
useBase64Logo = viewModel.useBase64Logo.value,
logoRelativeSize = viewModel.logoRelativeSize.value,
logoBackgroundColor = viewModel.logoBackgroundColor.value.toColorInfo(),
logoShape = viewModel.logoShape.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ data class QrCodeConfig(
val foregroundColor: ColorInfo,

val logo: String,
val logoBase64: String?,
val useBase64Logo: Boolean,
val logoRelativeSize: Double,
val logoBackgroundColor: ColorInfo,
val logoShape: LogoShape,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.eclipse.core.databinding.observable.Diffs
import org.eclipse.core.databinding.observable.value.AbstractObservableValue
import javax.swing.AbstractButton

class CheckboxObservable(
class ButtonSelectedObservable(
private val button: AbstractButton,
) : AbstractObservableValue<Boolean>() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@ import io.github.simonscholz.model.Mapper
import io.github.simonscholz.model.QrCodeConfig
import io.github.simonscholz.model.QrCodeConfigViewModel
import java.io.File
import java.util.prefs.Preferences
import java.nio.file.Paths
import kotlin.io.path.createDirectories

class ConfigService(
private val qrCodeConfigViewModel: QrCodeConfigViewModel,
) {
private val objectMapper = ObjectMapper().registerKotlinModule()
private val preferences = Preferences.userRoot().node("qr-code-app")

fun saveConfig() {
val config = Mapper.fromViewModel(qrCodeConfigViewModel)
val configJson = objectMapper.writeValueAsString(config)
preferences.put(QR_CODE_CONFIG_PREFERENCE_KEY, configJson)
runCatching {
val config = Mapper.fromViewModel(qrCodeConfigViewModel)
objectMapper.writeValue(getConfigFile(), config)
}.onFailure {
println("Failed to save config to preferences. ${it.message}")
it.printStackTrace()
}
}

fun loadConfig() {
preferences.get(QR_CODE_CONFIG_PREFERENCE_KEY, null)?.let {
val config = objectMapper.readValue(it, QrCodeConfig::class.java)
runCatching {
val config = objectMapper.readValue(getConfigFile(), QrCodeConfig::class.java)
Mapper.applyViewModel(config, qrCodeConfigViewModel)
}.onFailure {
println("Failed to load config from preferences. ${it.message}")
resetConfig()
}
}

fun resetConfig() {
preferences.remove(QR_CODE_CONFIG_PREFERENCE_KEY)
runCatching {
File(getQrCodeAppDataFolder(), QR_CODE_CONFIG_FILE).delete()
}.onFailure {
println("Failed to delete config file. ${it.message}")
it.printStackTrace()
}
}

fun saveConfigFile(filePath: String) {
Expand All @@ -46,7 +58,29 @@ class ConfigService(
Mapper.applyViewModel(config, qrCodeConfigViewModel)
}

private fun getConfigFile(): File {
val configDirectory = Paths.get(getQrCodeAppDataFolder()).createDirectories()
return File(configDirectory.toFile(), QR_CODE_CONFIG_FILE)
}

private fun getQrCodeAppDataFolder(): String {
val os = System.getProperty("os.name").lowercase()

return when {
os.contains("win") -> {
// Windows
System.getenv("APPDATA")?.let { "$it/qr-code-app" } ?: throw IllegalStateException("APPDATA environment variable not found.")
}
os.contains("nix") || os.contains("nux") || os.contains("mac") -> {
// Linux or macOS
val homeDir = System.getProperty("user.home")
"$homeDir/.config/qr-code-app"
}
else -> throw UnsupportedOperationException("Unsupported operating system: $os")
}
}

companion object {
private const val QR_CODE_CONFIG_PREFERENCE_KEY = "qrcode.config"
private const val QR_CODE_CONFIG_FILE = "config.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ 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.Image
import java.awt.image.BufferedImage
import java.io.File
Expand Down Expand Up @@ -36,7 +34,7 @@ class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel) {
outerBorderColor = qrCodeConfigViewModel.positionalSquareOuterBorderColor.value,
),
)
if (qrCodeConfigViewModel.logo.value.isNotBlank() && File(qrCodeConfigViewModel.logo.value).exists()) {
if (!qrCodeConfigViewModel.useBase64Logo.value && qrCodeConfigViewModel.logo.value.isNotBlank() && File(qrCodeConfigViewModel.logo.value).exists()) {
runCatching {
ImageIO.read(File(qrCodeConfigViewModel.logo.value)).let {
val scaledLogo = getScaledLogo(it, qrCodeConfigViewModel)
Expand All @@ -51,6 +49,13 @@ class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel) {
}.onFailure { _ ->
JOptionPane.showMessageDialog(null, "You did not select a proper image", "Image Loading Error", JOptionPane.ERROR_MESSAGE)
}
} else if (qrCodeConfigViewModel.useBase64Logo.value && qrCodeConfigViewModel.logoBase64.value.isNotBlank()) {
builder.qrLogoConfig(
base64Logo = qrCodeConfigViewModel.logoBase64.value,
relativeSize = qrCodeConfigViewModel.logoRelativeSize.value,
bgColor = qrCodeConfigViewModel.logoBackgroundColor.value,
shape = qrCodeConfigViewModel.logoShape.value,
)
}
val qrCodeConfig = builder.build()
return QrCodeFactory.createQrCodeApi().createQrCodeImage(qrCodeConfig)
Expand All @@ -70,21 +75,4 @@ class ImageService(private val qrCodeConfigViewModel: QrCodeConfigViewModel) {
val ratio = logo.getWidth(null).toDouble() / logo.getHeight(null).toDouble()
return logo.getScaledInstance((maxLogoSize * ratio).toInt(), maxLogoSize, Image.SCALE_SMOOTH)
}

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)
}
}
19 changes: 3 additions & 16 deletions qr-code-app/src/main/kotlin/io/github/simonscholz/ui/FileUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,16 @@ class FileUI(
private val codeGeneratorService: CodeGeneratorService,
private val configService: ConfigService,
private val imageService: ImageService,
private val alreadyAppliedOnceDelegate: () -> Boolean,
) {
fun copyBase64ImageToClipboard() {
val qrCodeImage = if (alreadyAppliedOnceDelegate()) {
imageService.renderImage()
} else {
imageService.renderInitialImage()
}
val qrCodeImage = imageService.renderImage()
val clipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard
val copyString = StringSelection(qrCodeImage.toBase64())
clipboard.setContents(copyString, null)
}

fun copyImageToClipboard() {
val qrCodeImage = if (alreadyAppliedOnceDelegate()) {
imageService.renderImage()
} else {
imageService.renderInitialImage()
}
val qrCodeImage = imageService.renderImage()
val transferableImage = ImageTransferable(qrCodeImage)
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
clipboard.setContents(transferableImage, null)
Expand Down Expand Up @@ -71,11 +62,7 @@ class FileUI(
File("${fileChooser.selectedFile.absolutePath}.png")
}

val qrCodeImage = if (alreadyAppliedOnceDelegate()) {
imageService.renderImage()
} else {
imageService.renderInitialImage()
}
val qrCodeImage = imageService.renderImage()
ImageIO.write(qrCodeImage, "png", fileToSave)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object ImageUI {
val imageContainer = JPanel(MigLayout("", "[center]"))
imageContainer.background = Color.WHITE

val image = imageService.renderInitialImage()
val image = imageService.renderImage()

val imageDrawPanel = ImagePanel().apply {
setImage(image)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.github.simonscholz.ui.properties

import io.github.simonscholz.extension.toButtonSelectedObservable
import io.github.simonscholz.extension.toDoubleObservable
import io.github.simonscholz.extension.toEnabledInvertedObservable
import io.github.simonscholz.extension.toEnabledObservable
import io.github.simonscholz.extension.toObservable
import io.github.simonscholz.extension.toSelectedItemObservable
import io.github.simonscholz.model.QrCodeConfigViewModel
Expand All @@ -9,11 +12,14 @@ import io.github.simonscholz.ui.CustomItems
import net.miginfocom.swing.MigLayout
import org.eclipse.core.databinding.DataBindingContext
import java.io.File
import javax.swing.ButtonGroup
import javax.swing.ImageIcon
import javax.swing.JButton
import javax.swing.JComboBox
import javax.swing.JFileChooser
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JRadioButton
import javax.swing.JSpinner
import javax.swing.JTextField
import javax.swing.SpinnerNumberModel
Expand All @@ -24,6 +30,20 @@ object LogoPropertiesUI {
fun createLogoPropertiesUI(dataBindingContext: DataBindingContext, qrCodeConfigViewModel: QrCodeConfigViewModel): JPanel {
val logoPropertiesPanel = JPanel(MigLayout())

logoPropertiesPanel.add(JLabel("Logo source:"))

val radioPanel = JPanel(MigLayout("nogrid"))
val useFileButton = JRadioButton("Use file")
val useBase64Button = JRadioButton("Use base 64")
ButtonGroup().run {
add(useFileButton)
add(useBase64Button)
}
radioPanel.add(useFileButton)
radioPanel.add(useBase64Button)
dataBindingContext.bindValue(useBase64Button.toButtonSelectedObservable(), qrCodeConfigViewModel.useBase64Logo)
logoPropertiesPanel.add(radioPanel, "wrap")

logoPropertiesPanel.add(JLabel("Logo:"))
val logoTextField = JTextField()
dataBindingContext.bindValue(logoTextField.toObservable(), qrCodeConfigViewModel.logo)
Expand All @@ -40,6 +60,22 @@ object LogoPropertiesUI {
}
}
logoPropertiesPanel.add(chooseFile, "wrap, growx, width 30:30:30")
dataBindingContext.bindValue(logoTextField.toEnabledInvertedObservable(), qrCodeConfigViewModel.useBase64Logo)
dataBindingContext.bindValue(chooseFile.toEnabledInvertedObservable(), qrCodeConfigViewModel.useBase64Logo)

logoPropertiesPanel.add(JLabel("Base64 encoded Logo:"))
val base64LogoTextField = JTextField().apply {
dataBindingContext.bindValue(this.toObservable(), qrCodeConfigViewModel.logoBase64)
logoPropertiesPanel.add(this, "growx, width 200:220:300")
dataBindingContext.bindValue(this.toEnabledObservable(), qrCodeConfigViewModel.useBase64Logo)
}

val deleteBase64LogoTextField = JButton()
deleteBase64LogoTextField.icon = ImageIcon(LogoPropertiesUI::class.java.classLoader.getResource("dustbin_remove16.png"))
deleteBase64LogoTextField.addActionListener {
base64LogoTextField.text = ""
}
logoPropertiesPanel.add(deleteBase64LogoTextField, "wrap, growx, width 30:30:30")

logoPropertiesPanel.add(JLabel("Relative Logo Size:"))
val sizeSpinnerModel = SpinnerNumberModel(.2, .0, 1.0, 0.01)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.simonscholz.ui.properties

import io.github.simonscholz.extension.toCheckboxObservable
import io.github.simonscholz.extension.toButtonSelectedObservable
import io.github.simonscholz.extension.toDoubleObservable
import io.github.simonscholz.extension.toEnabledInvertedObservable
import io.github.simonscholz.model.QrCodeConfigViewModel
Expand All @@ -20,15 +20,15 @@ object PositionalSquaresPropertiesUI {
positionalSquaresPropertiesPanel.add(JLabel("Is Circle Shape:"))
val isCircleShaped = JCheckBox("(Disables Border Radius)")
positionalSquaresPropertiesPanel.add(isCircleShaped, "wrap, growx, width 200:220:300")
dataBindingContext.bindValue(isCircleShaped.toCheckboxObservable(), qrCodeConfigViewModel.positionalSquareIsCircleShaped)
dataBindingContext.bindValue(isCircleShaped.toButtonSelectedObservable(), qrCodeConfigViewModel.positionalSquareIsCircleShaped)

positionalSquaresPropertiesPanel.add(JLabel("Positional Sqaure Border Radius:"))
val relativeSquareBorderRoundSpinnerModel = SpinnerNumberModel(.2, .0, 1.0, 0.01)
val relativePositionalSquareBorderRoundSpinner = JSpinner(relativeSquareBorderRoundSpinnerModel)
dataBindingContext.bindValue(relativePositionalSquareBorderRoundSpinner.toDoubleObservable(), qrCodeConfigViewModel.positionalSquareRelativeBorderRound)
positionalSquaresPropertiesPanel.add(relativePositionalSquareBorderRoundSpinner, "wrap, growx, width 200:220:300")

dataBindingContext.bindValue(relativePositionalSquareBorderRoundSpinner.toEnabledInvertedObservable(), isCircleShaped.toCheckboxObservable())
dataBindingContext.bindValue(relativePositionalSquareBorderRoundSpinner.toEnabledInvertedObservable(), isCircleShaped.toButtonSelectedObservable())

CustomItems.createColorPickerItem(positionalSquaresPropertiesPanel, "Center Color:", qrCodeConfigViewModel.positionalSquareCenterColor, dataBindingContext)
CustomItems.createColorPickerItem(positionalSquaresPropertiesPanel, "Inner Square Color:", qrCodeConfigViewModel.positionalSquareInnerSquareColor, dataBindingContext)
Expand Down
Binary file added qr-code-app/src/main/resources/dustbin_remove16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 26eb10c

Please sign in to comment.