mirror of
https://invent.kde.org/network/kdeconnect-android.git
synced 2025-12-12 20:35:58 +01:00
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:
committed by
Albert Vaca Cintora
parent
b170ba46d2
commit
0067ad51b1
@@ -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"
|
||||||
|
|||||||
10
res/drawable/ic_draw_24dp.xml
Normal file
10
res/drawable/ic_draw_24dp.xml
Normal 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>
|
||||||
10
res/drawable/ic_finger_24dp.xml
Normal file
10
res/drawable/ic_finger_24dp.xml
Normal 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>
|
||||||
10
res/drawable/ic_open_in_full_24dp.xml
Normal file
10
res/drawable/ic_open_in_full_24dp.xml
Normal 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>
|
||||||
43
res/layout/activity_digitizer.xml
Normal file
43
res/layout/activity_digitizer.xml
Normal 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>
|
||||||
17
res/menu/menu_digitizer.xml
Normal file
17
res/menu/menu_digitizer.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
31
res/xml/digitizer_preferences.xml
Normal file
31
res/xml/digitizer_preferences.xml
Normal 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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/org/kde/kdeconnect/Plugins/DigitizerPlugin/DrawingPadView.kt
Normal file
163
src/org/kde/kdeconnect/Plugins/DigitizerPlugin/DrawingPadView.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/org/kde/kdeconnect/Plugins/DigitizerPlugin/ToolEvent.kt
Normal file
21
src/org/kde/kdeconnect/Plugins/DigitizerPlugin/ToolEvent.kt
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user