I am trying to learn Kotlin and TornadoFX, and I am working from this repository
The idea currently is that the app will show three rows of 7 buttons. Each button represents a letter which is either known or unknown. If the letter is known, it is displayed on the button. If the letter is unknown, the button displays the index (0-20).
I did this by creating (using Java thinking I'm sure) a global array with 21 elements. Each element starts out null. I defined this in the main app, using the companion construct:
class InteractiveClientApp : App(MainView::class) {
companion object {
var knownLetters = arrayOfNulls<String>(21)
}
Then in the App constructor, I initialized a few random elements with values, just to see if this would work (it doesn't).
override fun init() {
knownLetters[4] = "T"
knownLetters[9] = "G"
knownLetters[17] = "Z"
}
Then in the LettersGridView I use forEachIndexed on the knownLetters array, so I would have access to the element from the array and the index for it.
import jcn.deduce.client.InteractiveClientApp.Companion.knownLetters
class LettersGridView : View() {
override val root = gridpane {
knownLetters.forEachIndexed { index, element ->
if (index == 0 || index % 7 == 0) {
row {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
} else {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
}
}
Whats actually happening is three buttons appear instead of the expected 21, and the value on the button is always the index, never a letter value. Also the indexes shown are 20, 7 and 14. Not the three I used when setting elements in the array. So I am missing something there.
Also I think I am not understanding this bit correctly:
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
What I am trying to say there is "If the element value is not null, use the element value, otherwise use the string value of index. This isn't working, because the buttons only ever have indexes on them, never letters.
I notice if I leave off the .toString() on element, I get an error that button expects String, not string?. Which seems somewhat similar to Java and String vs Optional < String >. However, when I add the toString(), I get an IDE warning that the toString() is redundant.
If I take off the trailing ? altogether, I get a clean compile, but still only three buttons render, and their labels are index, not element.
So I am pretty sure I got lost somewhere along the way, can anyone explain why my program isn't working?
Also, when I am debugging the app, I always wind up with two processes. I don't understand why, but this is what IntelliJ looks like:
Is this normal?
Your init function is working, you can confirm this by changing the initialisation of entry 20, 7 or 14 to a letter and you should see a letter appear when you next run it.
As for your main issue, the reason you are seeing 20, 7 and then 14 is because in this section:
if (index == 0 || index % 7 == 0) {
row {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
You are adding a row with a single button, these buttons will be 0, 7 and 14 (since they are all == 0 % 7). This means you'll only ever add three rows, each with one button on. You might be confused as to why it says 20 instead of 0... Well this is because the next section:
else {
button(element?.toString() ?: index.toString()) {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
Is also adding buttons, but not onto any row (notice how these buttons aren't inside a row { } lambda). This means all these buttons will get added to the gridpane on top of eachother, including that first 0 button. The last button to be added is 20, hence why you see 20, it is covering the 0!
Here's an example of how to approach this problem:
val rowSize = 7
val rows = knownLetters.toList().chunked(rowSize)
rows.forEachIndexed { rowIndex, elements ->
row {
elements.forEachIndexed { buttonIndex, element ->
val displayIndex = rowSize * rowIndex + buttonIndex
button("${element ?: displayIndex}") {
useMaxWidth = true
gridpaneConstraints {
marginBottom = 10.0
}
}
}
}
}
This takes the Kotlin libraries' "chunked" method to divide your 21 size array into three lists of size 7. You can then loop through (you do have to piece back together the display index with this approach) creating a new row for each list (3 lists makes 3 rows), whilst creating your buttons in a nested loop inside the row's lambda.
The key thing to take away here is that not all your buttons are being added within a row { } lambda.
As for the double process issue, I do not have this issue if I run the app using a main method like so:
fun main(args: Array<String>) {
launch<InteractiveClientApp>(args)
}
Hope this helps!
Related
I have a project where we display a graph, this graph is in an Box which is scrollable.
By opening the view, we need to center the root node which causes the problem currently.
Determining the position and setting the values of the states is currently done the following way:
.onGloballyPositioned { coordinates -> scrollBy = coordinates.positionInParent().y - dpstate!!.scrollState.firstVisibleItemScrollOffset
}
.onFocusChanged {
if(it.isFocused ){
print("is focused")
scope.launch { dpstate!!.scrollState.animateScrollBy(scrollBy)
dpstate!!.offsetState.value = Offset(leftX.toFloat(),dpstate!!.offsetState.value.y)
}
}
}
in the modifier of the box.
The state dpstate is an instance of the following:
data class DisplayState(
val scrollState: LazyListState,
val scaleState: MutableState<Float>,
val offsetState: MutableState<Offset>,
val editState: MutableState<Boolean>,
val showInfo:Map<Int, MutableState<Boolean>>,)
Important is, that I need to center by opening it, not by clicking a button.
My try was in the calling code of all of this the following code:
DisposableEffect(Unit){
com.github.tukcps.appel.ui.rendering.focusRequester!!.requestFocus()
onDispose { }
}
There is no exception, it just don't do anything, thanks for your help.
I've made a custom control based around a Canvas. It uses two .pointerInput modifiers, one to detect click and one to detect drag so the user can toggle a column of 50 buttons, either by clicking on one at a time or dragging across a number of them to set them all at once. It works well, and now I'd like to have a horizontally scrollable Row containing a number of these. The immediate problem is that the Row, when the .horizontalScroll modifier is applied, swallows vertical movement as well, and even taps, so although I can scroll through a number of controls I'm no longer able to interact with them.
The only example I can find that's similar is the nested scrolling in the Gestures documentation, but that's between two controls using scrolling, and although the outer control is clearly not preventing the inner control receiving events it's not clear how to apply it in my case.
Without pasting huge quantities of code, I'm defining the Row by
#Composable
fun ScrollBoxes() {
Row(
modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
.horizontalScroll(rememberScrollState())
) {
repeat(20) {
Column {
Text(
"Item $it", modifier = Modifier
.padding(2.dp)
.width(500.dp)
)
JetwashSlide(
model = JetwashSlideViewModel()
)
}
}
}
}
and the modifier of the Canvas is my custom control is set up as
modifier
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { ... }
},
onDragEnd = { ... },
onDrag = { change, dragAmount ->
change.consumeAllChanges();
...
}
}
)
}
.pointerInput(Unit) {
detectTapGestures(
onPress = { it ->
...
}
)
}
A crude approach would be to have a scrollable row of labels and use the presently selected label to determine which full width custom control is presently visible. This wouldn't be as aesthetically pleasing as having the controls scrolling horizontally. Does anyone have an idea how this could be achieved?
Ok, my starting point was this tutorial on how to make a scrolling pager from scratch;
https://fvilarino.medium.com/creating-a-viewpager-in-jetpack-compose-332d6a9181a5
Using that, you can get a horizontally scrolling row of instances of a control. The problem is that I wanted horizontal swipes to be handled by the pager, and taps and vertical swipes to be handled by the custom control, and although the custom control gets the gestures if made over the control, it couldn't pass them up to the pager if they were horizontal swipes, so you could only scroll using the gaps between the custom controls. I expected that the control could use .detectVerticalDragGestures and .detectTapGestures and the pager could use .detectHorizontalDragGestures, but it wasn't that simple.
I ended up pulling code from the Jetpack source into my own code so I could modify it to produce my own gesture detector which captures vertical scroll and tap events but does not capture horizontal scroll;
suspend fun PointerInputScope.detectVerticalDragOrTapGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
onClick: ((Offset) -> Unit) = { },
model: JetwashSlideViewModel
) {
forEachGesture {
awaitPointerEventScope {
model.inhibitGesture = false
val down = awaitFirstDown(requireUnconsumed = false)
//val drag = awaitVerticalTouchSlopOrCancellation(down.id, onVerticalDrag)
val drag = awaitTouchSlopOrCancellation(
down.id
) { change: PointerInputChange, dragAmount: Offset ->
if (abs(dragAmount.y) > abs(dragAmount.x)) {
//This is a real swipe down the slide
change.consumeAllChanges()
onVerticalDrag(change, dragAmount.y)
}
}
if (model.inhibitGesture) {
onDragCancel()
} else {
if (drag != null) {
onDragStart.invoke(drag.position)
if (
verticalDrag(drag.id) {
onVerticalDrag(it, it.positionChange().y)
}
) {
onDragEnd()
} else {
onDragCancel()
}
} else {
//click.
if (!model.inhibitGesture)
onClick(down.position)
}
}
}
}
}
Now in the pager, it was using .horizontalDrag. This is fine if you can actually drag purely horizontally or purely vertically, but you can't and swipes that were intended to be vertical on the inner control often had a tiny bit of horizontal motion which caused the pager to intercept it. So in the pager, I also had to copy and modify some code to make my own .horizontalDrag;
suspend fun AwaitPointerEventScope.horizontalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
motionFromChange = { if (abs(it.positionChangeIgnoreConsumed().x) > 10) it.positionChangeIgnoreConsumed().x else 0f },
motionConsumed = { it.positionChangeConsumed() }
)
This only triggers if the horizontal component of the movement is larger than 10px.
Finally, since a horizontal scroll may also have some element of vertical which could affect the inner controls, in my onPagerScrollStart and onPagerScrollFinished callbacks I set and clear a flag in the model, inhibitGesture which causes the inner control to disregard gestures that it happens to get while the pager is in the process of being scrolled.
I have a SpacingDecoration for my recyclerview which will add some extra spacing after the last item in the list.
Here is my Spacing Decoration
class SpacingDecoration(val context:Context):RecyclerView.ItemDecoration() {
private val twelveDp=getPixelValue(12)
private val seventyDp=getPixelValue(70)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val dataCount=parent.adapter!!.itemCount
val viewPosition=parent.getChildAdapterPosition(view)
outRect.left=twelveDp
outRect.right=twelveDp
when(viewPosition){
0->{
outRect.top=twelveDp
outRect.bottom=twelveDp/2
return
}
dataCount-1 ->{
outRect.top=twelveDp/2
outRect.bottom=seventyDp
return
}
else->{
outRect.top=twelveDp/2
outRect.bottom=twelveDp/2
}
}
}
private fun getPixelValue(Dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
Dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
}
This works nicely.
But the problem starts when a new item is added at the bottom of the list.
I am using DiffUtils to update the list.
When the list updates and adds a new item to the list, it adds the new item after the bottom spacing of seventy Dp.
Hope u understand my problem.
I want to add the last new item so that the spacing between the last item and the item before it reduces to twelveDp.
Please help as I am just a beginner
I found this answer helped: https://stackoverflow.com/a/62002161/9901599
Basically, call RecyclerView.invalidateItemDecorations() once you have refreshed your data. Since I was using ListAdapter, I found that I had to put this line in the callback after the list actually updates, like this:
myAdapter.submitList(newList) {
// this callback runs when the list is updated
myRecyclerView.invalidateItemDecorations()
}
I'm loading JSON data from internet, capturing data about items, like name, author and imageurl. Then I want to load them one under the other so I put them into the listview. I only add them once as a custom class, that only holds those variables.
I'm having a problem with those values duplicating and not appearing as they should. For example, it would load first 5 items(out of 20) and it would repeat them over the remaining 15. I don't understand why that's happening, also tried with looping over listview's items array and printing them out and they are all different, also tried doing refresh() on them, but it doesn't seem to change anything at all.
I'm adding a code that I use to create the listview and the piece that I'm using to fill it in.
val lv = listview<Item>{
anchorpaneConstraints {
topAnchor = 0.0
bottomAnchor = 0.0
leftAnchor = 0.0
rightAnchor = 0.0
}
cellFormat {
graphic = cache {
form {
fieldset {
hbox {
spacing = 10.0
println(it.name)
println(it.author)
println(it.imgurl)
println(it.desc)
imageview {
image = Image(it.imgurl)
prefWidth(256.0)
prefHeight(256.0)
}
vbox {
field("Name") {
label(it.name)
}
field("Author") {
label(it.author)
}
field("Description") {
label {
text = it.desc
wrapWidth = 150
}
}
}
}
}
}
}
}
}
val tmpItems = items.clone() as ArrayList<JsonObject>()
val arr = ArrayList<Item>()
for (m in tmpItems) {
arr.add(
Item(
m["name"].toString(),
m["author"].toString(),
m["desc"].toString(),
m["imgUrl"].toString()
)
)
}
lv.items.addAll(arr)
I expected the output to be 20 unique items, as that's what's in the lv.items, but the shown result is 5 unique items repeated over 20 lines.
When using cache you need to specify a unique id for each item, so the framework knows how to retrieve the cached ui elements for the currently displayed item in a given table cell. This is explained in detail in the javadoc for the cache function.
If you have an id field in your item, you can use that for example:
cache(rowItem.id) { }
You could even use the value for the cell, if that's unique:
cache(it) { }
I am working on a polymer2 shadow dom template project need to select children elements from parent elements. I found this article introduces a way to select child shadow dom elements that like this:
// No fun.
document.querySelector('x-tabs').shadowRoot
.querySelector('x-panel').shadowRoot
.querySelector('#foo');
// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');
However, when I tried in my polymer2 project, like this:
//First: works!!
document.querySelector('container')
.shadowRoot.querySelector('app-grid')
.shadowRoot.querySelector('#apps');
//Second: Doesn't work!// got null
document.querySelector('container::shadow app-grid::shadow #apps')
// Thrird: document.querySelector('* /deep/ #apps') // Doesn't work, got null
I really need the second way or the third, which to put selectors in (), but both couldn't work. Does anyone know why the second one doesn't work? Thank you so much!
::shadow and /deep/ has never(?) worked in Firefox, and is depraved in Chrome 63 and later.
Source
Eric Biedelman has written a nice querySelector method for finding all custom elements on a page using shadow DOM. I wouldn't use it myself, but I have implemented it so I can "querySelect" custom elements in the console. Here is his modified code:
// EXAMPLES
// findCustomElement('app-grid') // Returns app-grid element
// findCustomElements('dom-if') // Returns an array of dom-if elements (if there are several ones)
// findCustomElement('app-grid').props // Returns properties of the app-grid element
function findCustomElement(customElementName) {
const allCustomElements = [];
customElementName = (customElementName) ? customElementName.toLowerCase() : customElementName;
function isCustomElement(el) {
const isAttr = el.getAttribute('is');
// Check for <super-button> and <button is="super-button">.
return el.localName.includes('-') || isAttr && isAttr.includes('-');
}
function findAllCustomElements(nodes) {
for (let i = 0, el; el = nodes[i]; ++i) {
if (isCustomElement(el)) {
el.props = el.__data__ || el.__data || "Doesn't have any properties";
if (customElementName && customElementName === el.tagName.toLowerCase()) {
allCustomElements.push(el);
} else if (!customElementName) {
allCustomElements.push(el);
}
}
// If the element has shadow DOM, dig deeper.
if (el.shadowRoot) {
findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
}
}
}
findAllCustomElements(document.querySelectorAll('*'));
if (allCustomElements.length < 2) {
return allCustomElements[0] || customElementName + " not found";
} else if (customElementName) {
allCustomElements.props = "Several elements found of type " + customElementName;
}
return allCustomElements;
}
Remove the if (isCustomElement(el)) { statement, and you can querySelect whatever element and get an array of it if several of them exists. You can change findAllCustomElements to implement a smarter querySelect using the recursive loop on shadowDoom as base. Again, I wouldn't use this myself – and instead pass on variables from parent element(s) to children where the children have observers that activates specific behaviors – but I wanted to give you a general implementation of a fallback if nothing else works.
The problem with your question is that you don't give any specifics about WHY you want to select the children in the first place.