How to make a search field in Workspace header? - kotlin

As proposed in the tutorial, I've added a SearchView to Workspace header:
class AppWorkspace : Workspace() {
init {
add(SearchView::class)
}
}
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen { searchable }
}
}
So I need a property to enable that field in certain views:
val searchable = SimpleBooleanProperty(false)
Where I should define it and how to access it?
And how to implement onSearch or something?

I would suggest creating a Searchable interface:
interface Searchable {
fun onSearch(query: String)
}
Views that should enable the search field when they are docked would implement this interface. You can bind the enabled state of the search input by creating an observable boolean value to check for this:
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
Then you can make sure the search field action forwards to the onSearch function in the Searchable currently docked:
action {
(workspace.dockedComponent as Searchable).onSearch(text)
}
Now just implement onSearch in your Searchable view classes and your good to go. You could also clear the search field with an event:
object ClearSearch : FXEvent()
Listen to this event inside your search field:
subscribe<ClearSearch> { clear() }
Now your views can fire this event if they want to clear the search field:
override fun onSearch(query: String) {
println("Searching for $query...")
fire(ClearSearch)
}
For completeness, here is the SearchView:
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
action {
(workspace.dockedComponent as Searchable).onSearch(text)
}
subscribe<ClearSearch> { clear() }
}
}
And here is a class implementing Searchable:
class Editor1 : View("Editor 1"), Searchable {
override val root = borderpane {
center {
label("Nothing here yet")
}
}
override fun onSearch(query: String) {
println("Searching for $query...")
fire(ClearSearch)
}
}
Hope this helps :)

Following Edvin answer I get:
class MyApp: App(MyWorkspace::class)
class MyApp2: App(View3::class){
init{
FX.defaultWorkspace = MyWorkspace::class
}
}
class MyWorkspace: Workspace("My Workspace") {
override fun onDock() {
add(SearchView::class) //With this search view gets enable
}
init {
//add(SearchView::class) //With this search view doesn't get enable
root.setPrefSize(800.0, 600.0)
menubar {
menu("Windows"){
item("View1").action{dock<View1>()}
item("View2").action{dock<View2>()}
}
}
}
}
interface Searchable {
fun onSearch(consulta: String)
}
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
action {
(workspace.dockedComponent as? Searchable)?.onSearch(text)
}
subscribe<ClearSearch> { clear() }
}
}
object ClearSearch : FXEvent()
class View1 : View(), Searchable{
override val root = label("This is View 1")
override fun onSearch(consulta: String) {
println("I'm searching")
}
}
class View2 : View(){
override val root = label("This is View 2")
}
class View3 : View(){
override val root = button("Open Workspace"){
action{
workspace.openWindow()
}
}
}
The thing is when I put add(SearchView::class) in the init{} section and dock a Searchable view, the Search textfield doesn't get enable, if a put it in the onDock function it does get enable.
But if I open MyWorkspace from MyApp2, close it and open it again I get a error, since onDock is called again and try to add(SearchView::class) one more time. What the solution here so I can open MyWorkspace from some other window without any trouble?

Related

How to add a button to the view dynamically?

I'm new to Kotlin and TorandoFX. Maybe I'm missing something very basic in TornadoFX. I want to create from a list (which shoulde be mutable) buttons in the view. If the user clicks on the add button the list should get a new item and this should result in a new button in the view.
Thank you for your help.
I was thinking it should look like this:
import tornadofx.*
fun main(args: Array<String>) {
launch<MyApp>(args)
}
class MyApp: App(MainView::class)
class MainView: View("MainView") {
val values = ArrayList<Int>(listOf(1,2,3)).asObservable()
var count = 4
override val root = vbox {
values.forEach { x ->
button(x.toString())
}
button("add") {
action {
values.add(count)
println(values.toString())
count++
}
}
}
}
this code result in this view, but if I click the button the view doesnt refresh.
This code result in this view, but if I click the button the view doesnt refresh. I think I'm missing something about binding.
We figured out, I was right with the binding part. I just had to use bindChildren() function and give the function an observableArray and a function to for the conversion of the elements of the array as a parameter.
Thank you for the help.
import javafx.collections.FXCollections
import tornadofx.*
fun main(args: Array<String>) {
launch<MyApp>(args)
}
class MyApp: App(MainView::class)
class MainView: View("MainView") {
val values = FXCollections.observableArrayList<Int>(listOf(1,2,3))
var count = 4
override val root = vbox {
vbox {
bindChildren(values) { x ->
button(x.toString())
}
}
vbox() {
button("add") {
action {
values.add(count)
count++
}
}
}
}
}

Kotlin: Edit icon dashboard of icons between fragments

I'm trying to figure out the most efficient way to structure this problem..
I'd like to click on the 'EDIT' icon in the dashboard of the MainFragment, display a DialogFragment, allow user to select/deselect up to 5 icons, save the selection, close the DialogFragment, and update the MainFragment.
Should I use MutableLiveData/Observer from a ViewModel? Or is there a better approach? I currently cannot figure out how to use the ViewModel approach correctly...
So far, this is the code I have:
MainFragment: https://i.stack.imgur.com/5fRt2.png
DialogFragment: https://i.stack.imgur.com/ZvW3d.png
ViewModel Class:
class IconDashboardViewModel() : ViewModel(){
var liveDataDashIcons: MutableLiveData<MutableList<String>> = MutableLiveData()
var liveItemData: MutableLiveData<String> = MutableLiveData()
// Observer for live list
fun getLiveDataObserver(): MutableLiveData<MutableList<String>> {
return liveDataDashIcons
}
// Observer for each icon
fun getLiveItemObserver(): MutableLiveData<String> {
return liveItemData
}
// Set icon list
fun setLiveDashIconsList(iconList: MutableLiveData<MutableList<String>>) {
liveDataDashIcons.value = iconList.value
}
// Set data for data
fun setItemData(icon : MutableLiveData<String>) {
liveItemData.value = icon.toString()
}
var iconList = mutableListOf<String>()
}
MainFragment:
private fun populateIconList() : MutableLiveData<MutableList> {
var iconList = viewModel.liveDataDashIcons
// Roster icon
if (roster_dash_layout.visibility == View.VISIBLE) {
iconList.value!!.add(getString(R.string.roster))
} else {
if (iconList.value!!.contains(getString(R.string.roster))) {
iconList.value!!.remove(getString(R.string.roster))
}
}
}
DialogFragment:
private fun setIconList(iconList: MutableList){
var iconList = viewModel.iconList
Log.d(TAG, "viewModel iconList = " + iconList)
if (iconList.contains(getString(R.string.roster))) {
binding.radioButtonRosterPick.setBackgroundResource(R.drawable.icon_helmet_blue_bg)
}
}

Navigation controller AlertDialog click listner

I'm using Android's Navigation component and I'm wondering how to setup AlertDialog from a fragment with a click listener.
MyFragment
fun MyFragment : Fragment(), MyAlertDailog.MyAlertDialogListener {
...
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
// Here I would typically call setTargetFragment() and then show the dialog.
// but findnavcontroller doesn't have setTargetFragment()
findNavController.navigate(MyFragmentDirection.actionMyFragmentToMyAlertDialog())
}
}
MyAlertDialog
class MyAlertDialog : DialogFragment() {
...
internal lateinit var listener: MyAlertDialogListener
interface MyAlertDialogListener{
fun onDialogPostiveCLick(dialog: DialogFragment)
}
override fun onCreateDialog(savdInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage("My Dialog message")
.setPositiveButton("Positive", DialogInterface.OnClickListener {
listener = targetFragment as MyAlertDialogListener
listener.onDialogPositiveClick(this)
}
...
}
}
}
This currently receives a null point exception when initializing the listener in MyAlertDialog.
To use targetFragment, you have to set it first as you commented, unfortunately jetpack navigation does not do this for you (hence the null pointer exception). Check out this thread for alternative solution: https://stackoverflow.com/a/50752558/12321475
What I can offer you is an alternative. If the use-case is as simple as displaying a dialog above current fragment, then do:
import androidx.appcompat.app.AlertDialog
...
class MyFragment : Fragment() {
...
fun onDialogPostiveCLick() {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
AlertDialog.Builder(activity)
.setMessage("My Dialog message")
.setPositiveButton("Positive") { _, _ -> onDialogPostiveCLick() }
.setCancellable(false)
.create().show()
}
}

How to handle auth while showing loading screen?

How to show a loading screen and handle authorization at the same time?
Can I switch to LoadingView and return back to AuthView in AuthController or I need to move auth logic to LoadingController?
class AuthController : Controller() {
val authView : AuthView by inject()
val loadingView : LoadingView by inject()
fun tryAuth(login: String, password: String) {
runAsync {
login == "admin" && password == "admin"
} ui { successful ->
authView.replaceWith(loadingView, ViewTransition.Fade(0.5.seconds))
if (successful) {
// doesn't work
loadingView.replaceWith(MainView::class, ViewTransition.Metro(0.5.seconds))
} else {
// doesn't work
loadingView.replaceWith(AuthView::class, ViewTransition.Fade(0.5.seconds))
}
}
}
}
You are replacing the AuthView with the LoadingView and then the LoadingView with the MainView in the same pulse, so that's not going to give you what you want. Normally you'd want to change to the LoadingView on the UI thread before you evaluate the auth information. Using this approach, your code works, but it might not be what you want.
class AuthController : Controller() {
val authView : AuthView by inject()
val loadingView : LoadingView by inject()
fun tryAuth(login: String, password: String) {
authView.replaceWith(loadingView, ViewTransition.Fade(0.5.seconds))
runAsync {
// Simulate db access or http call
Thread.sleep(2000)
login == "admin" && password == "admin"
} ui { successful ->
if (successful) {
// doesn't work
loadingView.replaceWith(MainView::class, ViewTransition.Metro(0.5.seconds))
} else {
// doesn't work
loadingView.replaceWith(AuthView::class, ViewTransition.Fade(0.5.seconds))
}
}
}
}
class AuthView : View("Auth") {
val authController: AuthController by inject()
override val root = stackpane {
button(title).action {
authController.tryAuth("admin", "admin")
}
}
}
class LoadingView : View("Loading...") {
override val root = stackpane {
label(title)
}
}
class MainView : View("Main View") {
override val root = stackpane {
label(title)
}
}
You must keep in mind that replacing a view will not resize the window (though you could access the stage and ask it to resize to the current view), so you might be better off with opening each view in a separate window instead.

How do install a click handler on a dynamic listview (in tornadofx)

My application needs to permit additions to the listview. I've figured out how I can dynamically add to a listview by using observableArrayList. If I click on the button, an item gets added to the list and displayed.
Now I'm struggling to add a click handler (I want to handle the event that happens when someone clicks on any item within the list view). Where do I do this?
Here is my code.
package someapp
import javafx.collections.FXCollections
import javafx.geometry.Pos
import javafx.scene.layout.VBox
import javafx.scene.text.FontWeight
import tornadofx.*
class MyApp : App(HelloWorld::class) {
}
class HelloWorld : View() {
val leftSide: LeftSide by inject()
override val root = borderpane {
left = leftSide.root
}
}
class LeftSide: View() {
var requestView: RequestView by singleAssign()
override val root = VBox()
init {
with(root) {
requestView = RequestView()
this += requestView
this += button("Add Item") {
action {
requestView.responses.add( Request( "example.com",
"/foo/bar",
"{ \"foo\" : \"bar\"}".toByteArray()))
}
}
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>(
)
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray) {
}
To configure a callback when an item in a ListView is selected, use the onUserSelect callback:
onUserSelect {
information("You selected $it")
}
You can optionally pass how many clicks constitutes a select as well, default is 2:
onUserSelect(1) {
information("You selected $it")
}
You are using some outdated constructs in your code, here is an updated version converted to best practices :)
class MyApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = borderpane {
left(LeftSide::class)
}
}
class LeftSide : View() {
val requestView: RequestView by inject()
override val root = vbox {
add(requestView)
button("Add Item").action {
requestView.responses.add(Request("example.com",
"/foo/bar",
"""{ "foo" : "bar"}""".toByteArray()))
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>()
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
onUserSelect(1) {
information("You selected $it")
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray)