From 4651548b46ecaa548851ff48a75bddbc5a8ebf29 Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Sat, 18 Nov 2023 22:08:56 +0800 Subject: [PATCH 1/2] clipboard listening on mac side --- .../kotlin/com/clipevery/ClipeveryApp.kt | 3 +- .../com/clipevery/clip/AbstractClipboard.kt | 8 +- .../com/clipevery/clip/ClipboardMonitor.kt | 2 - .../kotlin/com/clipevery/PlatformClipboard.kt | 19 +++++ .../com/clipevery/macos/MacosClipboard.kt | 77 +++++++++++++++++++ .../com/clipevery/windows/WindowsClipboard.kt | 21 ++--- composeApp/src/desktopMain/kotlin/main.kt | 36 +++++++-- 7 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/PlatformClipboard.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt b/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt index 7ab42eeb1..d3c0151bb 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt @@ -23,6 +23,7 @@ import org.jetbrains.compose.resources.painterResource @Composable fun ClipeveryApp(clipboard: AbstractClipboard, copyText: MutableState) { MaterialTheme { + val pid: Long = ProcessHandle.current().pid() var showImage by remember { mutableStateOf(false) } var start by remember { mutableStateOf(true) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { @@ -39,7 +40,7 @@ fun ClipeveryApp(clipboard: AbstractClipboard, copyText: MutableState) { } start = !start }) { - Text(start.toString()) + Text(start.toString() + " " + pid) } AnimatedVisibility(showImage) { Image( diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/clip/AbstractClipboard.kt b/composeApp/src/commonMain/kotlin/com/clipevery/clip/AbstractClipboard.kt index 59b84ee7c..95b15449a 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/clip/AbstractClipboard.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/clip/AbstractClipboard.kt @@ -1,4 +1,10 @@ package com.clipevery.clip - interface AbstractClipboard: Runnable, ClipboardMonitor { + +import java.awt.datatransfer.Transferable +import java.util.function.Consumer + +interface AbstractClipboard: Runnable, ClipboardMonitor { + + val clipConsumer: Consumer } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/clip/ClipboardMonitor.kt b/composeApp/src/commonMain/kotlin/com/clipevery/clip/ClipboardMonitor.kt index 2eee4a539..e1bf4e553 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/clip/ClipboardMonitor.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/clip/ClipboardMonitor.kt @@ -4,6 +4,4 @@ interface ClipboardMonitor { fun start() fun stop() - - fun onChange(event: ClipboardEvent?) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/PlatformClipboard.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/PlatformClipboard.kt new file mode 100644 index 000000000..fd3196c6b --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/PlatformClipboard.kt @@ -0,0 +1,19 @@ +package com.clipevery + +import com.clipevery.clip.AbstractClipboard +import com.clipevery.macos.MacosClipboard +import com.clipevery.platform.currentPlatform +import com.clipevery.windows.WindowsClipboard +import java.awt.datatransfer.Transferable +import java.util.function.Consumer + +fun getClipboard(clipConsumer: Consumer): AbstractClipboard { + val platform = currentPlatform() + return if (platform.name == "Macos") { + MacosClipboard(clipConsumer) + } else if (platform.name == "Windows") { + WindowsClipboard(clipConsumer) + } else { + throw Exception("Unknown platform: ${platform.name}") + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt new file mode 100644 index 000000000..fcf937aad --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt @@ -0,0 +1,77 @@ +package com.clipevery.macos + +import com.clipevery.clip.AbstractClipboard +import java.awt.Toolkit +import java.awt.datatransfer.Clipboard +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + + +class MacosClipboard + (override val clipConsumer: Consumer) : AbstractClipboard { + + private var executor: ScheduledExecutorService? = null + + private var lastTransferable: Transferable? = null + + private var systemClipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard + + override fun run() { + try { + val contents: Transferable? = systemClipboard.getContents(null) + contents?.let { + if (lastTransferable == null) { + clipConsumer.accept(it) + lastTransferable = contents + return + } + + if (contents == lastTransferable) { + println("equals") + return + } + println("not equals") + if (it.isDataFlavorSupported(DataFlavor.stringFlavor)) { + val currentContent = contents.getTransferData(DataFlavor.stringFlavor) as String + val lastContent = lastTransferable?.getTransferData(DataFlavor.stringFlavor) as String? + if (currentContent != lastContent) { + clipConsumer.accept(it) + lastTransferable = contents + } + } + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + } + + override fun start() { + if (executor?.isShutdown != false) { + executor = Executors.newScheduledThreadPool(2) { r -> Thread(r, "Clipboard Monitor") } + } + executor?.scheduleAtFixedRate(this, 0, 300, TimeUnit.MILLISECONDS) + } + + override fun stop() { + executor?.let { + it.shutdown() + + try { + if (!it.awaitTermination(600, TimeUnit.MICROSECONDS)) { + it.shutdownNow() + if (!it.awaitTermination(600, TimeUnit.MICROSECONDS)) { + println("task did not terminate") + } + } + println("stop ") + } catch (ie: InterruptedException) { + Thread.currentThread().interrupt() + it.shutdownNow() + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/windows/WindowsClipboard.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/windows/WindowsClipboard.kt index b5575ffa7..f53fa5db9 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/windows/WindowsClipboard.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/windows/WindowsClipboard.kt @@ -1,8 +1,6 @@ package com.clipevery.windows -import androidx.compose.runtime.MutableState import com.clipevery.clip.AbstractClipboard -import com.clipevery.clip.ClipboardEvent import com.clipevery.windows.api.User32 import com.sun.jna.Pointer import com.sun.jna.platform.win32.Kernel32 @@ -12,17 +10,17 @@ import com.sun.jna.platform.win32.WinDef.WPARAM import com.sun.jna.platform.win32.WinUser.MSG import java.awt.Toolkit import java.awt.datatransfer.Clipboard -import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.Transferable import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import java.util.function.Consumer class WindowsClipboard - (private val copyText: MutableState) : AbstractClipboard, User32.WNDPROC { + (override val clipConsumer: Consumer) : AbstractClipboard, User32.WNDPROC { - private var systemClipboard: Clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() + private var systemClipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard private var executor: ExecutorService? = null private var viewer: HWND? = null @@ -91,17 +89,10 @@ class WindowsClipboard } } - override fun onChange(event: ClipboardEvent?) { + private fun onChange() { val contents: Transferable? = systemClipboard.getContents(null) contents?.let { - if (it.isDataFlavorSupported(DataFlavor.stringFlavor)) { - try { - copyText.value = contents.getTransferData(DataFlavor.stringFlavor).toString() - println(contents.getTransferData(DataFlavor.stringFlavor)) - } catch (e: Exception) { - e.printStackTrace() - } - } + clipConsumer.accept(it) } } @@ -122,7 +113,7 @@ class WindowsClipboard User32.WM_DRAWCLIPBOARD -> { try { - onChange(ClipboardEvent(this)) + onChange() } finally { User32.INSTANCE.SendMessage(nextViewer, uMsg, uParam, lParam) } diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt index 72c1ab11f..30ad58144 100644 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ b/composeApp/src/desktopMain/kotlin/main.kt @@ -5,14 +5,28 @@ import androidx.compose.runtime.remember import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import com.clipevery.ClipeveryApp -import com.clipevery.windows.WindowsClipboard +import com.clipevery.getClipboard +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.util.function.Consumer + fun main() = application { val copyText = remember { mutableStateOf("Hello World!") } - val windowsClipboard = WindowsClipboard(copyText) - windowsClipboard.start() + val consumer = Consumer { + if (it.isDataFlavorSupported(DataFlavor.stringFlavor)) { + try { + copyText.value = it.getTransferData(DataFlavor.stringFlavor).toString() + println(it.getTransferData(DataFlavor.stringFlavor)) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + val clipboard = getClipboard(consumer) + clipboard.start() Window(onCloseRequest = ::exitApplication) { - ClipeveryApp(windowsClipboard, copyText) + ClipeveryApp(clipboard, copyText) } } @@ -20,6 +34,16 @@ fun main() = application { @Composable fun AppDesktopPreview() { val copyText = remember { mutableStateOf("Hello World!") } - val windowsClipboard = WindowsClipboard(copyText) - ClipeveryApp(windowsClipboard, copyText) + val consumer = Consumer { + if (it.isDataFlavorSupported(DataFlavor.stringFlavor)) { + try { + copyText.value = it.getTransferData(DataFlavor.stringFlavor).toString() + println(it.getTransferData(DataFlavor.stringFlavor)) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + val clipboard = getClipboard(consumer) + ClipeveryApp(clipboard, copyText) } \ No newline at end of file From f0dea30ff04123389f39662ee975141bc4accc3d Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Sat, 18 Nov 2023 23:52:52 +0800 Subject: [PATCH 2/2] :sparkles: clipboard listening on mac side --- .../com/clipevery/macos/MacosClipboard.kt | 28 +++++------------- .../clipevery/macos/api/MacClipboardApi.kt | 13 ++++++++ .../darwin-x86-64/libClipboardHelper.dylib | Bin 0 -> 51560 bytes 3 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt create mode 100755 composeApp/src/desktopMain/resources/darwin-x86-64/libClipboardHelper.dylib diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt index fcf937aad..06f95eeff 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/MacosClipboard.kt @@ -1,9 +1,9 @@ package com.clipevery.macos import com.clipevery.clip.AbstractClipboard +import com.clipevery.macos.api.MacClipboard import java.awt.Toolkit import java.awt.datatransfer.Clipboard -import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.Transferable import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -16,32 +16,20 @@ class MacosClipboard private var executor: ScheduledExecutorService? = null - private var lastTransferable: Transferable? = null + private var changeCount = 0 private var systemClipboard: Clipboard = Toolkit.getDefaultToolkit().systemClipboard override fun run() { try { - val contents: Transferable? = systemClipboard.getContents(null) - contents?.let { - if (lastTransferable == null) { - clipConsumer.accept(it) - lastTransferable = contents - return - } - - if (contents == lastTransferable) { - println("equals") + MacClipboard.INSTANCE.clipboardChangeCount.let { currentChangeCount -> + if (changeCount == currentChangeCount) { return } - println("not equals") - if (it.isDataFlavorSupported(DataFlavor.stringFlavor)) { - val currentContent = contents.getTransferData(DataFlavor.stringFlavor) as String - val lastContent = lastTransferable?.getTransferData(DataFlavor.stringFlavor) as String? - if (currentContent != lastContent) { - clipConsumer.accept(it) - lastTransferable = contents - } + changeCount = currentChangeCount + val contents: Transferable? = systemClipboard.getContents(null) + contents?.let { + clipConsumer.accept(it) } } } catch (e: java.lang.Exception) { diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt new file mode 100644 index 000000000..f194412a8 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/macos/api/MacClipboardApi.kt @@ -0,0 +1,13 @@ +package com.clipevery.macos.api + +import com.sun.jna.Library +import com.sun.jna.Native + + +interface MacClipboard : Library { + val clipboardChangeCount: Int + + companion object { + val INSTANCE: MacClipboard = Native.load("ClipboardHelper", MacClipboard::class.java) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/resources/darwin-x86-64/libClipboardHelper.dylib b/composeApp/src/desktopMain/resources/darwin-x86-64/libClipboardHelper.dylib new file mode 100755 index 0000000000000000000000000000000000000000..c5d7b2c278d02147b8159f3b28f7435a26119c00 GIT binary patch literal 51560 zcmeI5Pi$Pp9mi)K2PY*Y3rH1B+u+iqqNuf#rbt!=WzE`V-I%Py+Q=6)dAmEi`_lD$ ztM}f;i59)p6EBYz|)M-EhSq*lr;fkV^Z@6F8m?b`e3 zN)?CpJJP(F?{DV!do!OmGd|7w{V)IduL&V?`-Bj8Q9efb^@BpZEb>$n;#-tI%?Tmp z%KX|&V(Nbs`lT>>KbdXj9#Tf?Qp7c#cEx)_+q<>{>2X`$mglUiUGsg##A803tkz11F&Kcf4^2{by$#o^OV?}o)N3%_pG>?p6M?mjy;o5Ft~1GZots^mmBsSeivGF<(|btC z>%?YWAyPK|omsNpJKiMqO;SPo81rnu^g4 zj_)<{=X5|9g7sH%+orxcj&b55HuZuj2ef5oW1De>svW2XE#qPF^e&{B6fZqb+FQLQ zexr$3Ydc}c5eMVt$)A&*C+l=PYU+!}>!UdTZl9Wj~v-b?v@%EQuc$)C=_UFq9o;rD!Y{e{~vluv!} z&VkQvymd7H@lVJ$NmB@OftY0H7fLyAV56Vu%=n5KDiu55;p zTAz9{O@jT_DDxP!-#bM5$6{J{o?ddRffH;_F9dXIZTP`2!|B;h=ZqUoHLPX&SrvpX zokr6|)28Fa=TmX--C-$Gwr5R6tJYzh*efJN@htHAaclFu* zL!}lY zjc@;bhcc&hncNAbyWRz|bGG-~xt>mZFZB>b$tRJL77l4rKGq^1C-Fo+PVTYNoi*Fz z>nsZs2kCAs5(t0*2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x{QnTp z|HnQrev|zF_`INhO3Nuuo_K%&2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=5Z3HIiO)bhX<^7a%lx;G<2Ou6G!%1tTlC>z@MVTu)v0u*~8zU8LA0MN9 zjPhfY*Mt~*-Ly-;dZ8xQ!)8T!b)l8tiDanS4I=~9R!*pTDso(Jwj24zUVlbK-N1X+ zX?Lkl`N>}tWpQb?Qjy2y*~+pLMyl#NLEXGBC&m3--6vkVOpo|7jpcZF>d|7`?Iit5 zs@+k+$F zWjzs#$%5{8%jk!Vp4z9|Pa5qQJ!ACmj4l}cq0uFybM&D&{<6`hjb1alYV>)d|7di} z=)W1=F*;8Rf#Y2=dco+}=u1YwYBWtp9H9@LH}jn`S{VHlX=>(fNvhH_CknjWlC(w7 zwEm0Mw7_)wMC%EwZ)ttN>Z25#{E3QAPiTG6YN_=%tp1hOcU%2?tq)oKp4RtReM4*W zm4!G;Gokk#vF$Tj>yJ;y)kNE$(-5LIpo0M&59mpX@jc4lr)=vF8BGP*67b!`7E}dGf<5-Puln7MH*uEqS^UM zwBCA0tv;1AXq0