diff --git a/desktop/app/build.gradle.kts b/desktop/app/build.gradle.kts index c88d0c6..ca2c1c8 100644 --- a/desktop/app/build.gradle.kts +++ b/desktop/app/build.gradle.kts @@ -65,7 +65,6 @@ dependencies { implementation(project(":integration:server")) implementation(project(":desktop:shared")) implementation(project(":desktop:tray")) - implementation(project(":desktop:external-draggable")) implementation(project(":desktop:custom-window-frame")) implementation(project(":shared:app-utils")) implementation(project(":shared:utils")) diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt index 3b5966b..8503788 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt @@ -674,16 +674,14 @@ class HomeComponent( currentActiveDrops.update { parsedLinks } } - fun onExternalFilesDraggedIn(getFilePaths: () -> List) { - val filePaths = getFilePaths().map { - URI.create(it) - } - .mapNotNull { - runCatching { File(it.path) }.getOrNull() - } + fun onExternalFilesDraggedIn(getFilePaths: () -> List) { + val filePaths = kotlin.runCatching { getFilePaths() } + .getOrNull()?.filter { it.length() <= 1024 * 1024 } ?: return onExternalTextDraggedIn { - filePaths.first() - .readText() + filePaths + .firstOrNull() + ?.readText() + .orEmpty() } } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt index c60199f..2c2b885 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomePage.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import com.abdownloadmanager.desktop.ui.widget.Text import androidx.compose.runtime.* -import com.abdownloadmanager.desktop.utils.externaldraggable.onExternalDrag import androidx.compose.ui.* import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip @@ -32,8 +31,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import com.abdownloadmanager.desktop.ui.widget.ActionButton import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.awtTransferable import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalDensity @@ -41,10 +44,11 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.window.Dialog import com.abdownloadmanager.desktop.ui.customwindow.* import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown -import com.abdownloadmanager.desktop.utils.externaldraggable.DragData import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.rememberIconPainter import ir.amirab.util.compose.action.MenuItem +import java.awt.datatransfer.DataFlavor +import java.io.File @Composable @@ -162,27 +166,40 @@ fun HomePage(component: HomeComponent) { Box( Modifier .fillMaxSize() - .onExternalDrag( - onDragStart = { - isDragging = true - it.availableDragData.get()?.also { - component.onExternalTextDraggedIn { it.readText() } - return@onExternalDrag - } - it.availableDragData.get()?.also { - //Caution FileList::readFiles sometimes throws exception - component.onExternalFilesDraggedIn { it.readFiles() } - return@onExternalDrag - } + .dragAndDropTarget( + shouldStartDragAndDrop = { + it.awtTransferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || + it.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor) }, - onDragExit = { - isDragging = false - component.onDragExit() + target = remember { + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + isDragging = true + if (event.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor)) { + component.onExternalTextDraggedIn { (event.awtTransferable.getTransferData(DataFlavor.stringFlavor) as String) } + return + } + if (event.awtTransferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + component.onExternalFilesDraggedIn { + (event.awtTransferable.getTransferData(DataFlavor.javaFileListFlavor) as List) + } + return + } + } + + override fun onEnded(event: DragAndDropEvent) { + isDragging = false + component.onDragExit() + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + isDragging = false + component.onDropped() + return true + } + } } - ) { - isDragging = false - component.onDropped() - } + ) ) { Column( Modifier.alpha( diff --git a/desktop/external-draggable/build.gradle.kts b/desktop/external-draggable/build.gradle.kts deleted file mode 100644 index 036e8d1..0000000 --- a/desktop/external-draggable/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -plugins{ - id(MyPlugins.kotlin) - id(MyPlugins.composeDesktop) -} -dependencies{ - implementation(project(":desktop:shared")) -} \ No newline at end of file diff --git a/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/CustomDropTarget.kt b/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/CustomDropTarget.kt deleted file mode 100644 index 7331f0e..0000000 --- a/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/CustomDropTarget.kt +++ /dev/null @@ -1,462 +0,0 @@ -package com.abdownloadmanager.desktop.utils.externaldraggable - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.unit.Density -import ir.amirab.util.desktop.LocalWindow -import java.awt.Component -import java.awt.GraphicsConfiguration -import java.awt.Point -import java.awt.Window -import java.awt.dnd.DnDConstants -import java.awt.dnd.DropTarget -import java.awt.dnd.DropTargetDragEvent -import java.awt.dnd.DropTargetDropEvent -import java.awt.dnd.DropTargetEvent -import java.awt.dnd.DropTargetListener - -/** - * Represent data that is being dragged (or dropped) to a component from outside an application. - */ -interface DragData { - /** - * Represents list of files drag and dropped to a component. - */ - interface FilesList : DragData { - /** - * Returns list of file paths drag and droppped to an application in a URI format. - */ - fun readFiles(): List - } - - /** - * Represents an image drag and dropped to a component. - */ - interface Image : DragData { - /** - * Returns an image drag and dropped to an application as a [Painter] type. - */ - fun readImage(): Painter - } - - /** - * Represent text drag and dropped to a component. - */ - interface Text : DragData { - /** - * Provides the best MIME type that describes text returned in [readText] - */ - val bestMimeType: String - - /** - * Returns a text dropped to an application. - */ - fun readText(): String - } -} - -/** - * Represent the current state of drag and drop to a component from outside an application. - * This state is passed to external drag callbacks. - * - * @see onExternalDrag - */ -@Immutable -class ExternalDragValue( - /** - * Position of the pointer relative to the component - */ - val dragPosition: Offset, - /** - * Data that it being dragged (or dropped) in a component bounds - */ - val availableDragData: AvailableDragData -) - -/** - * Adds detector of external drag and drop (e.g. files DnD from Finder to an application) - * - * @param onDragStart will be called when the pointer with external content entered the component. - * @param onDrag will be called for pointer movements inside the component. - * @param onDragExit is called if the pointer exited the component bounds. - * @param onDrop is called when the pointer is released. - */ -@Composable -fun Modifier.onExternalDrag( - enabled: Boolean = true, - onDragStart: (ExternalDragValue) -> Unit = {}, - onDrag: (ExternalDragValue) -> Unit = {}, - onDragExit: () -> Unit = {}, - onDrop: (ExternalDragValue) -> Unit = {}, -): Modifier = composed { - if (!enabled) { - return@composed Modifier - } - val window = LocalWindow.current ?: return@composed Modifier - - val componentDragHandler = rememberUpdatedState( - AwtWindowDropTarget.ComponentDragHandler(onDragStart, onDrag, onDragExit, onDrop) - ) - - var componentDragHandleId by remember { mutableStateOf(null) } - - DisposableEffect(window) { - when (val currentDropTarget = window.dropTarget) { - is AwtWindowDropTarget -> { - // if our drop target is already assigned simply add new drag handler for the current component - componentDragHandleId = - currentDropTarget.installComponentDragHandler(componentDragHandler) - } - - null -> { - // drop target is not installed for the window, so assign it and add new drag handler for the current component - val newDropTarget = AwtWindowDropTarget(window) - componentDragHandleId = - newDropTarget.installComponentDragHandler(componentDragHandler) - window.dropTarget = newDropTarget - } - - else -> { - error("Window already has unknown external dnd handler, cannot attach onExternalDrag") - } - } - - onDispose { - // stop drag events handling for this component when window is changed - // or the component leaves the composition - val dropTarget = window.dropTarget as? AwtWindowDropTarget ?: return@onDispose - val handleIdToRemove = componentDragHandleId ?: return@onDispose - dropTarget.stopDragHandling(handleIdToRemove) - } - } - - Modifier - .onGloballyPositioned { position -> - // provide new component bounds to Swing to properly detect drag events - val dropTarget = window.dropTarget as? AwtWindowDropTarget - ?: return@onGloballyPositioned - val handleIdToUpdate = componentDragHandleId ?: return@onGloballyPositioned - val componentBounds = position.boundsInWindow() - dropTarget.updateComponentBounds(handleIdToUpdate, componentBounds) - } -} - -/** - * Provides a way to subscribe on external drag for given [window] using [installComponentDragHandler] - * - * [Window] allows having only one [DropTarget], so this is the main [DropTarget] that handles all the drag subscriptions - */ -internal class AwtWindowDropTarget( - private val window: Window -) : DropTarget(window, DnDConstants.ACTION_MOVE, null, true) { - private var idsCounter = 0 - - // all components that are subscribed to external drag and drop for the window - // handler's callbacks can be changed on recompositions, so State is kept here - private val handlers = mutableMapOf>() - - // bounds of all components that are subscribed to external drag and drop for the window - private val componentBoundsHolder = mutableMapOf() - - // state of ongoing external drag and drop in the [window], contains pointer coordinates and data that is dragged - private var currentDragValue: AwtWindowDragTargetListener.WindowDragValue? = null - - val dragTargetListener = AwtWindowDragTargetListener( - window, - // notify components on window border that drag is started. - onDragEnterWindow = { newDragValue -> - currentDragValue = newDragValue - forEachPositionedComponent { handler, componentBounds -> - handleDragEvent( - handler, - oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragValue = null, currentDragValue = newDragValue, - ) - } - }, - // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them - onDragInsideWindow = { newDragValue -> - val oldDragValue = currentDragValue - currentDragValue = newDragValue - forEachPositionedComponent { handler, componentBounds -> - handleDragEvent( - handler, - oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragValue, newDragValue - ) - } - }, - // notify components on window border drag exited window - onDragExit = { - val oldDragValue = currentDragValue - currentDragValue = null - forEachPositionedComponent { handler, componentBounds -> - handleDragEvent( - handler, - oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragValue = oldDragValue, currentDragValue = null - ) - } - }, - // notify all components under the pointer that drop happened - onDrop = { newDragValue -> - var anyDrops = false - currentDragValue = null - forEachPositionedComponent { handler, componentBounds -> - val isInside = isExternalDragInsideComponent( - componentBounds, - newDragValue.dragPositionInWindow - ) - if (isInside) { - val offset = calculateOffset(componentBounds, newDragValue.dragPositionInWindow) - handler.onDrop(ExternalDragValue(offset, newDragValue.dragData)) - anyDrops = true - } - } - // tell swing whether some components accepted the drop - return@AwtWindowDragTargetListener anyDrops - } - ) - - init { - addDropTargetListener(dragTargetListener) - } - - override fun setActive(isActive: Boolean) { - super.setActive(isActive) - if (!isActive) { - currentDragValue = null - } - } - - /** - * Adds handler that will be notified on drag events for [window]. - * If component bounds are provided using [updateComponentBounds], - * given lambdas will be called on drag events. - * - * [handlerState]'s callbacks can be changed on recompositions. - * New callbacks won't be called with old events, they will be called on new AWT events only. - * - * @return handler id that can be used later to remove subscription using [stopDragHandling] - * or to update component bounds using [updateComponentBounds] - */ - fun installComponentDragHandler(handlerState: State): Int { - isActive = true - val handleId = idsCounter++ - handlers[handleId] = handlerState - return handleId - } - - /** - * Unsubscribes handler with [handleId]. - * Calls [ComponentDragHandler.onDragCancel] if drag is going and handler's component is under pointer - * - * Disable drag handling for [window] if there are no more handlers. - * - * @param handleId id provided by [installComponentDragHandler] function - */ - fun stopDragHandling(handleId: Int) { - val handler = handlers.remove(handleId) - val componentBounds = componentBoundsHolder.remove(handleId) - if (handler != null && componentBounds != null && - isExternalDragInsideComponent(componentBounds, currentDragValue?.dragPositionInWindow) - ) { - handler.value.onDragCancel() - } - - if (handlers.isEmpty()) { - isActive = false - } - } - - /** - * Updates component bounds within the [window], so drag events will be properly handled. - * If drag is going and component is under the pointer, onDragStart and onDrag will be called. - * If drag is going and component moved/became smaller, so that pointer now is not the component, onDragCancel is called. - * - * All further drag events will use [newComponentBounds] to notify handler with [handleId]. - * - * @param newComponentBounds new bounds of the component inside [window] used to properly detect when drag entered/exited component - */ - fun updateComponentBounds(handleId: Int, newComponentBounds: Rect) { - val handler = handlers[handleId] ?: return - val oldComponentBounds = componentBoundsHolder.put(handleId, newComponentBounds) - handleDragEvent( - handler.value, oldComponentBounds, newComponentBounds, - oldDragValue = currentDragValue, - currentDragValue = currentDragValue - ) - } - - private inline fun forEachPositionedComponent(action: (handler: ComponentDragHandler, bounds: Rect) -> Unit) { - for ((handleId, handler) in handlers) { - val bounds = componentBoundsHolder[handleId] ?: continue - action(handler.value, bounds) - } - } - - data class ComponentDragHandler( - val onDragStart: (ExternalDragValue) -> Unit, - val onDrag: (ExternalDragValue) -> Unit, - val onDragCancel: () -> Unit, - val onDrop: (ExternalDragValue) -> Unit - ) - - companion object { - private fun isExternalDragInsideComponent( - componentBounds: Rect?, - windowDragCoordinates: Offset? - ): Boolean { - if (componentBounds == null || windowDragCoordinates == null) { - return false - } - - return componentBounds.contains(windowDragCoordinates) - } - - private fun calculateOffset( - componentBounds: Rect, - windowDragCoordinates: Offset - ): Offset { - return windowDragCoordinates - componentBounds.topLeft - } - - /** - * Notifies [handler] about drag events. - * - * Note: this function is pure, so it doesn't update any states - */ - private fun handleDragEvent( - handler: ComponentDragHandler, - oldComponentBounds: Rect?, - currentComponentBounds: Rect?, - oldDragValue: AwtWindowDragTargetListener.WindowDragValue?, - currentDragValue: AwtWindowDragTargetListener.WindowDragValue?, - ) { - val wasDragInside = isExternalDragInsideComponent( - oldComponentBounds, - oldDragValue?.dragPositionInWindow - ) - val newIsDragInside = isExternalDragInsideComponent( - currentComponentBounds, - currentDragValue?.dragPositionInWindow - ) - if (!wasDragInside && newIsDragInside) { - val dragOffset = calculateOffset( - currentComponentBounds!!, - currentDragValue!!.dragPositionInWindow - ) - handler.onDragStart(ExternalDragValue(dragOffset, currentDragValue.dragData)) - return - } - - if (wasDragInside && !newIsDragInside) { - handler.onDragCancel() - return - } - - if (newIsDragInside) { - val dragOffset = calculateOffset( - currentComponentBounds!!, - currentDragValue!!.dragPositionInWindow - ) - handler.onDrag(ExternalDragValue(dragOffset, currentDragValue.dragData)) - return - } - } - } -} - -private val GraphicsConfiguration.density: Density - get() = Density( - defaultTransform.scaleX.toFloat(), - fontScale = 1f - ) -private val Component.density: Density get() = graphicsConfiguration.density - -internal class AwtWindowDragTargetListener( - private val window: Window, - val onDragEnterWindow: (WindowDragValue) -> Unit, - val onDragInsideWindow: (WindowDragValue) -> Unit, - val onDragExit: () -> Unit, - val onDrop: (WindowDragValue) -> Boolean, -) : DropTargetListener { - private val density = window.density.density - - override fun dragEnter(dtde: DropTargetDragEvent) { - onDragEnterWindow( - WindowDragValue( - dtde.location.windowOffset(), - AvailableDragData(dtde.transferable.dragData()) - ) - ) - } - - override fun dragOver(dtde: DropTargetDragEvent) { - onDragInsideWindow( - WindowDragValue( - dtde.location.windowOffset(), - AvailableDragData(dtde.transferable.dragData()) - ) - ) - } - - // takes title bar and other insets into account - private fun Point.windowOffset(): Offset { - val offsetX = (x - window.insets.left) * density - val offsetY = (y - window.insets.top) * density - - return Offset(offsetX, offsetY) - } - - override fun dropActionChanged(dtde: DropTargetDragEvent) { - // Should we notify about it? - } - - override fun dragExit(dte: DropTargetEvent) { - onDragExit() - } - - override fun drop(dtde: DropTargetDropEvent) { - dtde.acceptDrop(dtde.dropAction) - - val transferable = dtde.transferable - try { - onDrop(WindowDragValue(dtde.location.windowOffset(), transferable.dragData())) - dtde.dropComplete(true) - } catch (e: Exception) { - onDragExit() - dtde.dropComplete(false) - } - } - - data class WindowDragValue( - val dragPositionInWindow: Offset, - val dragData: AvailableDragData - ) -} - -data class AvailableDragData(val list: List) : List by list { - inline fun get(): T? { - for (i in this) { - if (i is T) { - return i - } - } - return null - } -} \ No newline at end of file diff --git a/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/DragDataExtensions.kt b/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/DragDataExtensions.kt deleted file mode 100644 index aef7322..0000000 --- a/desktop/external-draggable/src/main/kotlin/com/abdownloadmanager/desktop/utils/externaldraggable/DragDataExtensions.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.abdownloadmanager.desktop.utils.externaldraggable - -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toPainter -import java.awt.Image -import java.awt.datatransfer.DataFlavor -import java.awt.datatransfer.DataFlavor.selectBestTextFlavor -import java.awt.datatransfer.Transferable -import java.awt.image.BufferedImage -import java.io.File - -internal fun Transferable.dragData(): AvailableDragData { - val o = mutableListOf() - if (isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { - o.add(DragDataFilesListImpl(this)) - } - if (isDataFlavorSupported(DataFlavor.imageFlavor)) { - o.add(DragDataImageImpl(this)) - } - selectBestTextFlavor(transferDataFlavors)?.let { - o.add(DragDataTextImpl(it, this)) - } - return AvailableDragData(o) -} - - -private class DragDataFilesListImpl( - private val transferable: Transferable -) : DragData.FilesList { - override fun readFiles(): List { - val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> - return files.filterIsInstance().map { it.toURI().toString() } - } -} - -private class DragDataImageImpl( - private val transferable: Transferable -) : DragData.Image { - override fun readImage(): Painter { - return (transferable.getTransferData(DataFlavor.imageFlavor) as Image).painter() - } - - private fun Image.painter(): Painter { - if (this is BufferedImage) { - return this.toPainter() - } - val bufferedImage = - BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_ARGB) - - val g2 = bufferedImage.createGraphics() - try { - g2.drawImage(this, 0, 0, null) - } finally { - g2.dispose() - } - - return bufferedImage.toPainter() - } -} - -private class DragDataTextImpl( - private val bestTextFlavor: DataFlavor, - private val transferable: Transferable -) : DragData.Text { - override val bestMimeType: String = bestTextFlavor.mimeType - - override fun readText(): String { - val reader = bestTextFlavor.getReaderForText(transferable) - return reader.readText() - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d9f052c..57d0ef9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,7 +19,6 @@ include("desktop:app") include("desktop:custom-window-frame") include("desktop:shared") include("desktop:tray") -include("desktop:external-draggable") include("downloader:core") include("downloader:monitor") include("integration:server")