diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ecc2a8fd..fbc6d843 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,17 +34,17 @@ git checkout develop ### Build tool -The project itself is managed by [Gradle](http://gradle.org/). Gradle wrapper is not included, so you might want to -install it locally. Scripts should be compatible with Gradle `3.+`. If you consider working from sources, these are +The project itself is managed by [Gradle](http://gradle.org/). Gradle wrapper is included, but you can use a local +Gradle installation - scripts should be compatible with Gradle `3.+`. If you consider working from sources, these are some useful Gradle tasks that you can look into: -- `gradle build install` - builds the libraries archives and pushes them to Maven Local. -- `gradle check` - runs all tests in all projects. -- `gradle clean` - removes `build` directories. -- `gradle distZip` - prepares a zip archive with all jars in `build/distributions` folder. Useful for releases. -- `gradle uploadArchives` - pushes the archives to Maven Central. Requires proper `gradle.properties` with signing and -logging data. -- `gradle closeAndPromoteRepository` - closes and promotes Nexus repository. Should be run after `uploadArchives` in +- `build install` - builds the libraries archives and pushes them to Maven Local. +- `check` - runs all tests in all projects. +- `clean` - removes `build` directories. +- `distZip` - prepares a zip archive with all jars in `build/distributions` folder. Useful for releases. +- `uploadArchives` - pushes the archives to Maven Central. Requires proper `gradle.properties` with archive signing and +Sonatype logging data. +- `closeAndPromoteRepository` - closes and promotes Nexus repository. Should be run after `uploadArchives` in case of a non-snapshot upload to Maven Central. ### Versioning and uploading diff --git a/CHANGELOG.md b/CHANGELOG.md index b305ae8e..2437d11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ -#### 1.9.6-SNAPSHOT +#### 1.9.6-b4 -- **[UPDATE]** Updated to Kotlin 1.1.2. +- **[FEATURE]** (`ktx-collections`) Added `map`, `filter` and `flatten` extension methods that return LibGDX collections. +- **[FEATURE]** (`ktx-collections`) `PooledList` now properly implements `hashCode` and `equals`. +- **[FEATURE]** (`ktx-app`) Added `KtxGame`: **KTX** equivalent of LibGDX `Game`. +- **[FEATURE]** (`ktx-app`) Added `KtxScreen`: adapter of the LibGDX `Screen` interface making all methods optional to implement. +- **[FEATURE]** (`ktx-app`) Added `emptyScreen` utility method returning a no-op implementation of `Screen`. +- **[FEATURE]** (`ktx-inject`) `Context` now implements `Disposable` and allows to dispose of all registered singletons and providers. +- **[FEATURE]** (`ktx-inject`) Added `Context.remove` and `removeProvider` methods. Now providers for particular types can be removed without clearing the whole context. +- **[FEATURE]** (`ktx-inject`) `getProvider`, `setProvider` and `clear` methods of `Context` are now open and can be overridden. + +#### 1.9.6-b3 + +- **[UPDATE]** Updated to Kotlin 1.1.2-3. - **[UPDATE]** Updated to Kotlin Coroutines to 0.15. - **[CHANGE]** (`ktx-assets`) Static `AssetManager` instance container - `Assets` - was removed. All top level functions depending on the global `AssetManager` were removed. diff --git a/README.md b/README.md index c401b927..49ec4d64 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ compile "io.github.libktx:ktx-$module:$ktxVersion" Replace `$module` with the name of required **KTX** library. `$ktxVersion` usually matches LibGDX version it was compiled against - although it might end with `-b1` (if it is a beta release) or `-SNAPSHOT` (if you are using -the snapshots). For example, the first official beta release with the recent group ID was compiled against LibGDX +the snapshots). For example, the first official beta release with the current group ID was compiled against LibGDX `1.9.6` and its version was `1.9.6-b2`. You can browse through our releases [here](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.github.libktx%22). diff --git a/app/README.md b/app/README.md index 7b33aa0a..a71df7d3 100644 --- a/app/README.md +++ b/app/README.md @@ -21,6 +21,10 @@ time step duration and max time step with its constructor parameters. This is a implementation if you like working from scratch. - `KtxApplicationAdapter` is an `ApplicationListener` extension. Provides no-op implementations of all methods, without being an abstract class like `com.badlogic.gdx.ApplicationAdapter`. +- `KtxGame` is a bit more opinionated `Game` equivalent that not only delegates all game events to the current `Screen` +instance, but also ensures non-nullability of screens, manages screen clearing and fixed rendering step, and maintains +screens collection, which allows to switch screens knowing only their concrete class. `KtxScreen` is an interface +extending `Screen` that provides no-op method implementations, making all methods optional to override. #### `InputProcessor` implementations @@ -40,6 +44,7 @@ different screen sizes. overriding with optional, named parameters. - `use` inlined extension methods added to `Batch` and `ShaderProgram`. They allow to omit the `begin()` and `end()` calls before using batches and shader programs. +- `emptyScreen` provides no-op implementations of `Screen`. ### Usage examples @@ -95,6 +100,52 @@ class MyApplicationListener : KtxApplicationAdapter { } ``` +Implementing `KtxGame` with one screen that displays text: + +```Kotlin +import com.badlogic.gdx.Screen +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.graphics.g2d.BitmapFont +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import ktx.app.KtxGame +import ktx.app.KtxScreen +import ktx.app.use + +class ExampleScreen : KtxScreen { + // Notice no `lateinit var` - ExampleScreen has no create() + // method and is constructed after LibGDX is fully initiated + // in ExampleGame.create method. + val font = BitmapFont() + val batch = SpriteBatch().apply { + color = Color.WHITE + } + + override fun render(delta: Float) { + batch.use { + font.draw(it, "Hello Kotlin!", 100f, 100f) + } + } + + override fun dispose() { + // Will be automatically disposed of by the game instance. + font.dispose() + batch.dispose() + } +} + +/** ApplicationListener implementation. */ +class ExampleGame : KtxGame() { + override fun create() { + // Registering ExampleScreen in the game object: it will be + // accessible through ExampleScreen class: + addScreen(ExampleScreen()) + // Changing current screen to the registered instance of the + // ExampleScreen class: + setScreen() + } +} +``` + Implementing `KtxInputAdapter`: ```Kotlin diff --git a/app/build.gradle b/app/build.gradle index 4c3a5b14..564ada6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ dependencies { - testCompile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion" - testCompile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" + provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + testCompile "com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion" + testCompile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" } diff --git a/app/src/main/kotlin/ktx/app/game.kt b/app/src/main/kotlin/ktx/app/game.kt new file mode 100644 index 00000000..2e05951d --- /dev/null +++ b/app/src/main/kotlin/ktx/app/game.kt @@ -0,0 +1,221 @@ +package ktx.app + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Screen +import com.badlogic.gdx.utils.GdxRuntimeException +import com.badlogic.gdx.utils.ObjectMap + +/** + * An equivalent of [com.badlogic.gdx.Game] delegating game events to a [Screen]. On contrary to `Game`, [KtxGame] + * maintains a collection of screens mapped by their type and allows to change screens knowing only their type with + * [setScreen]. Thanks to this, its easier to cache screens without maintaining a singleton with all [Screen] instances + * manually. [ScreenType] generic type allows to users to use an extended specific base class (or interface) for all + * screens, without locking into [Screen]. + * + * Since this is a [KotlinApplication] extension, it also handles screen clearing and fixed rendering time step. See + * [KotlinApplication] for more info. + * + * @param firstScreen will be immediately used by the application. Note that it cannot use any resources initiated by + * the LibGDX (like the OpenGL context) in the constructor, as the screen will be created before the application is + * launched. Defaults to an empty, mock-up screen implementation that should be replaced with the first [setScreen] + * method call in [create]. Note: `firstScreen` still has to be explicitly registered with [addScreen] if you want it to + * be accessible with [getScreen]. + * @param fixedTimeStep see [KotlinApplication.fixedTimeStep]. + * @param maxDeltaTime see [KotlinApplication.maxDeltaTime]. + * @param ScreenType common base interface or class of all screens. Allows to use custom extended [Screen] API. + * @see KtxScreen + */ +open class KtxGame( + firstScreen: ScreenType? = null, + fixedTimeStep: Float = 1f / 60f, + maxDeltaTime: Float = 1f) : KotlinApplication(fixedTimeStep, maxDeltaTime) { + /** Holds references to all screens registered with [addScreen]. Allows to get a reference of the screen instance + * knowing only its type. */ + protected val screens: ObjectMap, ScreenType> = ObjectMap() + /** Currently shown screen. Unless overridden with [setScreen], uses an empty mock-up implementation to work around + * nullability and `lateinit` issues. [shownScreen] is a public property exposing this value as [ScreenType]. + * @see shownScreen */ + protected var currentScreen: Screen = firstScreen ?: emptyScreen() + /** Provides direct access to current [Screen] instance handling game events. */ + open val shownScreen: ScreenType + @Suppress("UNCHECKED_CAST") + get() = currentScreen as ScreenType + + /** + * By default, this method resizes ([Screen.resize]) and shows ([Screen.show]) initial view. You do not have to call + * super if you used [setScreen] in the overridden [create] method or the selected first view does not need to be + * resized and showed before usage. + */ + override fun create() { + currentScreen.resize(Gdx.graphics.width, Gdx.graphics.height) + currentScreen.show() + } + + override fun render(delta: Float) { + currentScreen.render(delta) + } + + override fun resize(width: Int, height: Int) { + currentScreen.resize(width, height) + } + + override fun pause() { + currentScreen.pause() + } + + override fun resume() { + currentScreen.resume() + } + + /** + * Registers an instance of [Screen]. + * @param Type concrete class of the [Screen] instance. The implementation assumes that screens are singletons and + * only one implementation of each class will be registered. + * @param screen instance of [Type]. After invocation of this method, [setScreen] can be used with the appropriate + * class to change current screen to this object. + * @throws GdxRuntimeException if a screen of selected type is already registered. Use [removeScreen] first to replace + * the screen. + * @see getScreen + * @see setScreen + * @see removeScreen + */ + inline fun addScreen(screen: Type) = addScreen(Type::class.java, screen) + + /** + * Registers an instance of [Screen]. Override this method to change the way screens are registered. + * @param type concrete class of the [Screen] instance. The implementation assumes that screens are singletons and + * only one implementation of each class will be registered. + * @param screen instance of [Type]. After invocation of this method, [setScreen] can be used with the appropriate + * class to change current screen to this object. + * @see getScreen + * @see setScreen + * @see removeScreen + */ + open fun addScreen(type: Class, screen: Type) { + !screens.containsKey(type) || throw GdxRuntimeException("Screen already registered to type: $type.") + screens.put(type, screen) + } + + /** + * Replaces current screen with the registered screen instance of the passed type. + * @param Type concrete class of the screen implementation. The screen instance of this type must have been added + * with [addScreen] before calling this method. + * @see addScreen + * @see shownScreen + */ + inline fun setScreen() = setScreen(Type::class.java) + + /** + * Replaces current screen with the registered screen instance of the passed type. Calls `hide` method of the + * previous screen and `show` method of the current screen. Override this method to control screen transitions. + * @param type concrete class of the screen implementation. The screen instance of this type must have been added with + * [addScreen] before calling this method. + * @see addScreen + * @see shownScreen + */ + open fun setScreen(type: Class) { + currentScreen.hide() + currentScreen = getScreen(type) + currentScreen.resize(Gdx.graphics.width, Gdx.graphics.height) + currentScreen.show() + } + + /** + * Returns cached instance of [Screen] of the selected type. + * @param Type concrete class of the screen implementation. The screen instance of this type must have been added with + * [addScreen] before calling this method. + * @return an instance of [Screen] extending passed [Type]. + * @throws GdxRuntimeException if instance of the selected [Type] was not registered with [addScreen]. + * @see addScreen + */ + inline fun getScreen(): Type = getScreen(Type::class.java) + + /** + * Returns cached instance of [Type] of the selected type. + * @param type concrete class of the screen implementation. The screen instance of this type must have been added with + * [addScreen] before calling this method. + * @return an instance of [Type] extending passed [type]. + * @throws GdxRuntimeException if instance of the selected [type] was not registered with [addScreen]. + * @see addScreen + */ + @Suppress("UNCHECKED_CAST") + open fun getScreen(type: Class): Type + = screens[type] as Type? ?: throw GdxRuntimeException("Missing screen instance of type: $type.") + + /** + * Removes cached instance of [Screen] of the selected type. Note that this method does not dispose of the screen and + * will not affect [shownScreen]. + * @param Type concrete class of the screen implementation. + * @return removed instance of [Screen] extending passed [Type] if was registered. `null` otherwise. + * @see addScreen + */ + inline fun removeScreen(): Type? = removeScreen(Type::class.java) + + /** + * Removes cached instance of [Screen] of the selected type. Note that this method does not dispose of the screen and + * will not affect [shownScreen]. + * @param type concrete class of the screen implementation. + * @return removed instance of [Screen] extending passed [type] if was registered. `null` otherwise. + * @see addScreen + */ + @Suppress("UNCHECKED_CAST") + open fun removeScreen(type: Class): Type? = screens.remove(type) as Type? + + /** + * Checks if screen of the given type is registered. + * @param Type concrete class of a screen implementation. + * @return true if a [Screen] is registered with the selected type, false otherwise. + */ + inline fun containsScreen(): Boolean = containsScreen(Type::class.java) + + /** + * Checks if screen of the given type is registered. + * @param type concrete class of a screen implementation. + * @return true if a [Screen] is registered with the selected type, false otherwise. + */ + open fun containsScreen(type: Class): Boolean = screens.containsKey(type) + + /** + * Disposes of all registered screens with [Screen.dispose]. Catches thrown errors and logs them with LibGDX + * application API by default. Override [onScreenDisposalError] method to change error handling behavior. Should be + * called automatically by LibGDX application lifecycle handler. + */ + override fun dispose() { + screens.values().forEach { + try { + it.dispose() + } catch (exception: Throwable) { + onScreenDisposalError(it, exception) + } + } + } + + /** + * Invoked during screens disposal on [dispose] if an error occurs. + * @param screen thrown [exception] during disposal. + * @param exception unexpected screen disposal exception. + */ + protected open fun onScreenDisposalError(screen: ScreenType, exception: Throwable) { + Gdx.app.error("KTX", "Unable to dispose of ${screen.javaClass} screen.", exception) + } +} + +/** + * Provides empty implementations of all [Screen] methods, making them optional to override. + * @see KtxGame + */ +interface KtxScreen : Screen { + override fun show() = Unit + override fun render(delta: Float) = Unit + override fun resize(width: Int, height: Int) = Unit + override fun pause() = Unit + override fun resume() = Unit + override fun hide() = Unit + override fun dispose() = Unit +} + +/** + * Provides a no-op implementation of [Screen]. A workaround of [Screen] nullability issues. + * @see KtxGame + */ +fun emptyScreen(): Screen = object : KtxScreen {} diff --git a/app/src/main/kotlin/ktx/app/letterboxingViewport.kt b/app/src/main/kotlin/ktx/app/letterboxingViewport.kt index fbe7a297..7541e262 100644 --- a/app/src/main/kotlin/ktx/app/letterboxingViewport.kt +++ b/app/src/main/kotlin/ktx/app/letterboxingViewport.kt @@ -21,8 +21,6 @@ import com.badlogic.gdx.utils.viewport.ScalingViewport * It is advised to pair this viewport with FitViewport - LetterboxingViewport can be used for the GUI, while * FitViewport is excellent for the actual (2D) game rendering. - * @author MJ - * * @param targetPpiX this is the targeted pixel per inch ratio on X axis, which allows to scale the viewport * correctly on different devices. Usually about 96 for desktop and WebGL platforms, 160 for mobiles. * Make sure to call [updateScale] after changing this variable. diff --git a/app/src/test/kotlin/ktx/app/applicationTest.kt b/app/src/test/kotlin/ktx/app/applicationTest.kt index b94164c3..e254b310 100644 --- a/app/src/test/kotlin/ktx/app/applicationTest.kt +++ b/app/src/test/kotlin/ktx/app/applicationTest.kt @@ -1,9 +1,7 @@ package ktx.app import com.badlogic.gdx.Gdx -import com.badlogic.gdx.Graphics import com.badlogic.gdx.graphics.GL20 -import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.verify import org.junit.After @@ -24,56 +22,59 @@ class KotlinApplicationTest { @Test fun `should not render if delta time is lower than fixed time step`() { Gdx.graphics = mockGraphicsWithDeltaTime(1 / 120f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 30f) + val application = MockKotlinApplication(fixedTimeStep = 1 / 30f) - assertFalse(app.rendered) - app.render() - assertFalse(app.rendered) + application.render() + + assertFalse(application.rendered) } @Test fun `should render if delta time is equal to fixed time step`() { Gdx.graphics = mockGraphicsWithDeltaTime(1 / 30f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 30f) + val application = MockKotlinApplication(fixedTimeStep = 1 / 30f) - app.render() + application.render() - assertTrue(app.rendered) - assertEquals(1, app.renderedTimes) + assertTrue(application.rendered) + assertEquals(1, application.renderedTimes) } @Test fun `should render if delta time is higher than fixed time step`() { Gdx.graphics = mockGraphicsWithDeltaTime(1 / 30f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 60f) + val application = MockKotlinApplication(fixedTimeStep = 1 / 60f) - app.render() + application.render() - assertTrue(app.rendered) - assertEquals(2, app.renderedTimes) + assertTrue(application.rendered) + assertEquals(2, application.renderedTimes) } @Test fun `should render if delta times are collectively equal to or higher than fixed time step`() { Gdx.graphics = mockGraphicsWithDeltaTime(1 / 50f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 30f) - - app.render() // 0.02 - 0.0 - assertEquals(0, app.renderedTimes) - app.render() // 0.04 - 0.0(3) - assertEquals(1, app.renderedTimes) - app.render() // 0.06 - 0.0(3) - assertEquals(1, app.renderedTimes) - app.render() // 0.08 - 0.0(6) - assertEquals(2, app.renderedTimes) + val application = MockKotlinApplication(fixedTimeStep = 1 / 30f) + + application.render() // 0.02 - 0.0 + assertEquals(0, application.renderedTimes) + + application.render() // 0.04 - 0.0(3) + assertEquals(1, application.renderedTimes) + + application.render() // 0.06 - 0.0(3) + assertEquals(1, application.renderedTimes) + + application.render() // 0.08 - 0.0(6) + assertEquals(2, application.renderedTimes) } @Test fun `should clear screen on render`() { Gdx.graphics = mockGraphicsWithDeltaTime(1 / 30f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 30f) + val application = MockKotlinApplication(fixedTimeStep = 1 / 30f) - app.render() + application.render() verify(Gdx.gl).glClearColor(0f, 0f, 0f, 1f) verify(Gdx.gl).glClear(GL20.GL_COLOR_BUFFER_BIT) @@ -82,21 +83,11 @@ class KotlinApplicationTest { @Test fun `should not render more times than max delta time limit allows`() { Gdx.graphics = mockGraphicsWithDeltaTime(1f) - val app = MockKotlinApplication(fixedTimeStep = 1 / 60f, maxDeltaTime = 5f / 60f) - - app.render() - - assertEquals(5, app.renderedTimes) - } + val application = MockKotlinApplication(fixedTimeStep = 1 / 60f, maxDeltaTime = 5f / 60f) - @Suppress("unused") - class `should implement KtxApplicationAdapter with no methods overridden` : KtxApplicationAdapter { - // Guarantees all KtxApplicationAdapter methods are optional to implement. - } + application.render() - @Suppress("unused") - class `should implement KtxInputAdapter with no methods overridden` : KtxInputAdapter { - // Guarantees all KtxInputAdapter methods are optional to implement. + assertEquals(5, application.renderedTimes) } @After @@ -106,15 +97,8 @@ class KotlinApplicationTest { Gdx.gl20 = null } - private fun mockGraphicsWithDeltaTime(delta: Float) = - mock { - on(it.deltaTime) doReturn delta - on(it.rawDeltaTime) doReturn delta - } - /** - * Example implementation of [KotlinApplication]. Reports rendering data for tests. - * @author MJ + * Test implementation of [KotlinApplication]. Reports rendering data for tests. */ class MockKotlinApplication(fixedTimeStep: Float = 1f / 60f, maxDeltaTime: Float = 1f) : KotlinApplication(fixedTimeStep, maxDeltaTime) { @@ -132,3 +116,13 @@ class KotlinApplicationTest { } } } + +@Suppress("unused") +class `should implement KtxApplicationAdapter with no methods overridden` : KtxApplicationAdapter { + // Guarantees all KtxApplicationAdapter methods are optional to implement. +} + +@Suppress("unused") +class `should implement KtxInputAdapter with no methods overridden` : KtxInputAdapter { + // Guarantees all KtxInputAdapter methods are optional to implement. +} diff --git a/app/src/test/kotlin/ktx/app/gameTest.kt b/app/src/test/kotlin/ktx/app/gameTest.kt new file mode 100644 index 00000000..9cd99303 --- /dev/null +++ b/app/src/test/kotlin/ktx/app/gameTest.kt @@ -0,0 +1,314 @@ +package ktx.app + +import com.badlogic.gdx.Application +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Graphics +import com.badlogic.gdx.Screen +import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.utils.GdxRuntimeException +import com.nhaarman.mockito_kotlin.* +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +/** + * Tests [KtxGame]: KTX equivalent of [com.badlogic.gdx.Game]. + */ +class KtxGameTest { + @Before + fun `set up OpenGL`() { + Gdx.gl20 = mock() + Gdx.gl = Gdx.gl20 + } + + @Test + fun `should not render if delta time is lower than fixed time step`() { + Gdx.graphics = mockGraphicsWithDeltaTime(1 / 120f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 30f, firstScreen = screen) + + game.render() + + assertFalse(screen.rendered) + } + + @Test + fun `should render if delta time is equal to fixed time step`() { + Gdx.graphics = mockGraphicsWithDeltaTime(1 / 30f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 30f, firstScreen = screen) + + game.render() + + assertTrue(screen.rendered) + assertEquals(1, screen.renderedTimes) + } + + @Test + fun `should render if delta time is higher than fixed time step`() { + Gdx.graphics = mockGraphicsWithDeltaTime(2 / 30f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 30f, firstScreen = screen) + + game.render() + + assertTrue(screen.rendered) + assertEquals(2, screen.renderedTimes) + } + + @Test + fun `should render if delta times are collectively equal to or higher than fixed time step`() { + Gdx.graphics = mockGraphicsWithDeltaTime(1 / 50f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 30f, firstScreen = screen) + + game.render() // 0.02 - 0.0 + assertEquals(0, screen.renderedTimes) + + game.render() // 0.04 - 0.0(3) + assertEquals(1, screen.renderedTimes) + + game.render() // 0.06 - 0.0(3) + assertEquals(1, screen.renderedTimes) + + game.render() // 0.08 - 0.0(6) + assertEquals(2, screen.renderedTimes) + } + + @Test + fun `should clear screen on render`() { + Gdx.graphics = mockGraphicsWithDeltaTime(1 / 30f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 30f, firstScreen = screen) + + game.render() + + verify(Gdx.gl).glClearColor(0f, 0f, 0f, 1f) + verify(Gdx.gl).glClear(GL20.GL_COLOR_BUFFER_BIT) + } + + @Test + fun `should display firstScreen without registration`() { + val screen = mock() + val game = KtxGame(firstScreen = screen) + Gdx.graphics = mock { + on(it.width) doReturn 800 + on(it.height) doReturn 600 + } + + game.create() + + assertSame(screen, game.shownScreen) + verify(screen).resize(800, 600) + verify(screen).show() + // addScreen must be called manually to keep firstScreen in context - should not contain initial Screen: + assertFalse(game.containsScreen()) + } + + @Test + fun `should not render more times than max delta time limit allows`() { + Gdx.graphics = mockGraphicsWithDeltaTime(1f) + val screen = MockScreen() + val game = KtxGame(fixedTimeStep = 1 / 60f, maxDeltaTime = 5f / 60f, firstScreen = screen) + + game.render() + + assertEquals(5, screen.renderedTimes) + } + + @Test + fun `should delegate resize call to current screen`() { + val screen = mock() + val game = KtxGame(screen) + + game.resize(100, 200) + + verify(screen).resize(100, 200) + } + + @Test + fun `should delegate pause call to current screen`() { + val screen = mock() + val game = KtxGame(screen) + + game.pause() + + verify(screen).pause() + } + + @Test + fun `should delegate resume call to current screen`() { + val screen = mock() + val game = KtxGame(screen) + + game.resume() + + verify(screen).resume() + } + + @Test(expected = GdxRuntimeException::class) + fun `should fail to provide non-registered Screen`() { + val game = KtxGame() + + game.getScreen() + } + + @Test + fun `should register Screen instance`() { + val screen = mock() + val game = KtxGame() + + game.addScreen(screen) + + assertTrue(game.containsScreen()) + assertSame(screen, game.getScreen()) + } + + @Test(expected = GdxRuntimeException::class) + fun `should fail to register Screen instance to same type multiple times`() { + val game = KtxGame() + + game.addScreen(mock()) + game.addScreen(mock()) + } + + @Test + fun `should not throw exception when trying to remove unregistered Screen`() { + val game = KtxGame() + + val removed = game.removeScreen() + + assertNull(removed) + assertFalse(game.containsScreen()) + } + + @Test + fun `should remove Screen`() { + val screen = mock() + val game = KtxGame() + game.addScreen(screen) + + val removed = game.removeScreen() + + assertSame(screen, removed) + assertFalse(game.containsScreen()) + verify(screen, never()).dispose() // Should not dispose of Screen upon removal. + } + + @Test + fun `should reassign Screen`() { + val game = KtxGame() + val initialScreen = mock() + val replacement = mock() + game.addScreen(initialScreen) + + game.removeScreen() + game.addScreen(replacement) + + assertTrue(game.containsScreen()) + assertSame(replacement, game.getScreen()) + verify(initialScreen, never()).dispose() // Should not dispose of previous KtxScreen upon removal. + } + + @Test + fun `should set current Screen`() { + val firstScreen = mock() + val secondScreen = mock() + val game = KtxGame(firstScreen) + game.addScreen(secondScreen) + Gdx.graphics = mock { + on(it.width) doReturn 800 + on(it.height) doReturn 600 + } + + game.setScreen() + + assertSame(secondScreen, game.shownScreen) + verify(firstScreen).hide() + verify(secondScreen).resize(800, 600) + verify(secondScreen).show() + } + + @Test(expected = GdxRuntimeException::class) + fun `should fail to set unregistered Screen`() { + val game = KtxGame() + + game.setScreen() + } + + @Test + fun `should dispose of all registered Screen instances`() { + val screen = mock() + val ktxScreen = mock() + val game = KtxGame() + game.addScreen(screen) + game.addScreen(ktxScreen) + + game.dispose() + + verify(screen).dispose() + verify(ktxScreen).dispose() + } + + @Test + fun `should dispose of all registered Screen instances with error handling`() { + Gdx.app = mock() + val screen = mock() + val ktxScreen = mock { + on(it.dispose()) doThrow GdxRuntimeException("Expected.") + } + val mockScreen = mock { + on(it.dispose()) doThrow GdxRuntimeException("Expected.") + } + val game = KtxGame() + game.addScreen(screen) + game.addScreen(ktxScreen) + game.addScreen(mockScreen) + + game.dispose() + + verify(screen).dispose() + verify(ktxScreen).dispose() + // Ensures exceptions were logged: + verify(Gdx.app, times(2)).error(eq("KTX"), any(), argThat { this is GdxRuntimeException }) + } + + @After + fun `clear static LibGDX variables`() { + Gdx.graphics = null + Gdx.gl = null + Gdx.gl20 = null + Gdx.app = null + } + + /** [Screen] implementation that tracks how many times it was rendered. */ + open class MockScreen : KtxScreen { + var lastDelta = -1f + var rendered = false + var renderedTimes = 0 + + override fun render(delta: Float) { + rendered = true + lastDelta = delta + renderedTimes++ + } + } +} + +/** + * Tests [Screen] utilities. + */ +class KtxScreenTest { + @Test + fun `should provide mock-up screen instance`() { + val screen = emptyScreen() + + assertTrue(screen is Screen) + } + + @Suppress("unused") + class `should implement KtxScreen with no methods overridden` : KtxScreen { + // Guarantees all KtxScreen methods are optional to implement. + } +} diff --git a/app/src/test/kotlin/ktx/app/utils.kt b/app/src/test/kotlin/ktx/app/utils.kt new file mode 100644 index 00000000..dc3da18e --- /dev/null +++ b/app/src/test/kotlin/ktx/app/utils.kt @@ -0,0 +1,14 @@ +package ktx.app + +import com.badlogic.gdx.Graphics +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock + +/** + * @param delta time since last render. + * @return [Graphics] mock that always returns the passed value as delta time. + */ +fun mockGraphicsWithDeltaTime(delta: Float): Graphics = mock { + on(it.deltaTime) doReturn delta + on(it.rawDeltaTime) doReturn delta +} diff --git a/assets/src/main/kotlin/ktx/assets/assets.kt b/assets/src/main/kotlin/ktx/assets/assets.kt index 05bd9480..1b57f748 100644 --- a/assets/src/main/kotlin/ktx/assets/assets.kt +++ b/assets/src/main/kotlin/ktx/assets/assets.kt @@ -10,7 +10,6 @@ import kotlin.reflect.KProperty /** * Common interface for asset wrappers. Provides access to an asset instance which might or might not be loaded. - * @author MJ * @see ManagedAsset * @see DelayedAsset */ @@ -60,7 +59,6 @@ inline operator fun Asset.getValue(receiver: Any?, property: KPrope /** * Default implementation of [Asset]. Keeps asset data in an [AssetDescriptor] and delegates asset loading to an * [AssetManager]. Assumes the asset was already scheduled for loading. - * @author MJ */ class ManagedAsset(val manager: AssetManager, val assetDescriptor: AssetDescriptor) : Asset { override val asset: Type @@ -80,7 +78,6 @@ class ManagedAsset(val manager: AssetManager, val assetDescriptor: AssetDe * Note that eager asset loading might cause other assets to be loaded synchronously rather than asynchronously, * so it is advised to load eager assets with another [AssetManager] instance or use them after all regular assets are * already loaded. - * @author MJ */ class DelayedAsset(val manager: AssetManager, val assetDescriptor: AssetDescriptor) : Asset { override val asset: Type diff --git a/collections/README.md b/collections/README.md index 5d359251..585d2da9 100644 --- a/collections/README.md +++ b/collections/README.md @@ -32,6 +32,7 @@ chained. - Missing `addAll` and `removeAll` methods for arrays and iterables were added. - `iterate` method allows to iterate over collection's elements, while providing reference to `MutableInterator`. Can be used to easily remove collection elements during iteration. +- `map`, `filter`, `flatten` and `flatMap` methods that work like methods in Kotlin stdlib but return `GdxArray`. - Every iterable and array can be converted to `Array` using `toGdxArray` method. - `IntArray`, `BooleanArray` and `FloatArray` can be converted to corresponding LibGDX primitive collections using `toGdxArray` method. @@ -57,6 +58,7 @@ chained. - Missing `addAll` and `removeAll` methods for arrays and iterables were added. - `iterate` method allows to iterate over collection's elements, while providing reference to `MutableIterator`. Can be used to easily remove collection elements during iteration. +- `map`, `filter`, `flatten` and `flatMap` methods that work like methods in Kotlin stdlib but return `GdxSet`. - Every iterable and array can be converted to `ObjectSet` using `toGdxSet` method. - `IntArray` can be converted to `IntSet` using `toGdxSet` method. - Type alias added for consistency with other collections: `GdxSet` - `com.badlogic.gdx.utils.ObjectSet`. @@ -75,6 +77,7 @@ if the variable is a possible null. `map.put(key, value)`. - `iterate` method allows to iterate over map elements with a reference to `MutableIterator`. Can be used to easily remove elements from the map. +- `map`, `filter`, `flatten` and `flatMap` methods that work like methods in Kotlin stdlib but return `GdxMap` and `GdxArray`. - Keys stored in the map can be quickly converted to an `ObjectSet` using `toGdxSet` method. - Every iterable and array can be converted to `ObjectMap` using `toGdxMap` method. A lambda that converts values to keys has to be provided - since the method is inlined, no new lambda object will be created at runtime. diff --git a/collections/src/main/kotlin/ktx/collections/arrays.kt b/collections/src/main/kotlin/ktx/collections/arrays.kt index 5f75960c..969cde85 100644 --- a/collections/src/main/kotlin/ktx/collections/arrays.kt +++ b/collections/src/main/kotlin/ktx/collections/arrays.kt @@ -1,4 +1,4 @@ -@file:Suppress("NOTHING_TO_INLINE") +@file:Suppress("NOTHING_TO_INLINE", "LoopToCallChain") package ktx.collections @@ -227,6 +227,50 @@ inline fun > GdxArray.sortByDescending(crossin if (size > 1) this.sort(compareByDescending(selector)) } +/** + * Returns a [GdxArray] containing the results of applying the given [transform] function + * to each element in the original [GdxArray]. + */ +inline fun GdxArray.map(transform: (Type) -> R): GdxArray { + val destination = GdxArray(this.size) + for (item in this) { + destination.add(transform(item)) + } + return destination +} + +/** + * Returns a [GdxArray] containing only elements matching the given [predicate]. + */ +inline fun GdxArray.filter(predicate: (Type) -> Boolean): GdxArray { + val destination = GdxArray() + for (item in this) { + if (predicate(item)) { + destination.add(item) + } + } + return destination +} + +/** + * Returns a single [GdxArray] of all elements from all collections in the given [GdxArray]. + */ +inline fun > GdxArray.flatten(): GdxArray { + val destination = GdxArray() + for (item in this) { + destination.addAll(item) + } + return destination +} + +/** + * Returns a single [GdxArray] of all elements yielded from results of transform function being invoked + * on each entry of original [GdxArray]. + */ +inline fun GdxArray.flatMap(transform: (Type) -> Iterable): GdxArray { + return this.map(transform).flatten() +} + /** * @param initialCapacity initial capacity of the set. Will be resized if necessary. Defaults to array size. * @param loadFactor decides how many elements the set might contain in relation to its total capacity before it is resized. diff --git a/collections/src/main/kotlin/ktx/collections/lists.kt b/collections/src/main/kotlin/ktx/collections/lists.kt index 86b8d9d0..a40002a3 100644 --- a/collections/src/main/kotlin/ktx/collections/lists.kt +++ b/collections/src/main/kotlin/ktx/collections/lists.kt @@ -1,3 +1,5 @@ +@file:Suppress("LoopToCallChain", "NOTHING_TO_INLINE") + package ktx.collections import com.badlogic.gdx.utils.Pool @@ -22,7 +24,6 @@ typealias GdxList = PooledList * This collection is excellent for queues or storage of objects that often have to be removed during iteration for * little cost. By design, it provides no random access to elements - it should be used when iteration is very often. * - * @author MJ * @param nodePool provides and manages [Node] instances. */ class PooledList(val nodePool: Pool>) : Iterable { @@ -220,20 +221,72 @@ class PooledList(val nodePool: Pool>) : Iterable { size = 0 } - override fun toString(): String { - return buildString { - append("[") - this@PooledList.forEachIndexed { id, it -> - append(it) - if (id < size - 1) append(", ") + /** + * @param transform will be invoked on each element. + * @return a [GdxList] containing the results of applying the given [transform] function + * to each element in the original [GdxList]. + */ + inline fun map(transform: (T) -> R): GdxList { + val destination = gdxListOf() + for (item in this) { + destination.add(transform(item)) + } + return destination + } + + /** + * @param predicate will be checked against each element. + * @return a [GdxList] containing only elements matching the given [predicate]. + */ + inline fun filter(predicate: (T) -> Boolean): GdxList { + val destination = gdxListOf() + for (item in this) { + if (predicate(item)) { + destination.add(item) } - append("]") } + return destination } + /** + * @param transform will be invoked on each element. + * @return a single [GdxList] of all elements yielded from results of transform function being invoked + * on each entry of original [GdxList]. + */ + inline fun flatMap(transform: (T) -> Iterable): GdxList = this.map(transform).flatten() + + override fun hashCode(): Int { + var hashCode = 1 + for (element in this) { + hashCode = 31 * hashCode + (element?.hashCode() ?: 0) + } + return hashCode + } + + override fun equals(other: Any?): Boolean = when { + other === this -> true + other !is PooledList<*> -> false + other.size != this.size -> false + else -> compareElements(other) + } + + private fun compareElements(other: PooledList<*>): Boolean { + val iterator = iterator() + val otherIterator = other.iterator() + while (iterator.hasNext()) { + if (!otherIterator.hasNext() || !iterator.next().isEqualElementTo(otherIterator.next())) { + return false + } + } + return true + } + + private fun Any?.isEqualElementTo(element: Any?): Boolean = this === element || (this != null && this == element) + + override fun toString(): String = joinToString(prefix = "[", separator = ", ", postfix = "]") + /** * Storage class for list elements. - * @author MJ */ class Node { var element: T? = null @@ -249,7 +302,6 @@ class PooledList(val nodePool: Pool>) : Iterable { /** * Allows to iterate over [PooledList] elements in both regular and reversed order. - * @author MJ * @see remove * @see insertBefore * @see insertAfter @@ -320,10 +372,20 @@ class PooledList(val nodePool: Pool>) : Iterable { } } +/** + * Returns a single [GdxList] of all elements from all collections in the given [GdxList]. + */ +inline fun > GdxList.flatten(): GdxList { + val destination = gdxListOf() + for (item in this) { + destination.addAll(item) + } + return destination +} + /** * Default and main [PooledList] [Node] pool. Provides and manages node instances. Has no max value - will store nearly * unlimited freed [Node] instances and should be cleared manually if necessary. - * @author MJ * @see clear */ object NodePool : Pool>() { diff --git a/collections/src/main/kotlin/ktx/collections/maps.kt b/collections/src/main/kotlin/ktx/collections/maps.kt index 7e0d64d7..56654d43 100644 --- a/collections/src/main/kotlin/ktx/collections/maps.kt +++ b/collections/src/main/kotlin/ktx/collections/maps.kt @@ -1,4 +1,4 @@ -@file:Suppress("NOTHING_TO_INLINE") +@file:Suppress("NOTHING_TO_INLINE", "LoopToCallChain") package ktx.collections @@ -355,3 +355,47 @@ inline operator fun ObjectIntMap.Entry.component1() = key!! * @return [ObjectIntMap.Entry.value] */ inline operator fun ObjectIntMap.Entry.component2() = value + +/** + * Returns a [GdxMap] containing the results of applying the given [transform] function + * to each entry in the original [GdxMap]. + */ +inline fun GdxMap.map(transform: (Entry) -> R): GdxMap { + val destination = GdxMap(this.size) + for (item in this) { + destination[item.key] = transform(item) + } + return destination +} + +/** + * Returns a [GdxMap] containing only entries matching the given [predicate]. + */ +inline fun GdxMap.filter(predicate: (Entry) -> Boolean): GdxMap { + val destination = GdxMap() + for (item in this) { + if (predicate(item)) { + destination[item.key] = item.value + } + } + return destination +} + +/** + * Returns a single [GdxArray] of all elements from all collections in the given [GdxMap]. + */ +inline fun > GdxMap.flatten(): GdxArray { + val destination = GdxArray() + for (item in this) { + destination.addAll(item.value) + } + return destination +} + +/** + * Returns a single [GdxArray] of all elements yielded from results of transform function being invoked + * on each entry of original [GdxMap]. + */ +inline fun GdxMap.flatMap(transform: (Entry) -> Iterable): GdxArray { + return this.map(transform).flatten() +} diff --git a/collections/src/main/kotlin/ktx/collections/sets.kt b/collections/src/main/kotlin/ktx/collections/sets.kt index f61f3fad..fee9fba4 100644 --- a/collections/src/main/kotlin/ktx/collections/sets.kt +++ b/collections/src/main/kotlin/ktx/collections/sets.kt @@ -1,8 +1,9 @@ -@file:Suppress("NOTHING_TO_INLINE") +@file:Suppress("NOTHING_TO_INLINE", "LoopToCallChain") package ktx.collections import com.badlogic.gdx.utils.IntSet +import com.badlogic.gdx.utils.ObjectMap import com.badlogic.gdx.utils.ObjectSet /** Alias for [com.badlogic.gdx.utils.ObjectSet]. Added for consistency with other collections and factory methods. */ @@ -144,6 +145,50 @@ inline fun GdxSet.iterate(action: (Type, MutableIterator) -> while (iterator.hasNext) action(iterator.next(), iterator) } +/** + * Returns a [GdxSet] containing the results of applying the given [transform] function + * to each entry in the original [GdxSet]. + */ +inline fun GdxSet.map(transform: (Type) -> R): GdxSet { + val destination = GdxSet(this.size) + for (item in this) { + destination.add(transform(item)) + } + return destination +} + +/** + * Returns a [GdxSet] containing only elements matching the given [predicate]. + */ +inline fun GdxSet.filter(predicate: (Type) -> Boolean): GdxSet { + val destination = GdxSet() + for (item in this) { + if (predicate(item)) { + destination.add(item) + } + } + return destination +} + +/** + * Returns a single [GdxSet] of all elements from all collections in the given [GdxSet]. + */ +inline fun > GdxSet.flatten(): GdxSet { + val destination = GdxSet() + for (item in this) { + destination.addAll(item) + } + return destination +} + +/** + * Returns a single [GdxSet] of all elements yielded from results of transform function being invoked + * on each element of original [GdxSet]. + */ +inline fun GdxSet.flatMap(transform: (Type) -> Iterable): GdxSet { + return this.map(transform).flatten() +} + /** * @param ordered if false, methods that remove elements may change the order of other elements in the array, * which avoids a memory copy. diff --git a/collections/src/test/kotlin/ktx/collections/arraysTest.kt b/collections/src/test/kotlin/ktx/collections/arraysTest.kt index 31621b15..dafedcbf 100644 --- a/collections/src/test/kotlin/ktx/collections/arraysTest.kt +++ b/collections/src/test/kotlin/ktx/collections/arraysTest.kt @@ -2,35 +2,37 @@ package ktx.collections import org.junit.Assert.* import org.junit.Test +import java.util.LinkedList /** * Tests utilities for LibGDX custom ArrayList equivalent - Array. - * @author MJ */ class ArraysTest { @Test - fun shouldCreateArray() { + fun `should create Array`() { val array = gdxArrayOf() + assertNotNull(array) assertTrue(array is GdxArray) assertEquals(0, array.size) } @Test - fun shouldCreateArrayWithCustomInitialCapacity() { + fun `should create Array with custom initial capacity`() { assertEquals(32, gdxArrayOf(initialCapacity = 32).items.size) assertEquals(128, gdxArrayOf(initialCapacity = 128).items.size) } @Test - fun shouldCreateArraysWithCustomOrderedSetting() { + fun `should create Arrays with custom ordered setting`() { assertFalse(gdxArrayOf(ordered = false).ordered) assertTrue(gdxArrayOf(ordered = true).ordered) } @Test - fun shouldCreateArraysWithCustomElements() { + fun `should create Array with custom elements`() { val array = gdxArrayOf("1", "2", "3") + assertEquals(3, array.size) assertTrue("1" in array) assertTrue("2" in array) @@ -38,136 +40,166 @@ class ArraysTest { } @Test - fun shouldReportSizeOfArray() { + fun `should report size of Array`() { val array = GdxArray.with("1", "2", "3") + assertEquals(3, array.size()) - assertEquals(array!!.size, array.size()) + assertEquals(array.size, array.size()) + } + + @Test + fun `should return 0 as null Array size`() { val nullArray: GdxArray? = null + assertEquals(0, nullArray.size()) } @Test - fun shouldReportSizeOfIntArray() { + fun `should report size of IntArray`() { val array = GdxIntArray.with(1, 2, 3) + assertEquals(3, array.size()) - assertEquals(array!!.size, array.size()) + assertEquals(array.size, array.size()) + } + + @Test + fun `should return 0 as null IntArray size`() { val nullArray: GdxIntArray? = null + assertEquals(0, nullArray.size()) } @Test - fun shouldReportSizeOfFloatArray() { + fun `should report size of FloatArray`() { val array = GdxFloatArray.with(1f, 2f, 3f) + assertEquals(3, array.size()) - assertEquals(array!!.size, array.size()) + assertEquals(array.size, array.size()) + } + + @Test + fun `should return 0 as null FloatArray size`() { val nullArray: GdxFloatArray? = null + assertEquals(0, nullArray.size()) } @Test - fun shouldReportSizeOfBooleanArray() { + fun `should report size of BooleanArray`() { val array = GdxBooleanArray.with(true, false, true) + assertEquals(3, array.size()) - assertEquals(array!!.size, array.size()) + assertEquals(array.size, array.size()) + } + + @Test + fun `should return 0 as null BooleanArray size`() { val nullArray: GdxBooleanArray? = null + assertEquals(0, nullArray.size()) } @Test - fun shouldReportEmptyStatus() { - val array: GdxArray? = GdxArray.with("1", "2", "3") - assertFalse(array.isEmpty()) - val emptyArray = GdxArray() - assertTrue(emptyArray.isEmpty()) - val nullArray: GdxArray? = null - assertTrue(nullArray.isEmpty()) + fun `should report empty status`() { + assertFalse(GdxArray.with("1", "2", "3").isEmpty()) + assertTrue(GdxArray().isEmpty()) + assertTrue((null as GdxArray?).isEmpty()) } @Test - fun shouldReportNonEmptyStatus() { - val array: GdxArray? = GdxArray.with("1", "2", "3") - assertTrue(array.isNotEmpty()) - val emptyArray = GdxArray() - assertFalse(emptyArray.isNotEmpty()) - val nullArray: GdxArray? = null - assertFalse(nullArray.isNotEmpty()) + fun `should report non empty status`() { + assertTrue(GdxArray.with("1", "2", "3").isNotEmpty()) + assertFalse(GdxArray().isNotEmpty()) + assertFalse((null as GdxArray?).isNotEmpty()) } @Test - fun shouldReturnLastValidIndexOfArray() { + fun `should return last valid index of Array`() { val array = GdxArray.with("1", "2", "3") + assertEquals(2, array.lastIndex) } @Test - fun shouldReturnLastValidIndexOfEmptyArray() { + fun `should return last valid index of empty Array`() { val emptyArray = GdxArray() + assertEquals(-1, emptyArray.lastIndex) } @Test - fun shouldReturnNegativeLastIndexForNullArray() { + fun `should return negative last index for null Array`() { val nullArray: GdxArray? = null + assertEquals(-1, nullArray.lastIndex) } @Test - fun shouldReturnLastValidIndexOfIntArray() { + fun `should return last valid index of IntArray`() { val array = GdxIntArray.with(1, 2, 3) + assertEquals(2, array.lastIndex) } @Test - fun shouldReturnLastValidIndexOfEmptyIntArray() { + fun `should return last valid index of empty IntArray`() { val emptyArray = GdxIntArray() + assertEquals(-1, emptyArray.lastIndex) } @Test - fun shouldReturnNegativeLastIndexForNullIntArray() { + fun `should return negative last index for null IntArray`() { val nullArray: GdxIntArray? = null + assertEquals(-1, nullArray.lastIndex) } @Test - fun shouldReturnLastValidIndexOfFloatArray() { + fun `should return last valid index of FloatArray`() { val array = GdxFloatArray.with(1f, 2f, 3f) + assertEquals(2, array.lastIndex) } @Test - fun shouldReturnLastValidIndexOfEmptyFloatArray() { + fun `should return last valid index of empty FloatArray`() { val emptyArray = GdxFloatArray() + assertEquals(-1, emptyArray.lastIndex) } @Test - fun shouldReturnNegativeLastIndexForNullFloatArray() { + fun `should return negative last index for null FloatArray`() { val nullArray: GdxFloatArray? = null + assertEquals(-1, nullArray.lastIndex) } @Test - fun shouldReturnLastValidIndexOfBooleanArray() { + fun `should return last valid index of BooleanArray`() { val array = GdxBooleanArray.with(true, false, true) + assertEquals(2, array.lastIndex) } @Test - fun shouldReturnLastValidIndexOfEmptyBooleanArray() { + fun `should return last valid index of empty BooleanArray`() { val emptyArray = GdxBooleanArray() assertEquals(-1, emptyArray.lastIndex) } @Test - fun shouldReturnNegativeLastIndexForNullBooleanArray() { + fun `should return negative last index for null BooleanArray`() { val nullArray: GdxBooleanArray? = null assertEquals(-1, nullArray.lastIndex) } @Test - fun shouldReturnAlternativeIfElementIsNull() { + @Suppress("ReplaceGetOrSet") + fun `should return alternative if element is null`() { val array = GdxArray.with("0", null, "2") + assertEquals("0", array.get(0, "3")) assertEquals("3", array[1, "3"]) // This method is also available through square bracket operator. assertEquals("2", array.get(2, "3")) @@ -175,14 +207,11 @@ class ArraysTest { } @Test - fun shouldAddAllValuesFromCustomIterable() { + fun `should add all values from Iterable`() { val array = GdxArray() - assertEquals(0, array.size) - assertFalse("1" in array) - assertFalse("2" in array) - assertFalse("3" in array) array.addAll(listOf("1", "2", "3")) + assertEquals(3, array.size) assertTrue("1" in array) assertTrue("2" in array) @@ -190,14 +219,11 @@ class ArraysTest { } @Test - fun shouldRemoveAllValuesFromCustomIterable() { + fun `should remove all values from Iterable`() { val array = GdxArray.with("1", "2", "3") - assertEquals(3, array.size) - assertTrue("1" in array) - assertTrue("2" in array) - assertTrue("3" in array) array.removeAll(listOf("1", "2", "3")) + assertEquals(0, array.size) assertFalse("1" in array) assertFalse("2" in array) @@ -205,14 +231,11 @@ class ArraysTest { } @Test - fun shouldRemoveAllValuesFromNativeArray() { + fun `should remove all values from native Array`() { val array = GdxArray.with("1", "2", "3") - assertEquals(3, array.size) - assertTrue("1" in array) - assertTrue("2" in array) - assertTrue("3" in array) array.removeAll(arrayOf("1", "2", "3")) + assertEquals(0, array.size) assertFalse("1" in array) assertFalse("2" in array) @@ -220,16 +243,17 @@ class ArraysTest { } @Test - fun shouldAddValuesWithPlusOperator() { + fun `should add values with + operator`() { val array = GdxArray() - assertEquals(0, array.size) array + "1" + assertEquals(1, array.size) assertTrue("1" in array) assertEquals("1", array[0]) array + "2" + "3" + assertEquals(3, array.size) assertTrue("2" in array) assertTrue("3" in array) @@ -238,11 +262,11 @@ class ArraysTest { } @Test - fun shouldAddIterablesWithPlusOperator() { + fun `should add Iterables with + operator`() { val array = GdxArray() - assertEquals(0, array.size) array + listOf("1", "2", "3") + assertEquals(3, array.size) assertTrue("1" in array) assertTrue("2" in array) @@ -253,11 +277,11 @@ class ArraysTest { } @Test - fun shouldAddArraysWithPlusOperator() { + fun `should add native Arrays with + operator`() { val array = GdxArray() - assertEquals(0, array.size) array + arrayOf("1", "2", "3") + assertEquals(3, array.size) assertTrue("1" in array) assertTrue("2" in array) @@ -268,58 +292,70 @@ class ArraysTest { } @Test - fun shouldRemoveValuesWithMinusOperator() { + fun `should remove values with - operator`() { val array = GdxArray.with("1", "2", "3", "4", "5", "6") - assertEquals(6, array.size) + array - "1" + assertEquals(5, array.size) assertFalse("1" in array) + array - "2" - "3" + assertEquals(3, array.size) assertFalse("2" in array) assertFalse("3" in array) + array - listOf("4", "5") + assertEquals(1, array.size) assertFalse("4" in array) assertFalse("5" in array) + array - arrayOf("6", "7") + assertEquals(0, array.size) assertFalse("6" in array) assertFalse("7" in array) } @Test - fun shouldChainOperators() { + fun `should chain operators`() { val array = GdxArray.with("1", "2", "3", "4") + array + "5" - "2" + GdxArray.with("7") - GdxArray.with("4", "6") + assertEquals(GdxArray.with("1", "3", "5", "7"), array) } @Test - fun shouldFindElementsWithInOperator() { + fun `should find elements with in operator`() { val array = GdxArray.with("1") val identityCheck = false // Will compare with equals(Object). + assertTrue(array.contains("1", identityCheck)) // Standard LibGDX API. assertTrue("1" in array) assertTrue(array.contains("1")) // Operator method alias. array.removeValue("1", identityCheck) + assertFalse(array.contains("1", identityCheck)) // Standard LibGDX API. assertFalse("1" in array) assertFalse(array.contains("1")) } @Test - fun shouldAllowToIterateWithIteratorReference() { + fun `should allow to iterate Array with iterator reference`() { val array = GdxArray.with("1", "2", "3") - assertEquals(3, array.size) + array.iterate { value, iterator -> if (value == "2") iterator.remove() } + assertEquals(2, array.size) assertFalse("2" in array) } @Test - fun shouldSortElementsInDescendingNaturalOrder() { + fun `should sort elements in descending natural order`() { val array = GdxArray.with(1, 2, 3) array.sortDescending() @@ -330,7 +366,7 @@ class ArraysTest { } @Test - fun shouldSortElementsByHigherOrderFunction() { + fun `should sort elements by property`() { val array = GdxArray.with("Twenty-one", "Eleven", "One") array.sortBy { it.length } @@ -341,7 +377,7 @@ class ArraysTest { } @Test - fun shouldSortElementsByHigherOrderFunctionInDescendingOrder() { + fun `should sort elements by property in descending order`() { val array = GdxArray.with("One", "Eleven", "Twenty-one") array.sortByDescending { it.length } @@ -352,17 +388,61 @@ class ArraysTest { } @Test - fun shouldConvertArrayToSet() { - val array = GdxArray.with("1", "2", "3").toGdxSet() - assertEquals(3, array.size) - assertTrue("1" in array) - assertTrue("2" in array) - assertTrue("3" in array) + fun `should map elements into a new GdxArray`() { + val array = GdxArray.with(1, 2, 3) + + val result = array.map { it * 2 } + + assertTrue(result is GdxArray) + assertEquals(GdxArray.with(2, 4, 6), result) } @Test - fun shouldConvertIterablesToArrays() { + fun `should filter elements into a new GdxArray`() { + val array = GdxArray.with(1, 2, 3, 4, 5) + + val result = array.filter { it % 2 == 1 } + + assertTrue(result is GdxArray) + assertEquals(GdxArray.with(1, 3, 5), result) + } + + @Test + fun `should flatten elements into a new GdxArray`() { + val array = GdxArray.with(GdxArray.with(1), listOf(), LinkedList(arrayListOf(2, 3))) + + val result = array.flatten() + + assertTrue(result is GdxArray) + assertEquals(GdxArray.with(1, 2, 3), result) + } + + @Test + fun `should map elements to lists and flatten them into a new GdxArray`() { + val array = GdxArray.with(1, 2, 3) + + val result = array.flatMap { counter -> List(counter) { counter } } + + assertTrue(result is GdxArray) + assertEquals(GdxArray.with(1, 2, 2, 3, 3, 3), result) + } + + @Test + fun `should convert Array to ObjectSet`() { + val array = GdxArray.with("1", "2", "3") + + val set = array.toGdxSet() + + assertEquals(3, set.size) + assertTrue("1" in set) + assertTrue("2" in set) + assertTrue("3" in set) + } + + @Test + fun `should convert Iterables to Arrays`() { val listAsArray = listOf("1", "2", "3").toGdxArray() + assertEquals(3, listAsArray.size) assertTrue("1" in listAsArray) assertTrue("2" in listAsArray) @@ -373,8 +453,9 @@ class ArraysTest { } @Test - fun shouldConvertNativeArraysToGdxArrays() { + fun `should convert native Arrays to GdxArrays`() { val array = arrayOf("1", "2", "3").toGdxArray() + assertEquals(3, array.size) assertTrue("1" in array) assertTrue("2" in array) @@ -385,8 +466,9 @@ class ArraysTest { } @Test - fun shouldConvertNativeIntArraysToGdxIntArrays() { + fun `should convert native IntArrays to GdxIntArrays`() { val intArray = intArrayOf(1, 2, 3).toGdxArray() + assertEquals(3, intArray.size) assertTrue(1 in intArray) assertTrue(2 in intArray) @@ -397,8 +479,9 @@ class ArraysTest { } @Test - fun shouldConvertNativeFloatArraysToGdxFloatArrays() { + fun `should convert native FloatArrays to GdxFloatArrays`() { val floatArray = floatArrayOf(1f, 2f, 3f).toGdxArray() + assertEquals(3, floatArray.size) assertTrue(1f in floatArray) assertTrue(2f in floatArray) @@ -409,8 +492,9 @@ class ArraysTest { } @Test - fun shouldConvertNativeBooleanArraysToGdxBooleanArrays() { + fun `should convert native BooleanArrays to GdxBooleanArrays`() { val booleanArray = booleanArrayOf(true, false, true).toGdxArray() + assertEquals(3, booleanArray.size) assertEquals(true, booleanArray[0]) assertEquals(false, booleanArray[1]) diff --git a/collections/src/test/kotlin/ktx/collections/listsTest.kt b/collections/src/test/kotlin/ktx/collections/listsTest.kt index 0c07af65..d786068c 100644 --- a/collections/src/test/kotlin/ktx/collections/listsTest.kt +++ b/collections/src/test/kotlin/ktx/collections/listsTest.kt @@ -2,11 +2,11 @@ package ktx.collections import org.junit.Assert.* import org.junit.Test +import java.util.LinkedList import java.util.NoSuchElementException /** * Tests general [PooledList] utilities. - * @author MJ */ class ListsTest { @Test @@ -49,13 +49,12 @@ class ListsTest { @Test fun `should provide alias for compatibility with other LibGDX collections`() { - assertTrue(GdxList(NodePool) is PooledList) + assertTrue(GdxList(NodePool) is PooledList) } } /** * Tests [PooledList] implementation - a KTX LinkedList equivalent. - * @author MJ */ class PooledListTest { @Test @@ -224,6 +223,49 @@ class PooledListTest { assertEquals(0, NodePool.free) // Nodes should not be returned to the pool. } + @Test + fun `should map elements into a new GdxList`() { + val list = gdxListOf(1, 2, 3) + val result = list.map { it * 2 } + + assertTrue(result is GdxList) + assertEquals(3, result.size) + assertEquals(2, result.first) + assertEquals(6, result.last) + } + + @Test + fun `should filter elements into a new GdxList`() { + val list = gdxListOf(1, 2, 3, 4, 5) + val result = list.filter { it % 2 == 1 } + + assertTrue(result is GdxList) + assertEquals(3, result.size) + assertEquals(1, result.first) + assertEquals(5, result.last) + } + + @Test + fun `should flatten elements into a new GdxList`() { + val list = gdxListOf(GdxArray.with(1), listOf(), LinkedList(arrayListOf(2, 3))) + val result = list.flatten() + + assertTrue(result is GdxList) + assertEquals(3, result.size) + assertEquals(3, result.size) + assertEquals(1, result.first) + assertEquals(3, result.last) + } + + @Test + fun `should map elements to lists and flatten them into a new GdxList`() { + val list = gdxListOf(1, 2, 3) + val result = list.flatMap { List(it) { "" } } + + assertTrue(result is GdxList) + assertEquals(6, result.size) + } + @Test(expected = NoSuchElementException::class) fun shouldThrowExceptionIfFirstElementIsRequestedButListIsEmpty() { PooledList(NodePool).first @@ -388,4 +430,27 @@ class PooledListTest { assertEquals("[single]", gdxListOf("single").toString()) assertEquals("[one, two, three]", gdxListOf("one", "two", "three").toString()) } + + @Test + fun `should calculate distinct hash code for lists with same elements`() { + val list = gdxListOf("a", "b", "c") + val same = gdxListOf("a", "b", "c") + val different = gdxListOf("b", "c", "a") + + assertEquals(same.hashCode(), list.hashCode()) + assertNotEquals(different.hashCode(), list.hashCode()) + } + + @Test + fun `should properly implement equals`() { + val list = gdxListOf("a", "b", "c") + + assertNotEquals(list, null) + assertNotEquals(list, "[a, b, c]") + assertNotEquals(list, gdxListOf()) + assertNotEquals(list, GdxArray.with("a", "b", "c")) // No common interface. + assertNotEquals(list, gdxListOf("a", "b")) + assertNotEquals(list, gdxListOf("a", "b", "c", "d")) + assertEquals(list, gdxListOf("a", "b", "c")) + } } diff --git a/collections/src/test/kotlin/ktx/collections/mapsTest.kt b/collections/src/test/kotlin/ktx/collections/mapsTest.kt index 94c382b6..53787503 100644 --- a/collections/src/test/kotlin/ktx/collections/mapsTest.kt +++ b/collections/src/test/kotlin/ktx/collections/mapsTest.kt @@ -4,10 +4,10 @@ import com.badlogic.gdx.utils.* import com.badlogic.gdx.utils.Array import org.junit.Assert.* import org.junit.Test +import java.util.* /** * Tests utilities for LibGDX custom HashMap equivalent - [ObjectMap]. - * @author MJ */ class MapsTest { @Test @@ -378,4 +378,43 @@ class MapsTest { assertEquals("Key", key) assertEquals(10, value) } + + @Test + fun `should map elements into a new GdxMap`() { + val map = gdxMapOf("One" to 1, "Two" to 2, "Three" to 3) + val result = map.map { it.value * 2 } + + assertTrue(result is GdxMap) + assertEquals(gdxMapOf("One" to 2, "Two" to 4, "Three" to 6), result) + } + + @Test + fun `should filter elements into a new GdxMap`() { + val map = gdxMapOf("One" to 1, "Two" to 2, "Three" to 3, "Four" to 4, "Five" to 5) + val result = map.filter { it.value % 2 == 1 } + + assertTrue(result is GdxMap) + assertEquals(gdxMapOf("One" to 1, "Three" to 3, "Five" to 5), result) + } + + @Test + fun `should flatten elements into a new GdxArray`() { + val map = gdxMapOf(1 to GdxArray.with(1), 2 to listOf(), 3 to LinkedList(arrayListOf(2, 3))) + val result = map.flatten() + + assertTrue(result is GdxArray) + assertEquals(3, result.size) + assertEquals(GdxArray.with(1, 2, 3), result) + } + + @Test + fun `should map elements to lists and flatten them into a new GdxArray`() { + val map = gdxMapOf("One" to 1, "Two" to 2, "Three" to 3) + val result = map.flatMap { e -> List(e.value) { e.value } } + result.sort() + + assertTrue(result is GdxArray) + assertEquals(GdxArray.with(1, 2, 2, 3, 3, 3), result) + } + } diff --git a/collections/src/test/kotlin/ktx/collections/setsTest.kt b/collections/src/test/kotlin/ktx/collections/setsTest.kt index 357be36c..53bc3344 100644 --- a/collections/src/test/kotlin/ktx/collections/setsTest.kt +++ b/collections/src/test/kotlin/ktx/collections/setsTest.kt @@ -3,10 +3,10 @@ package ktx.collections import com.badlogic.gdx.utils.ObjectSet import org.junit.Assert.* import org.junit.Test +import java.util.* /** * Tests utilities for LibGDX custom HashSet equivalent - [ObjectSet]. - * @author MJ */ class SetsTest { @Test @@ -221,6 +221,53 @@ class SetsTest { assertFalse("2" in set) } + @Test + fun `should map elements into a new GdxSet`() { + val set = GdxSet.with(1, 2, 3) + val result = set.map { it * 2 } + + assertTrue(result is GdxSet) + assertTrue(2 in result) + assertTrue(4 in result) + assertTrue(6 in result) + } + + @Test + fun `should filter elements into a new GdxSet`() { + val set = GdxSet.with(1, 2, 3, 4, 5) + val result = set.filter { it % 2 == 1 } + + assertTrue(result is GdxSet) + assertEquals(3, result.size) + assertTrue(1 in result) + assertTrue(3 in result) + assertTrue(5 in result) + } + + @Test + fun `should flatten elements into a new GdxSet`() { + val set = GdxSet.with(GdxArray.with(1, 2), listOf(), LinkedList(arrayListOf(2, 3))) + val result = set.flatten() + + assertTrue(result is GdxSet) + assertEquals(3, result.size) + assertTrue(1 in result) + assertTrue(2 in result) + assertTrue(3 in result) + } + + @Test + fun `should map elements to lists and flatten them into a new GdxSet`() { + val set = GdxSet.with(1, 2, 3) + val result = set.flatMap { count -> List(count) { it } } + + assertTrue(result is GdxSet) + assertEquals(3, result.size) + assertTrue(0 in result) + assertTrue(1 in result) + assertTrue(2 in result) + } + @Test fun shouldConvertSetToArray() { val setAsArray = ObjectSet.with("1", "2", "3").toGdxArray() diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..96c95cc4 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..27fc7062 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..4453ccea --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/i18n/src/test/kotlin/ktx/i18n/i18nTest.kt b/i18n/src/test/kotlin/ktx/i18n/i18nTest.kt index 9ea0444c..522f6ebe 100644 --- a/i18n/src/test/kotlin/ktx/i18n/i18nTest.kt +++ b/i18n/src/test/kotlin/ktx/i18n/i18nTest.kt @@ -13,7 +13,6 @@ import java.io.File /** * Tests internationalization-related classes and functions stored in *i18n.kt*. - * @author MJ */ class I18nTest { val bundleContent = """ diff --git a/inject/README.md b/inject/README.md index ad9f895b..457ad238 100644 --- a/inject/README.md +++ b/inject/README.md @@ -36,6 +36,13 @@ invoked. It should be used for all non-singleton components of the `Context` tha Instead of passing `Context` around, inject appropriate providers to your components to avoid excessive dependency on `ktx-inject` in your project. +`remove()` allows you to remove components from the `Context` that are no longer needed. + +`clear()` and `dispose()` methods can be used after you no longer need the `Context`. `clear()` removes references to +all registered singletons and providers. `dispose()`, additionally to clearing the context, attempts to dispose all +singletons and providers that implement the `Disposable` interface and logs all errors on the LibGDX error logging +level. Use `clear()` instead of `dispose()` if you want to fully control assets lifecycle. + ### Usage examples Creating a new `Context`: @@ -94,6 +101,22 @@ class ClassWithLazyInjectedValue(context: Context) { } ``` +Removing a registered provider: +```Kotlin +context.remove() +// Note that this method work for both singletons and providers. +``` + +Removing all components from the `Context`: +```Kotlin +context.clear() +``` + +Removing all components from the `Context` and disposing of all `Disposable` singletons and providers: +```Kotlin +context.dispose() +``` + ### Implementation notes > How does it work? diff --git a/inject/build.gradle b/inject/build.gradle index 6a513cc6..9e8927a9 100644 --- a/inject/build.gradle +++ b/inject/build.gradle @@ -1,3 +1,3 @@ dependencies { - provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + provided "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" } diff --git a/inject/src/main/kotlin/ktx/inject/inject.kt b/inject/src/main/kotlin/ktx/inject/inject.kt index d78055cb..c0761166 100644 --- a/inject/src/main/kotlin/ktx/inject/inject.kt +++ b/inject/src/main/kotlin/ktx/inject/inject.kt @@ -1,5 +1,8 @@ package ktx.inject +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.utils.Disposable + /** * Handles dependency injection mechanism. Allows to bind selected classes with their providers. * @@ -7,7 +10,7 @@ package ktx.inject * built context from within multiple threads, you should override [createProvidersMap] method and return a thread-safe * [MutableMap] implementation to avoid concurrency bugs. */ -open class Context { +open class Context : Disposable { @Suppress("LeakingThis") private val providers = createProvidersMap() @@ -24,7 +27,7 @@ open class Context { protected open fun createProvidersMap(): MutableMap, () -> Any> = mutableMapOf() /** - * Utility mirror function, allowing to call context as a function. + * Provides instance of the selected type. Utility method allowing to call context as a function. * @return instance of the class with the selected type if a provider is present in the context. * @see inject */ @@ -33,6 +36,7 @@ open class Context { } /** + * Provides instance of the selected type. * @return instance of the class with the selected type if a provider is present in the context. * @throws InjectionException if no provider is registered for the selected type. */ @@ -41,6 +45,7 @@ open class Context { } /** + * Extracts provider of instances of the selected type. * @return instance of a provider of objects with the selected type. * @throws InjectionException if no provider is registered for the selected type. */ @@ -49,6 +54,7 @@ open class Context { } /** + * Extracts provider of instances of the selected type. This method is internally used by inlined injection methods. * @param forClass type of objects provided by the selected provider. * @return provider instance bind to the selected class. * @throws InjectionException if no provider is registered for the selected type. @@ -56,33 +62,54 @@ open class Context { * @see inject */ @Suppress("UNCHECKED_CAST") - fun getProvider(forClass: Class): () -> Type { + open fun getProvider(forClass: Class): () -> Type { val provider = providers[forClass] - return if (provider == null) - throw InjectionException("No provider registered for class: $forClass") else provider as () -> Type + return if (provider == null) { + throw InjectionException("No provider registered for class: $forClass") + } else { + provider as () -> Type + } } /** + * Adds the selected provider to the [Context]. This method is internally used by inlined binding methods. * @param forClass type of objects provided by the registered provider. * @param provider will be bind to the selected class. * @throws InjectionException if provider is already defined. * @see bind * @see bindSingleton */ - fun setProvider(forClass: Class<*>, provider: () -> Any) { - if (forClass in providers) throw InjectionException("Already defined provider for class: $forClass") + open fun setProvider(forClass: Class<*>, provider: () -> Any) { + forClass !in providers || throw InjectionException("Provider already defined for class: $forClass") providers[forClass] = provider } /** - * @param type class of the provided components. - * @return true if there is a provider registered for the selected type. + * Removes provider of instances of the selected type. This method is internally used by inlined removal methods. + * @param ofClass type of objects provided by the selected provider. + * @return removed provider instance bind to the selected class if any was registered or null. + * @see remove */ - operator fun contains(type: Class<*>): Boolean = type in providers + @Suppress("UNCHECKED_CAST") + open fun removeProvider(ofClass: Class): (() -> Type)? { + return providers.remove(ofClass) as (() -> Type)? + } /** + * Removes singleton or provider of instances of the selected type registered in the [Context]. + * @return removed provider instance bind to the selected class if any was registered or null. + * @see bind + * @see bindSingleton + */ + inline fun remove(): (() -> Type)? = removeProvider(Type::class.java) + + /** + * @param type class of the provided components. * @return true if there is a provider registered for the selected type. */ + operator fun contains(type: Class<*>): Boolean = type in providers + + /** @return true if there is a provider registered for the selected type. */ inline fun contains(): Boolean = Type::class.java in this /** @@ -98,6 +125,7 @@ open class Context { } /** + * Allows to bind a provider producing instances of the selected type. * @param provider will be bind with the selected type. If no type argument is passed, it will be bind to the same * exact class as the object it provides. * @throws InjectionException if provider for the selected type is already defined. @@ -105,13 +133,15 @@ open class Context { inline fun bind(noinline provider: () -> Type) = setProvider(Type::class.java, provider) /** - * @param singleton will be converted to a provider that always returns the same instance. If no type argument is passed, - * it will be bind to its own class. + * Allows to bind a singleton to the chosen class. + * @param singleton will be converted to a provider that always returns the same instance. If no type argument is + * passed, it will be bind to its own class. * @throws InjectionException if provider for the selected type is already defined. */ - inline fun bindSingleton(singleton: Type) = setProvider(Type::class.java, { singleton }) + inline fun bindSingleton(singleton: Type) = bind(SingletonProvider(singleton)) /** + * Allows to bind a provider to multiple classes in hierarchy of the provided instances class. * @param to list of interfaces and classes in the class hierarchy of the objects provided by the provider. Any time * any of the passed classes will be requested for injection, the selected provider will be invoked. * @param provider provides instances of classes compatible with the passed types. @@ -120,21 +150,62 @@ open class Context { fun bind(vararg to: Class, provider: () -> Type) = to.forEach { setProvider(it, provider) } /** + * Allows to bind a singleton instance to multiple classes in its hierarchy. * @param singleton instance of class compatible with the passed types. * @param to list of interfaces and classes in the class hierarchy of the singleton. Any time any of the passed classes * will be requested for injection, the selected singleton will be returned. * @throws InjectionException if provider for any of the selected types is already defined. */ - fun bindSingleton(singleton: Type, vararg to: Class) = bind(*to) { singleton } + fun bindSingleton(singleton: Type, vararg to: Class) + = bind(*to, provider = SingletonProvider(singleton)) /** * Removes all user-defined providers and singletons from the context. [Context] itselfs will still be present and * injectable with [provider] and [inject]. */ - fun clear() { + open fun clear() { providers.clear() bindSingleton(this) } + + /** + * Disposes of all [Disposable] singletons and providers registered in the context and removes them. Note that if + * registered provider _provides_ [Disposable] instances, but it does not track the resources and does not implement + * [Disposable] itself, the provided assets will not be disposed by this method. [Context] does not track all injected + * assets: only directly registered objects are disposed. [clear] is called after all assets are disposed. Errors are + * caught and logged. + * + * bindSingleton(SpriteBatch()) // SpriteBatch would be disposed. + * bind { BitmapFont() } // Each provided BitmapFont would have to be disposed manually. + * + * @see clear + */ + override fun dispose() { + providers.remove(Context::class.java) + providers.values.filterIsInstance().forEach { provider -> + try { + provider.dispose() + } catch (error: Exception) { + Gdx.app.error("KTX", "Unable to dispose of component: $provider.", error) + } + } + clear() + } +} + +/** + * Wraps singletons registered in a [Context], allowing to dispose them. + * @param singleton will be always provided by this provider. + * @see Disposable + */ +data class SingletonProvider(val singleton: Type) : Disposable, () -> Type { + /** @return [singleton]. */ + override operator fun invoke(): Type = singleton + + /** Disposes of the [singleton] if it implements [Disposable] interface. */ + override fun dispose() { + (singleton as? Disposable)?.dispose() + } } /** diff --git a/inject/src/test/kotlin/ktx/inject/injectTest.kt b/inject/src/test/kotlin/ktx/inject/injectTest.kt index 65f65979..e4a3f032 100644 --- a/inject/src/test/kotlin/ktx/inject/injectTest.kt +++ b/inject/src/test/kotlin/ktx/inject/injectTest.kt @@ -1,5 +1,10 @@ package ktx.inject +import com.badlogic.gdx.Application +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.utils.Disposable +import com.badlogic.gdx.utils.GdxRuntimeException +import com.nhaarman.mockito_kotlin.* import org.junit.After import org.junit.Assert.* import org.junit.Test @@ -60,6 +65,37 @@ class DependencyInjectionTest { assertNotSame("Singleton", context.inject()) } + @Test + fun `should remove singletons`() { + val singleton = Random() + context.bindSingleton(singleton) + + val singletonProvider = context.remove() + + assertFalse(context.contains()) + assertNotNull(singletonProvider) + assertSame(singleton, singletonProvider?.invoke()) + } + + @Test + fun `should remove providers`() { + val provider = { Random() } + context.bind(provider) + + val removed = context.remove() + + assertFalse(context.contains()) + assertNotNull(removed) + assertSame(provider, removed) + } + + @Test + fun `should not throw an exception when trying to remove absent provider`() { + val removed = context.remove() + + assertNull(removed) + } + @Test fun `should inject providers`() { context.bind { Random() } @@ -84,6 +120,34 @@ class DependencyInjectionTest { assertNotSame(context.inject(), context.provider()()) } + @Test(expected = InjectionException::class) + fun `should throw exception when trying to register provider for same type multiple times`() { + context.bind { "Test." } + context.bind { "Should throw." } + } + + @Test(expected = InjectionException::class) + fun `should throw exception when trying to register singleton of the same type multiple times`() { + context.bindSingleton("Test.") + context.bindSingleton("Should throw.") + } + + @Test(expected = InjectionException::class) + fun `should throw exception when trying to register provider and singleton of same type`() { + context.bindSingleton("Test.") + context.bind { "Should throw." } + } + + @Test(expected = InjectionException::class) + fun `should throw exception when trying to register provider for same types`() { + context.bind(String::class.java, String::class.java) { "Should throw." } + } + + @Test(expected = InjectionException::class) + fun `should throw exception when trying to register singleton for same types`() { + context.bindSingleton("Should throw.", String::class.java, String::class.java) + } + @Test fun `should inject context`() { val injected = context.inject() @@ -95,7 +159,7 @@ class DependencyInjectionTest { } @Test - fun `should remove providers on clear except for context provider`() { + fun `should remove providers on clear except for Context provider`() { context.bind { "Test" } context.clear() @@ -134,8 +198,90 @@ class DependencyInjectionTest { assertNotNull(injected) } + @Test + fun `should remove providers on dispose except for Context provider`() { + context.bind { "Test" } + + context.dispose() + + assertTrue(context.contains()) + assertFalse(context.contains()) + } + + @Test + fun `should dispose of Disposable components`() { + val nonDisposable = Any() + context.bindSingleton(nonDisposable) + val singleton = mock() + context.bindSingleton(singleton) + val provider = mock>() + context.bind(provider) + + context.dispose() + + assertFalse(context.contains()) + assertFalse(context.contains()) + verify(singleton).dispose() + assertFalse(context.contains()) + verify(provider).dispose() + } + + @Test + fun `should dispose of Disposable components with error handling`() { + Gdx.app = mock() + val singleton = mock { + on(it.dispose()) doThrow GdxRuntimeException("Expected.") + } + context.bindSingleton(singleton) + val provider = mock>() + context.bind(provider) + + context.dispose() + + assertFalse(context.contains()) + verify(singleton).dispose() + verify(Gdx.app).error(eq("KTX"), any(), argThat { this is GdxRuntimeException }) // Ensures exception was logged. + assertFalse(context.contains()) + verify(provider).dispose() + } + @After fun `clear context`() { context.clear() } + + /** Implements both [Disposable] and provider interfaces. */ + interface DisposableProvider : Disposable, () -> Type +} + +/** + * Tests specialized [SingletonProvider] that allows to dispose of singletons registered in a [Context]. + */ +class SingletonProviderTest { + @Test + fun `should not throw exception when trying to dispose singleton that does not implement Disposable`() { + val singletonProvider = SingletonProvider(Random()) + + singletonProvider.dispose() + } + + @Test + fun `should dispose of Disposable singleton`() { + val singleton = mock() + val singletonProvider = SingletonProvider(singleton) + + singletonProvider.dispose() + + verify(singleton).dispose() + } + + @Test(expected = GdxRuntimeException::class) + fun `should rethrow Disposable exceptions`() { + val singleton = mock { + on(it.dispose()) doThrow GdxRuntimeException("Expected.") + } + val singletonProvider = SingletonProvider(singleton) + + singletonProvider.dispose() + } } diff --git a/scene2d/src/main/kotlin/ktx/scene2d/widget.kt b/scene2d/src/main/kotlin/ktx/scene2d/widget.kt index 38731c4f..6fdacd64 100644 --- a/scene2d/src/main/kotlin/ktx/scene2d/widget.kt +++ b/scene2d/src/main/kotlin/ktx/scene2d/widget.kt @@ -11,7 +11,6 @@ import com.badlogic.gdx.utils.Array as GdxArray /** * Common interface applied to so-called "parental" widgets. - * @author MJ */ @Scene2dDsl interface KWidget { @@ -34,7 +33,6 @@ interface KWidget { /** * Common interface applied to widgets that extend the original [Table] and keep their children in [Cell] instances. - * @author MJ */ @Scene2dDsl interface KTable : KWidget> { @@ -180,7 +178,6 @@ interface KTable : KWidget> { /** * Common interface applied to widgets that extend [WidgetGroup] or [Group] and keep their children in an internal * collection. - * @author MJ */ @Scene2dDsl interface KGroup : KWidget { @@ -210,10 +207,7 @@ interface KGroup : KWidget { } } -/** - * Common interface applied to widgets that keep their children in [Tree] [Node] instances. - * @author MJ - */ +/** Common interface applied to widgets that keep their children in [Tree] [Node] instances. */ @Scene2dDsl interface KTree : KWidget { /** @@ -266,15 +260,13 @@ interface KTree : KWidget { "Was it properly added to the tree? Was its user object cleared?") } -/** Extends [Button] API with type-safe widget builders. - * @author MJ */ +/** Extends [Button] API with type-safe widget builders. */ @Scene2dDsl class KButton(skin: Skin, style: String) : Button(skin, style), KTable /** * Extends [Table] API with type-safe widget builders. All [Button] instances added to this table will be automatically * included in an internal [ButtonGroup]. - * @author MJ * @see ButtonGroup * @param minCheckCount minimum amount of checked buttons. * @param maxCheckCount maximum amount of checked buttons. @@ -294,13 +286,11 @@ class KButtonTable(minCheckCount: Int, maxCheckCount: Int, skin: Skin) : Table(s } } -/** Extends [CheckBox] API with type-safe widget builders. - * @author MJ */ +/** Extends [CheckBox] API with type-safe widget builders. */ @Scene2dDsl class KCheckBox(text: String, skin: Skin, style: String) : CheckBox(text, skin, style), KTable -/** Extends [Container] API with type-safe widget builders. Note that this widget may store only a single child. - * @author MJ */ +/** Extends [Container] API with type-safe widget builders. Note that this widget may store only a single child. */ @Scene2dDsl class KContainer : Container(), KGroup { @Suppress("UNCHECKED_CAST") @@ -310,28 +300,23 @@ class KContainer : Container(), KGroup { } } -/** Extends [Dialog] API with type-safe widget builders. - * @author MJ */ +/** Extends [Dialog] API with type-safe widget builders. */ @Scene2dDsl class KDialog(title: String, skin: Skin, style: String) : Dialog(title, skin, style), KTable -/** Extends [HorizontalGroup] API with type-safe widget builders. - * @author MJ */ +/** Extends [HorizontalGroup] API with type-safe widget builders. */ @Scene2dDsl class KHorizontalGroup : HorizontalGroup(), KGroup -/** Extends [ImageButton] API with type-safe widget builders. - * @author MJ */ +/** Extends [ImageButton] API with type-safe widget builders. */ @Scene2dDsl class KImageButton(skin: Skin, style: String) : ImageButton(skin, style), KTable -/** Extends [ImageTextButton] API with type-safe widget builders. - * @author MJ */ +/** Extends [ImageTextButton] API with type-safe widget builders. */ @Scene2dDsl class KImageTextButton(text: String, skin: Skin, style: String) : ImageTextButton(text, skin, style), KTable -/** Extends LibGDX List widget with items building method. - * @author MJ */ +/** Extends LibGDX List widget with items building method. */ @Scene2dDsl class KListWidget(skin: Skin, style: String) : com.badlogic.gdx.scenes.scene2d.ui.List(skin, style) { /** @@ -351,8 +336,7 @@ class KListWidget(skin: Skin, style: String) : com.badlogic.gdx.scenes.scene2 } } -/** Extends [Tree] [Node] API with type-safe widget builders. - * @author MJ */ +/** Extends [Tree] [Node] API with type-safe widget builders. */ @Scene2dDsl class KNode(actor: Actor) : Node(actor), KTree { override fun add(actor: Actor): KNode { @@ -372,8 +356,7 @@ class KNode(actor: Actor) : Node(actor), KTree { } } -/** Extends [ScrollPane] API with type-safe widget builders. Note that this widget may store only a single child. - * @author MJ */ +/** Extends [ScrollPane] API with type-safe widget builders. Note that this widget may store only a single child. */ @Scene2dDsl class KScrollPane(skin: Skin, style: String) : ScrollPane(null, skin, style), KGroup { override fun addActor(actor: Actor?) { @@ -382,8 +365,7 @@ class KScrollPane(skin: Skin, style: String) : ScrollPane(null, skin, style), KG } } -/** Extends [SelectBox] with items building method. - * @author MJ */ +/** Extends [SelectBox] with items building method. */ @Scene2dDsl class KSelectBox(skin: Skin, style: String) : SelectBox(skin, style) { /** @@ -405,8 +387,7 @@ class KSelectBox(skin: Skin, style: String) : SelectBox(skin, style) { /** Extends [ScrollPane] API with type-safe widget builders. Note that this widget may store only a single child. * It is advised to use the inlined extension factory methods added by [KGroup] rather than set its widgets directly - * with [setFirstWidget] or [setSecondWidget]. - * @author MJ */ + * with [setFirstWidget] or [setSecondWidget]. */ @Scene2dDsl class KSplitPane(vertical: Boolean, skin: Skin, style: String) : SplitPane(null, null, vertical, skin, style), KGroup { override fun addActor(actor: Actor?) { @@ -418,23 +399,19 @@ class KSplitPane(vertical: Boolean, skin: Skin, style: String) : SplitPane(null, } } -/** Extends [Stack] API with type-safe widget builders. - * @author MJ */ +/** Extends [Stack] API with type-safe widget builders. */ @Scene2dDsl class KStack : Stack(), KGroup -/** Extends [Table] API with type-safe widget builders. - * @author MJ */ +/** Extends [Table] API with type-safe widget builders. */ @Scene2dDsl class KTableWidget(skin: Skin) : Table(skin), KTable -/** Extends [TextButton] API with type-safe widget builders. - * @author MJ */ +/** Extends [TextButton] API with type-safe widget builders. */ @Scene2dDsl class KTextButton(text: String, skin: Skin, style: String) : TextButton(text, skin, style), KTable -/** Extends [Tree] API with type-safe widget builders. - * @author MJ */ +/** Extends [Tree] API with type-safe widget builders. */ @Scene2dDsl class KTreeWidget(skin: Skin, style: String) : Tree(skin, style), KTree { override fun add(actor: Actor): KNode { @@ -444,12 +421,10 @@ class KTreeWidget(skin: Skin, style: String) : Tree(skin, style), KTree { } } -/** Extends [VerticalGroup] API with type-safe widget builders. - * @author MJ */ +/** Extends [VerticalGroup] API with type-safe widget builders. */ @Scene2dDsl class KVerticalGroup : VerticalGroup(), KGroup -/** Extends [Window] API with type-safe widget builders. - * @author MJ */ +/** Extends [Window] API with type-safe widget builders. */ @Scene2dDsl class KWindow(title: String, skin: Skin, style: String) : Window(title, skin, style), KTable diff --git a/scene2d/src/test/kotlin/ktx/scene2d/tooltipTest.kt b/scene2d/src/test/kotlin/ktx/scene2d/tooltipTest.kt index 52d4ad33..d23d3fd7 100644 --- a/scene2d/src/test/kotlin/ktx/scene2d/tooltipTest.kt +++ b/scene2d/src/test/kotlin/ktx/scene2d/tooltipTest.kt @@ -11,7 +11,6 @@ import org.junit.Test /** * Tests extension methods that allow to add [Tooltip] instances to all actors. - * @author MJ */ class TooltipFactoriesTest : NeedsLibGDX() { @Test diff --git a/version.txt b/version.txt index aab8c255..72636139 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.6-b3 +1.9.6-b4 diff --git a/vis/src/main/kotlin/ktx/vis/widgets.kt b/vis/src/main/kotlin/ktx/vis/widgets.kt index ec138ba7..d211289f 100644 --- a/vis/src/main/kotlin/ktx/vis/widgets.kt +++ b/vis/src/main/kotlin/ktx/vis/widgets.kt @@ -102,8 +102,7 @@ class KVisTree(styleName: String) : VisTree(styleName), TreeWidgetFactory { } } -/** Extends [Tree] [Node] API with type-safe widget builders. - * @author MJ */ +/** Extends [Tree] [Node] API with type-safe widget builders. */ @VisDsl class KNode(actor: Actor) : Node(actor), TreeWidgetFactory { override fun addActorToTree(actor: T): KNode { diff --git a/vis/src/test/kotlin/ktx/vis/widgetsTest.kt b/vis/src/test/kotlin/ktx/vis/widgetsTest.kt index e7b77aba..1bce7976 100644 --- a/vis/src/test/kotlin/ktx/vis/widgetsTest.kt +++ b/vis/src/test/kotlin/ktx/vis/widgetsTest.kt @@ -269,7 +269,6 @@ class KTabbedPaneTest : NeedsLibGDX() { /** * Tests [KVisTree] interface: base for all parental actors operating on tree nodes. - * @author MJ */ class KTreeTest : NeedsLibGDX() { @Test