I'm getting into JetBrains plugin development and currently working on a tool window with Kotlin UI DSL. The UI renders fine but typing on the text field is not updating the property it is bound to, in this case regex. Here's my code.
class RegexTesterToolWindowFactory : ToolWindowFactory {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val regexTesterToolWindow = RegexTesterToolWindow(toolWindow)
val contentFactory = ContentFactory.SERVICE.getInstance()
val content = contentFactory.createContent(regexTesterToolWindow.content, "", false)
toolWindow.contentManager.addContent(content)
}
}
private val LOG = logger<RegexTesterToolWindow>()
class RegexTesterToolWindow(toolWindow: ToolWindow) {
private var regex: String = ".*"
private var testText: String = ""
var content: JPanel
init {
content = panel {
row("Regex:") {
textField(::regex)
}
row {
button("Run") {
LOG.info(regex)
LOG.info(testText)
}
}
}
}
}
When I hit the Run button, regex is always the default value .* even after I manually type in the text field.
Related
Hey I'm pretty new to Kotlin and am trying my hand at a GUI as my first small project.
For this I am using Jetpack Compose Desktop. I have already written a first small login window ( not the one in the GIF), and would like to open a new window with the "actual" content after logging in (not an external one, but in the same window).
Here is a video that may help you to understand what I mean:
(Not mine but thanks to Agon Mustafa - uplabs)
So that one continues with the registration in the same window and does not have to open a separate window for it. Hope you can help me:)
Here is an example of how to open and close multiple windows:
fun main() = application {
val applicationState = remember { MyApplicationState() }
for (window in applicationState.windows) {
key(window) {
MyWindow(window)
}
}
}
#Composable
private fun ApplicationScope.MyWindow(
state: MyWindowState
) = Window(onCloseRequest = state::close, title = state.title) {
MenuBar {
Menu("File") {
Item("New window", onClick = state.openNewWindow)
Item("Exit", onClick = state.exit)
}
}
}
private class MyApplicationState {
val windows = mutableStateListOf<MyWindowState>()
init {
windows += MyWindowState("Initial window")
}
fun openNewWindow() {
windows += MyWindowState("Window ${windows.size}")
}
fun exit() {
windows.clear()
}
private fun MyWindowState(
title: String
) = MyWindowState(
title,
openNewWindow = ::openNewWindow,
exit = ::exit,
windows::remove
)
}
private class MyWindowState(
val title: String,
val openNewWindow: () -> Unit,
val exit: () -> Unit,
private val close: (MyWindowState) -> Unit
) {
fun close() = close(this)
}
You can read more in the
tutorials
of Compose for desktop
I have a class to pass notes from mysql to tableview in kotlin but i cant seem to make it work
Im a little new in kotlin for desktop, only used in android with firebase
This is my class to get the notes
class Notes(id_notes: Int = 0, title: String = "none", description: String = "none"){
private var id_notes: SimpleIntegerProperty = SimpleIntegerProperty(id_notes)
private var title: SimpleStringProperty = SimpleStringProperty(title)
private var description: SimpleStringProperty = SimpleStringProperty(description)
fun getId(): Int {
return id_notes.get()
}
fun setId(id: Int) {
id_notes.set(id)
}
fun getTitle(): String {
return title.get()
}
fun setTitle(Title: String) {
title.set(Title)
}
fun getDescription(): String {
return description.get()
}
fun setDescription(Description: String) {
description.set(Description)
}
then i have the actual code
tableview(data){
prefWidth = 400.0
column("ID", Notes::getId)
column("Title", Notes::getTitle)
rowExpander {
label {
this.text = Notes::getDescription.toString()
}
}
}
private fun getNotes(){
try {
val notes = Notes()
val sql = ("SELECT id_notes, title, description, date FROM notes")
val con: Connection? = Conn.connection()
stmt = con?.createStatement()
rs = stmt?.executeQuery(sql)
while (rs!!.next()) {
notes.setId(rs!!.getInt("id_notes"))
notes.setDescription(rs!!.getString("description"))
notes.setTitle(rs!!.getString("title"))
data.add(notes.toString())
}
} catch (ex: SQLException) {
alert(Alert.AlertType.ERROR, "Error", "Could not perform this action")
}
}
At the end I will try to solve your problem, but please, read this part first, because this is far more import for you than the actual answer. I believe your programing skills (for now) are not the required for the kind of things you are trying to accomplish, especially because you are converting your class to string before adding it to your data (which seem to be a collection of string not a collection of Notes), so I don’t know how you expect the tableview will get your Id, Title and Description.
Also, you have a constructor for Notes, but you are overcomplicating things by not using it and assign values later. In other hand, you getNotes() function is never call in your code, probably is called in some other part you are not showing.
Because of that, I think you should slow down a little bit, try to level up your basic skills (specially working with classes and collections), them read the tornadofx manual, and them try with this kind of stuff.
Now this is my solution. First try this without the database. I did it this way because I don’t know if there is any problem with your database. Them change the getNotes() function to the way is in your code, without converting the notes.toString(), just de data.add(notes). Remember to click the button to load the data.
class Prueba: View("MainView") {
//data should be an FXCollections.observableArrayList<Notes>
//You didn't show your data variable type, but apparently is some collection of string
val data = FXCollections.observableArrayList<Notes>()
override val root = vbox {
tableview(data){
prefWidth = 400.0
column("ID", Notes::getId)
column("Title", Notes::getTitle)
rowExpander() {
label() {
//Note the difference here, Notes::getDescription.toString() won't do what you want
this.text = it.getDescription()
}
}
}
//This button is calling the function getNotes(), so data can be loaded
button("Load Data") {
action {
getNotes()
}
}
}
//Note this function is out side root now
private fun getNotes() {
data.clear()
data.add(Notes(1,"Title 1", "Description 1"))
data.add(Notes(2,"Title 2", "Description 2"))
data.add(Notes(3,"Title 3", "Description 3"))
}
}
I have a ViewModel for a ListView with 3 players in it:
object PlayerListViewModel : ViewModel() {
lateinit var players : ObservableList<Player>
init{
}
fun loadPlayers(){
players = Engine.selectedGame.players.asObservable()
}
}
class PlayerListView : View() {
private val vm = PlayerListViewModel
override val root = VBox()
init {
vm.loadPlayers()
root.replaceChildren {
style {
spacing = 25.px
alignment = Pos.CENTER
padding = box(0.px, 15.px)
}
listview(vm.players){
style{
background = Background.EMPTY
prefWidth = 300.px
}
isFocusTraversable = false
isMouseTransparent = true
cellFragment(PlayerCardFragment::class)
}
}
}
}
For some reason the listview is creating 4 PlayerCardFragments, with the first having a null item property and the last 3 having the correct Player item reference. This is the PlayerCardFragment definition:
class PlayerCardFragment : ListCellFragment<Player>() {
private val logger = KotlinLogging.logger { }
private val vm = PlayerViewModel().bindTo(this)
private lateinit var nameLabel : Label
private lateinit var scoreLabel : Label
override val root = hbox {
addClass(UIAppStyle.playerCard)
nameLabel = label(vm.name) { addClass(UIAppStyle.nameLabel) }
scoreLabel = label(vm.score) { addClass(UIAppStyle.scoreLabel) }
}
init {
logger.debug { "Initializing fragment for ${this.item} and ${vm.name.value}" }
EventBus.channel(EngineEvent.PlayerChanged::class)
.observeOnFx()
.subscribe() {
vm.rollback() //force viewmodel (PlayerViewModel) refresh since model (Player) does not implement observable properties
logger.debug { "${vm.name.value}'s turn is ${vm.myTurn.value}" }
root.toggleClass(UIAppStyle.selected, vm.myTurn)
}
}
When running the application, the PlayerCardFragment initializations print out "Initializing fragment for null and null" four times, but the list appears perfectly correctly with the 3 Player items. Later during execution, wnen there is an Engine.PlayerChanged event received, the Oberver function prints:
"null's turn is false"
"Adam's turn is false"
"Chad's turn is true"
"Kyle's turn is false"
These are the correct players, with the correct turn statuses. The listview appears perfectly well with the styling changes. I'm just not sure where that first null ListCellFragment is coming from.
It seems like you're trying to give ItemViewModel functionality to a view model by having everything around it change. Why not change the PlayerViewModel to have the functionality instead? Easiest way to imagine is to create bindings out of generic properties, then have them all changed and committed with by listening to the itemProperty:
class Player(var foo: String?, var bar: Int?)
class PlayerViewModel() : ItemViewModel<Player>() {
val foo = bind { SimpleStringProperty() }
val bar = bind { SimpleIntegerProperty() }
init {
itemProperty.onChange {
foo.value = it?.foo
bar.value = it?.bar
}
}
override fun onCommit() {
item?.let { player ->
player.foo = foo.value
player.bar = bar.value?.toInt()
}
}
}
Is it pretty? No. Does it keep you from having to implement an event system? Yes.
I have a really basic question but I couldn't find an answer to it. I already searched via google for people with similar problems but I didn´t find anything useful.
(e.g. https://github.com/edvin/tornadofx-guide/blob/master/part1/11.%20Editing%20Models%20and%20Validation.md )
I have the following ViewModel
class MasterSizeModel(var size : Int) : ViewModel()
{
val value = bind { size.toProperty() }
}
And inject it into another class, where I do the following:
masterSize.size = order.masterStatSize
masterSize is my model.
Now in a third class, I want to bind the value from the label to a label.
private val recvMaster : Label by fxid("recvMaster")
/*....*/
recvMaster.bind(masterSizeModel.value)
But unfortunately, my attempts are failing completely. I can see size from my ModelView is updating as it should, but the changes are not present in the value nither are they shown in the label.
Edit:
I totally forgot to bind to the textProperty(), but I don´t get any further.
recvMaster.textProperty().bind(masterSizeModel.value/*?*/)
Edit 2:
After the request I add my complete code section:
class Setup : View() {
override val root : VBox by fxml()
/*Adding Buttons and Textfields*/
init {
//Binding all checkboxes to their text field
//Input validation....
//start Button
start.setOnAction {
val masterSize = 0
val masterSizeModel = MasterSizeModel((masterSize))
//Open a socket (see code below)
val req = Requester(ipAdress.text, masterSizeModel)
val reqModel = RequesterModel(SimpleObjectProperty<Requester>(req))
val scope = Scope()
setInScope(reqModel, scope)
setInScope(masterSizeModel, scope)
req.sendOrder(SetupOrder(/*Sending stuff throw the network*/))
val overview = find<Overview>(scope)
replaceWith(overview)
}
}
}
class Overview : View() {
override val root : VBox by fxml()
private val req : RequesterModel by inject()
private val masterSizeModel : MasterSizeModel by inject()
private val recvMaster : Label by fxid("recvMaster")
/*Adding buttons and stuff */
init{
/*A loop that will request stats from the server i keept it very simple so i can resolv the view problem first */
var run= true
val thread = thread(start= true, name="StatRequester"){while(run){req.req.get().sendOrder(StatOrder(OrderType.STAT))}}
//Change the label whenever the number of recived messages ist raised
recvMaster.textProperty().set(masterSizeModel.size.toString())
}
}
class Requester(val address: String = "localhost", var masterSize: MasterSizeModel ) : Sender() {
override val socket: ZMQ.Socket = context.socket(ZMQ.REQ)
override val port = "4993"
init {
socket.connect("$protocol$address:$port")
}
override fun sendOrder(order: Order) {
//ZeroMQ requires special care....
val message = packOrder(order)
val wrapper = packOrder(RequestOrder(order.type, message))
socket.send(wrapper,0)
val orderAsString = socket.recvStr(0)
handleOrder(orderAsString)
}
private fun handleOrder(orderString: String)
{
val orderDedec = unpackOrder<RequestOrder>(orderString)
when(orderDedec.type)
{
OrderType.STAT ->{
val order = unpackOrder<StatOrder>(orderDedec.order)
sleep(5000) //sleep for debugging only
println("$masterSize, ${order.masterStatSize}")
//Here I receive a new value and want to update the label in my view
masterSize.size = order.masterStatSize
}
OrderType.STOP ->{
close()
}
else ->{}
}
}
override fun close() {
socket.close()
context.term()
}
}
Consider folowing example:
class Item(name: String, number: Int) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val numberProperty by lazy { SimpleIntegerProperty(number) }
var number by numberProperty
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).makeEditable(NumberStringConverter())
enableCellEditing()
}
}
}
How can I add a validator while editing cells? Is the only way to do that is to add rowExpander with some textfield and try to validate a model there?
You can either implement your own cellfactory and return a cell that shows a textfield bound to a ViewModel when in edit mode and an label if not. Alternatively, if you're fine with always displaying a textfield, you can use cellFormat and bind the current item to an ItemModel so you can attach validation:
class ItemModel(item: Item) : ItemViewModel<Item>(item) {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFormat {
val model = ItemModel(rowItem)
graphic = textfield(model.number, NumberStringConverter()) {
validator {
if (model.number.value == 123) error("Invalid number") else null
}
}
}
}
}
}
It will look like this:
While it works, it's sort of wasteful since the nodes are recreated frequently. I would recommend approach number one if performance is a concern, until we get cellFragment support for TableView like we have for ListView.
EDIT: I implemented cellFragment support, so it's possible to create a more robust solution which will show a label when not in edit mode and a validating textfield when you enter edit mode.
class ItemModel : ItemViewModel<Item>() {
val name = bind(Item::nameProperty)
val number = bind(Item::numberProperty)
}
class MainView : View("Example") {
val items = listOf(Item("One", 1), Item("Two", 2)).observable()
override val root = vbox {
tableview(items) {
column("Name", Item::nameProperty).makeEditable()
column("Number", Item::numberProperty).cellFragment(NumberEditor::class)
}
}
}
class NumberEditor : TableCellFragment<Item, Number>() {
// Bind our ItemModel to the rowItemProperty, which points to the current Item
val model = ItemModel().bindToRowItem(this)
override val root = stackpane {
textfield(model.number, NumberStringConverter()) {
removeWhen(editingProperty.not())
validator {
if (model.number.value == 123L) error("Invalid number") else null
}
// Call cell.commitEdit() only if validation passes
action {
if (model.commit()) {
cell?.commitEdit(model.number.value)
}
}
}
// Label is visible when not in edit mode, and always shows committed value (itemProperty)
label(itemProperty) {
removeWhen(editingProperty)
}
}
// Make sure we rollback our model to avoid showing the last failed edit
override fun startEdit() {
model.rollback()
}
}
This will be possible starting from TornadoFX 1.7.9.