diff --git a/build.gradle.kts b/build.gradle.kts index 2bd31e0..2d681e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,9 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.7.0" - id("org.jetbrains.compose") version "1.2.0-alpha01-dev755" - kotlin("plugin.serialization") version "1.7.0" + kotlin("jvm") version "1.7.10" + id("org.jetbrains.compose") version "1.2.0-alpha01-dev774" + kotlin("plugin.serialization") version "1.7.10" } @@ -21,10 +21,8 @@ repositories { dependencies { implementation(compose.desktop.currentOs) implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-hocon:1.4.0") - implementation("org.deeplearning4j:deeplearning4j-core:1.0.0-beta6") - -// implementation("com.charleskorn.kaml:kaml:0.47.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-properties:1.4.0") +// implementation("org.deeplearning4j:deeplearning4j-core:1.0.0-beta6") } tasks.withType() { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/kotlin/ApplicationState.kt b/src/main/kotlin/ApplicationState.kt index aa731c6..0b6e3a1 100644 --- a/src/main/kotlin/ApplicationState.kt +++ b/src/main/kotlin/ApplicationState.kt @@ -5,7 +5,6 @@ import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.encodeToByteArray import ui.* -import java.io.File import java.nio.file.Path import kotlin.io.path.* @@ -20,10 +19,8 @@ class ApplicationState { val settings = Settings() val pens = mutableStateListOf(Pen(Color(0xA00000FF), 2f), Pen(Color.Blue, 1f)) -// val theme = mutableStateOf(LightMode()) - init { - THEME.value = if (settings.darkmode) DarkMode() else LightMode() + THEME.value = settings.theme } private val _windows = mutableStateListOf() @@ -36,87 +33,69 @@ class ApplicationState { ) } - fun loadDocument(docInfo: DocumentInformation): Document? { - return loadDocument(Path.of(docInfo.path)) - } +// fun loadDocument(docInfo: DocumentInformation): Document? { +// return loadDocument(docInfo.path) +// } fun loadDocument(docPath: Path): Document? { - return try { - val file = docPath.readBytes() - Cbor.decodeFromByteArray(file) - } catch (e: Exception) { - e.printStackTrace() - null + runCatching { + return Cbor.decodeFromByteArray(docPath.readBytes()) } + return null } fun loadDocumentInformation( - path: String = workingDirectoryPath.pathString, - depth: Int = 3, - loadedDoc: LoadedDoc - ): List { - val folders = mutableListOf() - File(path).listFiles()?.forEach { - if (it.isDirectory) { - folders.add( - DocumentInformation( - it.name, - DocumentInformationType.Folder, - it.path + path: Path = workingDirectoryPath, + prefix: String = "", + loadedDoc: LoadedDoc? = null + ): DirectoryInformation { + val document = DirectoryInformation(path.name, path, prefix) + + path.listDirectoryEntries().forEach { + if (it.isDirectory()) { + document.directories.add(loadDocumentInformation(it, "$prefix/${it.name}")) + } else if (it.extension == "ppc") { + document.files.add( + FileInformation( + it.nameWithoutExtension, + it, + "$prefix/${it.name}", + document, // backwards reference + it.fileSize() ) ) } } - addChildrenToFolder(folders, depth - 1) - - updateLoadedDoc(folders, loadedDoc) - return folders - } + if (loadedDoc != null) updateLoadedDoc(document, loadedDoc) - fun addChildrenToFolder(folders: List, remainingDepth: Int) { - folders.forEach { - if (it.type == DocumentInformationType.Folder) { - - File(it.path).listFiles()?.forEach { file -> - if (file.isFile && file.name.endsWith(".ppc")) { - it.children.add( - DocumentInformation( - file.name.substringBefore('.'), - DocumentInformationType.Document, - file.path - ) - ) - } else if (file.isDirectory) { - it.children.add(DocumentInformation(file.name, DocumentInformationType.Folder, file.path)) - } - } - if (remainingDepth > 0) addChildrenToFolder(it.children, remainingDepth - 1) - } - } + return document } - private fun updateLoadedDoc(folders: List, loadedDoc: LoadedDoc) { - loadedDoc.workbook.value = folders.firstOrNull { - it.path == loadedDoc.workbook.value?.path - } - loadedDoc.folder.value = loadedDoc.workbook.value?.children?.firstOrNull { - it.path == loadedDoc.folder.value?.path - } - + // TODO: Rework - cache latest opened + private fun updateLoadedDoc(document: DirectoryInformation, loadedDoc: LoadedDoc) { + loadedDoc.parent.value = document +// loadedDoc.workbook.value = document.directories.firstOrNull() +// loadedDoc.workbook.value = folders.firstOrNull { +// it.path == loadedDoc.workbook.value?.path +// } +// loadedDoc.folder.value = loadedDoc.workbook.value?.children?.firstOrNull { +// it.path == loadedDoc.folder.value?.path +// } } - fun newFolder(doc: DocumentInformation?, name: String) { - val parentPath = doc?.path ?: workingDirectoryPath.pathString + fun newFolder(path: Path): Boolean { runCatching { - Path(parentPath, name).createDirectory() + path.createDirectories() + return true } + return false } - fun newFile(doc: DocumentInformation?, name: String) { - val parentPath = doc?.path ?: workingDirectoryPath.pathString + fun newFile(doc: FileInformation?, name: String) { + val parentPath = doc?.path ?: workingDirectoryPath runCatching { Path(parentPath, "$name.ppc").createFile() } @@ -144,3 +123,4 @@ class ApplicationState { } +fun Path(path: Path, varargs: String): Path = Path(path.pathString, varargs) \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 2a569d4..3b8c780 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -11,11 +11,9 @@ import data.Settings // todo: proposed solutions: // todo: stroke has an global offset for the starting point afterwards only the between the points can be stored by using smaller datatypes like bytes fun main() = application { - PPCApplication(rememberApplicationState()) -// val a = 1.0 -// // override fun contains(value: T): Boolean = lessThanOrEquals(start, value) && lessThanOrEquals(value, endInclusive) -// print(a in -1.0..3.0) - + PPCApplication(rememberApplicationState()) +// val state = ApplicationState() +// state.loadDocumentInformation() } diff --git a/src/main/kotlin/control/Application.kt b/src/main/kotlin/control/Application.kt index 656dfb0..26152c7 100644 --- a/src/main/kotlin/control/Application.kt +++ b/src/main/kotlin/control/Application.kt @@ -12,11 +12,9 @@ import ui.documentView.DocumentView @Composable fun ApplicationScope.PPCApplication(state: ApplicationState) { - for (window in state.windows) { key(window) { val documentViewControlState = remember { DocumentViewControlState(DocumentController(Document(PageSize.A4)), state) } - PPCWindow(window, documentViewControlState, DocumentView) } } diff --git a/src/main/kotlin/control/AutoSaveJob.kt b/src/main/kotlin/control/AutoSaveJob.kt index 8c43830..a67d2d9 100644 --- a/src/main/kotlin/control/AutoSaveJob.kt +++ b/src/main/kotlin/control/AutoSaveJob.kt @@ -5,19 +5,15 @@ import data.Document import kotlinx.coroutines.* import java.nio.file.Path -class AutoSaveJob(val application: ApplicationState) : - CoroutineScope { // implement CoroutineScope to create local scope +class AutoSaveJob(private val application: ApplicationState) : CoroutineScope { private var job: Job = Job() override val coroutineContext get() = Dispatchers.Default + job - private var _doc: Document? = null - private var _path: Path? = null + var _doc: Document? = null + var _path: Path? = null - // this method will help to stop execution of a coroutine. - // Call it to cancel coroutine and to break the while loop defined in the coroutine below fun cancel() { - println("$_doc -0- $_path") if (_doc != null && _path != null) { application.saveDocument(_doc!!, _path!!) job.cancel() @@ -25,13 +21,17 @@ class AutoSaveJob(val application: ApplicationState) : } fun schedule(document: Document, path: Path) = launch { // launching the coroutine - val delaySeconds = 10L _doc = document _path = path while (coroutineContext.isActive) { - println("save") application.saveDocument(document, path) - delay(delaySeconds * 1000) + delay(application.settings.saveDelay.toLong() * 1000) + } + } + + fun save() { + if (_doc != null && _path != null) { + application.saveDocument(_doc!!, _path!!) } } } \ No newline at end of file diff --git a/src/main/kotlin/control/DocumentControllerBase.kt b/src/main/kotlin/control/DocumentControllerBase.kt index 2c0105b..2cb56f5 100644 --- a/src/main/kotlin/control/DocumentControllerBase.kt +++ b/src/main/kotlin/control/DocumentControllerBase.kt @@ -10,9 +10,9 @@ import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.encodeToByteArray import util.Point -abstract class DocumentControllerBase(document: Document): Controller { +abstract class DocumentControllerBase(document: Document) : Controller { val mouse = MouseInputHandler(this) - val state = mutableStateOf( DocumentControlState(this, document)) + val state = mutableStateOf(DocumentControlState(this, document)) val strokeController = StrokeController() private var canvasSize = Point(0, 0) @@ -32,11 +32,11 @@ abstract class DocumentControllerBase(document: Document): Controller { is Selector -> { if (selection.value != null && !selection.value!!.isEmpty()) selection.value?.selectionComplete?.value = true - else selection.value = null } - is TPen -> strokeController.endStroke() + + is TPen -> strokeController.endStroke() else -> {} @@ -59,9 +59,11 @@ abstract class DocumentControllerBase(document: Document): Controller { fun toolDown(mousePos: Point) { when (selectedTool.value) { is Selector -> { - if (selection.value?.selectionComplete?.value!!) +// if (selection.value?.selectionComplete?.value!!) // NOTE: crashes application when selection starts over the toolbar canvas + if (selection.value?.selectionComplete?.value == true) selection.value = null } + is TPen -> penDown(mousePos) else -> {} @@ -71,6 +73,7 @@ abstract class DocumentControllerBase(document: Document): Controller { fun toolClicked() { } + abstract fun penDown(mousePos: Point) abstract fun selectorMoved(globalPoint: Point) @@ -84,9 +87,6 @@ abstract class DocumentControllerBase(document: Document): Controller { abstract fun newStroke(start: Point) - - - fun resize(newXDim: Int, newYDim: Int, localCenter: Point) { canvasSize = Point(newXDim, newYDim) this.localCenter = localCenter @@ -96,15 +96,11 @@ abstract class DocumentControllerBase(document: Document): Controller { if (state.value.document.value.scrollY + scrollDelta < 0) return state.value.document.value.centerPoint.value += Point(0, scrollDelta.toDouble()) state.value.document.value.scrollY += scrollDelta - println(state.value.document.value.centerPoint.value) - } fun scrollX(scrollDelta: Float) { - println("X: $scrollDelta") state.value.document.value.centerPoint.value += Point(scrollDelta.toDouble(), 0) state.value.document.value.scrollX += scrollDelta - } diff --git a/src/main/kotlin/control/ShapeRecognitionController.kt b/src/main/kotlin/control/ShapeRecognitionController.kt index 1ba922a..5691076 100644 --- a/src/main/kotlin/control/ShapeRecognitionController.kt +++ b/src/main/kotlin/control/ShapeRecognitionController.kt @@ -1,37 +1,37 @@ -package control - -import org.deeplearning4j.nn.modelimport.keras.KerasModelImport -import org.deeplearning4j.nn.multilayer.MultiLayerNetwork -import org.nd4j.linalg.factory.Nd4j -import java.io.File - - -class ShapeRecognitionController { - private lateinit var model: MultiLayerNetwork - - init { - loadModel() - } - - fun loadModel(){ - val SIMPLE_MLP: String = File("saved_model-0.950.h5").getAbsolutePath() - - // Keras Sequential models correspond to DL4J MultiLayerNetworks. We enforce loading the training configuration - // of the model as well. If you're only interested in inference, you can safely set this to 'false'. - - // Keras Sequential models correspond to DL4J MultiLayerNetworks. We enforce loading the training configuration - // of the model as well. If you're only interested in inference, you can safely set this to 'false'. - model = KerasModelImport.importKerasSequentialModelAndWeights(SIMPLE_MLP, false) - } - - fun predict(){ - // Test basic inference on the model. - val input = Nd4j.create(256, 100) - val output = model.output(input) - - // Test basic model training. - - // Test basic model training. - model.fit(input, output) - } -} \ No newline at end of file +//package control +// +//import org.deeplearning4j.nn.modelimport.keras.KerasModelImport +//import org.deeplearning4j.nn.multilayer.MultiLayerNetwork +//import org.nd4j.linalg.factory.Nd4j +//import java.io.File +// +// +//class ShapeRecognitionController { +// private lateinit var model: MultiLayerNetwork +// +// init { +// loadModel() +// } +// +// fun loadModel(){ +// val SIMPLE_MLP: String = File("saved_model-0.950.h5").getAbsolutePath() +// +// // Keras Sequential models correspond to DL4J MultiLayerNetworks. We enforce loading the training configuration +// // of the model as well. If you're only interested in inference, you can safely set this to 'false'. +// +// // Keras Sequential models correspond to DL4J MultiLayerNetworks. We enforce loading the training configuration +// // of the model as well. If you're only interested in inference, you can safely set this to 'false'. +// model = KerasModelImport.importKerasSequentialModelAndWeights(SIMPLE_MLP, false) +// } +// +// fun predict(){ +// // Test basic inference on the model. +// val input = Nd4j.create(256, 100) +// val output = model.output(input) +// +// // Test basic model training. +// +// // Test basic model training. +// model.fit(input, output) +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/data/DocumentInformation.kt b/src/main/kotlin/data/DocumentInformation.kt index 3e2d019..ed3667f 100644 --- a/src/main/kotlin/data/DocumentInformation.kt +++ b/src/main/kotlin/data/DocumentInformation.kt @@ -1,10 +1,39 @@ package data -class DocumentInformation(val name: String, val type: DocumentInformationType, val path: String) { - val children = mutableListOf() +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import util.Point +import java.nio.file.Path +open class Information( + val name: String, + val path: Path, + val local: String = "", + val parent: DirectoryInformation? = null +) + +class DirectoryInformation(name: String, path: Path, local: String = "", parent: DirectoryInformation? = null) : + Information(name, path, local, parent) { + val files = mutableStateListOf() + val directories = mutableStateListOf() + val open = mutableStateOf(false) + val selected = mutableStateOf(true); + + override fun toString(): String { + return "$path: $name - $local (${files.size} files, ${directories.size} directories)" + } } -enum class DocumentInformationType{ - Document, Folder +class FileInformation( + name: String, + path: Path, + local: String = "", + parent: DirectoryInformation? = null, + val size: Long = 0 +) : + Information(name, path, local, parent) { + val selected = mutableStateOf(true); + override fun toString(): String { + return "$path: $name - $local ($size)" + } } \ No newline at end of file diff --git a/src/main/kotlin/data/DocumentViewControlState.kt b/src/main/kotlin/data/DocumentViewControlState.kt index 6abdef1..6f49b7f 100644 --- a/src/main/kotlin/data/DocumentViewControlState.kt +++ b/src/main/kotlin/data/DocumentViewControlState.kt @@ -8,80 +8,77 @@ import control.AutoSaveJob import control.DocumentController import control.DocumentControllerBase import java.nio.file.Path +import Path +import kotlin.io.path.exists +import kotlin.io.path.fileSize -class LoadedDoc(val documentViewControlState: DocumentViewControlState) { - var workbook: MutableState = mutableStateOf(null) - var folder: MutableState = mutableStateOf(null) - private var _file: MutableState = mutableStateOf(null) - var file: DocumentInformation? +class LoadedDoc(private val documentViewControlState: DocumentViewControlState, info: DirectoryInformation) { + val parent = mutableStateOf(info) + + private var _file: MutableState = mutableStateOf(null) + var file: FileInformation? get() = _file.value set(value) { _file.value = value if (value != null) { - documentViewControlState.loadDocument(value) + documentViewControlState.loadDocument(value.path) } } - - fun getDeepestParentFolder(): DocumentInformation? { - if (workbook.value == null) { - if (folder.value == null) { - return null - } - return folder.value - } - return workbook.value - } } -class DocumentViewControlState(docCon: DocumentController, private val application: ApplicationState) : WindowControlState { - - - var activeDialog: MutableState Unit), String, () -> Unit>?> = mutableStateOf(null) +class DocumentViewControlState(docCon: DocumentController, private val application: ApplicationState) : + WindowControlState { + var activeDialog: MutableState Unit), String, () -> Unit>?> = + mutableStateOf(null) var documentController = mutableStateOf(docCon) var sideBarActivated = mutableStateOf(false) - var loadedDoc = mutableStateOf(LoadedDoc(this)) - var folders: MutableState> = - mutableStateOf(application.loadDocumentInformation(loadedDoc = loadedDoc.value)) + var document: MutableState = + mutableStateOf(application.loadDocumentInformation()) + var loadedDoc = mutableStateOf(LoadedDoc(this, document.value)) var autoSaveJob = AutoSaveJob(application) - - - fun loadDocument(docInfo: DocumentInformation) { - loadDocument(Path.of(docInfo.path)) - } - fun loadDocument(docPath: Path) { application.loadDocument(docPath)?.let { autoSaveJob.cancel() - autoSaveJob = AutoSaveJob(application) - documentController.value = DocumentController(it) autoSaveJob.schedule(it, docPath) } } - fun createDocument(docParent: DocumentInformation, name: String) { - val doc = Document(PageSize.A4) - val path = Path.of(docParent.path, "$name.ppc") + fun saveDocument() { + autoSaveJob.save() + } + + fun createDocument(docParent: DirectoryInformation, name: String, size: PageSize = PageSize.A4) { + val doc = Document(size) + val path = Path(docParent.path, "$name.ppc") + if (path.exists()) return application.saveDocument(doc, path) loadDocument(path) + docParent.files.add(FileInformation(name, path, parent = docParent, size = path.fileSize())) + } + + fun createFolder(docParent: DirectoryInformation, name: String) { + val path = Path(docParent.path, name) + application.newFolder(path) + docParent.directories.add(DirectoryInformation(name, path)) } fun updateDocs() { - folders.value = application.loadDocumentInformation(loadedDoc = loadedDoc.value) + document.value = application.loadDocumentInformation(loadedDoc = loadedDoc.value) } override fun finalize() { autoSaveJob.cancel() } - fun addPen(pen: Pen){ + fun addPen(pen: Pen) { application.pens.add(pen) } - fun getPens(): MutableList{ + fun getPens(): MutableList { return application.pens } } diff --git a/src/main/kotlin/data/Settings.kt b/src/main/kotlin/data/Settings.kt index 37dd12c..f7acbd4 100644 --- a/src/main/kotlin/data/Settings.kt +++ b/src/main/kotlin/data/Settings.kt @@ -4,34 +4,35 @@ package data import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigRenderOptions import data.serializer.MutableStateSerializer import data.serializer.PathSerializer +import data.serializer.PropertiesSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.hocon.Hocon -import kotlinx.serialization.hocon.decodeFromConfig -import kotlinx.serialization.hocon.encodeToConfig -import java.io.File -//import kotlinx.serialization.properties.Properties -//import kotlinx.serialization.properties.encodeToStringMap +import kotlinx.serialization.properties.Properties +import kotlinx.serialization.properties.decodeFromMap +import kotlinx.serialization.properties.decodeFromStringMap +import kotlinx.serialization.properties.encodeToStringMap +import ui.THEMES +import ui.Theme import java.nio.file.Path -import kotlin.io.path.createDirectory -import kotlin.io.path.exists import kotlin.io.path.* +import java.util.Properties as JavaProperties class Settings { - lateinit var settings: SettingsData + private lateinit var settings: SettingsData + + // private lateinit var latest: LatestData lateinit var settingsDir: Path - lateinit var settingsFile: Path + private set + + private lateinit var settingsFile: Path + private lateinit var serializer: PropertiesSerializer - var darkmode: Boolean - get() = settings.darkmode.value + var theme: Theme + get() = settings.theme.value.theme set(value) { - settings.darkmode.value = value + settings.theme.value = THEMES.valueOf(value.toString()) } var ppcHome: Path @@ -40,63 +41,76 @@ class Settings { settings.ppcHome.value = value } + var saveDelay: Int + get() = settings.saveDelay.value + set(value) { + settings.saveDelay.value = value + } + init { getSettingsPath() - save() - read() + init() println(settings) } private fun getSettingsPath() { val os = System.getProperty("os.name").lowercase() - val dir = if (os.contains("win")) { - Path.of(System.getenv("APPDATA")) - } else { - Path.of(System.getProperty("user.home"), ".config/") - } + val dir = if (os.contains("win")) + Path(System.getenv("APPDATA")) + else + Path(System.getProperty("user.home"), ".config/") settingsDir = Path(dir.pathString, "ppc") - if (!settingsDir.exists()) settingsDir.createDirectory() - settingsFile = Path.of(settingsDir.pathString, "ppc.conf") + if (!settingsDir.exists()) settingsDir.createDirectories() + settingsFile = Path.of(settingsDir.pathString, "options.txt") } + @OptIn(ExperimentalSerializationApi::class) private fun read() { - if (!settingsFile.exists()) settingsFile.createFile() + val props = PropertiesSerializer.import(settingsFile) + settings = Properties.decodeFromStringMap(props) + } - // Decode - val conf = ConfigFactory.parseFile(settingsFile.toFile()) - settings = Hocon.decodeFromConfig(conf) + @OptIn(ExperimentalSerializationApi::class) + private fun init() { + if (!settingsFile.exists()) { + settingsFile.createFile() + val props = Properties.encodeToStringMap(SettingsData()) + PropertiesSerializer.export(settingsFile, props) + } + read() } + @OptIn(ExperimentalSerializationApi::class) fun save() { - if (!settingsFile.exists()) settingsFile.createFile() - - val s = SettingsData() - s.darkmode.value = true - - val config = Hocon.encodeToConfig(s) - val renderConfig = ConfigRenderOptions.concise() - renderConfig.setJson(false) - val output = config.root().render(renderConfig) - - - settingsFile.writeText(output) + val props = Properties.encodeToStringMap(settings) + PropertiesSerializer.export(settingsFile, props) } } @Serializable data class SettingsData( @Serializable(with = MutableStateSerializer::class) - val darkmode: MutableState = mutableStateOf(false), + val theme: MutableState = mutableStateOf(THEMES.LightTheme), @Serializable(with = MutableStateSerializer::class) - var ppcHome: MutableState<@Serializable(with = PathSerializer::class) Path> = mutableStateOf( + val ppcHome: MutableState<@Serializable(with = PathSerializer::class) Path> = mutableStateOf( Path( System.getProperty( "user.home" ), "ppc" ) ), -) \ No newline at end of file + + @Serializable(with = MutableStateSerializer::class) + val saveDelay: MutableState = mutableStateOf(10) +) + +//@Serializable +//data class LatestData( +// @Serializable(with = MutableStateSerializer::class) +// val latestDocPath: MutableState<@Serializable(with = PathSerializer::class) Path?> = mutableStateOf(null), +//) + diff --git a/src/main/kotlin/data/serializer/PropertySerializer.kt b/src/main/kotlin/data/serializer/PropertySerializer.kt new file mode 100644 index 0000000..1c427e2 --- /dev/null +++ b/src/main/kotlin/data/serializer/PropertySerializer.kt @@ -0,0 +1,21 @@ +package data.serializer + +import java.nio.file.Path +import java.util.* +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +// Modified https://stackoverflow.com/a/63494719 +object PropertiesSerializer { + fun export(path: Path, data: Map) { + val props = data.toProperties() + path.outputStream().use { props.store(it, "PPC Options V1") } + } + + fun import(path: Path): Map { + val props = Properties() + path.inputStream().use { props.load(it) } + // NOTE: .map { (key, value) -> key.toString() to value.toString() }.toMap() + return props.toMap() as Map + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/Theme.kt b/src/main/kotlin/ui/Theme.kt index 6b112b0..c6b12bd 100644 --- a/src/main/kotlin/ui/Theme.kt +++ b/src/main/kotlin/ui/Theme.kt @@ -3,7 +3,7 @@ package ui import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color -val THEME = mutableStateOf(LightMode()) +val THEME = mutableStateOf(LightTheme()) open class Theme { open val background: Color = Color(0xFFF8FCFF) @@ -19,14 +19,13 @@ open class Theme { open val dividerColor: Color = Color(0xFFD8D8D8) open val textFieldColor: Color = Color(0xFFCCCCCC) open val menuBody: Color = Color.White - } -class LightMode : Theme() { +class LightTheme : Theme() { } -class DarkMode : Theme() { +class DarkTheme : Theme() { override val background = Color(0xFF575757) override val pageForeground: Color = Color.White override val pageBackground: Color = Color(0xFF363636) @@ -37,8 +36,9 @@ class DarkMode : Theme() { override val iconColor: Color = Color(0xFFF2F2F2) override val highlightColor: Color = Color(0xFF1347FF) override val selectorColor: Color = Color(0xFFF2F2F2) +} - - - +enum class THEMES(val theme: Theme) { + LightTheme(LightTheme()), + DarkTheme(DarkTheme()); } \ No newline at end of file diff --git a/src/main/kotlin/ui/Window.kt b/src/main/kotlin/ui/Window.kt index 6bc0a88..f6f905e 100644 --- a/src/main/kotlin/ui/Window.kt +++ b/src/main/kotlin/ui/Window.kt @@ -2,9 +2,7 @@ package ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.input.key.* import androidx.compose.ui.window.* @@ -31,10 +29,9 @@ fun PPCWindow( val scope = rememberCoroutineScope() fun exit() = scope.launch { windowState.exit() } - Window( state = windowState.window, - title = "PPC", + title = windowState.title.value, onCloseRequest = { controlState.finalize() exit() @@ -42,19 +39,17 @@ fun PPCWindow( alwaysOnTop = true, onKeyEvent = { var retVal = false -// println("keyyy $controlState") if (controlState is DocumentViewControlState) { if (it.isCtrlPressed && it.type == KeyEventType.KeyDown) { if (it.key == Key.Z && !it.isShiftPressed) { - println("key down: undo") - controlState.documentController.value.undo() retVal = true } else if (it.key == Key.Y || it.key == Key.Z && it.isShiftPressed) { - println("key down") - controlState.documentController.value.redo() retVal = true + } else if (it.key == Key.S) { + controlState.saveDocument() + retVal = true } } } @@ -100,4 +95,4 @@ fun FrameWindowScope.FileDialog( } }, dispose = FileDialog::dispose -) +) \ No newline at end of file diff --git a/src/main/kotlin/ui/WindowState.kt b/src/main/kotlin/ui/WindowState.kt index 773f2af..83064d4 100644 --- a/src/main/kotlin/ui/WindowState.kt +++ b/src/main/kotlin/ui/WindowState.kt @@ -4,13 +4,13 @@ import ApplicationState import androidx.compose.runtime.* import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState -import data.DocumentInformation class PPCWindowState( val application: ApplicationState, private val exit: (PPCWindowState) -> Unit, ) { val window = WindowState(placement = WindowPlacement.Floating) + val title = mutableStateOf("PPC") suspend fun exit(): Boolean { exit(this) @@ -20,6 +20,4 @@ class PPCWindowState( fun newWindow() { application.newWindow() } - - } \ No newline at end of file diff --git a/src/main/kotlin/ui/dialogs/PPCDialog.kt b/src/main/kotlin/ui/dialogs/PPCDialog.kt new file mode 100644 index 0000000..7d7c1a5 --- /dev/null +++ b/src/main/kotlin/ui/dialogs/PPCDialog.kt @@ -0,0 +1,47 @@ +package ui.dialogs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.window.Dialog + +@Composable +fun InputDialog(title: String, fieldname: String, onResult: ((DialogResult, String) -> Unit)?) { + var isOpen by remember { mutableStateOf(true) } + var text by remember { mutableStateOf("") } + + fun result(result: DialogResult) { + if (onResult != null) onResult(result, text) + isOpen = false + } + + if (isOpen) { + Dialog({ result(DialogResult.Cancel) }, title = title) { + Column { + Row { + TextField(text, { text = it }, label = { Text(fieldname) }) + } + Row { + Button({ result(DialogResult.Ok) }) { Text("Create") } + Button({ result(DialogResult.Cancel) }) { Text("Cancel") } + } + } + } + } +} + +val CreateFileDialog = @Composable { onResult: ((DialogResult, String) -> Unit)? -> + InputDialog("Create File", "File Name", onResult) +} + +val CreateFolderDialog = @Composable { onResult: ((DialogResult, String) -> Unit)? -> + InputDialog("Create Folder", "Folder Name", onResult) +} + +enum class DialogResult { + Ok, + Cancel +} \ No newline at end of file diff --git a/src/main/kotlin/ui/documentView/sidebar/HeadBar.kt b/src/main/kotlin/ui/documentView/sidebar/HeadBar.kt new file mode 100644 index 0000000..d897fc4 --- /dev/null +++ b/src/main/kotlin/ui/documentView/sidebar/HeadBar.kt @@ -0,0 +1,28 @@ +package ui.documentView.sidebar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import data.DocumentViewControlState +import ui.THEME +import ui.documentView.toolbar.IconButton + +@Composable +fun HeadBar(documentViewControlState: DocumentViewControlState) { + Row( + Modifier.background(THEME.value.secondaryColor).height(70.dp).fillMaxWidth().padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { +// SearchBar() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + IconButton(false, "newFile.svg") + IconButton(false, "leftArrow.svg") { documentViewControlState.sideBarActivated.value = false } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/documentView/sidebar/Register.kt b/src/main/kotlin/ui/documentView/sidebar/Register.kt new file mode 100644 index 0000000..816b63c --- /dev/null +++ b/src/main/kotlin/ui/documentView/sidebar/Register.kt @@ -0,0 +1,79 @@ +package ui.documentView.sidebar + +import androidx.compose.foundation.ContextMenuArea +import androidx.compose.foundation.ContextMenuItem +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import data.DirectoryInformation +import data.DocumentViewControlState +import data.LoadedDoc +import ui.dialogs.CreateFileDialog +import ui.dialogs.CreateFolderDialog +import ui.dialogs.DialogResult + +const val REGISTER_INDENT = 10 + +@Composable +fun Register( + info: DirectoryInformation, + indent: Int, + loaded: LoadedDoc, + documentViewControlState: DocumentViewControlState +) { + var dialog: @Composable ((((DialogResult, String) -> Unit)?) -> Unit)? by remember { mutableStateOf(null) } + var dialogResult: ((DialogResult, String) -> Unit)? by remember { mutableStateOf(null) } + + ContextMenuArea(items = { + listOf( + ContextMenuItem("Add File") { + dialog = CreateFileDialog + dialogResult = { res, name -> + if (res == DialogResult.Ok) { + documentViewControlState.createDocument(info, name) + info.open.value = true + loaded.parent.value = info + } + dialogResult = null + dialog = null + } + }, + ContextMenuItem("Add Folder") { + dialog = CreateFolderDialog + dialogResult = { res, name -> + if (res == DialogResult.Ok) { + documentViewControlState.createFolder(info, name) + } + dialogResult = null + dialog = null + } + } + ) + }) { + if (dialog != null) dialog!!(dialogResult) + + Text( + "${info.name}${if (info.directories.isNotEmpty()) " .v." else ""}", + Modifier.padding(start = indent.dp, top = 2.dp, bottom = 2.dp).clickable { + if (info.open.value) { + closeAllRegisters(info) + loaded.parent.value = info + } else { + info.open.value = true + loaded.parent.value = info + } + }) + } + if (info.open.value) + Registers(info.directories, indent + REGISTER_INDENT, loaded, documentViewControlState) +} + +fun closeAllRegisters(info: DirectoryInformation) { + info.open.value = false + info.directories.forEach { + closeAllRegisters(it) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/documentView/sidebar/Registers.kt b/src/main/kotlin/ui/documentView/sidebar/Registers.kt new file mode 100644 index 0000000..8c19c21 --- /dev/null +++ b/src/main/kotlin/ui/documentView/sidebar/Registers.kt @@ -0,0 +1,19 @@ +package ui.documentView.sidebar + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import data.DirectoryInformation +import data.DocumentViewControlState +import data.LoadedDoc + +@Composable +fun Registers( + dirs: List, + indent: Int, + loaded: LoadedDoc, + documentViewControlState: DocumentViewControlState +) { + dirs.forEach { + Register(it, indent, loaded, documentViewControlState) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/documentView/sidebar/SideBar.kt b/src/main/kotlin/ui/documentView/sidebar/SideBar.kt index 72c25b0..7d86bc6 100644 --- a/src/main/kotlin/ui/documentView/sidebar/SideBar.kt +++ b/src/main/kotlin/ui/documentView/sidebar/SideBar.kt @@ -1,279 +1,69 @@ package ui.documentView.sidebar +import androidx.compose.foundation.ContextMenuArea +import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.GenericShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.awt.ComposeDialog import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.ExperimentalUnitApi -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import data.* +import androidx.compose.ui.window.Dialog +import data.DocumentViewControlState import ui.PPCWindowState import ui.THEME -import ui.documentView.toolbar.IconButton - -private val RegisterShape = GenericShape { size, _ -> - val cornerRadius = (size.width + size.height) / 2 - lineTo(size.width - cornerRadius, 0f) - cubicTo(size.width, 0f, size.width, 0f, size.width, cornerRadius) - lineTo(size.width, size.height - cornerRadius) - cubicTo(size.width, size.height, size.width, size.height, size.width - cornerRadius, size.height) - lineTo(0f, size.height) -} - -@OptIn(ExperimentalUnitApi::class) @Composable fun SideBar(documentViewControlState: DocumentViewControlState, windowState: PPCWindowState) { - val expanded = remember { mutableStateOf(false) } + val loaded = documentViewControlState.loadedDoc + val document = documentViewControlState.document + val hasFiles = loaded.value.parent.value.files.isNotEmpty() - val loadedDoc = documentViewControlState.loadedDoc - // Box(modifier = Modifier.fillMaxSize().clickable { documentViewControlState.sideBarActivated.value = false }) - Surface( - modifier = Modifier.fillMaxHeight().background(THEME.value.menuBody) - .fillMaxWidth(0.2f + (if (expanded.value) 0.2f else 0f)) - ) { -// var activatedRegister by remember { mutableStateOf(-1) } + val barWidth = if (hasFiles) 0.4f else 0.2f -// val folders = windowState.folders - // folder + println(windowState.window.size) - Column(Modifier.background(THEME.value.menuBody).fillMaxWidth()) { + Surface( + Modifier.background(THEME.value.menuBody) + .fillMaxHeight() + .fillMaxWidth(barWidth) + ) { + Column(Modifier.background(THEME.value.menuBody).fillMaxWidth().fillMaxHeight()) { HeadBar(documentViewControlState) - Row { - Column(Modifier.fillMaxWidth(if (expanded.value) 0.5f else 1f)) { - documentViewControlState.folders.value.forEachIndexed { i, it -> - - Register( - it, - Color.Red, - loadedDoc.value.workbook.value?.name == it.name, - it.children, - loadedDoc.value, - documentViewControlState, - expanded, - windowState - ) - } - AddDialog(documentViewControlState, windowState, null) + Row(Modifier.fillMaxHeight()) { + Column(Modifier.fillMaxWidth(if (hasFiles) 0.5f else 1f).fillMaxHeight().clickable { + loaded.value.parent.value = document.value + }) { + Registers(document.value.directories, 5, loaded.value, documentViewControlState) } - if (expanded.value) { - Column( - modifier = Modifier.fillMaxWidth().fillMaxHeight().fillMaxWidth() - .drawBehind { drawLine(THEME.value.dividerColor, Offset(0f, 0f), Offset(0f, size.height), 3f) }) { - - loadedDoc.value.folder.value?.children?.forEach { - - Row(Modifier.fillMaxWidth().clickable { - expanded.value = true - loadedDoc.value.file = it - }.fillMaxWidth()) { - Text( - text = it.name, - modifier = Modifier.padding(30.dp, 10.dp), - fontSize = TextUnit(1f, TextUnitType.Em), - fontWeight = FontWeight.SemiBold, - color = THEME.value.textColor - ) - - } + if (hasFiles) { + Column(Modifier.fillMaxWidth(0.5f) + .fillMaxHeight() + .drawBehind { + drawLine( + THEME.value.dividerColor, + Offset(0f, 0f), + Offset(0f, size.height), + 3f + ) + }) + { + loaded.value.parent.value.files.forEach { + Text(it.name, Modifier.padding(start = 5.dp).clickable { + documentViewControlState.loadDocument(it.path) + windowState.title.value = "PPC - ${it.local}" + }) } - AddDialog(documentViewControlState, windowState, loadedDoc.value.folder.value, true) - - } - } - } - } - } -} - -@OptIn(ExperimentalUnitApi::class) -@Composable -fun Register( - doc: DocumentInformation, - color: Color, - activated: Boolean = false, - children: MutableList, - loadedDoc: LoadedDoc, - documentViewControlState: DocumentViewControlState, - expanded: MutableState, - windowState: PPCWindowState -) { - val height = 30.dp - val lightColor = color.copy(alpha = 0.15f) - Row(modifier = Modifier.padding(vertical = 10.dp).background(if (activated) lightColor else Color.Transparent) - .fillMaxWidth() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(color = color) - ) { - loadedDoc.workbook.value = doc - expanded.value = false - } - - ) { - Box( - modifier = Modifier.size(20.dp, height).clip(RegisterShape).background(color) - ) - Text( - modifier = Modifier.height(height).padding(horizontal = 10.dp), - text = doc.name, - fontWeight = FontWeight.SemiBold, - fontSize = TextUnit(1.3f, TextUnitType.Em), - textAlign = TextAlign.Center, - color = THEME.value.textColor - ) - } - if (activated) { - children.forEach { - Row(Modifier.fillMaxWidth().clickable { - expanded.value = true - loadedDoc.folder.value = it - - }) { - Text( - text = it.name, - modifier = Modifier.padding(30.dp, 10.dp), - fontSize = TextUnit(1f, TextUnitType.Em), - fontWeight = FontWeight.SemiBold, - color = THEME.value.textColor - ) - if (it.type == DocumentInformationType.Folder) Icon(Icons.Filled.KeyboardArrowDown, "") - } - } - - AddDialog(documentViewControlState, windowState, doc) - } -} - -@Composable -fun HeadBar(documentViewControlState: DocumentViewControlState) { - Row( - Modifier.background(THEME.value.secondaryColor).height(70.dp).fillMaxWidth().padding(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - SearchBar() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - IconButton(false, "newFile.svg") - IconButton(false, "leftArrow.svg") { documentViewControlState.sideBarActivated.value = false } - } - } -} - -@Composable -fun SearchBar() { - val value = remember { mutableStateOf(TextFieldValue()) } - val intSource = remember { MutableInteractionSource() } - - BasicTextField( - value = value.value, - modifier = Modifier.fillMaxWidth(0.6f).fillMaxHeight(0.75f).clip(RoundedCornerShape(15.dp)), - interactionSource = intSource, - onValueChange = { - value.value = it - }, - singleLine = true, - decorationBox = { innerTextField -> - Row( - modifier = Modifier.fillMaxWidth().fillMaxHeight().background(THEME.value.textFieldColor) - .clip(RoundedCornerShape(10.dp)), verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Filled.Search, - "", - tint = THEME.value.iconColor, - modifier = Modifier.padding(5.dp) - ) - innerTextField() - } - } - ) -} - - -@Composable -fun AddDialog( - documentViewControlState: DocumentViewControlState, - windowState: PPCWindowState, - parent: DocumentInformation?, - file: Boolean = false -) { - - var active by remember { mutableStateOf(false) } - val value = remember { mutableStateOf(TextFieldValue()) } - val intSource = remember { MutableInteractionSource() } - - println(parent?.path) - - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.padding(20.dp)) { - - if (active) { - BasicTextField( - value = value.value, - modifier = Modifier.fillMaxWidth(0.6f).height(40.dp).clip(RoundedCornerShape(15.dp)), - interactionSource = intSource, - onValueChange = { - value.value = it - }, - singleLine = true, - decorationBox = { innerTextField -> - Row( - modifier = Modifier.fillMaxWidth().fillMaxHeight().background(THEME.value.textFieldColor) - .clip(RoundedCornerShape(10.dp)), verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.size(10.dp)) - innerTextField() - } } - ) - IconButton({ active = false; value.value = TextFieldValue() }) { - Icon(Icons.Filled.Close, "cancel", tint = THEME.value.iconColor) - } - - IconButton({ - if (value.value.text.isBlank()) return@IconButton - if (file) documentViewControlState.createDocument(parent!!, value.value.text) - else windowState.application.newFolder(parent, value.value.text) - - documentViewControlState.updateDocs() - active = false - value.value = TextFieldValue() - }) { - Icon(Icons.Filled.Check, "add", tint = THEME.value.iconColor) - } - - - } else { - Button({ - active = true - }) { - Text("Add") } } }