TornadoFX JavaFX Sync Scroll across tableviews - javafx

I am trying to synchronise scrolls across the tableviews. (Both Horizontal & Vertical)
The SyncScrollEx View has two tableView which is basically one Fragment placed side by side, with same dataset, and hence same table size layout.
Expected Behaviour: When I scroll on one tableview, the other tableview's scrollbar should also scroll for the same amount.
Below is my current progress:
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.scene.control.ScrollBar
import tornadofx.*
class SyncScrollEx : View() {
override val root = hbox {
setPrefSize(300.0, 150.0)
this += find<MyTableFrag>()
this += find<MyTableFrag>()
}
}
class MyTableFrag : Fragment() {
var addEventOnlyOnceFlag = false
val persons = FXCollections.observableArrayList<GameWarrior>(
GameWarrior(1,"Tyrion Lannister", "M"),
GameWarrior(2,"Ned Stark", "M"),
GameWarrior(3,"Sansa Stark", "F"),
GameWarrior(4,"Daenerys Targaryen", "F"),
GameWarrior(5,"Bran Stark", "M"),
GameWarrior(6,"Jon Snow", "M"),
GameWarrior(7,"Arya Stark", "F")
)
override val root = vbox {
tableview(persons) {
column("ID", GameWarrior::idProperty)
column("Name", GameWarrior::nameProperty)
column("Gender", GameWarrior::genderProperty)
subscribe<SyncScrollEvent> { event ->
//Sync the ScrollX & ScrollY of both the tables
event.node.value = event.newVal.toDouble()
}
//Hack, need to initialize this when the table/scroll is rendered
setOnMouseEntered {
//Hack for not triggering the lookupAll event on every mouse enter
if (!addEventOnlyOnceFlag) {
addEventOnlyOnceFlag = true
//INFO: Look up for the scroll bars in tableView and add a listener
this.lookupAll(".scroll-bar").map { node ->
if (node is ScrollBar) {
node.valueProperty().addListener {
value, oldValue, newValue ->
println(node.orientation.toString() + " " + newValue)
fire(SyncScrollEvent(node, newValue))
}
}
}
}
}
}
}
}
class GameWarrior(id: Int, name: String, gender: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val genderProperty = SimpleStringProperty(gender)
var gender by genderProperty
}
class SyncScrollEvent(val node: ScrollBar, val newVal: Number) : FXEvent()
The comments highlight the problems I am facing.
Also, I fail to understand how the "subscribe" will get invoked for both the tableviews in such scenario where Fire() happens inside a EventListener

First we need clean access to the scrollbars. When the TableView is assigned it's skin, the scrollbars will be available. We'll create a map keyed on orientation to keep track of them:
val scrollbars = HashMap<Orientation, ScrollBar>()
Once the skin is available we look up the scrollbars and assign them to our map and listen for changes so we can fire the event
skinProperty().onChange {
this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
scrollbars[bar.orientation] = bar
bar.valueProperty().onChange {
fire(SyncScrollEvent(bar, this))
}
}
}
We don't need the position in the event, since we can query the scrollbar for it's value, but it's easier to filter out the events if we add the source TableView. The SyncScrollEvent now looks like this:
class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()
Let's listen for the scroll events and make sure we only change our scrollbar value if the event originates from the other tableview, for the corresponding orientation:
subscribe<SyncScrollEvent> { event ->
if (event.table != this)
scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
}
For completeness, here is the whole modified app:
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.geometry.Orientation
import javafx.scene.control.ScrollBar
import javafx.scene.control.TableView
import tornadofx.*
import java.util.*
class SyncScrollEx : View() {
override val root = hbox {
setPrefSize(300.0, 150.0)
add(MyTableFrag::class)
add(MyTableFrag::class)
}
}
class MyTableFrag : Fragment() {
val persons = FXCollections.observableArrayList<GameWarrior>(
GameWarrior(1, "Tyrion Lannister", "M"),
GameWarrior(2, "Ned Stark", "M"),
GameWarrior(3, "Sansa Stark", "F"),
GameWarrior(4, "Daenerys Targaryen", "F"),
GameWarrior(5, "Bran Stark", "M"),
GameWarrior(6, "Jon Snow", "M"),
GameWarrior(7, "Arya Stark", "F")
)
val scrollbars = HashMap<Orientation, ScrollBar>()
override val root = vbox {
tableview(persons) {
column("ID", GameWarrior::idProperty)
column("Name", GameWarrior::nameProperty)
column("Gender", GameWarrior::genderProperty)
subscribe<SyncScrollEvent> { event ->
if (event.table != this)
scrollbars[event.scrollbar.orientation]?.value = event.scrollbar.value
}
skinProperty().onChange {
this.lookupAll(".scroll-bar").map { it as ScrollBar }.forEach { bar ->
scrollbars[bar.orientation] = bar
bar.valueProperty().onChange {
fire(SyncScrollEvent(bar, this))
}
}
}
}
}
}
class GameWarrior(id: Int, name: String, gender: String) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val genderProperty = SimpleStringProperty(gender)
var gender by genderProperty
}
class SyncScrollEvent(val scrollbar: ScrollBar, val table: TableView<*>) : FXEvent()

Related

Horizontal Scrolling is not working in RowsSupportFragment Android Leanback?

I am trying to implement a rowsSupportFragment using Android Leanback for android tv.
But the vertical Scrolling is not working is Keeps glitching.
class DetailsRowListFragment: RowsSupportFragment() {
private var rootAdapter : ArrayObjectAdapter = ArrayObjectAdapter(
ListRowPresenter(
FocusHighlight.ZOOM_FACTOR_MEDIUM,true)
)
private var episodeItemSelectedListener: ((Episode)->Unit)?= null
private var relatedItemSelectedListener: ((Relation)->Unit)?= null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//setting up adapter from within the class
adapter = rootAdapter
//Setting up listener
onItemViewSelectedListener = ItemViewSelectedListener()
}
fun bindData(animeData : AnimeDetails ){
val episodeObjectAdapter = ArrayObjectAdapter(EpisodePresenter())
animeData.episodes.forEach { it->
episodeObjectAdapter.add(it)
}
val episodeHeaderItem= HeaderItem("Episodes")
val listRow = ListRow(episodeHeaderItem,episodeObjectAdapter)
rootAdapter.add(listRow)
}
fun setOnContentSelectedListener(listener: (Episode)->Unit){
this.episodeItemSelectedListener=listener
}
private fun setRelatedRow(itemList: List<Relation>){
val relatedObjectAdapter = ArrayObjectAdapter(RelatedPresenter())
itemList.forEach { it->
relatedObjectAdapter.add(it)
}
val headerItem = HeaderItem("Related Media")
val listRow = ListRow(headerItem,relatedObjectAdapter)
rootAdapter.add(listRow)
}
inner class ItemViewSelectedListener : OnItemViewSelectedListener {
override fun onItemSelected(
itemViewHolder: Presenter.ViewHolder?,
item: Any?,
rowViewHolder: RowPresenter.ViewHolder?,
row: Row?
) {
if( row !=null && row!!.headerItem!!.name=="Episodes" && item is Episode){
episodeItemSelectedListener?.invoke(item as Episode)
}else{
Log.d("Selection Listener","Listener Activated")
}
}
}
The Implemetation of My RowsSupportFragment Class is given above
enter image description here
Whenever I try to scroll side to side using the emulator's remote the row jsut glitches and comes back to its initial position.
But I am able to move the list by dragging with my mouse cursor.

Async Loading of a TreeView

Hey I am very new to tornadofx struggeling with async loading of data for the treeview. I am loading categories from a rest endpoint, which I want to show in there.
It seems like there's no direct data binding to the children.
when using 'bindChildren' I can provide the observable list, but I have to convert them into Node's. which then would make the populate block kind of obsolete.
What's the recommended way of doing this? I cannot find anything about this.
// Category
interface Category<T : Category<T>> {
val id: String
val name: String
val subcategories: List<T>?
}
//default category:
class DefaultCategory(override val name: String) : Category<DefaultCategory> {
override val id: String = "default"
override val subcategories: List<DefaultCategory>? = null
}
//ViewModel
class CategoryViewModel : ViewModel() {
val sourceProperty = SimpleListProperty<Category<*>>()
fun loadData() {
// load items for treeview into 'newItems'
sourceProperty.value = newItems
}
}
// TreeViewFactoryMethod
private fun createTreeView(
listProperty: SimpleListProperty<Category<*>>
): TreeView<Category<*>> {
return treeview {
root = TreeItem(DefaultCategory("Categories"))
isShowRoot = false
root.isExpanded = true
root.children.forEach { it.isExpanded = true }
cellFormat { text = it.name }
populate { parent ->
when (parent) {
root -> listProperty.value
else -> parent.value.subcategories
}
}
}
}
Assuming that on a button click I call viewmodel.loadData(), I would expect the TreeView to update as soon as there's some new data. (If I would've found a way to bind)
I've never had to use bindChildren for TornadoFX before and your use of async isn't very relevant to what I think is your primary problem. So, admittedly, this question kind of confused me at first but I'm guessing you're just wondering why the list isn't appearing in your TreeView? I've made a test example with changes to make it work.
// Category
interface Category<T : Category<T>> {
val id: String
val name: String
val subcategories: List<T>?
}
//default category:
class DefaultCategory(override val name: String) : Category<DefaultCategory> {
override val id: String = "default"
override val subcategories: List<DefaultCategory>? = null
}
//Just a dummy category
class ChildCategory(override val name: String) : Category<ChildCategory> {
override val id = name
override val subcategories: List<ChildCategory>? = null
}
//ViewModel
class CategoryViewModel : ViewModel() {
//filled with dummy data
val sourceProperty = SimpleListProperty<Category<*>>(listOf(
ChildCategory("Categorya"),
ChildCategory("Categoryb"),
ChildCategory("Categoryc"),
ChildCategory("Categoryd")
).asObservable())
fun loadData() {
sourceProperty.asyncItems {
//items grabbed somehow
listOf(
ChildCategory("Category1"),
ChildCategory("Category2"),
ChildCategory("Category3"),
ChildCategory("Category4")
).asObservable()
}
}
}
class TestView : View() {
val model: CategoryViewModel by inject()
override val root = vbox(10) {
button("Refresh Items").action {
model.loadData()
}
add(createTreeView(model.sourceProperty))
}
// TreeViewFactoryMethod
private fun createTreeView(
listProperty: SimpleListProperty<Category<*>>
): TreeView<Category<*>> {
return treeview {
root = TreeItem(DefaultCategory("Categories"))
isShowRoot = false
root.isExpanded = true
root.children.forEach { it.isExpanded = true }
cellFormat { text = it.name }
populate { parent ->
when (parent) {
root -> listProperty
else -> parent.value.subcategories
}
}
}
}
}
There are 2 important distinctions that are important.
1. The more relevant distinction is that inside the populate block, root -> listProperty is used instead of root.listProperty.value. This will make your list appear. The reason is that a SimpleListProperty is not a list, it holds a list. So, yes, passing in a plain list is perfectly valid (like how you passed in the value of the list property). But now that means the tree view isn't listening to your property, just the list you passed in. With that in mind, I would be considerate over the categories' subcategory lists are implemented as well.
2. Secondly, notice the use of asyncItems in the ViewModel. This will perform whatever task asynchronously, then set the items to list on success. You can even add fail or cancel blocks to it. I'd recommend using this, as long/intensive operations aren't supposed to be performed on the UI thread.

javafx binding from list property to arbitrary object property

I am trying to get a class to have a property bound to another class's list property, where the 1st property is derived from a summarizing calculation over the objects in the list. The code below is a simplified version of my production code. (The production code is doing a summary over DateTime objects -- the essential part of the code below is the binding between a list and an object property (here, it is a String for simplicity).)
I have tried various things. One approach was using addListener on the list in the Summary class below but I was running into weird bugs with the listener callback making updates on the Summary object. After doing a bunch of reading I think that a binding between the summary string and the list is more appropriate but I don't know exactly how to hook up the binding to the property?
package com.example.demo.view
import javafx.beans.Observable
import javafx.beans.binding.StringBinding
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import tornadofx.View
import tornadofx.button
import tornadofx.label
import tornadofx.vbox
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
val yProperty = SimpleStringProperty("xyz")
}
class Collection {
private var things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
fun addThing(thing: Thing) {
things.add(thing)
}
}
class Summary(var collection: Collection) {
val summaryBinding = object : StringBinding() {
// The real code is more practical but
// this is just a minimal example.
override fun computeValue(): String {
val sum = collection.thingsProperty.value
.map { it.xProperty.value }
.fold(0, { total, next -> total + next })
return "There are $sum things."
}
}
// How to make this property update when collection changes?
val summaryProperty = SimpleStringProperty("There are ? things.")
}
class MainView : View() {
val summary = Summary(Collection())
override val root = vbox {
label(summary.summaryProperty)
button("Add Thing") {
summary.collection.addThing(Thing(5))
}
}
}
Keep in mind that I made this answer based on your minimal example:
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
var x by xProperty
val yProperty = SimpleStringProperty("xyz")
var y by yProperty
}
class MainView : View() {
val things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
val totalBinding = integerBinding(listProperty) {
value.map { it.x }.fold(0, { total, next -> total + next })
}
val phraseBinding = stringBinding(totalBinding) { "There are $value things." }
override val root = vbox {
label(phraseBinding)
button("Add Thing") {
action {
list.add(Thing(5))
}
}
}
}
I removed your other classes because I didn't see a reason for them based on the example. If the collection class has more functionality than holding a list property in your real project, then add just add it back in. If not, then there's no reason to give a list its own class. The summary class is really just two bindings (or one if you have no need to separate the total from the phrase). I don't see the need to give them their own class either unless you plan on using them in multiple views.
I think your biggest problem is that you didn't wrap your button's action in action {}. So your code just added a Thing(5) on init and had no action set.
P.S. The var x by xProperty stuff will only work if you import tornadofx.* for that file.

TableView cell request Focus

I'm new in JavaFX and have the following issue:
I have a tableview inside a BorderPane. I want it to focus on the last row/1st column when it's loaded. I have tried the following:
requestfocus()
scrollTo()
focusModel.focus()
selectionModel.select()
What happens is that the cell I want is indeed blue (as if it was selected) but the first cell has a blue border. So, when I try to use the arrow keys, the selected cell moves to the first row.
BTW, I'm using TornadoFX.
Any ideas?
Thanks in advance!
class CashflowTab : View() {
override val root: HBox by fxml()
private val mController : CashflowController by inject()
private val mainView : MainView by inject()
// Get the buttons
private val buttonCashflow : Button by fxid("btnCashflow")
init {
// Setup the buttons
buttonCashflow.action {
setupCashflowTable()
}
}
/** Displays the TableView for the Cashflow */
private fun setupCashflowTable() {
var initialFocus = true
// List of entries for the category ComboBox
val categoryList = mController.getCashFlowCategoryList()
// Create the table
val cashTable = tableview<CashEntry>(mController.getCashEntryList()) {
isEditable = true
column(Constants.COL_COUNT, CashEntry::countProperty)
column(Constants.COL_DATE, CashEntry::dateProperty).makeEditable(LocaleDateConverter())
column(Constants.COL_INCOME, CashEntry::incomeProperty).makeEditable(CurrencyConverter())
column(Constants.COL_EXPENSES, CashEntry::expensesProperty).makeEditable(CurrencyConverter())
column(Constants.COL_PROFIT, CashEntry::profitProperty).converter(CurrencyConverter())
column(Constants.COL_TOTAL_PROFIT, CashEntry::totalProfitProperty).converter(CurrencyConverter())
column(Constants.COL_COMMENTS, CashEntry::commentsProperty).makeEditable()
column(Constants.COL_CATEGORY, CashEntry::categoryProperty).useComboBox(categoryList)
// Scroll to and focus on the last cell on startup
if (initialFocus) {
val lastRow = mController.getCashEntryList().size - 1
requestFocus()
scrollTo(lastRow)
focusModel.focus(lastRow)
selectionModel.select(lastRow)
initialFocus = false
}
onEditCommit {entry ->
// Update the list
mController.updateCashEntryList(entry)
// Move to the next cell
requestFocus()
focusModel.focusRightCell()
#Suppress("UNCHECKED_CAST")
selectionModel.select(focusModel.focusedCell.row, focusModel.focusedCell.tableColumn as TableColumn<CashEntry, *>)
}
enableCellEditing()
// Enable edit on key typed
addEventHandler(KeyEvent.KEY_PRESSED) {keyEvent ->
if (keyEvent.code.isDigitKey || keyEvent.code.isLetterKey) {
if (editingCell == null) {
val currentSelectedCell = selectedCell
if (currentSelectedCell != null && currentSelectedCell.tableColumn.isEditable) {
edit(currentSelectedCell.row, currentSelectedCell.tableColumn)
}
}
}
}
}
// Add the table to the view
mainView.root.center = cashTable
cashTable.tableMenuButtonVisibleProperty()
// Ensure no other node can get focus
cashTable.focusedProperty().onChange {
val focusOwner = currentStage?.scene?.focusOwnerProperty()?.value
// Check if the focus owner is the table or a cell
if (focusOwner !is TableView<*> && focusOwner !is TextField) {
cashTable.requestFocus()
}
}
}
}
You should use
Platform.runLater(() -> {
requestFocus();
scrollTo(lastRow);
...
});
to update the GUI.

How to get SQLite Id from RecycleView adapter in Kotlin?

Sorry, I'm bad in English. I'm beginner in Android Studio.
I building same CREATE, UPDATE AND DELETE application in Android Studio, using custom RecycleView. But, I confused how to get row data when recycleview item clicked.
Can I get visible text in RecycleView item or not? Or can I get that data directly from SQLite.
How to get SQLite ID from this adapter?
package gh.alwathan.sipinsyar
import android.support.v7.widget.PopupMenu
import android.support.v7.widget.RecyclerView
import android.view.*
import android.widget.TextView
import android.widget.Toast
class NasabahAdapter(val userList: ArrayList<Nasabah>) : RecyclerView.Adapter<NasabahAdapter.ViewHolder>() {
//this method is returning the view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NasabahAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_nasabah, parent, false)
return ViewHolder(v)
}
//this method is binding the data on the list
override fun onBindViewHolder(holder: NasabahAdapter.ViewHolder, position: Int) {
holder.bindItems(userList[position])
holder.itemView.setOnLongClickListener {
//holder.nametxt.setText(players.get(position).getName());
//creating a popup menu
val popup = PopupMenu(holder.itemView.getContext(), holder.itemView, Gravity.RIGHT)
//inflating menu from xml resource
popup.inflate(R.menu.nasabah_item)
//adding click listener
popup.setOnMenuItemClickListener(PopupMenu.OnMenuItemClickListener { item: MenuItem? ->
when (item!!.itemId) {
R.id.nasabah_id-> {
// GET SQLITE ID FROM HERE
}
}
true
})
//displaying the popup
popup.show()
true
}
}
//this method is giving the size of the list
override fun getItemCount(): Int {
return userList.size
}
//the class is hodling the list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(user: Nasabah) {
val textViewName = itemView.findViewById(R.id.nasabah_id) as TextView
val textViewLastName = itemView.findViewById(R.id.nasabah_nama) as TextView
textViewName.text = user.id
textViewLastName.text = user.nama
}
}
}
data class Nasabah(val id: String, val nama: String)

Resources