android, desktop: update to Compose 1.7.0 (#5038)

* docs: correction

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-11-01 00:11:26 +07:00
committed by GitHub
parent 78510b6fd3
commit 24090fe350
19 changed files with 176 additions and 51 deletions
@@ -206,3 +206,35 @@ fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImag
}
return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null)
}
fun BufferedImage.saveInTmpFile(): File? {
val formats = arrayOf("jpg", "png")
for (format in formats) {
val tmpFile = File.createTempFile("image", ".$format", tmpDir)
try {
// May fail on JPG, using PNG as an alternative
val success = ImageIO.write(this, format, tmpFile)
if (success) {
tmpFile.deleteOnExit()
chatModel.filesToDelete.add(tmpFile)
return tmpFile
} else {
tmpFile.delete()
}
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
tmpFile.delete()
return null
}
}
return null
}
fun BufferedImage.hasAlpha(): Boolean {
for (x in 0 until width) {
for (y in 0 until height) {
if (getRGB(x, y) == 0) return true
}
}
return false
}
@@ -1,10 +1,17 @@
package chat.simplex.common.platform
import androidx.compose.foundation.contextMenuOpenDetector
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.draganddrop.*
import androidx.compose.ui.draganddrop.DragData
import androidx.compose.ui.input.pointer.*
import java.awt.Image
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
import java.awt.image.BufferedImage
import java.io.File
import java.net.URI
@@ -23,16 +30,61 @@ actual fun ProvideWindowInsets(
actual fun Modifier.desktopOnExternalDrag(
enabled: Boolean,
onFiles: (List<File>) -> Unit,
onImage: (Painter) -> Unit,
onImage: (File) -> Unit,
onText: (String) -> Unit
): Modifier =
onExternalDrag(enabled) {
when(val data = it.dragData) {
// data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later
// with such format when everywhere we use absolutePath in File() format
is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() })
is DragData.Image -> onImage(data.readImage())
is DragData.Text -> onText(data.readText())
): Modifier {
val callback = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
when (val data = event.dragData()) {
// data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later
// with such format when everywhere we use absolutePath in File() format
is DragData.FilesList -> {
val files = data.readFiles()
// When dragging and dropping an image from browser, it comes to FilesList section but no files inside
if (files.isNotEmpty()) {
onFiles(files.map { URI.create(it).toFile() })
} else {
try {
val transferable = event.awtTransferable
if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) {
onImage(DragDataImageImpl(transferable).bufferedImage().saveInTmpFile() ?: return false)
} else {
return false
}
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
return false
}
}
}
is DragData.Image -> onImage(DragDataImageImpl(event.awtTransferable).bufferedImage().saveInTmpFile() ?: return false)
is DragData.Text -> onText(data.readText())
}
return true
}
}
}
return dragAndDropTarget(shouldStartDragAndDrop = { true }, target = callback)
}
// Copied from AwtDragData and modified
private class DragDataImageImpl(private val transferable: Transferable) {
fun bufferedImage(): BufferedImage = (transferable.getTransferData(DataFlavor.imageFlavor) as Image).bufferedImage()
private fun Image.bufferedImage(): BufferedImage {
if (this is BufferedImage && hasAlpha()) {
// Such image cannot be drawn as JPG, only PNG
return this
}
// Creating non-transparent image which can be drawn as JPG
val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_RGB)
val g2 = bufferedImage.createGraphics()
try {
g2.drawImage(this, 0, 0, null)
} finally {
g2.dispose()
}
return bufferedImage
}
}
@@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.*
@@ -109,7 +110,7 @@ actual fun PlatformTextField(
maxLines = 16,
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
autoCorrect = true
autoCorrectEnabled = true
),
modifier = Modifier
.padding(vertical = 4.dp)
@@ -193,6 +194,7 @@ actual fun PlatformTextField(
interactionSource = remember { MutableInteractionSource() },
contentPadding = PaddingValues(),
visualTransformation = VisualTransformation.None,
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
)
Spacer(Modifier.height(10.dp))
}
@@ -10,20 +10,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.onRightClick
import chat.simplex.common.views.helpers.*
object NoIndication : Indication {
private object NoIndicationInstance : IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
}
}
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
return NoIndicationInstance
object NoIndication : IndicationNodeFactory {
// Should be as a class, not an object. Otherwise, crash
private class NoIndicationInstance : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() { drawContent() }
}
override fun create(interactionSource: InteractionSource): DelegatableNode = NoIndicationInstance()
override fun hashCode(): Int = -1
override fun equals(other: Any?) = other === this
}
@Composable
@@ -156,7 +156,9 @@ actual fun getFileSize(uri: URI): Long? = uri.toFile().length()
actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? =
try {
ImageIO.read(uri.inputStream()).toComposeImageBitmap()
uri.inputStream().use {
ImageIO.read(it).toComposeImageBitmap()
}
} catch (e: Exception) {
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
if (withAlertOnException) showImageDecodingException()