Digitizer plugin

Add plugin that allows users to use their devices as pressure-sensitive drawing tablets.

* Schemas MR: https://invent.kde.org/network/kdeconnect-meta/-/merge_requests/16
* KDE MR: https://invent.kde.org/network/kdeconnect-kde/-/merge_requests/862
This commit is contained in:
Martin Sh
2025-11-25 22:28:51 +02:00
committed by Albert Vaca Cintora
parent b170ba46d2
commit 0067ad51b1
14 changed files with 658 additions and 0 deletions

View File

@@ -403,6 +403,18 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" /> android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" />
</activity> </activity>
<activity
android:name="org.kde.kdeconnect.Plugins.DigitizerPlugin.DigitizerActivity"
android:configChanges="orientation|screenSize"
android:label="@string/pref_plugin_digitizer"
android:exported="false"
android:launchMode="singleTop"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity <activity
android:name="org.kde.kdeconnect.UserInterface.TrustedNetworksActivity" android:name="org.kde.kdeconnect.UserInterface.TrustedNetworksActivity"
android:label="@string/trusted_networks" android:label="@string/trusted_networks"

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18.85,10.39l1.06,-1.06c0.78,-0.78 0.78,-2.05 0,-2.83L18.5,5.09c-0.78,-0.78 -2.05,-0.78 -2.83,0l-1.06,1.06L18.85,10.39zM13.19,7.56L4,16.76V21h4.24l9.19,-9.19L13.19,7.56zM19,17.5c0,2.19 -2.54,3.5 -5,3.5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1c1.54,0 3,-0.73 3,-1.5c0,-0.47 -0.48,-0.87 -1.23,-1.2l1.48,-1.48C18.32,15.45 19,16.29 19,17.5zM4.58,13.35C3.61,12.79 3,12.06 3,11c0,-1.8 1.89,-2.63 3.56,-3.36C7.59,7.18 9,6.56 9,6c0,-0.41 -0.78,-1 -2,-1C5.74,5 5.2,5.61 5.17,5.64C4.82,6.05 4.19,6.1 3.77,5.76C3.36,5.42 3.28,4.81 3.62,4.38C3.73,4.24 4.76,3 7,3c2.24,0 4,1.32 4,3c0,1.87 -1.93,2.72 -3.64,3.47C6.42,9.88 5,10.5 5,11c0,0.31 0.43,0.6 1.07,0.86L4.58,13.35z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M398,840q-27,0 -51.5,-11.5T305,796L46,477l26,-25q19,-19 45,-22t47,12l116,81v-403q0,-17 11.5,-28.5T320,80q17,0 28.5,11.5T360,120v557l-111,-78 118,146q6,7 14,11t17,4h282q33,0 56.5,-23.5T760,680v-280q0,-17 11.5,-28.5T800,360q17,0 28.5,11.5T840,400v280q0,66 -47,113t-113,47L398,840ZM520,600ZM440,520v-240q0,-17 11.5,-28.5T480,240q17,0 28.5,11.5T520,280v240h-80ZM600,520v-200q0,-17 11.5,-28.5T640,280q17,0 28.5,11.5T680,320v200h-80Z"
android:fillColor="@android:color/white"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840v-320h80v184l504,-504L520,200v-80h320v320h-80v-184L256,760h184v80L120,840Z" />
</vector>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.kde.kdeconnect.Plugins.DigitizerPlugin.DigitizerActivity">
<include
android:id="@+id/toolbar_layout"
layout="@layout/toolbar" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<org.kde.kdeconnect.Plugins.DigitizerPlugin.DrawingPadView
android:id="@+id/drawing_pad"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/button_draw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/digitizer_draw_button"
android:enabled="false"
android:longClickable="false"
android:src="@drawable/ic_finger_24dp"
tools:layout_gravity="left|bottom" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_fullscreen"
android:icon="@drawable/ic_open_in_full_24dp"
android:title="@string/enable_fullscreen"
kdeconnect:iconTint="?colorOnBackground"
kdeconnect:showAsAction="ifRoom" />
<item
android:id="@+id/menu_open_settings"
android:title="@string/device_menu_plugins"
android:icon="@drawable/ic_settings_24dp"
kdeconnect:iconTint="?colorOnBackground"
kdeconnect:showAsAction="never"/>
</menu>

View File

@@ -9,6 +9,13 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto"> xmlns:kdeconnect="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_open_digitizer"
android:icon="@drawable/ic_draw_24dp"
android:title="@string/use_digitizer"
kdeconnect:iconTint="?colorOnBackground"
kdeconnect:showAsAction="ifRoom" />
<item <item
android:id="@+id/menu_show_keyboard" android:id="@+id/menu_show_keyboard"
android:icon="@drawable/ic_action_keyboard_24dp" android:icon="@drawable/ic_action_keyboard_24dp"

View File

@@ -24,6 +24,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="pref_plugin_battery_desc">Periodically report battery status</string> <string name="pref_plugin_battery_desc">Periodically report battery status</string>
<string name="pref_plugin_connectivity_report">Connectivity report</string> <string name="pref_plugin_connectivity_report">Connectivity report</string>
<string name="pref_plugin_connectivity_report_desc">Report network signal strength and status</string> <string name="pref_plugin_connectivity_report_desc">Report network signal strength and status</string>
<string name="pref_plugin_digitizer">Drawing tablet</string>
<string name="pref_plugin_digitizer_desc">Use your device as a pressure sensitive drawing tablet</string>
<string name="pref_plugin_sftp">Filesystem access</string> <string name="pref_plugin_sftp">Filesystem access</string>
<string name="pref_plugin_sftp_desc">Allows to browse this device\'s filesystem remotely</string> <string name="pref_plugin_sftp_desc">Allows to browse this device\'s filesystem remotely</string>
<string name="pref_plugin_clipboard">Clipboard sync</string> <string name="pref_plugin_clipboard">Clipboard sync</string>
@@ -321,6 +323,29 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="sftp_missing_permission_error">Permissions missing: filesystem access</string> <string name="sftp_missing_permission_error">Permissions missing: filesystem access</string>
<string name="no_players_connected">No players found</string> <string name="no_players_connected">No players found</string>
<string name="send_files">Send files</string> <string name="send_files">Send files</string>
<string name="use_digitizer">Use as drawing tablet</string>
<string name="digitizer_draw_button">Draw using finger</string>
<string name="digitizer_preference_key_hide_draw_button" translatable="false">digitizer_hide_draw_button</string>
<string name="digitizer_preference_title_hide_draw_button">Hide draw button</string>
<string name="digitizer_preference_summary_hide_draw_button">Completely hides the draw button, making finger drawing impossible. Only enable this if your device supports stylus input.</string>
<string name="digitizer_preference_key_draw_button_side" translatable="false">digitizer_draw_button_side</string>
<string name="digitizer_preference_title_draw_button_side">Side of “Draw with finger” button</string>
<string name="digitizer_preference_key_enable_exit_fullscreen_gesture" translatable="false">digitizer_enable_exit_fullscreen_gesture</string>
<string name="digitizer_preference_title_enable_exit_fullscreen_gesture">Enable fullscreen exit gesture</string>
<string name="digitizer_preference_summary_enable_exit_fullscreen_gesture">If enabled, tapping and holding on the drawing pad with your finger will disable fullscreen mode.</string>
<string-array name="digitizer_preference_values_draw_button_side" translatable="false">
<item>top_left</item>
<item>top_right</item>
<item>bottom_left</item>
<item>bottom_right</item>
</string-array>
<string-array name="digitizer_preference_entries_draw_button_side">
<item>Top left</item>
<item>Top right</item>
<item>Bottom left</item>
<item>Bottom right</item>
</string-array>
<string name="digitizer_preference_value_default_draw_button_side" translatable="false">bottom_left</string>
<string name="block_notification_contents">Block notification contents</string> <string name="block_notification_contents">Block notification contents</string>
<string name="block_notification_images">Block notification images</string> <string name="block_notification_images">Block notification images</string>
@@ -609,4 +634,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="device_host_invalid">Host is invalid. Use a valid hostname, IPv4, or IPv6</string> <string name="device_host_invalid">Host is invalid. Use a valid hostname, IPv4, or IPv6</string>
<string name="device_host_duplicate">Host already exists in the list</string> <string name="device_host_duplicate">Host already exists in the list</string>
<string name="enable_fullscreen">Enable fullscreen</string>
</resources> </resources>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:keep="@xml/digitizer_preferences">
<SwitchPreference
android:id="@+id/digitizer_preference_key_hide_draw_button"
android:defaultValue="false"
android:key="@string/digitizer_preference_key_hide_draw_button"
android:title="@string/digitizer_preference_title_hide_draw_button"
android:summary="@string/digitizer_preference_summary_hide_draw_button"/>
<ListPreference
android:id="@+id/digitizer_preference_key_draw_button_side"
android:defaultValue="@string/digitizer_preference_value_default_draw_button_side"
android:entries="@array/digitizer_preference_entries_draw_button_side"
android:entryValues="@array/digitizer_preference_values_draw_button_side"
android:key="@string/digitizer_preference_key_draw_button_side"
android:summary="%s"
android:title="@string/digitizer_preference_title_draw_button_side" />
</androidx.preference.PreferenceScreen>

View File

@@ -0,0 +1,187 @@
/*
* SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.DigitizerPlugin
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.core.content.edit
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.preference.PreferenceManager
import org.kde.kdeconnect.KdeConnect
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity
import org.kde.kdeconnect.base.BaseActivity
import org.kde.kdeconnect.extensions.viewBinding
import org.kde.kdeconnect_tp.R
import org.kde.kdeconnect_tp.databinding.ActivityDigitizerBinding
import kotlin.math.roundToInt
class DigitizerActivity : BaseActivity<ActivityDigitizerBinding>(), DrawingPadView.EventListener {
override val binding: ActivityDigitizerBinding by viewBinding(ActivityDigitizerBinding::inflate)
private lateinit var prefs: SharedPreferences
private lateinit var fullscreenBackCallback: OnBackPressedCallback
private lateinit var deviceId: String
private val plugin: DigitizerPlugin?
get() {
val plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, DigitizerPlugin::class.java)
if (plugin == null)
finish()
return plugin
}
private var prefHideDrawButton: Boolean
get() = prefs.getBoolean(getString(R.string.digitizer_preference_key_hide_draw_button), false)
set(value) = prefs.edit {
putBoolean(getString(R.string.digitizer_preference_key_hide_draw_button), value)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
deviceId = intent.getStringExtra("deviceId")!!
setSupportActionBar(binding.toolbarLayout.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setDisplayShowHomeEnabled(true)
prefs = PreferenceManager.getDefaultSharedPreferences(this)
binding.drawingPad.eventListener = this
@SuppressLint("ClickableViewAccessibility")
binding.buttonDraw.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
binding.drawingPad.fingerTouchEventsEnabled = true
true
}
MotionEvent.ACTION_UP -> {
binding.drawingPad.fingerTouchEventsEnabled = false
true
}
else -> false
}
}
fullscreenBackCallback = onBackPressedDispatcher.addCallback(this) {
disableFullscreen()
}
}
private fun enableFullscreen() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
supportActionBar!!.hide()
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
fullscreenBackCallback.isEnabled = true
}
private fun disableFullscreen() {
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
supportActionBar!!.show()
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
fullscreenBackCallback.isEnabled = false
}
override fun onSupportNavigateUp(): Boolean {
onBackPressedDispatcher.onBackPressed()
return true
}
override fun onStart() {
super.onStart()
// During onStart, the views aren't laid out yet.
// We must wait a frame for the view to get laid out before we can query its size.
binding.drawingPad.post {
plugin?.startSession(
binding.drawingPad.width,
binding.drawingPad.height,
(resources.displayMetrics.xdpi * INCHES_TO_MM).roundToInt(),
(resources.displayMetrics.ydpi * INCHES_TO_MM).roundToInt()
)
}
if (prefHideDrawButton) {
binding.buttonDraw.visibility = View.GONE
}
binding.buttonDraw.layoutParams = (binding.buttonDraw.layoutParams as FrameLayout.LayoutParams).also {
@SuppressLint("RtlHardcoded")
when (prefs.getString(getString(R.string.digitizer_preference_key_draw_button_side), "bottom_left")) {
"top_left" -> it.gravity = Gravity.TOP or Gravity.LEFT
"top_right" -> it.gravity = Gravity.TOP or Gravity.RIGHT
"bottom_left" -> it.gravity = Gravity.BOTTOM or Gravity.LEFT
"bottom_right" -> it.gravity = Gravity.BOTTOM or Gravity.RIGHT
}
}
}
override fun onStop() {
super.onStop()
plugin?.endSession()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_digitizer, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_fullscreen -> {
enableFullscreen()
true
}
R.id.menu_open_settings -> {
startActivity(
Intent(this, PluginSettingsActivity::class.java)
.putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId)
.putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, DigitizerPlugin::class.java.getSimpleName())
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onToolEvent(event: ToolEvent) {
plugin?.reportEvent(event)
}
override fun onFingerTouchEvent(touching: Boolean) {
binding.buttonDraw.isEnabled = touching
}
companion object {
private const val TAG = "DigitizerActivity"
private const val INCHES_TO_MM = 0.0393701
}
}

View File

@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.DigitizerPlugin
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.util.Log
import androidx.preference.PreferenceManager
import org.kde.kdeconnect.NetworkPacket
import org.kde.kdeconnect.Plugins.Plugin
import org.kde.kdeconnect.Plugins.PluginFactory
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment
import org.kde.kdeconnect_tp.R
@PluginFactory.LoadablePlugin
class DigitizerPlugin : Plugin() {
override val displayName: String
get() = context.resources.getString(R.string.pref_plugin_digitizer)
override val description: String
get() = context.resources.getString(R.string.pref_plugin_digitizer_desc)
override val icon: Int
get() = R.drawable.ic_draw_24dp
override fun onPacketReceived(np: NetworkPacket): Boolean {
Log.e(TAG, "The drawing tablet plugin should not be able to receive any packets!")
return false
}
override val actionName: String
get() = context.getString(R.string.use_digitizer)
override fun displayAsButton(context: Context): Boolean = shouldDisplayAsBigButton(context)
override fun startMainActivity(parentActivity: Activity) {
parentActivity.startActivity(Intent(parentActivity, DigitizerActivity::class.java).apply {
putExtra("deviceId", device.deviceId)
})
}
fun startSession(width: Int, height: Int, resolutionX: Int, resolutionY: Int) {
val np = NetworkPacket(PACKET_TYPE_DIGITIZER_SESSION).apply {
set("action", "start")
set("width", width)
set("height", height)
set("resolutionX", resolutionX)
set("resolutionY", resolutionY)
}
device.sendPacket(np)
}
fun endSession() {
val np = NetworkPacket(PACKET_TYPE_DIGITIZER_SESSION).apply {
set("action", "end")
}
device.sendPacket(np)
}
fun reportEvent(event: ToolEvent) {
Log.d(TAG, "reportEvent: $event")
val np = NetworkPacket(PACKET_TYPE_DIGITIZER).also { packet ->
event.active?.let { packet["active"] = it }
event.touching?.let { packet["touching"] = it }
event.tool?.let { packet["tool"] = it.name }
event.x?.let { packet["x"] = it }
event.y?.let { packet["y"] = it }
event.pressure?.let { packet["pressure"] = it }
}
device.sendPacket(np)
}
override fun hasSettings(): Boolean = true
override fun getSettingsFragment(activity: Activity): PluginSettingsFragment =
PluginSettingsFragment.newInstance(pluginKey, R.xml.digitizer_preferences)
override val supportedPacketTypes: Array<String>
get() = arrayOf()
override val outgoingPacketTypes: Array<String>
get() = arrayOf(
PACKET_TYPE_DIGITIZER_SESSION,
PACKET_TYPE_DIGITIZER,
)
companion object {
private const val PACKET_TYPE_DIGITIZER_SESSION = "kdeconnect.digitizer.session"
private const val PACKET_TYPE_DIGITIZER = "kdeconnect.digitizer"
private const val TAG = "DigitizerPlugin"
@JvmStatic
fun shouldDisplayAsBigButton(context: Context): Boolean =
context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE
}
}

View File

@@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.DigitizerPlugin
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import kotlin.math.ceil
import kotlin.math.roundToInt
class DrawingPadView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private fun mmToPixels(mm: Float): Float =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_MM,
mm,
resources.displayMetrics
)
val gridSpacing = mmToPixels(5.0f)
val gridOffset = mmToPixels(2.5f)
val backgroundPaint = Paint().apply {
color = MaterialColors.getColor(this@DrawingPadView, R.attr.colorSurfaceContainer, Color.WHITE)
style = Paint.Style.FILL
}
val linePaint = Paint().apply {
color = MaterialColors.getColor(this@DrawingPadView, R.attr.colorOutlineVariant, Color.WHITE)
style = Paint.Style.STROKE
strokeWidth = 1.0f
}
var eventListener: EventListener? = null
var fingerTouchEventsEnabled: Boolean = false
private fun convertTool(motionEventTool: Int): ToolEvent.Tool =
when (motionEventTool) {
MotionEvent.TOOL_TYPE_ERASER -> ToolEvent.Tool.Rubber
else -> ToolEvent.Tool.Pen
}
override fun onHoverEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_HOVER_ENTER -> eventListener?.onToolEvent(
ToolEvent(
active = true,
x = event.x.roundToInt(),
y = event.y.roundToInt()
)
)
MotionEvent.ACTION_HOVER_EXIT -> eventListener?.onToolEvent(
ToolEvent(
active = false,
)
)
MotionEvent.ACTION_HOVER_MOVE -> eventListener?.onToolEvent(
ToolEvent(
tool = convertTool(event.getToolType(0)),
x = event.x.roundToInt(),
y = event.y.roundToInt()
)
)
}
return true
}
private fun findPressure(event: MotionEvent): Double =
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER)
// We report constant 1.0 pressure for fingers, since most devices can't report
// pressure for fingers (so they report a constant 0.001).
if (fingerTouchEventsEnabled) 1.0
else 0.0
else
event.pressure.toDouble()
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
val toolType = event.getToolType(0)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
eventListener?.onToolEvent(
ToolEvent(
active = true,
touching = toolType != MotionEvent.TOOL_TYPE_FINGER,
tool = convertTool(toolType),
x = event.x.roundToInt(),
y = event.y.roundToInt(),
pressure = findPressure(event)
)
)
if (toolType == MotionEvent.TOOL_TYPE_FINGER)
eventListener?.onFingerTouchEvent(true)
}
MotionEvent.ACTION_UP -> {
eventListener?.onToolEvent(
ToolEvent(
// If the finger is lifted from the screen,
// we consider the device as "not tracking the tool".
active = toolType != MotionEvent.TOOL_TYPE_FINGER,
touching = false,
)
)
if (toolType == MotionEvent.TOOL_TYPE_FINGER)
eventListener?.onFingerTouchEvent(false)
// Set this variable to `false` if the user stopped drawing without first letting go
// of the "draw" button.
fingerTouchEventsEnabled = false
}
MotionEvent.ACTION_MOVE -> eventListener?.onToolEvent(
ToolEvent(
tool = convertTool(toolType),
x = event.x.roundToInt(),
y = event.y.roundToInt(),
pressure = findPressure(event)
)
)
}
return true
}
override fun onDraw(canvas: Canvas) {
canvas.drawPaint(backgroundPaint)
for (i in 0..<ceil(width / gridSpacing).toInt()) {
val x = gridOffset + i * gridSpacing
canvas.drawLine(x, 0f, x, height.toFloat(), linePaint)
}
for (i in 0..<ceil(height / gridSpacing).toInt()) {
val y = gridOffset + i * gridSpacing
canvas.drawLine(0f, y, width.toFloat(), y, linePaint)
}
}
interface EventListener {
fun onToolEvent(event: ToolEvent)
fun onFingerTouchEvent(touching: Boolean)
}
companion object {
const val TAG = "DrawingPadView"
}
}

View File

@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2025 Martin Sh <hemisputnik@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.DigitizerPlugin
data class ToolEvent(
val active: Boolean? = null,
val touching: Boolean? = null,
val tool: Tool? = null,
val x: Int? = null,
val y: Int? = null,
val pressure: Double? = null
) {
enum class Tool {
Pen,
Rubber,
}
}

View File

@@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.kde.kdeconnect.KdeConnect; import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.Plugins.DigitizerPlugin.DigitizerPlugin;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect.base.BaseActivity; import org.kde.kdeconnect.base.BaseActivity;
import org.kde.kdeconnect_tp.R; import org.kde.kdeconnect_tp.R;
@@ -231,8 +232,14 @@ public class MousePadActivity
boolean mouseButtonsEnabled = prefs boolean mouseButtonsEnabled = prefs
.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true); .getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true);
boolean digitizerEnabled =
KdeConnect.getInstance().getDevicePlugin(deviceId, DigitizerPlugin.class) != null;
menu.findItem(R.id.menu_right_click).setVisible(!mouseButtonsEnabled); menu.findItem(R.id.menu_right_click).setVisible(!mouseButtonsEnabled);
menu.findItem(R.id.menu_middle_click).setVisible(!mouseButtonsEnabled); menu.findItem(R.id.menu_middle_click).setVisible(!mouseButtonsEnabled);
menu.findItem(R.id.menu_open_digitizer).setVisible(
digitizerEnabled && !DigitizerPlugin.shouldDisplayAsBigButton(this)
);
return true; return true;
} }
@@ -251,6 +258,16 @@ public class MousePadActivity
.putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId) .putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId)
.putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, MousePadPlugin.class.getSimpleName()); .putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, MousePadPlugin.class.getSimpleName());
startActivity(intent); startActivity(intent);
return true;
} else if (id == R.id.menu_open_digitizer) {
DigitizerPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, DigitizerPlugin.class);
if (plugin == null) {
finish();
return true;
}
plugin.startMainActivity(this);
return true; return true;
} else if (id == R.id.menu_show_keyboard) { } else if (id == R.id.menu_show_keyboard) {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);