12 Commits

21 changed files with 175 additions and 83 deletions

View File

@@ -7,13 +7,16 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased] ## [Unreleased]
## Added ## Added
- Dokumentation `EventDispatcher`. - Dokumentation erweitert.
- Caching-Mechanismen in Klasse `util.Cache` ausgelagert.
- `util.io.ImageLoader` und `util.io.FontLoader` verwenden `Cache`.
## Changed ## Changed
- Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird. - Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird.
## Fixed ## Fixed
- `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt. - `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt.
- Timing-Problem beim Aufruf von `AudioListener.playbackStopped()` in `Sound` behoben.
## Removed ## Removed
- `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar. - `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar.

View File

@@ -33,24 +33,16 @@ dependencies {
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
} }
javadoc { jar {
options { manifest {
encoding = "UTF-8" attributes 'Class-Path': '.'
overview = "src/main/java/overview.html"
// title = "Die Zeichenmaschine"
// options.links 'https://docs.oracle.com/javase/8/docs/api/'
// options.links 'https://docs.oracle.com/javaee/7/api'
options.links 'https://docs.oracle.com/en/java/javase/11/docs/api'
} }
options.addStringOption("charset", "UTF-8")
}
test {
useJUnitPlatform()
} }
tasks.register('jarMP3SPI', Jar) { tasks.register('jarMP3SPI', Jar) {
group "build"
description "Build jar with MP3SPI included"
archiveClassifier = 'mp3spi' archiveClassifier = 'mp3spi'
duplicatesStrategy = 'exclude' duplicatesStrategy = 'exclude'
// archivesBaseName = 'zeichenmaschine-mp3spi' // archivesBaseName = 'zeichenmaschine-mp3spi'
@@ -61,8 +53,45 @@ tasks.register('jarMP3SPI', Jar) {
} }
task buildAll { task buildAll {
group "build"
description "Build all jar packages"
dependsOn 'jar' dependsOn 'jar'
dependsOn 'jarMP3SPI' dependsOn 'jarMP3SPI'
dependsOn 'sourcesJar' dependsOn 'sourcesJar'
dependsOn 'javadocJar' dependsOn 'javadocJar'
} }
javadoc {
options {
encoding = "UTF-8"
overview = "src/resources/java/overview.html"
// title = "Die Zeichenmaschine"
// options.links 'https://docs.oracle.com/javase/8/docs/api/'
// options.links 'https://docs.oracle.com/javaee/7/api'
options.links 'https://docs.oracle.com/en/java/javase/11/docs/api'
}
options.addStringOption("charset", "UTF-8")
}
task mkdocs(type: Exec) {
group "documentation"
description "Build MKDocs site"
workingDir "${projectDir}"
commandLine ".venv/bin/python", "-m", "mkdocs", "build"
}
task buildDocs {
group "documentation"
description "Run all documentation tasks"
dependsOn 'javadoc'
dependsOn 'javadocJar'
dependsOn 'mkdocs'
}
test {
useJUnitPlatform()
}

View File

@@ -1,4 +1,3 @@
<figure markdown> <figure markdown>
![Zeichenmaschine.xyz](assets/icon_512.png){ width=128 } ![Zeichenmaschine.xyz](assets/icon_512.png){ width=128 }
</figure> </figure>
@@ -9,10 +8,11 @@ Informatikunterricht.</h2>
## Projektidee ## Projektidee
Die **Zeichenmaschine** ist eine für den Informatikunterricht entwickelte Bibliothek, Die **Zeichenmaschine** ist eine für den Informatikunterricht entwickelte
die unter anderem an [Processing](https://processing.org/) angelehnt ist. Die Bibliothek, die unter anderem an [Processing](https://processing.org/) angelehnt
Bibliothek soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen ist. Die Bibliothek soll einige der üblichen Anfängerschwierigkeiten mit Java
und für Schülerinnen und Schüler im Unterricht nutzbar machen. vereinfachen und grafische Ausgaben für Schülerinnen und Schüler im Unterricht
leichter nutzbar machen.
!!! warning !!! warning
@@ -29,12 +29,39 @@ und für Schülerinnen und Schüler im Unterricht nutzbar machen.
(Gleiches gilt für diese Webseite zum Projekt.) (Gleiches gilt für diese Webseite zum Projekt.)
## Dokumentation ## Dokumentation
* [Schnellstart](quickstart.md) * [Schnellstart](quickstart.md)
* [Installation](installation.md) * [Installation](installation.md)
* {{ javadoc_link() }} * {{ javadoc_link() }}
## Über die Zeichenmaschine ## Über die Zeichenmaschine
Die _Zeichenmaschine_ ist aus dem Wunsch entstanden, nach einer Einführung in
die Grundlagen der Programmiersprache Java mit Processing, einen Übergang zur
objektorientierten Modellierung und Programmierung mit BlueJ zu leichter zu
ermöglichen. Mit Processing kann zwar auch objektorientiert programmiert werden,
aber mit Blick auf die Sekundarstufe II in NRW ist ein Wechsel zu einer
generellen Programmierumgebung wie BlueJ wünschenswert.
Die Motivation von Processing, schnell grafische Ergebnisse der eigenen
Programme zu sehen, sollte aber für den Übergang erhalten bleiben. So ist eine
kleine Bibliothek mit minimalen Abhängigkeiten entstanden, die an Processing
angelehnt einfache Schnittstellen bereitstellte, um Programmen eine grafische
Ausgabe zu ermöglichen, ohne viel "Boilerplate" Code schreiben zu müssen.
Aus diesen Anfängen ist nach und nach eine umfassende Grafikbibliothek
entstanden, die als _Zeichenmaschine_ veröffentlicht wurde.
### Was die Zeichenmaschine nicht ist
Die Bibliothek hat nicht den Anspruch, ein Konkurrent zu Processing oder
anderen, seit Jahren etablierten und ausgereiften, Grafiksystemen zu sein. Vor
allem ist die _Zeichenmaschine_ keine vollwertige Game Engine, die auf die
Ausführung komplexer Spiele spezialisiert ist. Für diese Zwecke gibt es genügend
Alternativen, von deren Nutzung gar nicht abgeraten werden soll.
## Aufbau der Zeichenmaschine
!!! info !!! info
In der Zeichenmaschine werden bewusst nur englischsprachige Bezeichner für In der Zeichenmaschine werden bewusst nur englischsprachige Bezeichner für

View File

@@ -67,7 +67,7 @@ public class Constants {
/** /**
* Patchversion der Zeichenmaschine. * Patchversion der Zeichenmaschine.
*/ */
public static final int APP_VERSION_REV = 32; public static final int APP_VERSION_REV = 34;
/** /**
* Version der Zeichenmaschine als Text-String. * Version der Zeichenmaschine als Text-String.

View File

@@ -2,6 +2,7 @@ package schule.ngb.zm;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
@@ -10,6 +11,7 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
@@ -174,12 +176,14 @@ public class Zeichenfenster extends JFrame {
// Das Icon des Fensters ändern // Das Icon des Fensters ändern
try { try {
if( Zeichenmaschine.MACOS ) { if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png"); InputStream iconStream = this.getClass().getResourceAsStream("icon_512.png");
if( iconUrl != null ) { if( iconStream != null ) {
Image icon = ImageIO.read(iconUrl); Image icon = ImageIO.read(iconStream);
// Dock Icon in macOS setzen // Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar(); Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon); taskbar.setIconImage(icon);
} else {
LOG.warn("Could not load dock-icon");
} }
} else { } else {
ArrayList<Image> icons = new ArrayList<>(4); ArrayList<Image> icons = new ArrayList<>(4);
@@ -190,8 +194,12 @@ public class Zeichenfenster extends JFrame {
} }
} }
if( icons.isEmpty() ) {
LOG.warn("Could not load dock-icon");
} else {
this.setIconImages(icons); this.setIconImages(icons);
} }
}
} catch( IllegalArgumentException | IOException e ) { } catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage()); LOG.warn("Could not load image icons: %s", e.getMessage());
} catch( SecurityException | UnsupportedOperationException macex ) { } catch( SecurityException | UnsupportedOperationException macex ) {

View File

@@ -797,6 +797,7 @@ public class Zeichenmaschine extends Constants {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight()); BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics(); Graphics2D g = img.createGraphics();
// TODO: Transparente Hintergründe beim Speichern von png erlauben
g.setColor(DEFAULT_BACKGROUND.getJavaColor()); g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight()); g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g); canvas.draw(g);

View File

@@ -923,12 +923,43 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
} }
} }
public void curveTo( double ctrlX, double ctrlY, double x, double y ) {
if( !pathStarted ) {
path.moveTo(x, y);
pathStarted = true;
} else {
path.quadTo(
ctrlX, ctrlY,
x, y
);
}
}
public void curveTo( double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x, double y ) {
if( !pathStarted ) {
path.moveTo(x, y);
pathStarted = true;
} else {
path.curveTo(
ctrlX1, ctrlY1,
ctrlX2, ctrlY2,
x, y
);
}
}
/** /**
* Beendet eine zuvor {@link #beginShape() begonnene} Freihand-Form und * Beendet eine zuvor {@link #beginShape() begonnene} Freihand-Form und
* zeichent sie auf die Zeichenebene. * zeichent sie auf die Zeichenebene.
*/ */
public void endShape() { public void endShape() {
endShape(CLOSED);
}
public void endShape( Options.PathType closingType ) {
if( closingType == Options.PathType.CLOSED ) {
path.closePath(); path.closePath();
}
path.trimToSize(); path.trimToSize();
pathStarted = false; pathStarted = false;

View File

@@ -235,8 +235,8 @@ public class Sound implements Audio {
} }
/** /**
* Lädt falls nötig den {@link Clip} für die * Lädt, falls nötig, den {@link Clip} für die
* {@link #audioSource Audioquelle} und startet die Wiedergabe. * {@link #audioSource Audioquelle}.
* *
* @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false} * @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false}
* sonst. * sonst.
@@ -264,10 +264,6 @@ public class Sound implements Audio {
} }
} else if( event.getType() == LineEvent.Type.STOP ) { } else if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped(); playbackStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
} }
} }
}); });
@@ -313,6 +309,10 @@ public class Sound implements Audio {
private void playbackStopped() { private void playbackStopped() {
playing = false; playing = false;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
if( disposeAfterPlay ) { if( disposeAfterPlay ) {
this.dispose(); this.dispose();
disposeAfterPlay = false; disposeAfterPlay = false;

View File

@@ -20,6 +20,12 @@ import java.util.stream.Collectors;
* Collector gelöscht worden sein kann. * Collector gelöscht worden sein kann.
* <p> * <p>
* Als interne Map wird einen {@link ConcurrentHashMap} verwendet. * Als interne Map wird einen {@link ConcurrentHashMap} verwendet.
* <p>
* Ein passender Cache wird mittels der Fabrikmethoden {@link #newSoftCache()}
* und {@link #newWeakCache()} erstellt.
* <pre><code>
* Cache&lt;String, Image&gt; imageCache = Cache.newSoftCache();
* </code></pre>
* *
* @param <K> Der Typ der Schlüssel. * @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte. * @param <V> Der Typ der Objekte.
@@ -70,10 +76,6 @@ public final class Cache<K, V> implements Map<K, V> {
return cache.isEmpty(); return cache.isEmpty();
} }
private boolean containsRef( Object key ) {
return cache.containsKey(key);
}
@Override @Override
public boolean containsKey( Object key ) { public boolean containsKey( Object key ) {
return cache.containsKey(key) && cache.get(key).get() != null; return cache.containsKey(key) && cache.get(key).get() != null;
@@ -93,17 +95,33 @@ public final class Cache<K, V> implements Map<K, V> {
return null; return null;
} }
public void nocache( K key ) { /**
* Deaktiviert das Caching für den angegebenen Schlüssel.
* <p>
* Folgende Aufrufe von {@link #put(Object, Object)} mit demselben Schlüssel
* haben keinen Effekt. Um das Caching wieder zu aktivieren, muss
* {@link #remove(Object)} mit dem Schlüssel aufgerufen werden,
*
* @param key Der Schlüssel.
*/
public void disableCache( K key ) {
cache.put(key, NOCACHE); cache.put(key, NOCACHE);
} }
public boolean isNocache( K key ) { /**
* Prüft, ob der für den angegebenen Schlüssel zuvor
* {@link #disableCache(Object)} aufgerufen wurde.
*
* @param key Der Schlüssel.
* @return {@code true}, wenn der Schlüssel nicht gespeichert wird.
*/
public boolean isCachingDisabled( K key ) {
return cache.get(key) == NOCACHE; return cache.get(key) == NOCACHE;
} }
@Override @Override
public V put( K key, V value ) { public V put( K key, V value ) {
if( !isNocache(key) ) { if( !isCachingDisabled(key) ) {
V prev = remove(key); V prev = remove(key);
cache.put(key, refSupplier.apply(value)); cache.put(key, refSupplier.apply(value));
return prev; return prev;

View File

@@ -5,7 +5,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Eine Helferklasse, um Dinge zu zählen. * Eine Hilfsklasse, um Dinge zu zählen.
* <p> * <p>
* Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden, * Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden,
* die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder * die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder

View File

@@ -28,7 +28,7 @@ import static java.lang.Math.log;
import static schule.ngb.zm.Constants.random; import static schule.ngb.zm.Constants.random;
/** /**
* Eine Helferklasse, um zufällige Beispieldaten zu erzeugen. * Eine Hilfsklasse, um zufällige Beispieldaten zu erzeugen.
* <p> * <p>
* Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen. * Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen.
* Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder. * Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder.

View File

@@ -3,7 +3,7 @@ package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Helferklasse zur Zeitmessung im Nanosekundenbereich. * Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p> * <p>
* Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus * Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus
* gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn * gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn

View File

@@ -1,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen, die das Listener-Entwurfsmuster * Dieses Paket enthält Hilfsklassen, die das Listener-Entwurfsmuster
* umsetzen. * umsetzen.
*/ */
package schule.ngb.zm.util.events; package schule.ngb.zm.util.events;

View File

@@ -13,7 +13,7 @@ import java.util.List;
import java.util.function.Function; import java.util.function.Function;
/** /**
* Helferklasse, um Textdateien in verschiedenen Formaten einzulesen. * Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
*/ */
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public final class FileLoader { public final class FileLoader {

View File

@@ -14,7 +14,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Eine Helferklasse mit Klassenmethoden, um Schriftarten zu laden. * Eine Hilfsklasse mit Klassenmethoden, um Schriftarten zu laden.
* <p> * <p>
* Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten, * Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten,
* die aus Dateien geladen wurden, werden in einem internen Cache gespeichert * die aus Dateien geladen wurden, werden in einem internen Cache gespeichert

View File

@@ -16,10 +16,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.Map;
/** /**
* Eine Helferklasse mit Klassenmethoden, um Bilder zu laden. * Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
* <p> * <p>
* Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die * Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die
* Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche, * Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche,
@@ -71,8 +70,8 @@ public final class ImageLoader {
Validator.requireNotNull(source, "Image source may not be null"); Validator.requireNotNull(source, "Image source may not be null");
Validator.requireNotEmpty(source, "Image source may not be empty."); Validator.requireNotEmpty(source, "Image source may not be empty.");
if( caching && isCached(source) ) { if( caching && imageCache.containsKey(source) ) {
return getCache(source); return imageCache.get(source);
} }
BufferedImage img = null; BufferedImage img = null;
@@ -83,7 +82,7 @@ public final class ImageLoader {
img = ImageIO.read(in); img = ImageIO.read(in);
if( caching && img != null ) { if( caching && img != null ) {
putCache(source, img); imageCache.put(source, img);
} }
} catch( IOException ioex ) { } catch( IOException ioex ) {
LOG.error(ioex, "Error loading image file from source <%s>.", source); LOG.error(ioex, "Error loading image file from source <%s>.", source);
@@ -111,7 +110,7 @@ public final class ImageLoader {
public static boolean preloadImage( String name, String source ) { public static boolean preloadImage( String name, String source ) {
BufferedImage img = loadImage(source, true); BufferedImage img = loadImage(source, true);
if( img != null ) { if( img != null ) {
putCache(name, img); imageCache.put(name, img);
return true; return true;
} }
return false; return false;
@@ -132,7 +131,7 @@ public final class ImageLoader {
* @param img ZU speicherndes Bild. * @param img ZU speicherndes Bild.
*/ */
public static void preloadImage( String name, BufferedImage img ) { public static void preloadImage( String name, BufferedImage img ) {
putCache(name, img); imageCache.put(name, img);
} }
/** /**
@@ -156,30 +155,6 @@ public final class ImageLoader {
imageCache.remove(name); imageCache.remove(name);
} }
/**
* Speichert ein Bild als {@link SoftReference} im Cache.
*
* @param name Name des Bildes im Zwischenspeicher.
* @param img Das zu speichernde Bild.
*/
private static void putCache( final String name, final BufferedImage img ) {
imageCache.put(name, img);
}
/**
* Holt ein Bild aus dem Cache.
* <p>
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
* werden kann.
*
* @param name
* @return
*/
private static BufferedImage getCache( final String name ) {
return imageCache.get(name);
}
/** /**
* Deaktiviert den Cache für die angegebene Quelle. * Deaktiviert den Cache für die angegebene Quelle.
* <p> * <p>
@@ -189,14 +164,14 @@ public final class ImageLoader {
* *
* @param name Die Quelle des Bildes. * @param name Die Quelle des Bildes.
*/ */
public static void preventCache( final String name ) { public static void disableCache( final String name ) {
imageCache.nocache(name); imageCache.disableCache(name);
} }
/** /**
* Leer den Cache und löschte alle bisher gespeicherten Bilder. * Leer den Cache und löschte alle bisher gespeicherten Bilder.
* <p> * <p>
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden * Auch vorher mit {@link #disableCache(String)} verhinderte Caches werden
* gelöscht und müssen neu gesetzt werden. * gelöscht und müssen neu gesetzt werden.
*/ */
public static void clearCache() { public static void clearCache() {

View File

@@ -13,7 +13,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
/** /**
* Helferklasse, um {@link InputStream}s für Ressourcen zu erhalten. * Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ResourceStreamProvider { public class ResourceStreamProvider {

View File

@@ -1,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen, um Ressourcen aus verschiedenen Quellen * Dieses Paket enthält Hilfsklassen, um Ressourcen aus verschiedenen Quellen
* zu laden. * zu laden.
*/ */
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;

View File

@@ -1,4 +1,4 @@
/** /**
* Dieses Paket enthält Helferklassen für verschiedene Einsatzzwecke. * Dieses Paket enthält Hilfsklassen für verschiedene Einsatzzwecke.
*/ */
package schule.ngb.zm.util; package schule.ngb.zm.util;

View File

@@ -1,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen zur Verarbeitung paralleler Aufgaben * Dieses Paket enthält Hilfsklassen zur Ausführung paralleler Aufgaben
* innerhalb der Zeichenmaschine. * innerhalb der Zeichenmaschine.
*/ */
package schule.ngb.zm.util.tasks; package schule.ngb.zm.util.tasks;