From ed921ee85bb01f3042044ea31c7c6e160b1efb5c Mon Sep 17 00:00:00 2001 From: Kazik Pogoda Date: Thu, 15 Aug 2024 15:31:33 +0200 Subject: [PATCH] RxJava replaced with coroutines and Kotlin flows --- .gitignore | 4 +- README.md | 266 +++++-------- build.gradle.kts | 83 ++-- demo/mvp-implementation/build.gradle.kts | 28 ++ .../src/main/kotlin/DefaultInternet.kt | 38 ++ .../src/main/kotlin/MvpBrowserApp.kt | 36 ++ .../src/main/kotlin/MvpBrowserView.kt | 74 ++++ .../src/main/kotlin/MvpMultiBrowserApp.kt | 36 ++ demo/mvp-presenter/build.gradle.kts | 36 ++ .../src/main/kotlin/MvpBrowserPresenter.kt | 104 +++++ .../src/test/kotlin/BrowserPresenterTest.kt | 208 ++++++++++ demo/simple-java/build.gradle.kts | 27 ++ .../kotlin/swing/demo/MyBrowserJava.java | 102 +++-- demo/simple-kotlin-dsl/build.gradle.kts | 27 ++ .../src/main/kotlin/ComponentCatalog.kt | 64 ++++ .../src/main/kotlin/MousePosition.kt | 22 +- .../src/main/kotlin/MyBrowserKotlin.kt | 82 ++++ .../src/main/kotlin/TimeTicks.kt | 46 +++ docs/xemantic-kotlin-swing-dsl-example.png | Bin 10647 -> 7087 bytes gradle.properties | 2 + gradle/libs.versions.toml | 16 + gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 9 + src/main/kotlin/Swings.kt | 361 ------------------ src/test/kotlin/SwingKotlinWay.kt | 64 ---- ...KotlinWayWithReactiveModelViewPresenter.kt | 120 ------ .../build.gradle.kts | 30 ++ .../src/main/kotlin/Borders.kt | 64 ++-- .../src/main/kotlin/Events.kt | 97 +++++ .../src/main/kotlin/Panels.kt | 128 +++++++ .../src/main/kotlin/Widgets.kt | 60 +++ .../src/main/kotlin/Windows.kt | 154 ++++++++ .../build.gradle.kts | 29 ++ .../src/main/kotlin/SwingTests.kt | 38 ++ 34 files changed, 1612 insertions(+), 845 deletions(-) create mode 100644 demo/mvp-implementation/build.gradle.kts create mode 100644 demo/mvp-implementation/src/main/kotlin/DefaultInternet.kt create mode 100644 demo/mvp-implementation/src/main/kotlin/MvpBrowserApp.kt create mode 100644 demo/mvp-implementation/src/main/kotlin/MvpBrowserView.kt create mode 100644 demo/mvp-implementation/src/main/kotlin/MvpMultiBrowserApp.kt create mode 100644 demo/mvp-presenter/build.gradle.kts create mode 100644 demo/mvp-presenter/src/main/kotlin/MvpBrowserPresenter.kt create mode 100644 demo/mvp-presenter/src/test/kotlin/BrowserPresenterTest.kt create mode 100644 demo/simple-java/build.gradle.kts rename src/test/java/com/xemantic/kotlin/swing/SwingJavaWay.java => demo/simple-java/src/main/java/com/xemantic/kotlin/swing/demo/MyBrowserJava.java (52%) create mode 100644 demo/simple-kotlin-dsl/build.gradle.kts create mode 100644 demo/simple-kotlin-dsl/src/main/kotlin/ComponentCatalog.kt rename src/test/kotlin/SwingSchedulerExample.kt => demo/simple-kotlin-dsl/src/main/kotlin/MousePosition.kt (63%) create mode 100644 demo/simple-kotlin-dsl/src/main/kotlin/MyBrowserKotlin.kt create mode 100644 demo/simple-kotlin-dsl/src/main/kotlin/TimeTicks.kt create mode 100644 gradle/libs.versions.toml delete mode 100644 src/main/kotlin/Swings.kt delete mode 100644 src/test/kotlin/SwingKotlinWay.kt delete mode 100644 src/test/kotlin/SwingKotlinWayWithReactiveModelViewPresenter.kt create mode 100644 xemantic-kotlin-swing-dsl-core/build.gradle.kts rename src/test/kotlin/Presenter.kt => xemantic-kotlin-swing-dsl-core/src/main/kotlin/Borders.kt (52%) create mode 100644 xemantic-kotlin-swing-dsl-core/src/main/kotlin/Events.kt create mode 100644 xemantic-kotlin-swing-dsl-core/src/main/kotlin/Panels.kt create mode 100644 xemantic-kotlin-swing-dsl-core/src/main/kotlin/Widgets.kt create mode 100644 xemantic-kotlin-swing-dsl-core/src/main/kotlin/Windows.kt create mode 100644 xemantic-kotlin-swing-dsl-test/build.gradle.kts create mode 100644 xemantic-kotlin-swing-dsl-test/src/main/kotlin/SwingTests.kt diff --git a/.gitignore b/.gitignore index 28e09e2..bd4e30d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # gradle files -/build/ +build/ /.gradle # idea files @@ -8,4 +8,4 @@ !/.idea/dictionaries/ !/.idea/codeStyles/ /*.iml -/out +out diff --git a/README.md b/README.md index 2fcb855..c0e34df 100644 --- a/README.md +++ b/README.md @@ -2,196 +2,132 @@ _Express your Swing code easily in Kotlin_ -This project was born when I had an urgent need to quickly provide a simple UI for remote JVM -application. I needed a [remote control for my Robot](https://xemantic.com/#we-are-the-robots), -talking over [OSC](https://en.wikipedia.org/wiki/Open_Sound_Control) protocol, already coded in -Kotlin and based on -[functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) -principles (See [we-are-the-robots](https://github.com/xemantic/we-are-the-robots) on GitHub). I -decided to go for Swing, to stay in the same ecosystem, but I remember the experience of coding GUI -in Swing years ago, and I remember what I liked about it and what was painful. Fortunately now I -also have the experience of building Domain Specific Languages in Kotlin and I quickly realized that -I can finally swing the way I always wanted to. +## Why? +Kotlin provides incredible language sugar over pure Java. Using Kotlin +instead of Java for writing Swing UI already makes the code more concise, but +**what if the Swing was written from scratch, to provide Kotlin-idiomatic way of doing things?** +This is the intent behind the `xemantic-kotlin-swing-dsl` library - to deliver a +[Domain Specific Language](https://en.wikipedia.org/wiki/Domain-specific_language) +for building Swing based UI and react to UI events by utilizing +[Kotlin coroutines](https://kotlinlang.org/docs/coroutines-overview.html). + +## Usage + +Add to your `build.gradle.kts` + +```kotlin +dependencies { + implementation("com.xemantic.kotlin:xemantic-kotlin-swing-dsl-core:$libVersion") + runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutinesVersion") +} +``` ## Example +Here is a simple internet browser. For the sake of example, instead of rendering +the full HTML, it will just download a content from provided URL address +and display it as a text. + ```kotlin -import com.badoo.reaktive.observable.* -import com.badoo.reaktive.scheduler.ioScheduler +import com.xemantic.kotlin.swing.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import java.awt.Dimension -import javax.swing.SwingConstants +import java.net.URI -fun main() = mainFrame("My Browser") { - val contentBox = label("") { - horizontalAlignment = SwingConstants.CENTER - verticalAlignment = SwingConstants.CENTER +fun main() = MainWindow("My Browser") { + val urlBox = TextField() + val goButton = Button("Go!") { isEnabled = false } + val contentBox = TextArea { preferredSize = Dimension(300, 300) } - val urlBox = textField(10) - val goAction = button("Go!") { - isEnabled = false - actionEvents - .withLatestFrom(urlBox.textChanges) { _, url: String -> url } - .doOnAfterNext { url -> - isEnabled = false - contentBox.text = "Loading: $url" + + urlBox.textChanges.listen { url -> + goButton.isEnabled = url.isNotBlank() + } + + merge( + goButton.actionEvents, + urlBox.actionEvents + ) + .filter { goButton.isEnabled } + .onEach { goButton.isEnabled = false } + .map { urlBox.text } + .flowOn(Dispatchers.Main) + .map { + try { + URI(it).toURL().readText() + } catch (e : Exception) { + e.message + } + } + .flowOn(Dispatchers.IO) + .listen { + contentBox.text = it + goButton.isEnabled = true + } + + Border.empty(4) { + BorderPanel { + layout { + hgap = 4 + vgap = 4 } - .delay(1000, ioScheduler) // e.g. REST request - .observeOn(swingScheduler) - .subscribe { url -> - isEnabled = true - contentBox.text = "Ready: $url" + north { + BorderPanel { + layout { + hgap = 4 + vgap = 4 + } + west { + Label("URL") } + center { urlBox } + east { goButton } + } } - } - urlBox.textChanges.subscribe { url -> - goAction.isEnabled = url.isNotBlank() - contentBox.text = "Will load: $url" - } - contentPane = borderPanel { - layout.hgap = 4 - panel.border = emptyBorder(4) - north = borderPanel { - west = label("URL") - center = urlBox - east = goAction + center { ScrollPane { contentBox } } } - center = contentBox } + } ``` -will produce: +When run it will produce: ![example app image](docs/xemantic-kotlin-swing-dsl-example.png) -Benefits: - -* compact code, minimal verbosity -* declarative instead of imperative -* functional reactive programming way of handling events - ([Reaktive](https://github.com/badoo/Reaktive)) -* component encapsulation - communication through well defined event streams -* `swingScheduler` for receiving asynchronously produced events (see below) - -And here is an equivalent code in Java for the sake of comparison: +### Notable conventions -```java -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.lang.reflect.InvocationTargetException; -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; +* No `J` prefix in UI component names (historically `J` was added to differentiate + Swing components from AWT components and is mostly irrelevant for modern purposes). +* Master JFrame is created with the `WainWindow` builder, which also takes care of setting + up the `SwingScope` holding a coroutine scope bound to Swing's event dispatcher thread. +* Instead of callbacks, events are delivered through `Flow`s, the `listen()` function will + collect the flow in the newly launched coroutine, which is cancelled when the window is closed. +* Other coroutine dispatchers, like `IO`, can be used in the event processing pipeline + by adding the `flowOn`. This makes the cumbersome `SwingWorker` obsolete. +* Each UI component can be immediately configured with direct access to its properties. +* Panels are specified with special builders, allowing intuitive development of the + component tree. -public class SwingJavaWay { +### Key benefits - public static void main(String[] args) throws InvocationTargetException, InterruptedException { - SwingUtilities.invokeAndWait(SwingJavaWay::createFrame); - } +* compact UI code with minimal verbosity +* declarative instead of imperative UI building +* reactive event handling using `Flow`s, - private static void createFrame() { - - JFrame frame = new JFrame("My Browser"); - - final JLabel contentBox = new JLabel(); - contentBox.setHorizontalAlignment(SwingConstants.CENTER); - contentBox.setVerticalAlignment(SwingConstants.CENTER); - contentBox.setPreferredSize(new Dimension(300, 300)); - - JPanel contentPanel = new JPanel(new BorderLayout()); - - JPanel northContent = new JPanel(new BorderLayout(4, 0)); - northContent.add(new JLabel(("URL")), BorderLayout.WEST); - northContent.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); - - JTextField urlBox = new JTextField(10); - JButton goAction = new JButton("Go!"); - goAction.setEnabled(false); - - goAction.addActionListener( - e -> { - contentBox.setText("Loading: " + urlBox.getText()); - goAction.setEnabled(false); - Timer timer = - new Timer( - 1000, - t -> { - contentBox.setText("Ready: " + urlBox.getText()); - goAction.setEnabled(true); - }); - timer.setRepeats(false); - timer.start(); - }); - northContent.add(goAction, BorderLayout.EAST); - - urlBox - .getDocument() - .addDocumentListener( - new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - fireChange(e); - } - - @Override - public void removeUpdate(DocumentEvent e) { - fireChange(e); - } - - @Override - public void changedUpdate(DocumentEvent e) { - fireChange(e); - } - - private void fireChange(DocumentEvent e) { - String text = urlBox.getText(); - contentBox.setText("Will load: " + text); - goAction.setEnabled(!text.trim().isEmpty()); - } - }); - - northContent.add(urlBox, BorderLayout.CENTER); - contentPanel.add(northContent, BorderLayout.NORTH); - contentPanel.add(contentBox, BorderLayout.CENTER); - - frame.setContentPane(contentPanel); - frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - frame.pack(); - frame.setVisible(true); - } -} -``` - -:information_source: There is also another more elaborate Kotlin example: -[SwingKotlinWayWithReactiveModelViewPresenter](src/test/kotlin/SwingKotlinWayWithReactiveModelViewPresenter.kt) -, showing how to use [Model-View-Presenter](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) -pattern with this library, where events from the view are represented as reactive `Observable`s. +And here is an equivalent code in Java for the sake of comparison: +The code above is taken from the +[MyBrowserKotlin](demo/simple-kotlin-dsl/src/main/kotlin/MyBrowserKotlin.kt) demo. -## Swing scheduler +### How would it look in pure Java Swing? -By default everything in Swing is supposed to run on the same thread. Any update to any GUI -component should happen there, otherwise concurrency might cause consistency issues. But in many -cases long running computation, or asynchronous IO, will require us to receive results in one thread -and display them in the main Swing thread. This is what `swingScheduler` is for: +For the sake of comparison the +[MyBrowserKotlin](demo/simple-java/src/main/java/com/xemantic/kotlin/swing/demo/MyBrowserJava.java) +demo implements exactly the same "browser" in pure Java. -```kotlin -fun main() = mainFrame("swingScheduler example") { - contentPane = label{ - observableInterval(1000, swingScheduler) - .subscribe { tick -> - text = tick.toString() - } - preferredSize = Dimension(100, 100) - horizontalAlignment = SwingConstants.CENTER - } -} -``` +## Other examples -The `observableInterval` creates a constant stream of ticks produced by another thread. -Once they happen, the subscription code will be handled by the Swing thread. -Most of the time it is not needed to specify `swingScheduler` for simple event handling, -because the default scheduler for receiving events will be usually the same as the one -used for publishing them, and this one is already the -Swing event thread. +The [demo](demo) folder contains additional examples. diff --git a/build.gradle.kts b/build.gradle.kts index b654ded..c20cbff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ /* * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. * - * Copyright (C) 2021 Kazimierz Pogoda + * Copyright (C) 2024 Kazimierz Pogoda * * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as @@ -19,44 +19,23 @@ */ plugins { - kotlin("jvm") version "1.5.21" - signing `maven-publish` + signing + alias(libs.plugins.versions) } -group = "com.xemantic.kotlin" -version = "1.0-SNAPSHOT" - -val javaCompatibilityVersion = "1.8" -val kotlinCompatibilityVersion = "1.5" -val reaktiveVersion = "1.2.1" - -repositories { - mavenCentral() -} - -dependencies { - implementation("com.badoo.reaktive:reaktive:$reaktiveVersion") -} - -tasks { - - compileKotlin { - kotlinOptions { - jvmTarget = javaCompatibilityVersion - apiVersion = kotlinCompatibilityVersion - languageVersion = kotlinCompatibilityVersion - } - sourceCompatibility = javaCompatibilityVersion +allprojects { + repositories { + mavenCentral() } - } publishing { publications { create("mavenJava") { - from(components["kotlin"]) + groupId = "com.xemantic.kotlin" + //from(components["kotlin"]) pom { name.set("xemantic-kotlin-swing-dsl") description.set("Kotlin goodies for Java Swing") @@ -81,35 +60,33 @@ publishing { } } } +} - repositories { -// maven { -// name = "OSSRH" -// setUrl("https://oss.sonatype.org/service/local/staging/deploy/maven2/") -// credentials { -// username = System.getenv("MAVEN_USERNAME") -// password = System.getenv("MAVEN_PASSWORD") -// } -// } - maven { - name = "GitHubPackages" - setUrl("https://maven.pkg.github.com/xemantic/xemantic-kotlin-swing-dsl") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } +repositories { + maven { + name = "OSSRH" + setUrl("https://s01.oss.sonatype.org/content/repositories/snapshots/") + credentials { + username = System.getenv("MAVEN_USERNAME") + password = System.getenv("MAVEN_PASSWORD") } } - - publications.withType { - - // Stub javadoc.jar artifact -// artifact(javadocJar.get()) - - + maven { + name = "GitHubPackages" + setUrl("https://maven.pkg.github.com/xemantic/xemantic-kotlin-swing-dsl") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } } } signing { -// sign(publishing.publications) + if ( + project.hasProperty("signing.keyId") + && project.hasProperty("signing.password") + && project.hasProperty("signing.secretKeyRingFile") + ) { + sign(publishing.publications) + } } diff --git a/demo/mvp-implementation/build.gradle.kts b/demo/mvp-implementation/build.gradle.kts new file mode 100644 index 0000000..884d35f --- /dev/null +++ b/demo/mvp-implementation/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +dependencies { + implementation(project(":xemantic-kotlin-swing-dsl-core")) + implementation(project(":demo:mvp-presenter")) +} diff --git a/demo/mvp-implementation/src/main/kotlin/DefaultInternet.kt b/demo/mvp-implementation/src/main/kotlin/DefaultInternet.kt new file mode 100644 index 0000000..799bec5 --- /dev/null +++ b/demo/mvp-implementation/src/main/kotlin/DefaultInternet.kt @@ -0,0 +1,38 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +package com.xemantic.kotlin.swing.demo.mvp + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URI + +class DefaultInternet( + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : Internet { + + override suspend fun download( + url: String + ): String = withContext(ioDispatcher) { + URI(url).toURL().readText() + } + +} diff --git a/demo/mvp-implementation/src/main/kotlin/MvpBrowserApp.kt b/demo/mvp-implementation/src/main/kotlin/MvpBrowserApp.kt new file mode 100644 index 0000000..462128a --- /dev/null +++ b/demo/mvp-implementation/src/main/kotlin/MvpBrowserApp.kt @@ -0,0 +1,36 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.demo.mvp + +import com.xemantic.kotlin.swing.* +import kotlinx.coroutines.launch + +fun main(vararg args: String) = MainWindow("My Browser") { + val internet = DefaultInternet() + val view = SwingBrowserView() + val presenter = BrowserPresenter(view, scope, internet) + if (args.isNotEmpty()) { + val url = args[0] + scope.launch { + presenter.open(url) + } + } + view.swingComponent +} diff --git a/demo/mvp-implementation/src/main/kotlin/MvpBrowserView.kt b/demo/mvp-implementation/src/main/kotlin/MvpBrowserView.kt new file mode 100644 index 0000000..c817b24 --- /dev/null +++ b/demo/mvp-implementation/src/main/kotlin/MvpBrowserView.kt @@ -0,0 +1,74 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +package com.xemantic.kotlin.swing.demo.mvp + +import com.xemantic.kotlin.swing.* +import java.awt.Dimension + +/** + * Swing implementation of the [BrowserView], could be also JavaFX, or HTML + JS or native iOS. + */ +class SwingBrowserView : BrowserView { + private val urlField = TextField() + private val goAction = Button("Go!") { + isEnabled = false + } + private val contentBox = TextArea { + preferredSize = Dimension(300, 300) + } + + override val urlEdits = urlField.textChanges + override val goActions = goAction.actionEvents.asActions + override val urlActions = urlField.actionEvents.asActions + override var goActionEnabled: Boolean + get() = goAction.isEnabled + set(value) { goAction.isEnabled = value } + override var content: String + get() = contentBox.text + set(value) { contentBox.text = value } + override var url: String + get() = urlField.text + set(value) { urlField.text = value } + + val swingComponent = Border.empty(4) { + BorderPanel { + layout { + hgap = 4 + vgap = 4 + } + north { + BorderPanel { + layout { + hgap = 4 + vgap = 4 + } + west { Label("URL") } + center { urlField } + east { goAction } + } + } + center { + contentBox + } + } + } + +} diff --git a/demo/mvp-implementation/src/main/kotlin/MvpMultiBrowserApp.kt b/demo/mvp-implementation/src/main/kotlin/MvpMultiBrowserApp.kt new file mode 100644 index 0000000..98e199f --- /dev/null +++ b/demo/mvp-implementation/src/main/kotlin/MvpMultiBrowserApp.kt @@ -0,0 +1,36 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.demo.mvp + +import com.xemantic.kotlin.swing.* + +fun main() = MainWindow("Browser spawner") { + val internet = DefaultInternet() + val newBrowserButton = Button("New browser") + var browserCounter = 1 + newBrowserButton.actionEvents.listen { + frame("Browser ${browserCounter++}") { + val view = SwingBrowserView() + BrowserPresenter(view, scope, internet) + view.swingComponent + } + } + newBrowserButton +} diff --git a/demo/mvp-presenter/build.gradle.kts b/demo/mvp-presenter/build.gradle.kts new file mode 100644 index 0000000..0fc7192 --- /dev/null +++ b/demo/mvp-presenter/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +dependencies { + implementation(project(":xemantic-kotlin-swing-dsl-core")) + + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.kotest.assertions.core) + testImplementation(project(":xemantic-kotlin-swing-dsl-test")) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/demo/mvp-presenter/src/main/kotlin/MvpBrowserPresenter.kt b/demo/mvp-presenter/src/main/kotlin/MvpBrowserPresenter.kt new file mode 100644 index 0000000..d7b9797 --- /dev/null +++ b/demo/mvp-presenter/src/main/kotlin/MvpBrowserPresenter.kt @@ -0,0 +1,104 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.demo.mvp + +import com.xemantic.kotlin.swing.Action +import com.xemantic.kotlin.swing.swingScope +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +/** + * Abstract representation of all the interactions the [BrowserPresenter] + * might have with the [BrowserView]. This way the view can be easily mocked and + * presenter can be tested with assertions run against such a mock. + * Passive view can be also implemented in any UI toolkit on any + * supported platform. It is designed according to the Model-View-Presenter + * pattern. + * + * All the events coming from the view are represented as [Flow]s + * which can be subscribed to. A mutable view state is represented as + * simple `var` properties, but it could be also a function call. + */ +interface BrowserView { + val urlEdits: Flow + val goActions: Flow + val urlActions: Flow + var goActionEnabled: Boolean + var content: String + var url: String +} + +/** + * Represents asynchronous retrieval of the data from the Internet. + */ +interface Internet { + suspend fun download(url: String): String +} + +class BrowserPresenter( + private val view: BrowserView, + scope: CoroutineScope, + internet: Internet +) { + + var loading = false + var url = "" + private set + + private val urlFlow = MutableSharedFlow() + + init { + scope.swingScope { + view.urlEdits.listen("urls") { + url = it + view.goActionEnabled = url.isNotBlank() + } + merge( + urlFlow, + view.goActions.map { url }, + view.urlActions.filter { url.isNotBlank() }.map { url } + ) + .filterNot { loading } + .onEach { + loading = true + view.goActionEnabled = false + } + .map { + try { + internet.download(url) + } catch (e : Exception) { + "$e" + } + } + .listen("content") { + loading = false + view.goActionEnabled = true + view.content = it + } + } + } + + suspend fun open(url: String) { + this.url = url + view.url = url + urlFlow.emit(url) + } + +} diff --git a/demo/mvp-presenter/src/test/kotlin/BrowserPresenterTest.kt b/demo/mvp-presenter/src/test/kotlin/BrowserPresenterTest.kt new file mode 100644 index 0000000..5ba2fe0 --- /dev/null +++ b/demo/mvp-presenter/src/test/kotlin/BrowserPresenterTest.kt @@ -0,0 +1,208 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.demo.mvp + +import com.xemantic.kotlin.swing.Action +import com.xemantic.kotlin.swing.action +import com.xemantic.kotlin.swing.dsl.test.runSwingTest +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.beBlank +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) +class BrowserPresenterTest { + + @Test + fun `content should be blank and go action should be disabled initially`() = runSwingTest { + BrowserPresenter(view, this, internet) + advanceUntilIdle() + + // then + view.apply { + content should beBlank() + goActionEnabled shouldBe false + } + } + + @Test + fun `go action should be enabled after typing some characters`() = runSwingTest { + // given + val presenter = BrowserPresenter(view, this, internet) + advanceUntilIdle() + + // when + view.urlEdits.emit("foo") + advanceUntilIdle() + + // then + presenter.apply { + loading shouldBe false + url shouldBe "foo" + } + view.apply { + content should beBlank() + goActionEnabled shouldBe true + } + } + + @Test + fun `go action should download and open the URL`() = runSwingTest { + // given + val presenter = BrowserPresenter(view, this, internet) + advanceUntilIdle() + view.urlEdits.emit("https://example.com") + + // when + view.goActions.emit(action) + advanceUntilIdle() + + // then + presenter.apply { + loading shouldBe false + url shouldBe "https://example.com" + } + view.apply { + content shouldBe "pong: https://example.com" + goActionEnabled shouldBe true + } + } + + @Test + fun `url action should download and open the URL`() = runSwingTest { + // given + val presenter = BrowserPresenter(view, this, internet) + advanceUntilIdle() + view.urlEdits.emit("https://example.com") + + // when + view.urlActions.emit(action) + advanceUntilIdle() + + // then + presenter.apply { + loading shouldBe false + url shouldBe "https://example.com" + } + view.apply { + content shouldBe "pong: https://example.com" + goActionEnabled shouldBe true + } + } + + @Test + fun `open(url) should download and open the URL`() = runSwingTest { + // given + val presenter = BrowserPresenter(view, this, internet) + advanceUntilIdle() + + // when + presenter.open("https://example.com") + advanceUntilIdle() + + // then + presenter.apply { + loading shouldBe false + url shouldBe "https://example.com" + } + view.apply { + content shouldBe "pong: https://example.com" + goActionEnabled shouldBe true + url shouldBe "https://example.com" + } + } + + @Test + fun `should be in loading state until content is downloaded`() = runSwingTest { + // given + val internet = object : Internet { + val completable = CompletableDeferred() + override suspend fun download(url: String): String { + completable.await() + return "pong: $url" + } + fun downloaded() { + completable.complete(Unit) + } + } + val presenter = BrowserPresenter(view, this, internet) + advanceUntilIdle() + view.urlEdits.emit("https://example.com") + + // when + view.urlActions.emit(action) + advanceUntilIdle() + + // then + presenter.loading shouldBe true + view.goActionEnabled shouldBe false + + // when + internet.downloaded() + advanceUntilIdle() + + // then + presenter.apply { + loading shouldBe false + url shouldBe "https://example.com" + } + view.apply { + content shouldBe "pong: https://example.com" + goActionEnabled shouldBe true + } + } + + @Test + fun `should display error if exception is thrown`() = runSwingTest { + // given + val internet = object : Internet { + override suspend fun download(url: String): String { + throw IllegalArgumentException("Invalid URL: $url") + } + } + BrowserPresenter(view, this, internet) + advanceUntilIdle() + view.urlEdits.emit("foo") + + // when + view.urlActions.emit(action) + advanceUntilIdle() + + // then + view.content shouldBe "java.lang.IllegalArgumentException: Invalid URL: foo" + } + + private val view = object : BrowserView { + override val urlEdits: MutableSharedFlow = MutableSharedFlow() + override val goActions: MutableSharedFlow = MutableSharedFlow() + override val urlActions: MutableSharedFlow = MutableSharedFlow() + override var goActionEnabled: Boolean = false + override var content: String = "" + override var url: String = "" + } + + private val internet = object : Internet { + override suspend fun download(url: String): String = "pong: $url" + } + +} diff --git a/demo/simple-java/build.gradle.kts b/demo/simple-java/build.gradle.kts new file mode 100644 index 0000000..19e3502 --- /dev/null +++ b/demo/simple-java/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + java +} + +dependencies { + implementation(project(":xemantic-kotlin-swing-dsl-core")) +} diff --git a/src/test/java/com/xemantic/kotlin/swing/SwingJavaWay.java b/demo/simple-java/src/main/java/com/xemantic/kotlin/swing/demo/MyBrowserJava.java similarity index 52% rename from src/test/java/com/xemantic/kotlin/swing/SwingJavaWay.java rename to demo/simple-java/src/main/java/com/xemantic/kotlin/swing/demo/MyBrowserJava.java index c07333a..354f83d 100644 --- a/src/test/java/com/xemantic/kotlin/swing/SwingJavaWay.java +++ b/demo/simple-java/src/main/java/com/xemantic/kotlin/swing/demo/MyBrowserJava.java @@ -1,7 +1,7 @@ /* * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. * - * Copyright (C) 2021 Kazimierz Pogoda + * Copyright (C) 2024 Kazimierz Pogoda * * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as @@ -17,55 +17,88 @@ * along with xemantic-kotlin-swing-dsl. If not, * see . */ - -package com.xemantic.kotlin.swing; +package com.xemantic.kotlin.swing.demo; import java.awt.BorderLayout; import java.awt.Dimension; +import java.awt.event.ActionListener; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URL; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -public class SwingJavaWay { +public class MyBrowserJava { - public static void main(String[] args) throws InvocationTargetException, InterruptedException { - SwingUtilities.invokeAndWait(SwingJavaWay::createFrame); + public static void main( + String[] args + ) throws InvocationTargetException, InterruptedException { + SwingUtilities.invokeAndWait(MyBrowserJava::mainWindow); } - private static void createFrame() { + private static void mainWindow() { JFrame frame = new JFrame("My Browser"); - final JLabel contentBox = new JLabel(); - contentBox.setHorizontalAlignment(SwingConstants.CENTER); - contentBox.setVerticalAlignment(SwingConstants.CENTER); - contentBox.setPreferredSize(new Dimension(300, 300)); - - JPanel contentPanel = new JPanel(new BorderLayout()); - - JPanel northContent = new JPanel(new BorderLayout(4, 0)); - northContent.add(new JLabel(("URL")), BorderLayout.WEST); - northContent.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + final JTextArea content = new JTextArea(); + content.setPreferredSize( + new Dimension(300, 300) + ); + + JPanel contentPanel = new JPanel( + new BorderLayout() + ); + + JPanel northContent = new JPanel( + new BorderLayout(4, 0) + ); + northContent.add( + new JLabel("URL"), + BorderLayout.WEST + ); + northContent.setBorder( + BorderFactory.createEmptyBorder(4, 4, 4, 4) + ); JTextField urlBox = new JTextField(10); JButton goAction = new JButton("Go!"); goAction.setEnabled(false); - goAction.addActionListener( - e -> { - contentBox.setText("Loading: " + urlBox.getText()); - goAction.setEnabled(false); - Timer timer = - new Timer( - 1000, - t -> { - contentBox.setText("Ready: " + urlBox.getText()); - goAction.setEnabled(true); - }); - timer.setRepeats(false); - timer.start(); - }); + ActionListener action = e -> { + goAction.setEnabled(false); + + new SwingWorker() { + @Override + protected String doInBackground() { + try { + URL url = new URI(urlBox.getText()).toURL(); + try (InputStream in = url.openStream()) { + byte[] data = in.readAllBytes(); + return new String(data); + } + } catch (Exception e) { + return e.toString(); + } + } + + @Override + protected void done() { + goAction.setEnabled(true); + try { + String text = get(); + content.setText(text); + } catch (Exception e) { + e.printStackTrace(); + // should never happen + } + } + }.execute(); + }; + + goAction.addActionListener(action); + urlBox.addActionListener(action); northContent.add(goAction, BorderLayout.EAST); urlBox @@ -89,16 +122,15 @@ public void changedUpdate(DocumentEvent e) { private void fireChange(DocumentEvent e) { String text = urlBox.getText(); - contentBox.setText("Will load: " + text); - goAction.setEnabled(!text.trim().isEmpty()); + goAction.setEnabled(!text.isBlank()); } }); northContent.add(urlBox, BorderLayout.CENTER); contentPanel.add(northContent, BorderLayout.NORTH); - contentPanel.add(contentBox, BorderLayout.CENTER); + contentPanel.add(content, BorderLayout.CENTER); - frame.setContentPane(contentPanel); + frame.setContentPane(new JScrollPane(contentPanel)); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); diff --git a/demo/simple-kotlin-dsl/build.gradle.kts b/demo/simple-kotlin-dsl/build.gradle.kts new file mode 100644 index 0000000..0f01736 --- /dev/null +++ b/demo/simple-kotlin-dsl/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +dependencies { + implementation(project(":xemantic-kotlin-swing-dsl-core")) +} diff --git a/demo/simple-kotlin-dsl/src/main/kotlin/ComponentCatalog.kt b/demo/simple-kotlin-dsl/src/main/kotlin/ComponentCatalog.kt new file mode 100644 index 0000000..e62f144 --- /dev/null +++ b/demo/simple-kotlin-dsl/src/main/kotlin/ComponentCatalog.kt @@ -0,0 +1,64 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +package com.xemantic.kotlin.swing.demo + +import com.xemantic.kotlin.swing.* +import java.awt.Dimension + +fun main() = MainWindow("Components") { + ScrollPane { + BoxPanel(BoxLayoutAxis.Y) { + +Border.title("flowPanel") { + FlowPanel { + +Button("button") + +Label("label") + +RadioButton("radioButton") + +CheckBox("checkBox") + +TextField("textField") + } + } + +Border.title("textArea") { + ScrollPane { + TextArea { + preferredSize = Dimension(300, 100) + } + } + } + +Border.title("borderPanel") { + BorderPanel { + north { Button("North") } + east { Button("North") } + center { Button("Center") } + west { Button("West") } + south { Button("South") } + } + } + +Border.title("grid") { + Grid(2, 2) { + +Button("Button 1") + +Button("Button 2") + +Button("Button 3") + +Button("Button 4") + } + } + } + } +} diff --git a/src/test/kotlin/SwingSchedulerExample.kt b/demo/simple-kotlin-dsl/src/main/kotlin/MousePosition.kt similarity index 63% rename from src/test/kotlin/SwingSchedulerExample.kt rename to demo/simple-kotlin-dsl/src/main/kotlin/MousePosition.kt index ea05acf..08f7338 100644 --- a/src/test/kotlin/SwingSchedulerExample.kt +++ b/demo/simple-kotlin-dsl/src/main/kotlin/MousePosition.kt @@ -1,7 +1,7 @@ /* * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. * - * Copyright (C) 2021 Kazimierz Pogoda + * Copyright (C) 2024 Kazimierz Pogoda * * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as @@ -18,20 +18,16 @@ * see . */ -package com.xemantic.kotlin.swing +package com.xemantic.kotlin.swing.demo -import com.badoo.reaktive.observable.observableInterval -import com.badoo.reaktive.observable.subscribe +import com.xemantic.kotlin.swing.* import java.awt.Dimension -import javax.swing.SwingConstants -fun main() = mainFrame("swingScheduler example") { - contentPane = label{ - observableInterval(1000, swingScheduler) - .subscribe { tick -> - text = tick.toString() - } - preferredSize = Dimension(100, 100) - horizontalAlignment = SwingConstants.CENTER +fun main() = MainWindow("Mouse Position") { + Label { + preferredSize = Dimension(400, 400) + mouseMoves.listen { + text = "x: ${it.x}, y: ${it.y}" + } } } diff --git a/demo/simple-kotlin-dsl/src/main/kotlin/MyBrowserKotlin.kt b/demo/simple-kotlin-dsl/src/main/kotlin/MyBrowserKotlin.kt new file mode 100644 index 0000000..1be9878 --- /dev/null +++ b/demo/simple-kotlin-dsl/src/main/kotlin/MyBrowserKotlin.kt @@ -0,0 +1,82 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.demo + +import com.xemantic.kotlin.swing.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.awt.Dimension +import java.net.URI + +fun main() = MainWindow("My Browser") { + val urlBox = TextField() + val goButton = Button("Go!") { isEnabled = false } + val contentBox = TextArea { + preferredSize = Dimension(300, 300) + } + + urlBox.textChanges.listen { url -> + goButton.isEnabled = url.isNotBlank() + } + + merge( + goButton.actionEvents, + urlBox.actionEvents + ) + .filter { goButton.isEnabled } + .onEach { goButton.isEnabled = false } + .map { urlBox.text } + .flowOn(Dispatchers.Main) + .map { + try { + URI(it).toURL().readText() + } catch (e : Exception) { + e.message + } + } + .flowOn(Dispatchers.IO) + .listen { + contentBox.text = it + goButton.isEnabled = true + } + + Border.empty(4) { + BorderPanel { + layout { + hgap = 4 + vgap = 4 + } + north { + BorderPanel { + layout { + hgap = 4 + vgap = 4 + } + west { + Label("URL") } + center { urlBox } + east { goButton } + } + } + center { ScrollPane { contentBox } } + } + } + +} diff --git a/demo/simple-kotlin-dsl/src/main/kotlin/TimeTicks.kt b/demo/simple-kotlin-dsl/src/main/kotlin/TimeTicks.kt new file mode 100644 index 0000000..2dc3870 --- /dev/null +++ b/demo/simple-kotlin-dsl/src/main/kotlin/TimeTicks.kt @@ -0,0 +1,46 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +package com.xemantic.kotlin.swing.demo + +import com.xemantic.kotlin.swing.Label +import com.xemantic.kotlin.swing.MainWindow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import java.awt.Dimension +import javax.swing.SwingConstants +import kotlin.time.Duration.Companion.seconds + +fun main() = MainWindow("Time Ticks") { + Label { + preferredSize = Dimension(100, 100) + horizontalAlignment = SwingConstants.CENTER + flow { + var count = 0 + while (true) { + emit(count++) + delay(1.seconds) // delay is suspending the coroutine + // so we are not blocking Swing event dispatcher thread + } + }.listen { + text = "$it" + } + } +} diff --git a/docs/xemantic-kotlin-swing-dsl-example.png b/docs/xemantic-kotlin-swing-dsl-example.png index fb436b6b256a0ce716bc1048b79e8888e55dda67..f7f3f9d2170ce867a73bb66896cc7299745737ec 100644 GIT binary patch literal 7087 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 literal 10647 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. - */ - -package com.xemantic.kotlin.swing - -import com.badoo.reaktive.base.setCancellable -import com.badoo.reaktive.observable.Observable -import com.badoo.reaktive.observable.map -import com.badoo.reaktive.observable.observable -import com.badoo.reaktive.scheduler.Scheduler -import com.badoo.reaktive.scheduler.computationScheduler -import java.awt.* -import java.awt.event.ActionEvent -import java.awt.event.ActionListener -import java.awt.event.MouseEvent -import java.awt.event.MouseListener -import javax.swing.* -import javax.swing.border.Border -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener -import javax.swing.text.JTextComponent - -private val factory = DefaultJComponentFactory() - -fun mainFrame(title: String, build: JFrame.() -> Unit) = SwingUtilities.invokeAndWait { - val frame = JFrame(title) - build(frame) - frame.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE - frame.pack() - frame.isVisible = true -} - -fun borderPanel(build: BorderPanelBuilder.() -> Unit): JPanel = - factory.borderPanel(build) - -fun flowPanel(build: DefaultPanelBuilder.() -> Unit): JPanel = - factory.flowPanel(build) - -fun verticalPanel(build: DefaultPanelBuilder.() -> Unit): JPanel = - factory.verticalPanel(build) - -fun grid(rows: Int, cols: Int, build: DefaultPanelBuilder.() -> Unit): JPanel = - factory.grid(rows, cols, build) - -fun button(label: String, build: (JButton.() -> Unit)? = null): JButton = - factory.button(label, build) - -fun label(label: String = "", build: (JLabel.() -> Unit)? = null): JLabel = - factory.label(label, build) - -fun textField(columns: Int, build: (JTextField.() -> Unit)? = null): JTextField = - factory.textField(columns, build) - -fun textArea( - text: String? = null, rows: Int = 0, columns: Int = 0, build: (JTextArea.() -> Unit)? = null -): JTextArea = - factory.textArea(text, rows, columns, build) - -fun border(title: String, build: () -> T): T = - factory.border(title, build) - -fun emptyBorder(width: Int): Border = emptyBorder(width, width, width, width) - -fun emptyBorder(top: Int, left: Int, bottom: Int, right: Int): Border = - BorderFactory.createEmptyBorder(top, left, bottom, right) - -interface PanelBuilder { - val panel: JPanel -} - -class BorderPanelBuilder(override val panel: JPanel) : PanelBuilder { - var north: JComponent - get() = throw NotImplementedError() - set(value) = panel.add(value, BorderLayout.NORTH) - var south: JComponent - get() = throw NotImplementedError() - set(value) = panel.add(value, BorderLayout.SOUTH) - var east: JComponent - get() = throw NotImplementedError() - set(value) = panel.add(value, BorderLayout.EAST) - var west: JComponent - get() = throw NotImplementedError() - set(value) = panel.add(value, BorderLayout.WEST) - var center: JComponent - get() = throw NotImplementedError() - set(value) = panel.add(value, BorderLayout.CENTER) - val layout = panel.layout as BorderLayout -} - -interface JComponentFactory { - - fun borderPanel(build: BorderPanelBuilder.() -> Unit): JPanel - - fun flowPanel(build: DefaultPanelBuilder.() -> Unit): JPanel - - fun verticalPanel(build: DefaultPanelBuilder.() -> Unit): JPanel - - fun grid(rows: Int, cols: Int, build: DefaultPanelBuilder.() -> Unit): JPanel - - fun button(label: String, build: (JButton.() -> Unit)? = null): JButton - - fun label(label: String = "", build: (JLabel.() -> Unit)? = null): JLabel - - fun textField(columns: Int, build: (JTextField.() -> Unit)? = null): JTextField - - fun textArea( - text: String? = null, rows: Int = 0, columns: Int = 0, build: (JTextArea.() -> Unit)? = null - ): JTextArea - - fun radioButton(label: String, build: (JRadioButton.() -> Unit)? = null): JRadioButton - - fun border(title: String, build: () -> T): T - -} - -class DefaultPanelBuilder(override val panel: JPanel) : - PanelBuilder, JComponentFactory { - - override fun borderPanel(build: BorderPanelBuilder.() -> Unit): JPanel = - add(factory.borderPanel(build)) - - override fun flowPanel(build: DefaultPanelBuilder.() -> Unit): JPanel = - add(factory.flowPanel(build)) - - override fun verticalPanel(build: DefaultPanelBuilder.() -> Unit): JPanel = - add(factory.verticalPanel(build)) - - override fun grid( - rows: Int, cols: Int, build: DefaultPanelBuilder.() -> Unit - ): JPanel = - add(factory.grid(rows, cols, build)) - - override fun button(label: String, build: (JButton.() -> Unit)?): JButton = - add(factory.button(label, build)) - - override fun label(label: String, build: (JLabel.() -> Unit)?): JLabel = - add(factory.label(label, build)) - - override fun textField(columns: Int, build: (JTextField.() -> Unit)?): JTextField = - add(factory.textField(columns, build)) - - override fun textArea( - text: String?, rows: Int, columns: Int, build: (JTextArea.() -> Unit)? - ): JTextArea = - add(factory.textArea(text, rows, columns, build)) - - override fun radioButton(label: String, build: (JRadioButton.() -> Unit)?): JRadioButton = - add(factory.radioButton(label, build)) - - override fun border(title: String, build: () -> T): T = - add(factory.border(title, build)) - - fun add(component: T): T { - panel.add(component) - return component - } - - @Suppress("UNCHECKED_CAST") - val layout = panel.layout as L - -} - -class DefaultJComponentFactory : JComponentFactory { - - override fun borderPanel(build: BorderPanelBuilder.() -> Unit): JPanel { - val panel = JPanel(BorderLayout()) - build(BorderPanelBuilder(panel)) - return panel - } - - override fun flowPanel(build: DefaultPanelBuilder.() -> Unit): JPanel { - val panel = JPanel() - build(DefaultPanelBuilder(panel)) - return panel - } - - override fun verticalPanel(build: DefaultPanelBuilder.() -> Unit): JPanel { - val panel = JPanel() - val layout = BoxLayout(panel, BoxLayout.Y_AXIS) - panel.layout = layout - build(DefaultPanelBuilder(panel)) - return panel - } - - override fun grid( - rows: Int, cols: Int, build: DefaultPanelBuilder.() -> Unit - ): JPanel { - val panel = JPanel(GridLayout(rows, cols)) - build(DefaultPanelBuilder(panel)) - return panel - } - - override fun button(label: String, build: (JButton.() -> Unit)?): JButton = - make(JButton(label), build) - - override fun label(label: String, build: (JLabel.() -> Unit)?): JLabel = - make(JLabel(label), build) - - override fun textField(columns: Int, build: (JTextField.() -> Unit)?): JTextField = - make(JTextField(columns), build) - - override fun textArea( - text: String?, rows: Int, columns: Int, build: (JTextArea.() -> Unit)? - ): JTextArea = - make(JTextArea(text, rows, columns), build) - - override fun radioButton(label: String, build: (JRadioButton.() -> Unit)?): JRadioButton = - make(JRadioButton(label), build) - - override fun border(title: String, build: () -> T): T { - val component = build() - component.border = BorderFactory.createTitledBorder(title) - return component - } - - private fun make(component: T, build: ((T) -> Unit)?): T { - if (build != null) build(component) - return component - } - -} - -// TODO all these properties should be probably cached - -val JButton.actionEvents: Observable - get() = observable { emitter -> - val listener = ActionListener { e -> emitter.onNext(e) } - addActionListener(listener) - emitter.setCancellable { removeActionListener(listener) } - } - -val JButton.mouseEvents: Observable - get() = observable { emitter -> - val listener = object : MouseListener { - override fun mouseClicked(e: MouseEvent) { - fireChange(e) - } - - override fun mouseEntered(e: MouseEvent) { - fireChange(e) - } - - override fun mouseExited(e: MouseEvent) { - fireChange(e) - } - - override fun mousePressed(e: MouseEvent) { - fireChange(e) - } - - override fun mouseReleased(e: MouseEvent) { - fireChange(e) - } - - private fun fireChange(e: MouseEvent) { - emitter.onNext(e) - } - } - addMouseListener(listener) - emitter.setCancellable { removeMouseListener(listener) } - } - -val JTextField.actionEvents: Observable - get() = observable { emitter -> - val listener = ActionListener { e -> emitter.onNext(e) } - addActionListener(listener) - emitter.setCancellable { removeActionListener(listener) } - } - -val JRadioButton.actionEvents: Observable - get() = observable { emitter -> - val listener = ActionListener { e -> emitter.onNext(e) } - addActionListener(listener) - emitter.setCancellable { removeActionListener(listener) } - } - -val JTextComponent.documentChanges: Observable - get() = observable { emitter -> - val listener = object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) { - fireChange(e) - } - - override fun removeUpdate(e: DocumentEvent) { - fireChange(e) - } - - override fun changedUpdate(e: DocumentEvent) { - fireChange(e) - } - - private fun fireChange(e: DocumentEvent) { - emitter.onNext(e) - } - } - document.addDocumentListener(listener) - emitter.setCancellable { document.removeDocumentListener(listener) } - } - -val JTextComponent.textChanges: Observable - get() = documentChanges.map { text } - - -val swingScheduler = object : Scheduler { - - private val executor = object : Scheduler.Executor { - - private val waiter = computationScheduler.newExecutor() - - override fun submit(delayMillis: Long, task: () -> Unit) { - waiter.submit(delayMillis) { - SwingUtilities.invokeLater(task) - } - } - - override fun submitRepeating( - startDelayMillis: Long, - periodMillis: Long, - task: () -> Unit - ) { - waiter.submitRepeating(startDelayMillis, periodMillis) { - SwingUtilities.invokeLater(task) - } - } - - override val isDisposed: Boolean = waiter.isDisposed - - override fun cancel() { - waiter.cancel() - } - - override fun dispose() { - waiter.dispose() - } - - } - - override fun newExecutor(): Scheduler.Executor = executor - - override fun destroy() { - /* does nothing for swing */ - } - -} diff --git a/src/test/kotlin/SwingKotlinWay.kt b/src/test/kotlin/SwingKotlinWay.kt deleted file mode 100644 index 6e65a60..0000000 --- a/src/test/kotlin/SwingKotlinWay.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. - * - * Copyright (C) 2021 Kazimierz Pogoda - * - * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with xemantic-kotlin-swing-dsl. If not, - * see . - */ - -package com.xemantic.kotlin.swing - -import com.badoo.reaktive.observable.* -import com.badoo.reaktive.scheduler.ioScheduler -import java.awt.Dimension -import javax.swing.SwingConstants - -fun main() = mainFrame("My Browser") { - val contentBox = label("") { - horizontalAlignment = SwingConstants.CENTER - verticalAlignment = SwingConstants.CENTER - preferredSize = Dimension(300, 300) - } - val urlBox = textField(10) - val goAction = button("Go!") { - isEnabled = false - actionEvents - .withLatestFrom(urlBox.textChanges) { _, url: String -> url } - .doOnAfterNext { url -> - isEnabled = false - contentBox.text = "Loading: $url" - } - .delay(1000, ioScheduler) // e.g. REST request - .observeOn(swingScheduler) - .subscribe { url -> - isEnabled = true - contentBox.text = "Ready: $url" - } - } - urlBox.textChanges.subscribe { url -> - goAction.isEnabled = url.isNotBlank() - contentBox.text = "Will load: $url" - } - contentPane = borderPanel { - layout.hgap = 4 - panel.border = emptyBorder(4) - north = borderPanel { - west = label("URL") - center = urlBox - east = goAction - } - center = contentBox - } -} diff --git a/src/test/kotlin/SwingKotlinWayWithReactiveModelViewPresenter.kt b/src/test/kotlin/SwingKotlinWayWithReactiveModelViewPresenter.kt deleted file mode 100644 index 4c5038e..0000000 --- a/src/test/kotlin/SwingKotlinWayWithReactiveModelViewPresenter.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. - * - * Copyright (C) 2021 Kazimierz Pogoda - * - * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with xemantic-kotlin-swing-dsl. If not, - * see . - */ - -package com.xemantic.kotlin.swing - -import com.badoo.reaktive.observable.* -import com.badoo.reaktive.scheduler.Scheduler -import com.badoo.reaktive.scheduler.ioScheduler -import java.awt.Dimension -import java.awt.event.ActionEvent -import javax.swing.SwingConstants - -/** - * Abstract representation of all the interactions the Presenter - * might have with the View. This way view can be easily mocked and - * presenter can be tested with assertions run against such a mock. - * Passive view can be also easily implemented in any UI toolkit on - * any platform. Therefore so called Model-View-Presenter pattern - * might be very powerful with Kotlin Multiplatform Projects. - * - * All the events coming from the view are represented as [Observable]s - * which can be subscribed to. A mutable view state is represented as - * simple `var` properties, but it can be also a function. - */ -interface BrowserView { - val urlEditEvents: Observable - val goActions: Observable - var goActionEnabled: Boolean - var content: String -} - -/** - * The presenter is mostly defining streams of events which - * will be wired together once [Presenter.start] is called. - * - * @param scheduler the scheduler to use for Swing state mutations - * if we are waiting on another scheduler we whould observe back - * on this one. - */ -class BrowserPresenter(scheduler: Scheduler) : Presenter({ - observe { - urlEditEvents.doOnAfterNext { url -> - goActionEnabled = url.isNotBlank() - content = "Will try: $url" - } - } - observe { - goActions - .withLatestFrom(urlEditEvents) { _, url: String -> url } - .doOnAfterNext { url -> - goActionEnabled = false - content = "Loading: $url" - } - .delay(1000, ioScheduler) // e.g. REST request - .observeOn(scheduler) - .doOnAfterNext { url -> - println("Thread ${Thread.currentThread()}") - goActionEnabled = true - content = "Showing: $url" - } - } -}) - -fun main() = mainFrame("My Browser") { - val view = SwingBrowserView() - val presenter = BrowserPresenter(swingScheduler) - contentPane = view.component - presenter.start(view) -} - -/** - * Swing adaptation of the [BrowserView], can be also JavaFX, or HTML + JS or native iOS. - */ -class SwingBrowserView : BrowserView { - private val urlText = textField(10) - private val goAction = button("Go!") - private val contentBox = label("") { - horizontalAlignment = SwingConstants.CENTER - verticalAlignment = SwingConstants.CENTER - preferredSize = Dimension(300, 300) - } - - override val urlEditEvents = urlText.textChanges - override val goActions = goAction.actionEvents - override var goActionEnabled - get() = goAction.isEnabled - set(value) { goAction.isEnabled = value } - override var content: String - get() = contentBox.text - set(value) { contentBox.text = value } - - val component = borderPanel { - layout.hgap = 4 - panel.border = emptyBorder(4) - north = borderPanel { - west = label("URL") - center = urlText - east = goAction - } - center = contentBox - } - -} diff --git a/xemantic-kotlin-swing-dsl-core/build.gradle.kts b/xemantic-kotlin-swing-dsl-core/build.gradle.kts new file mode 100644 index 0000000..f6a54f3 --- /dev/null +++ b/xemantic-kotlin-swing-dsl-core/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + `maven-publish` + signing +} + +dependencies { + api(libs.kotlinx.coroutines.core) + runtimeOnly(libs.kotlinx.coroutines.swing) +} diff --git a/src/test/kotlin/Presenter.kt b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Borders.kt similarity index 52% rename from src/test/kotlin/Presenter.kt rename to xemantic-kotlin-swing-dsl-core/src/main/kotlin/Borders.kt index edbbc2e..a0654dd 100644 --- a/src/test/kotlin/Presenter.kt +++ b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Borders.kt @@ -1,7 +1,7 @@ /* * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. * - * Copyright (C) 2021 Kazimierz Pogoda + * Copyright (C) 2024 Kazimierz Pogoda * * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as @@ -17,44 +17,46 @@ * along with xemantic-kotlin-swing-dsl. If not, * see . */ - package com.xemantic.kotlin.swing -import com.badoo.reaktive.disposable.Disposable -import com.badoo.reaktive.observable.Observable -import com.badoo.reaktive.observable.subscribe +import javax.swing.* -/** - * A presenter base. - */ -abstract class Presenter( - builder: Presenter.Builder.() -> Unit -) { +class Border { - private val starters = mutableListOf Observable<*>>() + companion object { - inner class Builder { - fun observe(block: V.() -> Observable<*>) { - starters.add(block) + fun title( + title: String, + block: () -> T + ): T { + val component = block() + component.border = BorderFactory.createTitledBorder(title) + return component } - } - - init { - builder(Builder()) - } - private val observables = mutableListOf>() - - private lateinit var subscriptions: List - - fun start(view: V) { - subscriptions = starters - .map { it(view) } - .map { it.subscribe() } - } + fun empty( + width: Int, + block: () -> T + ): T = empty( + top = width, + left = width, + bottom = width, + right = width, + block + ) + + fun empty( + top: Int, + left: Int, + bottom: Int, + right: Int, + block: () -> T + ): T { + val component = block() + component.border = BorderFactory.createEmptyBorder(top, left, bottom, right) + return component + } - fun stop() { - subscriptions.forEach { it.dispose() } } } diff --git a/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Events.kt b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Events.kt new file mode 100644 index 0000000..b247f7d --- /dev/null +++ b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Events.kt @@ -0,0 +1,97 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import java.awt.Component +import java.awt.event.* +import javax.swing.* +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener +import javax.swing.text.Document +import javax.swing.text.JTextComponent + +/** + * Represents and action, like click or touch event, without + * any additional attributes. + */ +class Action internal constructor() + +/** + * An action instance. + */ +val action = Action() + +val Flow.asActions get() = map { action } + +val Component.mouseEvents: Flow get() = callbackFlow { + val listener = object : MouseListener { + override fun mouseClicked(e: MouseEvent) { trySend(e) } + override fun mouseEntered(e: MouseEvent) { trySend(e) } + override fun mouseExited(e: MouseEvent) { trySend(e) } + override fun mousePressed(e: MouseEvent) { trySend(e) } + override fun mouseReleased(e: MouseEvent) { trySend(e) } + } + val motionListener = object : MouseMotionListener { + override fun mouseDragged(e: MouseEvent) { trySend(e) } + override fun mouseMoved(e: MouseEvent) { trySend(e) } + } + addMouseListener(listener) + addMouseMotionListener(motionListener) + awaitClose { + removeMouseListener(listener) + removeMouseMotionListener(motionListener) + } +} + +val Component.mouseMoves: Flow get() = + mouseEvents.filter { it.id == MouseEvent.MOUSE_MOVED } + +val Component.mouseClicks: Flow get() = + mouseEvents.filter { it.id == MouseEvent.MOUSE_CLICKED } + +val JTextField.actionEvents: Flow get() = callbackFlow { + val listener = ActionListener { e -> trySend(e) } + addActionListener(listener) + awaitClose { removeActionListener(listener) } +} + +val AbstractButton.actionEvents: Flow get() = callbackFlow { + val listener = ActionListener { e -> trySend(e) } + addActionListener(listener) + awaitClose { removeActionListener(listener) } +} + +val Document.documentChanges: Flow get() = callbackFlow { + val listener = object : DocumentListener { + override fun insertUpdate(e: DocumentEvent) { trySend(e) } + override fun removeUpdate(e: DocumentEvent) { trySend(e) } + override fun changedUpdate(e: DocumentEvent) { trySend(e) } + } + addDocumentListener(listener) + awaitClose { removeDocumentListener(listener) } +} + +val JTextComponent.documentChanges: Flow + get() = document.documentChanges + +val JTextComponent.textChanges: Flow + get() = documentChanges.map { text } diff --git a/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Panels.kt b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Panels.kt new file mode 100644 index 0000000..b6dbb9c --- /dev/null +++ b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Panels.kt @@ -0,0 +1,128 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +@file:Suppress("FunctionName") + +package com.xemantic.kotlin.swing + +import java.awt.* +import javax.swing.* + +enum class BoxLayoutAxis( + val code: Int +) { + X(BoxLayout.X_AXIS), + Y(BoxLayout.Y_AXIS), + LINE(BoxLayout.LINE_AXIS), + PAGE(BoxLayout.PAGE_AXIS) +} + +fun BorderPanel( + block: BorderPanelBuilder.() -> Unit +): JPanel = JPanel( + BorderLayout() +).apply { + block(BorderPanelBuilder(this)) +} + +fun FlowPanel( + block: PanelBuilder.() -> Unit +): JPanel = JPanel( + FlowLayout() +).apply { + block(PanelBuilder(this)) +} + +fun BoxPanel( + axis: BoxLayoutAxis, + block: PanelBuilder.() -> Unit +): JPanel = JPanel().apply { + layout = BoxLayout(this, axis.code) + block(PanelBuilder(this)) +} + +fun ScrollPane( + block: () -> Component +) = JScrollPane(block()) + +fun Grid( + rows: Int, + columns: Int, + block: PanelBuilder.() -> Unit +): JPanel = JPanel( + GridLayout(rows, columns) +).apply { + block(PanelBuilder(this)) +} + +class BorderPanelBuilder( + val panel: JPanel +) { + + fun north(block: () -> JComponent) { + panel.add(block(), BorderLayout.NORTH) + } + + fun south(block: () -> JComponent) { + panel.add(block(), BorderLayout.SOUTH) + } + + fun east(block: () -> JComponent) { + panel.add(block(), BorderLayout.EAST) + } + + var west: JComponent + get() = throw NotImplementedError() + set(value) = panel.add(value, BorderLayout.WEST) + + fun west(block: () -> JComponent) { + west = block() + } + + var center: JComponent + get() = throw NotImplementedError() + set(value) = panel.add(value, BorderLayout.CENTER) + fun center(block: () -> JComponent) { + center = block() + } + + val layout = panel.layout as BorderLayout + + fun layout(builder: BorderLayout.() -> Unit) { + builder(panel.layout as BorderLayout) + } + +} + +class PanelBuilder( + val panel: JPanel +) { + + operator fun Component.unaryPlus() { + panel.add(this) + } + + @Suppress("UNCHECKED_CAST") + val layout get() = panel.layout as L + + fun layout(builder: L.() -> Unit) { + builder(layout) + } + +} diff --git a/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Widgets.kt b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Widgets.kt new file mode 100644 index 0000000..f46bfe2 --- /dev/null +++ b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Widgets.kt @@ -0,0 +1,60 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +@file:Suppress("FunctionName") + +package com.xemantic.kotlin.swing + +import javax.swing.* + +fun Button( + text: String, + block: (JButton.() -> Unit) = {} +): JButton = JButton(text).apply(block) + +fun Label( + text: String = "", + block: (JLabel.() -> Unit) = {} +): JLabel = JLabel(text).apply(block) + +fun TextField( + text: String? = null, + block: (JTextField.() -> Unit) = {} +): JTextField = JTextField(text).apply(block) + +fun TextArea( + text: String? = null, + rows: Int = 0, + columns: Int = 0, + block: (JTextArea.() -> Unit) = {} +): JTextArea = JTextArea(text, rows, columns).apply(block) + +fun RadioButton( + label: String? = null, + block: (JRadioButton.() -> Unit) = {} +): JRadioButton = JRadioButton(label).apply { + block(this) +} + +fun CheckBox( + label: String? = null, + block: (JCheckBox.() -> Unit) = {} +): JCheckBox = JCheckBox(label).apply { + block(this) +} diff --git a/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Windows.kt b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Windows.kt new file mode 100644 index 0000000..cd203ae --- /dev/null +++ b/xemantic-kotlin-swing-dsl-core/src/main/kotlin/Windows.kt @@ -0,0 +1,154 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +@file:Suppress("FunctionName") + +package com.xemantic.kotlin.swing + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import java.awt.Container +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.JDialog +import javax.swing.JFrame +import javax.swing.WindowConstants + +/** + * Creates new main [JFrame] with given title. The frame will be + * constructed in the new [MainScope] associated with this frame. This + * coroutine scope will be cancelled when the window is closed. + */ +fun MainWindow( + title: String, + block: SwingScope.(frame: JFrame) -> Container +) { + val scope = MainScope() + val swingScope = SwingScope(scope) + scope.launch(CoroutineName("MainWindow")) { + JFrame(title).apply { + contentPane = block(swingScope, this) + pack() + defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE + onClosing { + MainScope().launch { + scope.coroutineContext.job.cancelAndJoin() + System.exit(0) + } + } + isVisible = true + } + } +} + +class SwingScope( + val scope: CoroutineScope +) { + + inline fun Flow.listen( + name: String? = null, + collector: FlowCollector + ) { + val listenerName = if (name != null) "-$name" else "" + scope.launch( + CoroutineName( + "listen$listenerName[${T::class.java.name}]" + ) + ) { + collect(collector) + } + } + + fun frame( + title: String, + block: suspend SwingScope.(frame: JFrame) -> Container + ): JFrame { + val frame = JFrame(title) + scope.launch(SupervisorJob(scope.coroutineContext.job)) { + val frameScope = this + val frameSwingScope = SwingScope(frameScope) + frame.apply { + contentPane = block(frameSwingScope, this) + pack() + defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE + onClosing { + scope.launch { + frameScope.coroutineContext.job.cancelAndJoin() + dispose() + } + } + isVisible = true + } + } + return frame + } + + fun JFrame.dialog( + title: String, + modal: Boolean = false, + block: SwingScope.(dialog: JDialog) -> Container, + ): JDialog { + val dialog = JDialog(this@dialog, title, modal) + scope.launch(SupervisorJob(scope.coroutineContext.job)) { + val dialogScope = this + val dialogSwingScope = SwingScope(dialogScope) + dialog.apply { + contentPane = block(dialogSwingScope, this) + pack() + defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE + onClosing { + scope.launch { + dialogScope.coroutineContext.job.cancelAndJoin() + dispose() + } + } + isVisible = true + } + } + return dialog + } + +} + +fun CoroutineScope.swingScope( + block: SwingScope.() -> Unit +): SwingScope { + val swingScope = SwingScope(this) + block(swingScope) + return swingScope +} + +private fun JFrame.onClosing(block: () -> Unit) { + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + removeWindowListener(this) + block() + } + }) +} + +private fun JDialog.onClosing(block: () -> Unit) { + addWindowListener(object : WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + removeWindowListener(this) + block() + } + }) +} diff --git a/xemantic-kotlin-swing-dsl-test/build.gradle.kts b/xemantic-kotlin-swing-dsl-test/build.gradle.kts new file mode 100644 index 0000000..b62ff90 --- /dev/null +++ b/xemantic-kotlin-swing-dsl-test/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + `maven-publish` + signing +} + +dependencies { + api(libs.kotlinx.coroutines.test) +} diff --git a/xemantic-kotlin-swing-dsl-test/src/main/kotlin/SwingTests.kt b/xemantic-kotlin-swing-dsl-test/src/main/kotlin/SwingTests.kt new file mode 100644 index 0000000..486de89 --- /dev/null +++ b/xemantic-kotlin-swing-dsl-test/src/main/kotlin/SwingTests.kt @@ -0,0 +1,38 @@ +/* + * This file is part of xemantic-kotlin-swing-dsl - Kotlin goodies for Java Swing. + * + * Copyright (C) 2024 Kazimierz Pogoda + * + * xemantic-kotlin-swing-dsl is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * xemantic-kotlin-swing-dsl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with xemantic-kotlin-swing-dsl. If not, + * see . + */ +package com.xemantic.kotlin.swing.dsl.test + +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.job +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +fun runSwingTest( + context: CoroutineContext = EmptyCoroutineContext, + timeout: Duration = 2.seconds, + testBody: suspend TestScope.() -> Unit +) = runTest(context, timeout) { + testBody() + coroutineContext.job.cancelChildren() +}