Bonjour tout le monde avec Kotlin et JavaFX

introduction

Java Reading Group BOF organise depuis janvier 2018 un groupe de lecture «Kotlin in Action».

Si vous voulez apprendre Kotlin de toutes vos forces, j'ai décidé de réécrire l'interface graphique que le message Hello world circule dans le JavaFX créé précédemment avec Kotlin parce que je voulais faire quelque chose.

Cet article vise à remplacer le code Java ci-dessus par Kotlin. Par conséquent, veuillez vous référer au blog ci-dessus pour l'explication de JavaFX.

Stockez le code créé dans le référentiel suivant (URL de la visionneuse du référentiel)

http://www.torutk.com/projects/swe/repository/revisions/master/show/learn/kotlin/javafx/MessageBoard

Préparer l'environnement

Si vous souhaitez éviter les problèmes liés à la création d'un environnement et utiliser beaucoup de support de codage tel que l'achèvement de code, l'environnement de développement de JetBrains IntelliJ IDEA, qui développe Kotlin, ou Android Studio, basé sur IntelliJ IDEA, est presque le choix. est.

Dans l'article "Hello world qui coule avec JavaFX", j'ai présenté comment créer dans l'environnement de ligne de commande Java. La connaissance de l'environnement d'exécution Java est utile et doit être connue. Cependant, dans le cas de Kotlin, il y a beaucoup de travail dans la préparation de l'environnement Kotlin en plus de l'environnement Java, et je n'ai pas beaucoup l'occasion d'utiliser ces connaissances, donc cette fois j'utiliserai l'EDI depuis le début.

IntelliJ IDEA Community Edition IntelliJ IDEA est un produit d'environnement de développement commercial, mais Community Edition est fourni gratuitement. L'environnement de développement de Kotlin est inclus dans Community Edition, nous allons donc l'utiliser cette fois.

Je pense qu'il peut être utilisé dans Android Studio (également fourni gratuitement par Google) de la même manière, mais je ne l'ai pas vérifié.

Localisation japonaise d'IntelliJ IDEA (si vous le souhaitez)

Pleiades, un outil de localisation japonais familier dans Eclipse, prend également en charge la localisation japonaise d'IntelliJ IDEA. Ensuite, j'écris une petite méthode d'introduction.

Polices IntelliJ IDEA dans l'environnement Windows japonais (si vous le souhaitez)

Dans l'environnement Windows japonais, les polices européennes sont belles, mais les polices japonaises sont MS Gothic et dentelées. Ensuite, j'écris un paramètre pour nettoyer un peu la police.

Créer un projet Kotlin

Dans IntelliJ IDEA, créez un projet pour un programme Hello world fluide.

Créer un fichier source Kotlin

MessageBoard.kt


class MessageBoard {
}

De là, nous appellerons JavaFX et créerons un monde Hello fluide.

Programmation Hello World avec Kotlin et JavaFX

Appelez JavaFX avec Kotlin et programmez l'interface graphique du message Hello world.

Hériter de la classe Application JavaFX

JavaFX hérite de la classe Application et prépare une classe qui est l'entrée du traitement JavaFX.

Si vous continuez avec les connaissances d'un programmeur Java qui a mordu Kotlin ...

Laissez la classe créée par l'application hériter de la classe Application JavaFX.

MessageBoard.kt


class MessageBoard : Application {
}

Dans Kotlin, l'héritage est spécifié par un symbole deux-points. Ici, la classe Application doit être importée, donc dans le code IntelliJ IDEA, le message contextuel suivant s'affiche.

image.png

Suivez le message contextuel et appuyez sur Alt + Entrée et sélectionnez javafx.application.Application dans la liste des candidats.

image.png

Une instruction d'importation est générée.

MessageBoard.kt


import javafx.application.Application

class MessageBoard : Application {
}

Cependant, il est toujours incomplet et est dans un état d'erreur de compilation. L'écran suivant montre la situation dans laquelle le contenu de l'erreur est affiché.

image.png

Vous devez définir une méthode de démarrage. Dans le menu Code> Implémenter la méthode, générez le début de méthode abstraite de la classe Application.

image.png

Le code généré est:

MessageBoard.kt


import javafx.application.Application
import javafx.stage.Stage

class MessageBoard : Application {
    override fun start(primaryStage: Stage?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

Cependant, il y a encore des erreurs. Une ligne ondulée rouge est dessinée sous l'application. Lorsque vous déplacez le curseur sur Application, le contenu de l'erreur apparaîtra comme suit.

image.png

Le message est "Ce type a un constructeur et doit donc être initialisé ici". Qu'est-ce que c'est? Les détails seront décrits dans la section suivante.

Lors de l'héritage dans une classe qui n'a pas de constructeur

«Kotlin en action» à la page 106 de la section 4.2.1

Open class Button ← Un constructeur par défaut sans argument est généré

Si vous héritez de la classe Button et n'avez pas de constructeur, vous devez appeler explicitement le constructeur de cette super classe, même si le constructeur de la super classe n'a pas d'arguments.

class RadioButton : Button()

Donc, cette fois, nous ne définissons pas de constructeur dans la classe MessageBoard, donc la bonne réponse est de mettre des parenthèses dans la superclasse héritée.

MessageBoard.kt


import javafx.application.Application
import javafx.stage.Stage

class MessageBoard : Application() {
    override fun start(primaryStage: Stage?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

Vous avez maintenant le code avec l'erreur de compilation supprimée.

Où la méthode principale est définie

Mais je ne peux toujours pas le faire. Définissez la méthode principale qui est le point d'entrée du programme. Dans Kotlin, au lieu de définir la méthode principale comme une méthode statique dans la classe comme en Java, elle est définie comme une fonction de niveau supérieur.

MessageBoard.kt


class MessageBoard : Application() {...}

fun main(args: Array<String>) {
    //Écrivez le code qui appelle la méthode de lancement de la classe Application ici
}

Kotlin n'a pas de méthodes statiques / champs statiques, mais il est possible d'accéder aux méthodes statiques / champs statiques définis dans les classes Java, et leurs expressions d'appel sont les mêmes qu'en Java.

La méthode de lancement de la classe Application JavaFX a des arguments différents lorsqu'elle est appelée à partir d'une sous-classe de la classe Application et lorsqu'elle est appelée depuis l'extérieur de cette sous-classe. Cette fois, comme il s'agit de l'extérieur de la sous-classe, Java appelle la méthode de lancement du format suivant.

public static void launch(
        Class<? extends Application> appClass,
        String... args
)

Il y a deux défis ici:

La première consiste à récupérer la référence de classe (type KClass) avec :: class pour la classe, puis à appeler et à acquérir la propriété étendue java de type KClass.

Le second utilise l'opérateur de diffusion pour étendre le tableau.

fun main(args: Array<String>) {
    Application.launch(MessageBoard::class.java, *args)
}

Où spécifier des arguments de longueur variable avec l'opérateur de propagation, voir «Kotlin en action», Section 3.4.2 (p.76).

Kotlin doit récupérer explicitement le contenu du tableau, de sorte que tous les éléments du tableau soient des arguments indépendants de la fonction appelante. Techniquement, cette fonctionnalité est appelée en utilisant l'opérateur spread, mais c'est en fait aussi simple que de mettre un * devant l'argument correspondant.

fun main(args: Array<String>) {
    val list = listOf("args: ", *args)← L'opérateur Spread récupère le contenu d'un tableau
    print(list)
}

Il y a.

Ceci termine l'implémentation minimale. Lançons-le (bien que la fenêtre ne soit pas encore affichée).

Paramètres d'exécution

Si vous définissez la fonction principale au niveau supérieur du fichier, l'icône suivante sera ajoutée sur le côté gauche de la ligne.

image.png

Cliquez sur cette icône pour afficher les options de couverture d'exécution, de débogage et d'exécution. Sélectionnez Exécuter.

image.png

Le code sera ensuite construit et exécuté. Lorsqu'il est exécuté à partir de cette icône pour la première fois, [Exécuter] est activé dans le menu [Exécuter], et le nom de classe (nom de fichier) du fichier dans lequel la fonction principale est définie dans la liste dans le menu [Exécuter]> [Exécuter la configuration de démarrage]. Une classe nommée avec Kt) sera ajoutée et peut être sélectionnée.

image.png

Lorsqu'il est exécuté, le code d'implémentation du modèle TODO de la méthode start est appelé pour afficher une exception.

"C:\Program Files\Java\jdk-9\bin\java" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\lib\idea_rt.jar=52509:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "D:\work\java\repos\swe.primus\learn\kotlin\javafx\MessageBoard\out\production\MessageBoard;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-reflect.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-test.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk7.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk8.jar" MessageBoardKt
Exception in Application start method
Exception in thread "main" Exception in thread "Thread-2" java.lang.RuntimeException: Exception in Application start method
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:973)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:198)
	at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: kotlin.NotImplementedError: An operation is not implemented: not implemented
	at MessageBoard.start(MessageBoard.kt:6)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:919)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(PlatformImpl.java:449)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
	at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:175)
	... 1 more
java.lang.IncompatibleClassChangeError

Afficher la fenêtre de niveau supérieur

Il est enfin temps de mettre en œuvre. Tout d'abord, décrivez la visualisation de la scène avec la méthode de démarrage. En tant que programmeur Java, je coderais ce qui suit, qui est une erreur de compilation. Le fait est que le type d'argument est Stage? Et a un? À la fin.

MessageBoard.kt


    override fun start(primaryStage: Stage?) {
        primaryStage.show()
    }

L'erreur de compilation s'affiche dans la fenêtre contextuelle suivante.

image.png

Les types d'argument de la méthode start sont Stage? Type et Nullable type. Par conséquent, il est nécessaire de vérifier null avant d'appeler la méthode ou d'appeler la méthode pour null.

Ici, si l'argument primaryStage devient nul, rien n'est fait, donc la méthode show est appelée avec l'opérateur d'appel sécurisé (?.).

MessageBoard.kt


    override fun start(primaryStage: Stage?) {
        primaryStage?.show()
    }

Dans «Kotlin en action», section 6.1.3 (p.181)

L'opérateur d'appel sécurisé?. Il s'agit d'une combinaison de vérification de null et d'invocation de méthode en un seul opérateur. (Omis) Autrement dit, si la valeur à laquelle vous essayez d'appeler une méthode n'est pas nulle, l'appel de méthode sera exécuté normalement, si la valeur est nulle, l'appel sera ignoré et nul sera utilisé comme valeur à la place.

Il y a.

Lors de l'exécution, une fenêtre vide apparaîtra.

image.png

Placement du message

Affichez le texte dans une fenêtre vide.

Tout d'abord, générez un texte qui affiche les caractères. La différence avec Java est qu'il n'y a pas de nouvel opérateur et que le type de variable est déclaré avec une valeur immuable.

MessageBoard.kt


val message = Text("Hello, world. This is JavaFX from Kotlin.")

Créez un groupe pour regrouper les nœuds JavaFX. La création d'instances et les types de variables sont similaires au texte ci-dessus. Ensuite, ce qu'on appelait setLayoutY (50d) en Java devient une instruction d'affectation à une propriété dans Kotlin. De plus, en java, Kotlin n'a pas de D (ou d) qui indique un littéral numérique double, donc la valeur après la virgule décimale est spécifiée comme .0. En passant, si vous écrivez 50 et un littéral entier (type Int), une erreur se produira si les types sont différents.

MessageBoard.kt


val group = Group(message)
group.layoutY = 50.0

Générez une scène. Quant à la taille (largeur, hauteur), si le type est différent pour les entiers, une erreur se produira, donc les valeurs après la virgule décimale sont spécifiées. De plus, en java, il est défini sur primaryStage avec la méthode setScene, et dans Kotlin, il est affecté à une propriété.

MessageBoard.kt


        val scene = Scene(group, 320.0, 160.0)
        primaryStage?.scene = scene

La méthode de démarrage entière ressemble à ceci:

MessageBoard.kt


    override fun start(primaryStage: Stage?) {
        val message = Text("Hello, world. This is JavaFX from Kotlin.")
        val group = Group(message)
        group.layoutY = 50.0
        val scene = Scene(group, 320.0, 160.0)
        primaryStage?.scene = scene
        primaryStage?.show()
    }

Je le ferai.

image.png

Envoyer un message

Utilisez l'animation JavaFX pour déplacer le texte de droite à gauche.

Dans la méthode de démarrage, définissez TranslateTransition pour l'animation.

MessageBoard.kt


val messageTransition = TranslateTransition(Duration.seconds(8.0), message)
messageTransition.fromX = 320.0
messageTransition.toX = -320.0
messageTransition.interpolator = Interpolator.LINEAR
messageTransition.cycleCount = TranslateTransition.INDEFINITE

Encore une fois, la différence est que vous n'avez pas besoin du nouvel opérateur et que la méthode set est une affectation à une propriété, mais c'est presque la même chose que le code Java. Appelez également play () à la fin.

MessageBoard.kt


messageTransition.play()

Une fois exécuté, le monde Hello qui coule s'affiche. Ce qui suit est un GIF animé de l'écran affiché à 10 fps (le plus fou est le problème du FPS lors de la création d'un GIF animé, JavaFX dessine à un maximum de 60 fps).

HelloWorld.gif

Dans le code ci-dessus, la description de TranslateTransition est redondante pour Kotlin (l'attribution de propriété à une variable locale apparaît à plusieurs reprises), donc apply est utilisé pour réduire la quantité de description. La méthode de démarrage complète après réduction est indiquée ci-dessous.

MessageBoard.kt


    override fun start(primaryStage: Stage?) {
        val message = Text("Hello, world. This is JavaFX from Kotlin.")

        val group = Group(message)
        group.layoutY = 80.0

        val scene = Scene(group, 320.0, 160.0)

        primaryStage?.scene = scene
        primaryStage?.show()

        TranslateTransition(Duration.seconds(8.0), message).apply {
            fromX = 320.0
            toX = -320.0
            interpolator = Interpolator.LINEAR
            cycleCount = TranslateTransition.INDEFINITE
        }.play()

    }

Ajustements d'apparence, couleurs et polices

Enfin, définissez la couleur et la taille du texte du message.

MessageBoard.kt


    override fun start(primaryStage: Stage?) {
        val message = Text().apply {
            text = "Hello, world. This is JavaFX from Kotlin."
            fill = Color.DARKMAGENTA
            font = Font.font("Serif", FontWeight.SEMI_BOLD, 32.0)
        }
        val messageWidth = message.layoutBounds.width
        val messageHeight = message.layoutBounds.height

        val group = Group(message)
        group.layoutY = messageHeight * 2

        val scene = Scene(group, messageWidth, messageHeight * 3)

        primaryStage?.scene = scene
        primaryStage?.show()

        TranslateTransition(Duration.seconds(8.0), message).apply {
            fromX = messageWidth
            toX = -messageWidth
            interpolator = Interpolator.LINEAR
            cycleCount = TranslateTransition.INDEFINITE
        }.play()
    }

Je le ferai.

HelloWorldColorFont.gif

Distribution de Hello world qui coule

Cette fois, le but est de le regrouper dans un JAR exécutable.

Générer un JAR exécutable avec IntelliJ IDEA

Dans l'EDI NetBeans, lorsque j'ai créé un projet Java, il a créé un fichier JAR qui pouvait être exécuté sans aucun paramètre supplémentaire.

Après avoir étudié ce que IntelliJ IDEA devrait être en mesure de faire, vous pouvez générer un fichier JAR exécutable en suivant les étapes ci-dessous.

image.png

Quelle est la classe principale à Kotlin?

Vous devez spécifier la classe principale. Dans Kotlin, la fonction principale est écrite au niveau supérieur du fichier. Une fois compilé en code d'octet Java, ce sera une classe nommée d'après le nom du fichier (inclus dans la classe MessageBoardKt si la fonction principale est définie au niveau supérieur du fichier MessageBoard.kt).

Lors de la construction et de l'exécution avec IntelliJ IDEA, le fichier de classe suivant est généré sous out \ production \ MessageBoard sous le répertoire du projet.

Examinez les méthodes définies dans chaque fichier de classe avec la commande javap du JDK.

D:~> javap MessageBoard
Compiled from "MessageBoard.kt"
public final class MessageBoard extends javafx.application.Application {
  public void start(javafx.stage.Stage);
  public MessageBoard();
}

D:~>javap MessageBoardKt
Compiled from "MessageBoard.kt"
public final class MessageBoardKt {
  public static final void main(java.lang.String[]);
}

La classe qui a la méthode principale est MessageBoardKt.class. Donc, dans la suite de la précédente, spécifiez la classe principale sur l'écran suivant. Cliquez sur […] à l'extrémité droite de la colonne principale de la classe.

image.png

Cela fera apparaître l'écran Sélectionner la classe principale. Dans l'onglet Recherche par nom, entrez une partie du nom de la classe principale (ici «M») car il est initialement vide. Ensuite, la classe correspondant à ce nom sera répertoriée, donc sélectionnez [MessageBoardKt] ici.

image.png

Le champ de fichier JAR de la bibliothèque est défini sur Extraire vers JRA cible par défaut. Cela inclura les fichiers de classe de la bibliothèque ensemble dans le fichier exécutable JAR lors de l'utilisation d'une bibliothèque standard non Java SE. [Copier dans le répertoire de sortie et lien via le manifeste] est la différence entre la préparation d'un fichier de bibliothèque séparément du fichier JAR exécutable et l'écriture d'une référence au fichier de bibliothèque dans le manifeste dans le fichier JAR exécutable.

Cette fois, nous laisserons la valeur par défaut pour qu'elle puisse être exécutée en ne distribuant qu'un seul fichier JAR. Cliquez sur OK pour afficher la configuration comme indiqué ci-dessous.

image.png

Si vous cochez [Inclure dans la construction du projet], un fichier JAR sera généré lors de l'exécution de la construction. Si vous n'aimez pas le temps nécessaire, laissez-la décochée et choisissez le menu Construire> Créer des artefacts pour générer un fichier JAR. Sélectionnez Build / Rebuild avec le nom de la configuration que vous venez de créer.

image.png

Le fichier JAR a été généré dans le répertoire du projet out \ artifacts \ MessageBoard_jar \ MessageBoard.jar. La capacité est aussi grande que 3,3 Mo. C'est parce qu'il inclut également la bibliothèque d'exécution Kotlin.

Si vous distribuez ce fichier JAR indépendamment, vous pouvez exécuter le programme créé par Kotlin dans l'environnement où JRE est inclus.

Version avancée

Appliquer la déclaration de décomposition à la classe Java

Si le code suivant commence à se sentir redondant,

val messageWidth = message.layoutBounds.width
val messageHeight = message.layoutBounds.height

Je souhaite appliquer la déclaration de décomposition

val (messageWidth, messageHeight) = message.layoutBounds

Je veux ecrire. Cependant, les classes Java n'ont pas de méthodes définies component1 (), component2 (), ... pour appliquer les déclarations de décomposition.

Par conséquent, définissez-le avec une méthode d'extension.

operator fun Bounds.component1(): Double = width

operator fun Bounds.component2(): Double = height

Cela permettra à la classe Bounds d'être utilisée dans les déclarations de décomposition.

Cependant, c'est une grande exagération. Ce n'est «pas de grâce» (il est inévitable de passer).

Recommended Posts

Bonjour tout le monde avec Kotlin et JavaFX
Bonjour tout le monde avec Kotlin et Tornado FX
Hello World avec Docker et langage C
Hello World avec GWT 2.8.2 et Maven
"Hello, World!" Avec Kotlin + CLI en 5 minutes
Hello World avec Micronaut
Comparez Hello, world! Avec Spring Boot avec Java, Kotlin et Groovy
Bonjour tout le monde avec Spring Boot!
Bonjour tout le monde avec VS Code!
Hello World avec Spring Boot
Hello World avec SpringBoot / Gradle
Bonjour tout le monde! Avec Asakusa Framework!
Jusqu'à "Hello World" avec Spring Boot
(Intellij) Hello World avec Spring Boot
Hello World avec GlassFish 5.1 + Servlet + JSP
Créez un PDF avec itext7 ~ HelloWorld ~
Faites glisser et déposez des fichiers avec JavaFX
"Hello world" pour ImageJ avec Eclipse
Version du système d'exploitation Android (7.1.2) et Hello World
Bonjour tout le monde en Java et Gradle
[Swift] Créez un projet avec Xcode (ver 12.1) et affichez "Hello, World!"
Jusqu'à ce que vous exécutiez Hello World of JavaFX avec VSCode + Gradle
Hello World avec Eclipse + Spring Boot + Maven
Bonjour tout le monde avec le moteur de modèle Java Thymeleaf
Agrandir / réduire et mouvement parallèle avec JavaFX Canvas (Revenge)
Développement Java avec Codenvy: Hello World! Run
Comment Spring Security fonctionne avec Hello World
(IntelliJ + gradle) Hello World avec Spring Boot
Construction d'un environnement Java minimal et Hello World
JavaFX et HiDPI
HelloFX avec JavaFX
Lire "Hello world"
Essayez d'écrire "Hello, World" avec une combinaison de plusieurs langues et bibliothèques
Java, bonjour le monde!
Java Hello World
Bonjour tout le monde! Avec Spring Boot (Marven + éditeur de texte)
Hello World à une vitesse explosive avec Spring Initializr! !! !!
Créer un environnement Java et afficher hello world [Débutant]
Exécutez JSP Hello World avec Tomcat sur Docker
Jusqu'à ce que vous installiez Gradle et sortiez "Hello World"
[Java] Hello World avec Java 14 x Spring Boot 2.3 x JUnit 5 ~
Afficher un simple Hello World avec SpringBoot + IntelliJ
Essayez d'afficher Hello World avec Spring + Gradle
Vous pouvez éliminer @Param avec Kotlin 1.1 et MyBatis 3.4.1+! !!
Un simple jeu de ciseaux-papier-pierre avec JavaFX et SceneBuilder
Premier JavaFX ~ Introduction facile Bonjour création d'interface graphique universelle ~
Facile à afficher Hello World avec Rails + Docker
Hello World avec Java Servlet et JSP (démarrage facile du serveur Web avec Maven + Jetty)
Joyeux Noël avec JavaFX !!
"Hello World" en Java
Hello World (API REST) avec Apache Camel + Spring Boot 2
Boutons et étiquettes JavaFX
Apprendre Java (1) - Hello World
Lire System.out.println ("bonjour, monde")
Écrivons Hello World
Hello World en Java
Étudier Java-Partie 1-Hello World
Je veux faire des transitions d'écran avec kotlin et java!
Préparer l'environnement pour java11 et javaFx avec Ubuntu 18.4