milan jurkulák пре 2 година
родитељ
комит
3b8cb415b2

+ 38 - 6
build.gradle.kts

@@ -1,4 +1,5 @@
 import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.config.JvmTarget
 import org.mjdev.gradle.extensions.ProjectExt.createTask
 import org.mjdev.gradle.extensions.ProjectExt.resolve
 
@@ -16,6 +17,32 @@ plugins {
 group = libs.versions.packageName.get()
 version = libs.versions.packageVersion.get()
 
+kotlin {
+//    compilerOptions.jvmTarget = JvmTarget.JVM_17
+//    jvm {
+//        jvmToolchain(11)
+//        withJava()
+//    }
+//    sourceSets {
+//        val jvmMain by getting {
+//            dependencies {
+//                implementation(compose.desktop.currentOs)
+//            }
+//        }
+//        val jvmTest by getting
+//    }
+}
+
+//configurations {
+//    "implementation" {
+//        exclude(group = "androidx.compose.animation")
+//        exclude(group = "androidx.compose.foundation")
+//        exclude(group = "androidx.compose.material")
+//        exclude(group = "androidx.compose.runtime")
+//        exclude(group = "androidx.compose.ui")
+//    }
+//}
+
 allprojects {
     repositories {
         maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
@@ -83,9 +110,14 @@ allprojects {
 //        implementation(libs.dbus.java.core)
 //        implementation(libs.dbus.java.transport.native.unixsocket)
         // wayland
-        implementation("org.freedesktop:wayland-client:1.5.1")
+//        implementation("org.freedesktop:wayland-client:1.5.1")
         // gettext
         implementation("org.gnu.gettext:libintl:0.18.3")
+        // constraint
+        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.4.0")
+        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03")
+        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03-shaded-core")
+        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03-shaded")
         // paths
 //        implementation("me.sujanpoudel.multiplatform.utils:multiplatform-paths:0.2.2")
         // files
@@ -103,15 +135,15 @@ allprojects {
         // web
         implementation("io.github.kevinnzou:compose-webview-multiplatform:1.9.40-alpha01")
         // bottom sheet flexible
-        implementation("com.github.skydoves:flexible-bottomsheet-material3:0.1.5")
-        // fs watcher
-        implementation("io.github.irgaly.kfswatch:kfswatch:1.3.0")
+//        implementation("com.github.skydoves:flexible-bottomsheet-material3:0.1.5")
         // stt
-        implementation("com.alphacephei:vosk:0.3.32+")
+//        implementation("com.alphacephei:vosk:0.3.32+")
+        // fs watcher
+//        implementation("io.github.irgaly.kfswatch:kfswatch:1.3.0")
         // custom components
         implementation("com.composables:core:1.12.0")
         // lib-app-indicator
-        implementation("org.purejava:libappindicator-gtk3-java-full:1.4.1")
+//        implementation("org.purejava:libappindicator-gtk3-java-full:1.4.1")
         // internationalization
 //        implementation("org.swiftshire:ji18n-core:1.0")
         // sikulix

+ 35 - 23
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsBottomBar.kt

@@ -1,53 +1,63 @@
 package eu.mjdev.desktop.components.appsmenu
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.*
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
 import eu.mjdev.desktop.components.icon.ShapedIcon
-import eu.mjdev.desktop.components.input.SearchField
+import eu.mjdev.desktop.components.input.SearchFieldPassive
+import eu.mjdev.desktop.extensions.Compose.clear
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.icons.Icons
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Suppress("FunctionName")
 @Composable
 fun AppsBottomBar(
     modifier: Modifier = Modifier,
-    backButtonVisible: Boolean = false,
+    backButtonVisible: Boolean = true,
+    searchTextState: MutableState<String> = rememberState(""),
     onContextMenuClick: () -> Unit = {},
     onHideMenu: () -> Unit = {},
-    onSearch: (String) -> Unit = {},
     onBackClick: () -> Unit = {}
 ) = withDesktopScope {
-    val textState = rememberState("")
-    Row(
-        modifier = modifier.padding(8.dp)
-    ) {
-        if (backButtonVisible && textState.value.isEmpty()) {
+    TopAppBar(
+        modifier = modifier.focusable(false),
+        colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
+        title = {
+            SearchFieldPassive(
+                modifier = Modifier.fillMaxWidth().focusable(true),
+                textState = searchTextState,
+                textColor = iconsTintColor,
+                textStyle = TextStyle(
+                    fontWeight = FontWeight.Bold
+                ),
+                onClearClick = { searchTextState.clear() }
+            )
+        },
+        navigationIcon = {
             ShapedIcon(
+                modifier = Modifier.focusable(false),
+                visible = backButtonVisible && searchTextState.value.isEmpty(),
                 imageVector = Icons.BackArrow,
                 iconColor = borderColor,
                 iconBackgroundColor = iconsTintColor,
                 onRightClick = onContextMenuClick,
                 onClick = onBackClick
             )
-        }
-//        SearchField(
-//            modifier = Modifier.fillMaxWidth().background(Color.Red),
-//            textState = textState
-//        )
-        Row(
-            modifier = Modifier.wrapContentWidth()
-        ) {
+        },
+        actions = {
             ShapedIcon(
+                modifier = Modifier,
                 imageVector = Icons.RestartComputer,
                 iconColor = borderColor,
                 iconBackgroundColor = iconsTintColor,
@@ -59,6 +69,7 @@ fun AppsBottomBar(
                 }
             )
             ShapedIcon(
+                modifier = Modifier,
                 imageVector = Icons.PowerOffComputer,
                 iconColor = borderColor,
                 iconBackgroundColor = iconsTintColor,
@@ -70,6 +81,7 @@ fun AppsBottomBar(
                 }
             )
             ShapedIcon(
+                modifier = Modifier,
                 imageVector = Icons.LogOutUser,
                 iconColor = borderColor,
                 iconBackgroundColor = iconsTintColor,
@@ -81,7 +93,7 @@ fun AppsBottomBar(
                 }
             )
         }
-    }
+    )
 }
 
 @Preview

+ 59 - 30
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -24,10 +24,16 @@ import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememb
 import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.data.Category
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
+import eu.mjdev.desktop.extensions.Compose.clear
 import eu.mjdev.desktop.extensions.Compose.launchedEffect
 import eu.mjdev.desktop.extensions.Compose.onMouseEnter
+import eu.mjdev.desktop.extensions.Compose.plus
 import eu.mjdev.desktop.extensions.Compose.rememberCalculated
+import eu.mjdev.desktop.extensions.Compose.rememberComputed
+import eu.mjdev.desktop.extensions.Compose.rememberState
+import eu.mjdev.desktop.extensions.Compose.removeLast
 import eu.mjdev.desktop.extensions.Modifier.dropShadow
+import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.keyEventHandler
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
 import eu.mjdev.desktop.provider.DesktopScope
@@ -36,27 +42,39 @@ import eu.mjdev.desktop.windows.ChromeWindow
 import eu.mjdev.desktop.windows.ChromeWindowState
 import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
 
-@Suppress("FunctionName", "LocalVariableName")
+@Suppress("FunctionName")
 @Composable
 fun AppsMenu(
     panelState: VisibilityState = rememberVisibilityState(),
     menuState: VisibilityState = rememberVisibilityState(),
     enterAnimation: EnterTransition = AppsMenuEnterAnimation,
     exitAnimation: ExitTransition = AppsMenuExitAnimation,
-    onFocusChange: (Boolean) -> Unit = {},
+    searchTextState: MutableState<String> = rememberState(""),
+    onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
     onAppClick: DesktopScope.(App) -> Unit = { app -> startApp(app) },
     onAppContextMenuClick: DesktopScope.(App) -> Unit = {},
     onCategoryContextMenuClick: (Category) -> Unit = {},
     onUserAvatarClick: () -> Unit = {}
 ) = withDesktopScope {
-    // todo remove those three
-    val appMenuMinWidth by remember { theme.appMenuMinWidthState }
-    val appMenuMinHeight by remember { theme.appMenuMinHeightState }
-    val menuPadding by remember { theme.appMenuOuterPaddingState }
-    //
-    var items: List<Any> by remember(appCategories) { mutableStateOf(appCategories) }
-    val onCategoryClick: (Category) -> Unit = { category ->
-        items = appCategoriesAndApps[category.name]?.sortedBy { it.name } ?: emptyList()
+    var category by rememberState("")
+    val items by rememberComputed(searchTextState.value, category) {
+        when {
+            searchTextState.value.isNotEmpty() -> appsProvider.allApps
+                .filter { app ->
+                    app.desktopFile?.fileData?.contains(searchTextState.value) == true
+                }
+
+            category.isNotEmpty() -> appCategoriesAndApps[category]?.sortedBy { app ->
+                app.name
+            } ?: emptyList<App>()
+
+            else -> appCategories
+        }
+    }
+    val onCategoryClick: (Category) -> Unit = remember {
+        { c ->
+            category = c.name
+        }
     }
     val position by rememberCalculated {
         WindowPosition.Absolute(
@@ -65,26 +83,36 @@ fun AppsMenu(
         )
     }
     val windowState: ChromeWindowState = rememberChromeWindowState(position = position)
-    val Container: @Composable (content: @Composable () -> Unit) -> Unit = { content ->
-        if (isDebug) {
-            content()
-        } else {
-            ChromeWindow(
-                visible = menuState.isVisible,
-                enterAnimation = enterAnimation,
-                exitAnimation = exitAnimation,
-                windowState = windowState,
-                onFocusChange = { focused ->
-                    menuState.onFocusChange(focused)
-                    onFocusChange(focused)
-                },
-                content = {
-                    content()
+    ChromeWindow(
+        visible = menuState.isVisible,
+        enterAnimation = enterAnimation,
+        exitAnimation = exitAnimation,
+        windowState = windowState,
+        onFocusChange = { focused ->
+            menuState.onFocusChange(focused)
+            onFocusChange(focused)
+        },
+        onKeyEvent = { event ->
+            keyEventHandler(event) {
+                onEscape {
+                    menuState.hide()
                 }
-            )
+                onBack {
+                    searchTextState.clear()
+                }
+                onBackSpace {
+                    searchTextState.removeLast()
+                }
+                onDelete {
+                    searchTextState.clear()
+                }
+                onChar { char ->
+                    searchTextState + char
+                }
+            }
+            true
         }
-    }
-    Container {
+    ) {
         Box(
             modifier = Modifier
                 .width(appMenuMinWidth)
@@ -192,11 +220,12 @@ fun AppsMenu(
                             .wrapContentHeight()
                             .background(backgroundColor),
                         backButtonVisible = items.firstOrNull() is App,
+                        searchTextState = searchTextState,
                         onHideMenu = {
                             menuState.hide()
                         },
                         onBackClick = {
-                            items = appCategories
+                            category = ""
                         },
                         onContextMenuClick = {
                             // todo : context menu
@@ -207,7 +236,7 @@ fun AppsMenu(
         }
         launchedEffect(menuState.isVisible) {
             if (!menuState.isVisible) {
-                items = appCategories
+                category = ""
             }
         }
     }

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -47,7 +47,7 @@ fun ControlCenter(
     scope: CoroutineScope = rememberCoroutineScope(),
     enterAnimation: EnterTransition = ControlCenterEnterAnimation,
     exitAnimation: ExitTransition = ControlCenterExitAnimation,
-    onFocusChange: (Boolean) -> Unit = {},
+    onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
     onContextMenuClick: () -> Unit = {}
 ) = withDesktopScope {
     // todo remove those

+ 4 - 5
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/AIPage.kt

@@ -35,7 +35,6 @@ fun AIPage() = ControlCenterPage(
     condition = { ai.isAvailable() }
 ) {
     val clipboardManager = LocalClipboardManager.current
-    val iconSize = 24.dp
     val questionsList = remember { mutableStateListOf<Pair<String, String>>() }
     val request = rememberState("")
     val iconSend = androidx.compose.runtime.remember {
@@ -103,7 +102,7 @@ fun AIPage() = ControlCenterPage(
                                 )
                                 Icon(
                                     modifier = Modifier.padding(4.dp)
-                                        .size(iconSize)
+                                        .size(24.dp)
                                         .align(Alignment.TopEnd)
                                         .onClick {
                                             clipboardManager.setText(AnnotatedString(textData.first))
@@ -130,7 +129,7 @@ fun AIPage() = ControlCenterPage(
                                 )
                                 Icon(
                                     modifier = Modifier.padding(4.dp)
-                                        .size(iconSize)
+                                        .size(24.dp)
                                         .align(Alignment.TopEnd)
                                         .onClick {
                                             clipboardManager.setText(AnnotatedString(textData.second))
@@ -170,7 +169,7 @@ fun AIPage() = ControlCenterPage(
                             if (request.value.isNotEmpty()) {
                                 Icon(
                                     modifier = Modifier.padding(4.dp)
-                                        .size(iconSize)
+                                        .size(24.dp)
                                         .onClick {
                                             clipboardManager.setText(AnnotatedString(request.value))
                                         },
@@ -182,7 +181,7 @@ fun AIPage() = ControlCenterPage(
                             if (iconSend.value != null) {
                                 Icon(
                                     modifier = Modifier.padding(4.dp)
-                                        .size(iconSize)
+                                        .size(24.dp)
                                         .onClick { onDone() },
                                     imageVector = iconSend.value!!,
                                     tint = Color.White.copy(alpha = 0.9f),

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -63,7 +63,7 @@ fun DesktopPanel(
     position: WindowPosition.Aligned = WindowPosition.Aligned(Alignment.BottomCenter),
     onMenuIconClicked: () -> Unit = {},
     onMenuIconContextMenuClicked: () -> Unit = {},
-    onFocusChange: (Boolean) -> Unit = {},
+    onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
     onAppClick: DesktopScope.(App) -> Unit = { app -> startApp(app) },
     onAppContextMenuClick: (App) -> Unit = {},
     onLanguageClick: () -> Unit = {},

+ 29 - 24
src/main/kotlin/eu/mjdev/desktop/components/icon/ShapedIcon.kt

@@ -32,33 +32,38 @@ fun ShapedIcon(
     iconShape: Shape = CircleShape,
     innerPadding: PaddingValues = PaddingValues(4.dp),
     outerPadding: PaddingValues = PaddingValues(2.dp),
+    visible: Boolean = true,
     onRightClick: () -> Unit = {},
     onClick: () -> Unit = {}
-) = Box(
-    modifier = modifier
-        .padding(outerPadding)
-        .size(iconSize + innerPadding.size + outerPadding.size)
-        .background(iconBackgroundColor, iconShape)
-        .clip(iconShape)
-        .onMousePress {
-            onLeftClick { onClick() }
-            onRightClick { onRightClick() }
-        }
 ) {
-    Box(
-        modifier = Modifier
-            .fillMaxSize()
-            .padding(innerPadding),
-    ) {
-        if (customContent != null) {
-            customContent()
-        } else {
-            Icon(
-                modifier = Modifier.fillMaxSize(),
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                tint = iconColor
-            )
+    if (visible) {
+        Box(
+            modifier = modifier
+                .padding(outerPadding)
+                .size(iconSize + innerPadding.size + outerPadding.size)
+                .background(iconBackgroundColor, iconShape)
+                .clip(iconShape)
+                .onMousePress {
+                    onLeftClick { onClick() }
+                    onRightClick { onRightClick() }
+                }
+        ) {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(innerPadding),
+            ) {
+                if (customContent != null) {
+                    customContent()
+                } else {
+                    Icon(
+                        modifier = Modifier.fillMaxSize(),
+                        imageVector = imageVector,
+                        contentDescription = contentDescription,
+                        tint = iconColor
+                    )
+                }
+            }
         }
     }
 }

+ 0 - 25
src/main/kotlin/eu/mjdev/desktop/components/input/SearchField.kt

@@ -1,25 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2024.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package eu.mjdev.desktop.components.input
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
-
-@Composable
-fun SearchField(
-    modifier: Modifier = Modifier,
-    textState: MutableState<String> = rememberSaveable { mutableStateOf("") },
-) = SelectableOutlineEditText(
-    value = textState.value,
-    onValueChange = { v: String -> textState.value = v },
-    modifier = modifier
-)

+ 64 - 0
src/main/kotlin/eu/mjdev/desktop/components/input/SearchFieldPassive.kt

@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.onClick
+import androidx.compose.material.Icon
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.icons.Icons
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun SearchFieldPassive(
+    modifier: Modifier = Modifier,
+    textState: MutableState<String> = mutableStateOf(""),
+    textColor: Color = Color.White,
+    textStyle: TextStyle = TextStyle.Default,
+    onClearClick: () -> Unit = {}
+) = SelectableOutlineEditText(
+    value = textState.value,
+    modifier = modifier.focusable(false),
+    enabled = false,
+    singleLine = true,
+    textStyle = textStyle,
+    colors = TextFieldDefaults.outlinedTextFieldColors(
+        backgroundColor = Color.Transparent,
+        textColor = textColor,
+        disabledTextColor = textColor,
+        cursorColor = Color.Transparent,
+        errorCursorColor = Color.Transparent,
+        focusedBorderColor = Color.Transparent,
+        unfocusedBorderColor = Color.Transparent,
+        disabledBorderColor = Color.Transparent,
+        errorBorderColor = Color.Transparent
+    ),
+    trailingIcon = {
+        if (textState.value.isNotEmpty()) {
+            Icon(
+                modifier = Modifier.padding(4.dp)
+                    .size(24.dp)
+                    .onClick { onClearClick() },
+                imageVector = Icons.TextFieldClearText,
+                tint = textColor,
+                contentDescription = ""
+            )
+        }
+    }
+)

+ 3 - 3
src/main/kotlin/eu/mjdev/desktop/components/input/SelectableOutlineEditText.kt

@@ -17,12 +17,12 @@ import androidx.compose.ui.text.input.VisualTransformation
 
 @Composable
 fun SelectableOutlineEditText(
-    value: String,
-    onValueChange: (String) -> Unit,
+    value: String = "",
+    onValueChange: (String) -> Unit = {},
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     readOnly: Boolean = false,
-    textStyle: TextStyle = LocalTextStyle.current,
+    textStyle: TextStyle = TextStyle.Default,
     label: @Composable (() -> Unit)? = null,
     placeholder: @Composable (() -> Unit)? = null,
     leadingIcon: @Composable (() -> Unit)? = null,

+ 21 - 20
src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt

@@ -27,23 +27,6 @@ fun MainWindow() = withDesktopScope {
     val menuState = rememberVisibilityState()
     val installWindowSate = rememberVisibilityState()
     val infoWindowSate = rememberVisibilityState(false) //(api.isFirstStart || api.isDebug) // todo
-    val handleMenuFocus: (Boolean) -> Unit = { focus ->
-        if (!menuState.isWindowFocus && !focus && !controlCenterState.isWindowFocus) {
-            menuState.hide()
-        }
-    }
-    val handlePanelFocus: (Boolean) -> Unit = { focus ->
-        if (!focus) {
-            if (!menuState.isVisible) {
-                panelState.hide()
-            }
-        }
-    }
-    val handleControlCenterFocus: (Boolean) -> Unit = { focus ->
-        if (!focus) {
-            controlCenterState.hide()
-        }
-    }
     DesktopWindow(
         panelState = panelState,
         controlCenterState = controlCenterState,
@@ -56,16 +39,34 @@ fun MainWindow() = withDesktopScope {
             panelState = panelState,
             menuState = menuState,
             onMenuIconClicked = { if (!menuState.isVisible) menuState.show() },
-            onFocusChange = { focus -> handlePanelFocus(focus) }
+            onFocusChange = { focus ->
+                if (!focus) {
+                    if (!menuState.isVisible) {
+                        panelState.hide()
+                    }
+                }
+            }
         )
         AppsMenu(
             menuState = menuState,
             panelState = panelState,
-            onFocusChange = { focus -> handleMenuFocus(focus) }
+            onFocusChange = { focus ->
+                if (!menuState.isWindowFocus && !focus && !controlCenterState.isWindowFocus) {
+                    menuState.hide()
+                } else {
+                    if(menuState.isVisible) {
+                        requestFocus()
+                    }
+                }
+            }
         )
         ControlCenter(
             controlCenterState = controlCenterState,
-            onFocusChange = { focus -> handleControlCenterFocus(focus) }
+            onFocusChange = { focus ->
+                if (!focus) {
+                    controlCenterState.hide()
+                }
+            }
         )
         Greeter()
         InfoWindow(

+ 12 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -244,6 +244,18 @@ object Compose {
         } else false
     }
 
+    fun MutableState<String>.clear() {
+        value = ""
+    }
+
+    fun MutableState<String>.removeLast() {
+        value.take(value.length - 1)
+    }
+
+    operator fun MutableState<String>.plus(text: String) {
+        value += text
+    }
+
     @Composable
     fun Modifier.requestFocusOnTouch(
         focusRequester: FocusRequester,

+ 62 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/internal/KeyEventHandler.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.internal
+
+import androidx.compose.ui.input.key.*
+
+@Suppress("CanBeParameter", "unused", "MemberVisibilityCanBePrivate")
+class KeyEventHandler(
+    private val event: KeyEvent,
+    private val block: KeyEventHandler.() -> Unit
+) {
+    private val listeners = mutableListOf<KeyEventListener>()
+
+    fun addListener(key: Key?, block: (String) -> Unit) {
+        listeners.add(KeyEventListener(key, block))
+    }
+
+    init {
+        block()
+        if (event.type == KeyEventType.KeyDown) {
+            val char = Char(event.key.keyCode.toInt())
+            when {
+                char.isPrintable -> listeners.filter { l -> l.key == null }.forEach { l -> l.block(parseCharacter(event)) }
+                else -> listeners.filter { l -> l.key == event.key }.forEach { l -> l.block(parseCharacter(event)) }
+            }
+        }
+    }
+
+    private val Char.isPrintable : Boolean get() {
+        val block = Character.UnicodeBlock.of(this)
+        return (!Character.isISOControl(this)) &&
+                this != java.awt.event.KeyEvent.CHAR_UNDEFINED &&
+                block != null &&
+                block != Character.UnicodeBlock.SPECIALS
+    }
+
+    private fun parseCharacter(event: KeyEvent) = Char(event.key.keyCode.toInt()).let { c ->
+        if (event.isShiftPressed) c.uppercase() else c.lowercase()
+    }
+
+    fun onChar(block: (String) -> Unit) = addListener(null, block)
+    fun onEscape(block: (String) -> Unit) = addListener(Key.Escape, block)
+    fun onDelete(block: (String) -> Unit) = addListener(Key.Delete, block)
+    fun onEnter(block: (String) -> Unit) = addListener(Key.Enter, block)
+    fun onBack(block: (String) -> Unit) = addListener(Key.Back, block)
+    fun onBackSpace(block: (String) -> Unit) = addListener(Key.Back, block)
+
+    class KeyEventListener(val key: Key?, val block: (String) -> Unit)
+
+    companion object {
+        fun keyEventHandler(
+            event: KeyEvent,
+            block: KeyEventHandler.() -> Unit
+        ) = KeyEventHandler(event, block)
+    }
+}

+ 1 - 0
src/main/kotlin/eu/mjdev/desktop/icons/Icons.kt

@@ -10,6 +10,7 @@ object Icons {
     val SendIcon = Icons.AutoMirrored.Filled.Send
     val TextFieldVisiblePassword = Icons.Filled.Visibility
     val TextFieldInVisiblePassword = Icons.Filled.VisibilityOff
+    val TextFieldClearText = Icons.Filled.Clear // todo
     val CopyToClipboard = Icons.Filled.ContentCopy
     val TextSnippet = Icons.AutoMirrored.Filled.TextSnippet
     val Campaign = Icons.Filled.Campaign

+ 26 - 26
src/main/kotlin/eu/mjdev/desktop/managers/FileSystemWatcher.kt

@@ -1,29 +1,29 @@
 package eu.mjdev.desktop.managers
 
-import io.github.irgaly.kfswatch.KfsDirectoryWatcher
-import io.github.irgaly.kfswatch.KfsDirectoryWatcherEvent
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+//import io.github.irgaly.kfswatch.KfsDirectoryWatcher
+//import io.github.irgaly.kfswatch.KfsDirectoryWatcherEvent
+//import kotlinx.coroutines.CoroutineScope
+//import kotlinx.coroutines.Dispatchers
+//import kotlinx.coroutines.launch
 
-@Suppress("MemberVisibilityCanBePrivate")
-class FileSystemWatcher(
-    val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
-    val mountpoints: List<String> = listOf("/mnt", "/media"),
-    val watcher: KfsDirectoryWatcher = KfsDirectoryWatcher(scope),
-    val onEvent: KfsDirectoryWatcherEvent.() -> Unit = {}
-) {
-    fun init() = scope.launch(Dispatchers.IO) {
-        mountpoints.forEach {
-            watcher.add(it)
-        }
-        watcher.onEventFlow.collect { event ->
-            onEvent(event)
-        }
-    }
-
-    fun dispose() = scope.launch {
-        watcher.removeAll()
-        watcher.close()
-    }
-}
+//@Suppress("MemberVisibilityCanBePrivate")
+//class FileSystemWatcher(
+//    val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+//    val mountpoints: List<String> = listOf("/mnt", "/media"),
+//    val watcher: KfsDirectoryWatcher = KfsDirectoryWatcher(scope),
+//    val onEvent: KfsDirectoryWatcherEvent.() -> Unit = {}
+//) {
+//    fun init() = scope.launch(Dispatchers.IO) {
+//        mountpoints.forEach {
+//            watcher.add(it)
+//        }
+//        watcher.onEventFlow.collect { event ->
+//            onEvent(event)
+//        }
+//    }
+//
+//    fun dispose() = scope.launch {
+//        watcher.removeAll()
+//        watcher.close()
+//    }
+//}

+ 25 - 24
src/main/kotlin/eu/mjdev/desktop/managers/ProcessManager.kt

@@ -11,6 +11,7 @@ package eu.mjdev.desktop.managers
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import eu.mjdev.desktop.data.App
+import eu.mjdev.desktop.extensions.Custom.orElse
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import kotlinx.coroutines.*
@@ -49,7 +50,7 @@ class ProcessManager(
         iterator().apply {
             while (hasNext()) {
                 val pw = next()
-                if (!pw.processHandle.isAlive) {
+                if (pw.processHandle?.isAlive != true) {
                     remove()
                     listeners.filterIsInstance<ProcessRemoveListener>().forEach { l ->
                         l.onProcessRemoved(pw.processHandle)
@@ -80,21 +81,21 @@ class ProcessManager(
         job?.cancel()
     }
 
-    fun hasAppProcess(app: App?): Boolean = if (app == null) false
-    else {
-        val appCmd = app.cmd
-        val appName = app.name
-        val appFullName = app.fullAppName
-        any { ph ->
-            ph.command.contains(appName) ||
-                    ph.command.contains(appCmd) ||
-                    ph.command.contains(appFullName) ||
-                    ph.commandLine.contains(appName) ||
-                    ph.commandLine.contains(appCmd) ||
-                    ph.commandLine.contains(appFullName)
+    // todo : something raises error
+    fun hasAppProcess(app: App?): Boolean = runCatching {
+        val appCmd = app?.cmd
+        val appName = app?.name
+        val appFullName = app?.fullAppName
+        any { pw ->
+            (appName != null && pw.command.contains(appName)) ||
+                    (appCmd != null && pw.command.contains(appCmd)) ||
+                    (appFullName != null && pw.command.contains(appFullName)) ||
+                    (appName != null && pw.commandLine.contains(appName)) ||
+                    (appCmd != null && pw.commandLine.contains(appCmd)) ||
+                    (appFullName != null && pw.commandLine.contains(appFullName))
 
         }
-    }
+    }.getOrNull() ?: false
 
     companion object {
         fun ArrayList<ProcessWrapper>.containsProcess(ph: ProcessHandle) = ph.pid().let { pid ->
@@ -105,23 +106,23 @@ class ProcessManager(
 
         @Composable
         fun processManagerListener(
-            onChanged: ProcessManager.(processHandle: ProcessHandle) -> Unit,
+            onChanged: ProcessManager.(processHandle: ProcessHandle?) -> Unit,
         ) = processManagerListener(onChanged, onChanged)
 
         @Composable
         fun processManagerListener(
-            onAdd: ProcessManager.(processHandle: ProcessHandle) -> Unit,
-            onRemove: ProcessManager.(processHandle: ProcessHandle) -> Unit,
+            onAdd: ProcessManager.(processHandle: ProcessHandle?) -> Unit,
+            onRemove: ProcessManager.(processHandle: ProcessHandle?) -> Unit,
             api: DesktopProvider = LocalDesktop.current,
             processManger: ProcessManager = api.processManager
         ) {
             val addListener = object : ProcessAddListener {
-                override fun onProcessAdded(processHandle: ProcessHandle) {
+                override fun onProcessAdded(processHandle: ProcessHandle?) {
                     onAdd.invoke(processManger, processHandle)
                 }
             }
             val removeListener = object : ProcessRemoveListener {
-                override fun onProcessRemoved(processHandle: ProcessHandle) {
+                override fun onProcessRemoved(processHandle: ProcessHandle?) {
                     onRemove.invoke(processManger, processHandle)
                 }
             }
@@ -139,17 +140,17 @@ class ProcessManager(
     interface ProcessListener
 
     interface ProcessAddListener : ProcessListener {
-        fun onProcessAdded(processHandle: ProcessHandle)
+        fun onProcessAdded(processHandle: ProcessHandle?)
     }
 
     interface ProcessRemoveListener : ProcessListener {
-        fun onProcessRemoved(processHandle: ProcessHandle)
+        fun onProcessRemoved(processHandle: ProcessHandle?)
     }
 
     class ProcessWrapper(
-        val processHandle: ProcessHandle,
-        val pid: Long = processHandle.pid(),
-        val info: ProcessHandle.Info? = processHandle.info(),
+        val processHandle: ProcessHandle?,
+        val pid: Long = processHandle?.pid().orElse { -1L },
+        val info: ProcessHandle.Info? = processHandle?.info(),
         val command: String = info?.command()?.getOrNull().orEmpty(),
         val commandLine: String = info?.commandLine()?.getOrNull().orEmpty()
     ) {

+ 7 - 0
src/main/kotlin/eu/mjdev/desktop/provider/DesktopScope.kt

@@ -81,6 +81,9 @@ open class DesktopScope(
     val appCategoriesAndApps
         get() = appsProvider.categoriesAndApps
 
+    val allApps
+        get() = appsProvider.allApps
+
     val favoriteApps
         get() = appsProvider.favoriteApps
 
@@ -110,6 +113,10 @@ open class DesktopScope(
     val disabledColor
         get() = api.palette.disabledColor
 
+    val appMenuMinWidth = theme.appMenuMinWidth
+    val appMenuMinHeight = theme.appMenuMinHeight
+    val menuPadding = theme.appMenuOuterPadding
+
     fun startApp(app: App) =
         api.appsProvider.startApp(app)