How do I display a new image in tornadofx imageview? - kotlin

I want to display a WritableImage in imageview, but I want that image to change when the user loads in a new file from the file browser. I know that there is a bind() function for strings that change over time, but I could not find a similar option for images. I could solve the problem for images that are the same size as the default loaded one (with writing through the pixels), but that only works if they are the same size, since I cant modify the size of the image that I displayed.
My Kotlin code so far:
class PhotoView : View("Preview") {
val mainController: mainController by inject()
override val root = hbox {
imageview{
image = mainController.moddedImg
}
hboxConstraints {
prefWidth = 1000.0
prefHeight = 1000.0
}
}
class ControlView: View(){
val mainController: mainController by inject()
override val root = hbox{
label("Controls")
button("Make BW!"){
action{
mainController.makeBW()
}
}
button("Choose file"){
action{
mainController.setImage()
mainController.update()
}
}
}
}
class mainController: Controller() {
private val ef = arrayOf(FileChooser.ExtensionFilter("Image files (*.png, *.jpg)", "*.png", "*.jpg"))
private var sourceImg=Image("pic.png")
var moddedImg = WritableImage(sourceImg.pixelReader, sourceImg.width.toInt(), sourceImg.height.toInt())
fun setImage() {
val fn: List<File> =chooseFile("Choose a photo", ef, FileChooserMode.Single)
sourceImg = Image(fn.first().toURI().toString())
print(fn.first().toURI().toString())
}
fun makeBW() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color.grayscale())
}
}
}
fun update() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color)
}
}
}
}

ImageView has a property for the image that you can bind:
class PhotoView : View("Preview") {
val main: MainController by inject()
val root = hbox {
imageview { imageProperty().bind(main.currentImageProperty) }
...
}
...
}
class MainController : Controller() {
val currentImageProperty = SimpleObjectProperty<Image>(...)
var currentImage by currentImageProperty // Optional
...
}
From there, any time you set the currentImage in MainController, it will update in the PhotoView.

Related

How to add the views on the second line if there is no space on the first line?

I am trying to split some words on two lines to create a sentence. When there is no more space on the first line, the words should automatically go to the second line, but no matter what I have tried so far, only the first line is used, while the second line remains empty all the time.
Here is a screen capture.
MainActivity:
class MainActivity : AppCompatActivity(), RemoveAnswerListener {
private var binding: ActivityMainBinding? = null
var listAnswers = mutableListOf<Answer>()
private lateinit var actualAnswer: List<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
actualAnswer = listOf(
"What",
"did",
"you",
"want",
"to",
"ask",
"me",
"yesterday?"
)
val answerOption = listOf(
"me",
"yesterday?",
"did",
"to",
"want",
"you",
"ask",
"What"
)
answerOption.forEach {
val key = TextView(binding?.answerBox?.context)
with(key){
binding?.answerBox?.addView(this)
setBackgroundColor(Color.WHITE)
text = it
textSize = 18F
setPadding(40, 20, 40, 20)
val margin = key.layoutParams as FlexboxLayout.LayoutParams
margin.setMargins(30, 30, 30, 30)
layoutParams = margin
setOnClickListener {
moveToAnswer(it)
}
}
}
}
private fun moveToAnswer(view: View) {
if(listAnswers.size < actualAnswer.size){
view.setOnClickListener(null)
listAnswers.add(Answer(view, view.x, view.y, (view as TextView).text.toString(), this#MainActivity))
val topPosition = binding?.lineFirst?.y?.minus(120F)
// val topPosition1 = binding?.lineSecond?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
// var leftPosition1 = binding?.lineSecond?.x
if (listAnswers.size > 0) {
var allWidth = 0F
listAnswers.forEach {
allWidth += (it.view?.width)?.toFloat()!! + 20F
}
allWidth -= (view.width).toFloat()
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}else{
// leftPosition1 = binding?.lineSecond?.x?.plus(allWidth)
}
}
val completeMove = object: Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
}
}
if (leftPosition != null) {
if (topPosition != null) {
view.animate()
.setListener(completeMove)
.x(leftPosition)
.y(topPosition)
}
}
}
}
}
And this is the data class "Answer":
data class Answer(var view: View? = null,
var actualPositionX: Float = 0F,
var actualPositionY: Float = 0F,
var text: String = "",
var removeListener: RemoveAnswerListener? = null
){
init {
view?.setOnClickListener {
it.animate()
.x(actualPositionX)
.y(actualPositionY)
removeListener?.onRemove(this)
}
}
}
interface RemoveAnswerListener{
fun onRemove(answer: Answer)
}
You don't seem to be offsetting the View to the second line anywhere? You just initialise its position to the start of the first line:
val topPosition = binding?.lineFirst?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
And then you adjust the x position if there's room for it, otherwise it stays at the start
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}
(unless I'm misunderstanding things - I don't know what lineFirst is, a containing view for the first line I guess)
So you're not moving the view down, or to lineSecond or whatever - and you're not adjusting the x position based on the contents of the second line either.
Honestly if I were you, I'd look into the Flow helper for ConstraintLayout - it works like flexbox and basically moves elements below as they fill up the horizontal space, so it automatically does what you're doing here. Here's a guide on it that should give you the idea. Might save you a lot of hassle!

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)
}
}

Incompatible types: Int and Article

I am developing news app I have implemented multipleview types in recyclerview adapter class but I am getting following error
Incompatible types: Int and Article
below BBCSportAdapter class where I have implemented multipleview types
#Suppress("UNREACHABLE_CODE")
class BBCSportAdapter(private val listViewType: List<Int>) : RecyclerView.Adapter<BBCSportAdapter.MyViewHolder>() {
companion object {
val ITEM_A = 1
var ITEM_B = 2
}
var articleList: List<Article> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater =
LayoutInflater.from(parent.context)
return when (viewType) {
ITEM_A -> ViewHolderItemA(inflater.inflate(R.layout.bbc_sport_list, null))
else -> {
ViewHolderItemB(inflater.inflate(R.layout.bbc_sport_item, null))
}
}
}
#SuppressLint("NewApi")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val viewType = articleList[position]
when (viewType) {
ITEM_A -> {
val viewHolderA = holder as ViewHolderItemA
Picasso.get().load(articleList[position].urlToImage)
.into(viewHolderA.topFlameImageView)
}else -> {
val viewHolderB = holder as ViewHolderItemB
}
}
}
override fun getItemCount(): Int {
return articleList.size
}
// holder.articleTitle.text = articleList[position].title
// holder . articleSourceName . text = articleList [position].source.name
// Picasso . get ().load(articleList.get(position).urlToImage).into(holder.image)
//
// val input = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX")
// val output = SimpleDateFormat("dd/MM/yyyy")
// var d = Date()
// try {
// d = input.parse(articleList[5].publishedAt)
// } catch (e: ParseException) {
// try {
// val fallback = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
// fallback.timeZone = TimeZone.getTimeZone("UTC")
// d = fallback.parse(articleList[5].publishedAt)
// } catch (e2: ParseException) {
// // TODO handle error
// val formatted = output.format(d)
// val timelinePoint = LocalDateTime.parse(formatted)
// val now = LocalDateTime.now()
//
// var elapsedTime = Duration.between(timelinePoint, now)
//
// println(timelinePoint)
// println(now)
// elapsedTime.toMinutes()
//
// holder.articleTime.text = "${elapsedTime.toMinutes()}"
// }
// }
// }
fun setMovieListItems(articleList: List<Article>) {
this.articleList = articleList
notifyDataSetChanged()
}
open inner class MyViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!){}
inner class ViewHolderItemA(itemView: View) : MyViewHolder(itemView) {
val topFlameImageView: ImageView = itemView.findViewById(R.id.topFlameImageView)
}
inner class ViewHolderItemB(itemView: View?) : MyViewHolder(itemView) {
val image: ImageView = itemView!!.findViewById(R.id.imageView)
val articleTitle: TextView = itemView!!.findViewById(R.id.articleTitle)
val articleSourceName: TextView = itemView!!.findViewById(R.id.articleSourceName)
val imageCategory: ImageView = itemView!!.findViewById(R.id.imageCategory)
val articleTime: TextView = itemView!!.findViewById(R.id.articleTime)
}
}
I have followed this link https://github.com/CoderJava/Multiple-View-Type-RecyclerView-Kotlin-Android/blob/master/app/src/main/java/com/ysn/multipleviewtypeexample/AdapterRecyclerView.kt
In your onBindViewHolder articleList is list of artical but in your when statement you are comparing Article with an Int i.e. ITEM_A, which is wrong. Instead you should have some type in your article object and comparison is based on that type. Moreover you have not implemented getItemViewType() where you can make decision which view will be inflated. In your case viewType in parameter of oncreateViewHolder will always return 0 and else condition will be executed always and you will always have single type of view.

Progressbar TaskStatus does listen to all async runs

I made a small example for you guyes to see what i mean.
By Running it you will see that on clicking into the Yellow area the progress bar is shown... But i only want it to be shown if the Green area is clicked.
Am i doing sth wrong or is this actually an expected behavior?
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.VBox
import tornadofx.*
class RunAsyncExample : View() {
override val root = VBox()
private val runAsyncOne: RunAsyncOne by inject()
private val runAsyncTwo: RunAsyncTwo by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 300.0
add(runAsyncOne)
add(runAsyncTwo)
}
}
}
class RunAsyncOne : View() {
override val root = AnchorPane()
init {
with(root) {
var checker = false
minWidth = 400.0
minHeight = 150.0
style {
backgroundColor += c("YELLOW")
}
setOnMouseClicked {
checker = !checker
runAsync {
while (checker) {
Thread.sleep(100)
println("AsyncOne")
}
}
}
}
}
}
class RunAsyncTwo : View() {
override val root = VBox()
private val status: TaskStatus by inject()
init {
with(root) {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync {
var b = 0.0
while (b < 100.0) {
b+=1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen { status.running }
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}
}
When i click into the green area:
When i click into the yellow area while the AsyncTwo is running,
the progress bar is changing but i dont want that...
When you inject a TaskStatus object into your View, you will get a "global" TaskStatus object that by default will report status of any async calls. What you want is to create a local TaskStatus object for the RunAsyncTwo View and pass that to the runAsync call.
If you don't pass a TaskStatus object to runAsync, the default for your scope will be used, hence the behavior you're seeing. Here is an example with a local TaskStatus object for the second view. Please also note that I'm using a more sensible syntax for defining the root node, you should absolutely pick this pattern up :)
class RunAsyncTwo : View() {
private val status = TaskStatus()
override val root = vbox {
minWidth = 400.0
minHeight = 150.0
setOnMouseClicked {
runAsync(status) {
var b = 0.0
while (b < 100.0) {
b += 1
updateProgress(b, 100.0)
updateMessage("$b / 100.0")
Thread.sleep(100)
println("AsyncTwo")
}
}
}
stackpane {
visibleWhen(status.running)
progressbar(status.progress) {
progress = 0.0
minWidth = 400.0
}
label(status.message)
}
style {
backgroundColor += c("GREEN")
}
}
}

Why does my tornadoFX ObservableList not receive updates?

I have a simple tornadoFX program that generates some circles in random locations on the screen. However, none of the circles get drawn. I've added some debug code to print a line when a circle is drawn, and it only prints once.
I would expect circles to appear at 100ms intervals, as well as when I click the "Add actor" button.
private const val WINDOW_HEIGHT = 600
private const val WINDOW_WIDTH = 1024
fun main(args: Array<String>) {
Application.launch(MainApp::class.java, *args)
}
class MainApp : App(WorldView::class, Stylesheet::class)
data class Actor(val x: Double, val y: Double)
class WorldView: View("Actor Simulator") {
override val root = VBox()
private val actors = ArrayList<Actor>(0)
init {
tornadofx.runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
actors.add(Actor(x, y))
Thread.sleep(100)
}
}
}
init {
with(root) {
stackpane {
group {
bindChildren(actors.observable()) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
also {
println("drew circle")
}
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}
}
}
Oddly, if I put a breakpoint during the circle draw code, circles will draw and the debug line will print.
Some observations:
Calling someList.observable() will create an observable list backed by the underlying list, but mutations on the underlying list will not emit events. You should instead initialize actors as an observable list right away.
Access to an observable list must happen on the UI thread, so you need to wrap mutation calls in runLater.
For people trying to run your example - you didn't include a stylesheet, but references one in your App subclass, so the IDEA will most probably import the TornadoFX Stylesheet class. This will not end well :)
The also call has no effect, so I removed it.
I updated your code to best practices here and there, for example with regards to how to create the root node :)
Updated example taking these points into account looks like this:
private const val WINDOW_HEIGHT = 600.0
private const val WINDOW_WIDTH = 1024.0
class MainApp : App(WorldView::class)
data class Actor(val x: Double, val y: Double)
class WorldView : View("Actor Simulator") {
private val actors = FXCollections.observableArrayList<Actor>()
override fun onDock() {
runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
runLater {
actors.add(Actor(x, y))
}
Thread.sleep(100)
}
}
}
override val root = stackpane {
setPrefSize(WINDOW_WIDTH, WINDOW_HEIGHT)
group {
bindChildren(actors) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
println("drew circle")
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}