Skip to content

Commit

Permalink
Changed: Removed experimental RecyclerView support, added options to …
Browse files Browse the repository at this point in the history
…make links in TextViews clickable and text selectable.
  • Loading branch information
tareksander committed Jan 7, 2022
1 parent ba4975f commit 6c05430
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 349 deletions.
4 changes: 3 additions & 1 deletion Protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ Due to Android limitations, methods that return a value fail when the Activity i
- parent: The View id of the parent in the Layout hierarchy. if not specified, this will replace the root of the hierarchy and delete all existing views.
- aid: The id of the Activity in which to create the View.
- text: For Button, TextView and EditText, this is the initial Text.
- selectableText: For TextViews, this specifies whether the text can be selected. Default is false.
- clickableLinks: For TextViews, this specifies whether links can be clicked or not. Default is false.
- vertical: For LinearLayout, this specifies if the Layout is vertical or horizontal. If not specified, vertical is assumed.
- snapping: NestedScrollView and HorizontalScrollView snap to the nearest item if this is set to true. Default is false.
- fillviewport: Makes the child of a HorizontalScrollView or a NestedScrollView automatically expand to the ScrollView size. Default is false.
Expand Down Expand Up @@ -450,7 +452,7 @@ The additional values are:
- action: one of "up", "down", "pointer_up", "pointer_down", "cancel", "move", corresponding to [MotionEvent values](https://developer.android.com/reference/android/view/MotionEvent#constants_1) for ACTION_DOWN etc.
- index: for "pointer_up" and "pointer_down" this is the index of the pointer removed/added.
- time: The time of the event in milliseconds since boot excluding sleep. Use this when checking for gestures or other time-sensitive things.
- pointers: An array of pointer data objects, each containing:
- pointers: An array of arrays of pointer data objects. The first dimension is for grouping multiple move events together. The second dimension is for the pointer positions in each event. The elements in the array are:
- x, y: The coordinates of the pointer inside the View (not in the window). For ImageView, these are the coordinates of the pixel in the displayed image or buffer, so you don't need to convert the position yourself.
- id: The pointer id. This stays consistent for each pointer in the frame between "up" and "down" events

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ Releases on f-droid will be provided as soon as possible. When there is a releas
Protocol.md describes the Protocol used and the available functions you can use.
If you want to use overlay windows or be able to open windows from the background, go into the app settings for Termux:GUI, open the advanced section and enable "Display over other apps".

### Comparison with native apps

| Native app | With Termux:GUI |
|-----------------------------------------------------------|-----------------------------------------------------------------------------|
| Has to be installed | Program can be run in Termux |
| Full access to the Android API | Access to the Android API through Termux:GUI and Termux:API |
| Limited to C, C++, Kotlin and Java for native development | Any programming language can be used, prebuilt library for python available |
| | Lower performance caused by IPC |
| Accessing files in Termux only possible via SAF | Direct access to files in Termux |
| Has to be started with `am` from Termux | Can be started like any other program in Termux |
| | Can receive command line arguments and output back to the Terminal |


## Language Bindings

- [Python](https://github.com/tareksander/termux-gui-python-bindings)
Expand Down
6 changes: 5 additions & 1 deletion TODO
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Roadmap:
[x] timezone
[x] locale
[x] TabLayout
[x] add the ability to make text selectable in a TextView and hyperlinks clickable
[x] https://stackoverflow.com/questions/6025818/select-copy-text-in-a-textview
[x] https://stackoverflow.com/questions/2734270/how-to-make-links-in-a-textview-clickable
[ ] update the f-droid metadata
[x] write a changelog
[ ] more screenshots
Expand All @@ -51,6 +54,7 @@ Roadmap:
[ ] Rework the widgets
[ ] option to set an executable as a onClick handler in widgets, that then gets started sa a Termux task. That should work even when no program is connected to listen to widget events.
[ ] add everything to the python library
[ ] add the ability to intercept touch events in layouts
[ ] add a small helper script to termux-gui-packages, that can be used as a daemon for small stateful widgets.
[ ] registers itself to start at boot with Termux:Boot, so the widget state can be restored at startup
[ ] watches with inotify on ~/.image-shortcuts
Expand Down Expand Up @@ -88,7 +92,7 @@ Roadmap:
the methods should improve performance
[ ] You should also be able to replace the arguments with arrays, in case you want that argument to have a different value for each view, like the text for TextViews
[ ] think about creating a batch api to group multiple messages together into one runnable for the ui thread
[ ]
[ ] add the binary protocol type using protobuf



Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/termux/gui/ConnectionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ConnectionHandler(private val request: GUIService.ConnectionRequest, val s


override fun run() {
println("Socket address: " + request.mainSocket)
//println("Socket address: " + request.mainSocket)

val main = LocalSocket(LocalSocket.SOCKET_STREAM)
val event = LocalSocket(LocalSocket.SOCKET_STREAM)
Expand Down Expand Up @@ -92,7 +92,7 @@ class ConnectionHandler(private val request: GUIService.ConnectionRequest, val s
}
}
eventWorker!!.start()
println("listening")
//println("listening")
when (pversion) {
0 -> V0(app).handleConnection(ptype, main, eventQueue)
}
Expand Down
125 changes: 2 additions & 123 deletions app/src/main/java/com/termux/gui/GUIActivity.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
package com.termux.gui

import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.drawable.ColorDrawable
import android.hardware.HardwareBuffer
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.gson.JsonElement
import com.termux.gui.protocol.v0.GUIRecyclerViewAdapter
import java.io.Serializable
import java.util.*
import java.util.concurrent.LinkedBlockingQueue
Expand All @@ -25,17 +14,11 @@ import kotlin.collections.HashMap
open class GUIActivity : AppCompatActivity() {


// a Tree to store the view hierarchy. The existence of Views isn't stored by android itself
private data class Node(val parent: Node?, val id: Int, val claz: Class<out View>, val children: LinkedList<Node> = LinkedList<Node>()) : Serializable



companion object {
private const val THEME_KEY = "tgui_heme"
private const val IDS_KEY = "gui_used_ids"
private const val VIEWS_KEY = "gui_views"
private const val THEME_KEY = "gui_theme"
private const val DATA_KEY = "gui_data"
private const val RECYCLERVIEWS_KEY = "rec_data"
}
val usedIds: TreeSet<Int> = TreeSet()
init {
Expand All @@ -56,8 +39,6 @@ open class GUIActivity : AppCompatActivity() {
data class GUITheme(val statusBarColor: Int, val colorPrimary: Int, var windowBackground: Int, val textColor: Int, val colorAccent: Int) : Serializable
var eventQueue : LinkedBlockingQueue<ConnectionHandler.Event>? = null

val recyclerviews = HashMap<Int, GUIRecyclerViewAdapter>()

@Suppress("UNCHECKED_CAST")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -72,38 +53,10 @@ open class GUIActivity : AppCompatActivity() {
setContentView(R.layout.activity_gui)
if (savedInstanceState != null) {
theme = savedInstanceState.getSerializable(THEME_KEY) as? GUITheme?
/*
val ids = savedInstanceState.getSerializable(IDS_KEY) as? TreeSet<*>
if (ids != null) {
usedIds.clear()
val l = ids.filterIsInstance(Int::class.java)
for (id in l) {
usedIds.add(id)
}
}
*/
val d = savedInstanceState.getSerializable(DATA_KEY) as? ActivityData
if (d != null) {
data = d
}
// Disabled the view saving and restoring for now, because it's unstable
/*
val tree = savedInstanceState.getSerializable(VIEWS_KEY) as? Node
if (tree != null) {
findViewById<FrameLayout>(R.id.root).addView(buildHierarchyFromTree(tree))
}
val recs = savedInstanceState.getSerializable(RECYCLERVIEWS_KEY) as? HashMap<Int, Pair<LinkedList<GUIRecyclerViewAdapter.ViewData>, Pair<LinkedList<Node>, LinkedList<SparseArray<Parcelable>>>>>
if (recs != null) {
for (r in recs) {
val rec = GUIRecyclerViewAdapter(this)
for (i in 0 until r.value.first.size) {
r.value.first[i].v = buildHierarchyFromTree(r.value.second.first[i])
r.value.first[i].v.restoreHierarchyState(r.value.second.second[i])
}
rec.importViewList(r.value.first)
}
}
*/
}
}

Expand Down Expand Up @@ -137,32 +90,6 @@ open class GUIActivity : AppCompatActivity() {
super.onConfigurationChanged(newConfig)
eventQueue?.offer(ConnectionHandler.Event("config", configToJson(newConfig)))
}


private fun buildHierarchyFromTree(tree: Node): View {
val v = tree.claz.getConstructor(Context::class.java).newInstance(this)
v.id = tree.id
if (v is ViewGroup) {
for (n in tree.children) {
v.addView(buildHierarchyFromTree(n))
}
}

if (v is Button || v is CheckBox || v is SwitchCompat || v is ToggleButton) {
eventQueue?.let { Util.setClickListener(v, data.toString(), true, it) }
}
if (v is RadioGroup) {
eventQueue?.let { Util.setCheckedListener(v, data.toString(), it) }
}
if (v is Spinner) {
eventQueue?.let { Util.setSpinnerListener(v, data.toString(), it) }
}
if (v is SwipeRefreshLayout) {
eventQueue?.let { Util.setRefreshListener(v, data.toString(), it) }
}

return v
}

@Suppress("DEPRECATION")
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
Expand Down Expand Up @@ -192,60 +119,12 @@ open class GUIActivity : AppCompatActivity() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable(THEME_KEY, theme)
/*
outState.putSerializable(IDS_KEY, usedIds)
val root = findViewById<FrameLayout>(R.id.root).getChildAt(0)
if (root != null) {
val tree = createViewTree(root, null)
outState.putSerializable(VIEWS_KEY, tree)
}
val recs = HashMap<Int, Pair<LinkedList<GUIRecyclerViewAdapter.ViewData>, Pair<LinkedList<Node>, LinkedList<SparseArray<Parcelable>>>>>()
for (r in recyclerviews) {
val l = r.value.exportViewList()
val n = LinkedList<Node>()
val pl = LinkedList<SparseArray<Parcelable>>()
for (v in l) {
val p = SparseArray<Parcelable>()
val view = v.v
n.add(createViewTree(view, null))
view.saveHierarchyState(p)
pl.add(p)
}
recs[r.key] = Pair(l, Pair(n, pl))
}
outState.putSerializable(RECYCLERVIEWS_KEY, recs)
*/
outState.putSerializable(DATA_KEY, data)
}

private fun createViewTree(start: View, parent: Node?) : Node {
val children = LinkedList<Node>()
if (start !is ViewGroup) {
return Node(parent, start.id, start::class.java, children)
}
val tree = Node(parent, start.id, start::class.java, children)
for (i in 0 until start.childCount) {
val c = start.getChildAt(i)
if (Class.forName("androidx.swiperefreshlayout.widget.CircleImageView").isInstance(c)) {
continue
}
children.add(createViewTree(c, tree))
}
return tree
}

@Suppress("UNCHECKED_CAST")
fun <T> findViewReimplemented(id: Int, recid: Int?, recindex: Int?) : T? {
if (recid != null && recindex != null) {
val rec = recyclerviews[recid]
if (rec != null) {
val el = rec.getViewByIndex(recindex)
if (el != null) {
return el.v.findViewById<View>(id) as? T
}
}
return null
}
fun <T> findViewReimplemented(id: Int) : T? {
return findViewById(id)
}

Expand Down
20 changes: 5 additions & 15 deletions app/src/main/java/com/termux/gui/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.tabs.TabLayout
import com.termux.gui.protocol.v0.GUIRecyclerViewAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -109,11 +107,11 @@ class Util {
if (t != null) {
v.setBackgroundColor(t.windowBackground)
}
val fl = a.findViewReimplemented(R.id.root, recid, recindex) as? FrameLayout
val fl = a.findViewReimplemented(R.id.root) as? FrameLayout
fl?.removeAllViews()
fl?.addView(v)
} else {
val g = a.findViewReimplemented<ViewGroup>(parent, recid, recindex)
val g = a.findViewReimplemented<ViewGroup>(parent)
if (g is LinearLayout) {
val p = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1F)
if (g.orientation == LinearLayout.VERTICAL) {
Expand Down Expand Up @@ -322,19 +320,11 @@ class Util {
}


fun removeViewRecursive(v: View?, usedIds: MutableSet<Int>, recyclerviews: HashMap<Int, GUIRecyclerViewAdapter>) {
fun removeViewRecursive(v: View?, usedIds: MutableSet<Int>) {
if (v != null) {
if (v is ViewGroup) {
if (v is RecyclerView) {
val rv = recyclerviews[v.id]
if (rv != null) {
usedIds.removeAll(rv.exportViewList().map { it.id }.toSet())
recyclerviews.remove(v.id)
}
} else {
while (v.childCount > 0) {
removeViewRecursive(v.getChildAt(0), usedIds, recyclerviews)
}
while (v.childCount > 0) {
removeViewRecursive(v.getChildAt(0), usedIds)
}
}
val p = v.parent
Expand Down
Loading

0 comments on commit 6c05430

Please sign in to comment.