diff --git a/build.gradle.kts b/build.gradle.kts index 41b22a0..960f3ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(compose.desktop.currentOs) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.desktop.components.splitPane) + implementation(compose("org.jetbrains.compose.ui:ui-util")) implementation("org.eclipse.jgit:org.eclipse.jgit:6.2.0.202206071550-r") implementation("org.apache.sshd:sshd-core:2.9.0") implementation("com.google.dagger:dagger:2.43.2") @@ -56,7 +57,7 @@ tasks.withType { compose.desktop { application { - mainClass = "MainKt" + mainClass = "com.jetpackduba.gitnuro.MainKt" nativeDistributions { includeAllModules = true diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/images/NetworkImageLoader.kt b/src/main/kotlin/com/jetpackduba/gitnuro/images/NetworkImageLoader.kt index f5b542a..60e76f7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/images/NetworkImageLoader.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/images/NetworkImageLoader.kt @@ -63,7 +63,7 @@ fun rememberNetworkImageOrNull(url: String, placeHolderImageRes: String? = null) val cacheImageUsed = remember { ValueHolder(false) } var image by remember(url) { - val cachedImage = NetworkImageLoader.loadCachedImage(url) + val cachedImage = networkImageLoader.loadCachedImage(url) val image: ImageBitmap? = when { cachedImage != null -> { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/main.kt b/src/main/kotlin/com/jetpackduba/gitnuro/main.kt index ab92ee3..db5ac50 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/main.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/main.kt @@ -1,4 +1,4 @@ -import com.jetpackduba.gitnuro.App +package com.jetpackduba.gitnuro fun main() { val main = App() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Color.kt b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Color.kt index 2aa9072..2d14c00 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Color.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Color.kt @@ -27,6 +27,7 @@ val lightTheme = ColorsScheme( hoverScrollbar = Color(0xFF0070D8), diffLineAdded = Color(0xFFd7ebd0), diffLineRemoved = Color(0xFFf0d4d4), + isLight = true, ) @@ -55,7 +56,7 @@ val darkBlueTheme = ColorsScheme( hoverScrollbar = Color(0xFFCCCCCC), diffLineAdded = Color(0xFF566f5a), diffLineRemoved = Color(0xFF6f585e), - + isLight = false, ) val darkGrayTheme = ColorsScheme( @@ -83,4 +84,5 @@ val darkGrayTheme = ColorsScheme( hoverScrollbar = Color(0xFFCCCCCC), diffLineAdded = Color(0xFF5b7059), diffLineRemoved = Color(0xFF74595c), + isLight = false, ) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/theme/ColorsScheme.kt b/src/main/kotlin/com/jetpackduba/gitnuro/theme/ColorsScheme.kt index 6588972..b9bd9fa 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/theme/ColorsScheme.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/theme/ColorsScheme.kt @@ -14,7 +14,6 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -// TODO Add line added + removed colors, graph colors and icons color for added/modified/removed files. @Serializable data class ColorsScheme( val primary: Color, @@ -42,6 +41,7 @@ data class ColorsScheme( val hoverScrollbar: Color, val diffLineAdded: Color, val diffLineRemoved: Color, + val isLight: Boolean, ) { fun toComposeColors(): Colors { return Colors( @@ -57,7 +57,7 @@ data class ColorsScheme( onBackground = this.primaryText, onSurface = this.primaryText, onError = this.onError, - isLight = true, // property specific for some colors, we don't care about this as all our components are customized + isLight = isLight, ) } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Typography.kt b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Typography.kt index a80576f..2ab4781 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Typography.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Typography.kt @@ -55,6 +55,7 @@ fun typography() = Typography( body2 = TextStyle( fontSize = 13.sp, color = MaterialTheme.colors.primaryTextColor, + fontWeight = FontWeight.Normal, letterSpacing = LETTER_SPACING.sp, ), caption = TextStyle( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt index e454ca6..a36ea2c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt @@ -1,6 +1,5 @@ package com.jetpackduba.gitnuro.ui -import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme @@ -16,11 +15,11 @@ import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.theme.secondaryTextColor import com.jetpackduba.gitnuro.ui.components.SideMenuPanel import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.branchContextMenuItems import com.jetpackduba.gitnuro.viewmodels.BranchesViewModel import org.eclipse.jgit.lib.Ref -@OptIn(ExperimentalFoundationApi::class) @Composable fun Branches( branchesViewModel: BranchesViewModel, @@ -69,7 +68,7 @@ private fun BranchLineEntry( onPushToRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit, ) { - ContextMenuArea( + ContextMenu( items = { branchContextMenuItems( branch = branch, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt index 26e8abb..2a30945 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt @@ -21,6 +21,7 @@ import com.jetpackduba.gitnuro.viewmodels.CommitChangesStatus import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.theme.* +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.eclipse.jgit.diff.DiffEntry @@ -228,7 +229,7 @@ fun CommitLogChanges( .fillMaxSize() ) { items(items = diffEntries) { diffEntry -> - ContextMenuArea( + ContextMenu( items = { commitedChangesEntriesContextMenuItems( diffEntry, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt index a91ff27..6d63241 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt @@ -1,9 +1,7 @@ -@file:OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) +@file:OptIn(ExperimentalComposeUiApi::class) package com.jetpackduba.gitnuro.ui -import androidx.compose.foundation.ContextMenuArea -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -23,6 +21,7 @@ import com.jetpackduba.gitnuro.theme.primaryTextColor import com.jetpackduba.gitnuro.ui.components.SideMenuPanel import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry import com.jetpackduba.gitnuro.ui.components.VerticalExpandable +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.remoteBranchesContextMenu import com.jetpackduba.gitnuro.ui.context_menu.remoteContextMenu import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog @@ -30,6 +29,7 @@ import com.jetpackduba.gitnuro.viewmodels.RemoteView import com.jetpackduba.gitnuro.viewmodels.RemotesViewModel import org.eclipse.jgit.lib.Ref +@OptIn(ExperimentalComposeUiApi::class) @Composable fun Remotes( remotesViewModel: RemotesViewModel, @@ -85,7 +85,6 @@ fun Remotes( } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun RemoteRow( remote: RemoteView, @@ -106,7 +105,7 @@ private fun RemoteRow( val branches = remote.remoteInfo.branchesList Column { branches.forEach { branch -> - ContextMenuArea( + ContextMenu( items = { remoteBranchesContextMenu( onDeleteBranch = { onDeleteBranch(branch) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt index ca8c96d..2517ae9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt @@ -1,9 +1,5 @@ -@file:OptIn(ExperimentalFoundationApi::class) - package com.jetpackduba.gitnuro.ui -import androidx.compose.foundation.ContextMenuArea -import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -11,6 +7,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.res.painterResource import com.jetpackduba.gitnuro.ui.components.SideMenuPanel import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement import com.jetpackduba.gitnuro.ui.context_menu.stashesContextMenuItems import com.jetpackduba.gitnuro.viewmodels.StashStatus import com.jetpackduba.gitnuro.viewmodels.StashesViewModel @@ -58,9 +56,9 @@ fun Stashes( private fun StashRow( stash: RevCommit, onClick: () -> Unit, - contextItems: List, + contextItems: List, ) { - ContextMenuArea( + ContextMenu( items = { contextItems } ) { SideMenuSubentry( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt index f502094..2867e76 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt @@ -1,7 +1,5 @@ package com.jetpackduba.gitnuro.ui -import androidx.compose.foundation.ContextMenuArea -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -15,11 +13,11 @@ import com.jetpackduba.gitnuro.theme.secondaryTextColor import com.jetpackduba.gitnuro.ui.components.SideMenuPanel import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry import com.jetpackduba.gitnuro.ui.components.Tooltip +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.submoduleContextMenuItems import com.jetpackduba.gitnuro.viewmodels.SubmodulesViewModel import org.eclipse.jgit.submodule.SubmoduleStatus -@OptIn(ExperimentalFoundationApi::class) @Composable fun Submodules( submodulesViewModel: SubmodulesViewModel, @@ -42,13 +40,12 @@ fun Submodules( ) } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun SubmoduleLineEntry( submodulePair: Pair, onInitializeModule: () -> Unit, ) { - ContextMenuArea( + ContextMenu( items = { submoduleContextMenuItems( submodulePair.second, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt index 7862727..d40f242 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt @@ -1,6 +1,5 @@ package com.jetpackduba.gitnuro.ui -import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -9,6 +8,7 @@ import androidx.compose.ui.res.painterResource import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.ui.components.SideMenuPanel import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.tagContextMenuItems import com.jetpackduba.gitnuro.viewmodels.TagsViewModel import org.eclipse.jgit.lib.Ref @@ -47,7 +47,7 @@ private fun TagRow( onCheckoutTag: () -> Unit, onDeleteTag: () -> Unit, ) { - ContextMenuArea( + ContextMenu( items = { tagContextMenuItems( onCheckoutTag = onCheckoutTag, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt index c168ef8..c02e9be 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt @@ -35,14 +35,11 @@ import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.SecondaryButton -import com.jetpackduba.gitnuro.ui.context_menu.DropDownContent -import com.jetpackduba.gitnuro.ui.context_menu.DropDownContentData -import com.jetpackduba.gitnuro.ui.context_menu.EntryType -import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems import com.jetpackduba.gitnuro.viewmodels.StageStatus import com.jetpackduba.gitnuro.viewmodels.StatusViewModel import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.theme.* +import com.jetpackduba.gitnuro.ui.context_menu.* import org.eclipse.jgit.lib.RepositoryState @Composable @@ -496,7 +493,7 @@ private fun EntriesList( lazyListState: LazyListState, onDiffEntrySelected: (StatusEntry) -> Unit, onDiffEntryOptionSelected: (StatusEntry) -> Unit, - onGenerateContextMenu: (StatusEntry) -> List, + onGenerateContextMenu: (StatusEntry) -> List, onAllAction: () -> Unit, allActionTitle: String, selectedEntryType: DiffEntryType?, @@ -571,7 +568,7 @@ private fun FileEntry( actionColor: Color, onClick: () -> Unit, onButtonClick: () -> Unit, - onGenerateContextMenu: (StatusEntry) -> List, + onGenerateContextMenu: (StatusEntry) -> List, ) { val hoverInteraction = remember { MutableInteractionSource() } val isHovered by hoverInteraction.collectIsHoveredAsState() @@ -582,7 +579,7 @@ private fun FileEntry( .fillMaxWidth() .hoverable(hoverInteraction) ) { - ContextMenuArea( + ContextMenu( items = { onGenerateContextMenu(statusEntry) }, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt index 695a5f4..c8d0e63 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt @@ -1,7 +1,5 @@ package com.jetpackduba.gitnuro.ui.components -import androidx.compose.foundation.ContextMenuArea -import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -10,6 +8,8 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement @OptIn(ExperimentalFoundationApi::class) @Composable @@ -21,13 +21,13 @@ fun SideMenuPanel( onExpand: () -> Unit, itemContent: @Composable (T) -> Unit, headerHoverIcon: @Composable (() -> Unit)? = null, - contextItems: () -> List = { emptyList() }, + contextItems: () -> List = { emptyList() }, ) { VerticalExpandable( isExpanded = isExpanded, onExpand = onExpand, header = { - ContextMenuArea( + ContextMenu( items = contextItems ) { SideMenuEntry( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt index fdad43b..915d881 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt @@ -1,12 +1,10 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource import com.jetpackduba.gitnuro.extensions.isHead import com.jetpackduba.gitnuro.extensions.simpleLogName import org.eclipse.jgit.lib.Ref -@OptIn(ExperimentalFoundationApi::class) fun branchContextMenuItems( branch: Ref, isCurrentBranch: Boolean, @@ -19,47 +17,51 @@ fun branchContextMenuItems( onDeleteRemoteBranch: () -> Unit = {}, onPushToRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit, -): List { - return mutableListOf().apply { +): List { + return mutableListOf().apply { if (!isCurrentBranch) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Checkout branch", + icon = { painterResource("start.svg") }, onClick = onCheckoutBranch ) ) if (currentBranch != null && !currentBranch.isHead) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Merge branch", onClick = onMergeBranch ) ) add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Rebase branch", onClick = onRebaseBranch ) ) + + add(ContextMenuElement.ContextSeparator) } } if (isLocal && !isCurrentBranch) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Delete branch", + icon = { painterResource("delete.svg") }, onClick = onDeleteBranch ) ) } if (!isLocal && currentBranch != null && !currentBranch.isHead) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Push ${currentBranch.simpleLogName} to ${branch.simpleLogName}", onClick = onPushToRemoteBranch ) ) add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Pull ${branch.simpleLogName} to ${currentBranch.simpleLogName}", onClick = onPullFromRemoteBranch ) @@ -67,11 +69,16 @@ fun branchContextMenuItems( } if (!isLocal) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Delete remote branch", + icon = { painterResource("delete.svg") }, onClick = onDeleteRemoteBranch ), ) } + + if(lastOrNull() == ContextMenuElement.ContextSeparator) { + removeLast() + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/CommitedChangesEntriesContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/CommitedChangesEntriesContextMenu.kt index 117a5a1..5f9bf8a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/CommitedChangesEntriesContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/CommitedChangesEntriesContextMenu.kt @@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource import org.eclipse.jgit.diff.DiffEntry @OptIn(ExperimentalFoundationApi::class) @@ -9,19 +10,20 @@ fun commitedChangesEntriesContextMenuItems( diffEntry: DiffEntry, onBlame: () -> Unit, onHistory: () -> Unit, -): List { - return mutableListOf().apply { +): List { + return mutableListOf().apply { if (diffEntry.changeType != DiffEntry.ChangeType.ADD || diffEntry.changeType != DiffEntry.ChangeType.DELETE ) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Blame file", + icon = { painterResource("blame.svg") }, onClick = onBlame, ) ) add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "File history", onClick = onHistory, ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/ContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/ContextMenu.kt new file mode 100644 index 0000000..6508c98 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/ContextMenu.kt @@ -0,0 +1,226 @@ +package com.jetpackduba.gitnuro.ui.context_menu + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.unit.* +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import com.jetpackduba.gitnuro.keybindings.KeybindingOption +import com.jetpackduba.gitnuro.keybindings.matchesBinding +import com.jetpackduba.gitnuro.theme.primaryTextColor +import com.jetpackduba.gitnuro.theme.secondaryTextColor +import java.awt.event.MouseEvent +import kotlin.math.abs + +private var lastCheck: Long = 0 +private const val MIN_TIME_BETWEEN_POPUPS = 20 + +@Composable +fun ContextMenu(items: () -> List, function: @Composable () -> Unit) { + Box(modifier = Modifier.contextMenu(items), propagateMinConstraints = true) { + function() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun Modifier.contextMenu(items: () -> List): Modifier { + var lastMouseEventState by remember { mutableStateOf(null) } + val mod = this.pointerInput(Unit) { + + while (true) { + val lastMouseEvent = awaitPointerEventScope { awaitEventFirstDown() } + val mouseEvent = lastMouseEvent.awtEventOrNull + + if (mouseEvent != null) { + if (!lastMouseEvent.toString().contains("MOUSE_MOVED")) + println(lastMouseEvent.toString()) + + if (lastMouseEvent.button.isSecondary) { + val currentCheck = System.currentTimeMillis() + if (lastCheck != 0L && currentCheck - lastCheck < MIN_TIME_BETWEEN_POPUPS) { + println("IGNORE POPUP TRIGGERED!") + } else { + println("POPUP TRIGGERED!") + println("X: ${mouseEvent.x}\nY: ${mouseEvent.y}") + lastCheck = currentCheck + + lastMouseEventState = mouseEvent + } + } + } + } + } + + if (lastMouseEventState != null) { + showPopup( + lastMouseEventState!!.x, + lastMouseEventState!!.y, + items(), + onDismissRequest = { lastMouseEventState = null }) + } + + return mod +} + +@Composable +fun showPopup(x: Int, y: Int, contextMenuElements: List, onDismissRequest: () -> Unit) { + LaunchedEffect(contextMenuElements) { + println("Items count ${contextMenuElements.count()}") + } + Popup( + focusable = true, + popupPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + val resultY = if (popupContentSize.height > windowSize.height) { + 0 // If the popup is taller than the window itself + } else if (y + popupContentSize.height > windowSize.height) { + // If the end of the popup would go out of bounds. + // Move the Y a bit to the top to make it fit + y - abs(windowSize.height - (y + popupContentSize.height)) + } else { + y + } + + val resultX = if (x + popupContentSize.width > windowSize.width && popupContentSize.width < x) { + // If the end of the popup would go out of bounds. + // Move the X a bit to the left to make it fit + x - abs(windowSize.width - (x + popupContentSize.width)) + } else { + x + } + + return IntOffset(resultX, resultY) + } + }, + onDismissRequest = onDismissRequest + ) { + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Box( + modifier = Modifier + .shadow(4.dp) + .width(300.dp) + .background(MaterialTheme.colors.background) + .run { + return@run if (!MaterialTheme.colors.isLight) { + this.border(1.dp, MaterialTheme.colors.primaryTextColor.copy(alpha = 0.2f)) + } else + this + } + .focusRequester(focusRequester) + .focusable() + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.matchesBinding(KeybindingOption.EXIT)) { + onDismissRequest() + true + } else + false + }, + ) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (item in contextMenuElements) { + when (item) { + is ContextMenuElement.ContextTextEntry -> TextEntry(item, onDismissRequest = onDismissRequest) + ContextMenuElement.ContextSeparator -> Separator() + } + + } + } + } + } +} + +@Composable +fun Separator() { + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colors.primaryTextColor.copy(alpha = 0.4f)) + ) +} + +@Composable +internal fun focusRequesterAndModifier(): Pair { + val focusRequester = remember { FocusRequester() } + return focusRequester to Modifier.focusRequester(focusRequester) +} + +@Composable +fun TextEntry(contextTextEntry: ContextMenuElement.ContextTextEntry, onDismissRequest: () -> Unit) { + val icon = contextTextEntry.icon + + Row( + modifier = Modifier + .clickable { + onDismissRequest() + contextTextEntry.onClick() + } + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.size(24.dp).padding(end = 8.dp)) { + if (icon != null) { + Icon( + painter = icon(), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + tint = (MaterialTheme.colors.secondaryTextColor.copy(alpha = 0.8f)) + ) + } + } + + Text( + contextTextEntry.label, + style = MaterialTheme.typography.body2, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +sealed interface ContextMenuElement { + data class ContextTextEntry( + val label: String, + val icon: @Composable (() -> Painter)? = null, + val onClick: () -> Unit = {} + ) : ContextMenuElement + + object ContextSeparator : ContextMenuElement +} + +private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { + var event: PointerEvent + do { + event = awaitPointerEvent() + } while ( + !event.changes.fastAll { it.changedToDown() } + ) + return event +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/LogContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/LogContextMenu.kt index b6f7236..07a2a79 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/LogContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/LogContextMenu.kt @@ -1,7 +1,6 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource fun logContextMenu( onCheckoutCommit: () -> Unit, @@ -12,32 +11,38 @@ fun logContextMenu( onResetBranch: () -> Unit, onRebaseInteractive: () -> Unit, ) = listOf( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Checkout commit", + icon = { painterResource("start.svg") }, onClick = onCheckoutCommit ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Create branch", + icon = { painterResource("branch.svg") }, onClick = onCreateNewBranch ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Create tag", + icon = { painterResource("tag.svg") }, onClick = onCreateNewTag ), - ContextMenuItem( + ContextMenuElement.ContextSeparator, + ContextMenuElement.ContextTextEntry( label = "Rebase interactive", onClick = onRebaseInteractive ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Revert commit", + icon = { painterResource("revert.svg") }, onClick = onRevertCommit ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Cherry-pick commit", onClick = onCherryPickCommit ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Reset current branch to this commit", + icon = { painterResource("undo.svg") }, onClick = onResetBranch ), ) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteBranchesContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteBranchesContextMenu.kt index 44bf769..e6964a9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteBranchesContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteBranchesContextMenu.kt @@ -4,13 +4,15 @@ package com.jetpackduba.gitnuro.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource fun remoteBranchesContextMenu( onDeleteBranch: () -> Unit -): List { - return mutableListOf( - ContextMenuItem( +): List { + return listOf( + ContextMenuElement.ContextTextEntry( label = "Delete remote branch", + icon = { painterResource("delete.svg") }, onClick = onDeleteBranch ), ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteContextMenu.kt index 9f69829..d205e00 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/RemoteContextMenu.kt @@ -1,13 +1,9 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi - -@OptIn(ExperimentalFoundationApi::class) fun remoteContextMenu( onEditRemotes: () -> Unit, -) = listOf( - ContextMenuItem( +): List = listOf( + ContextMenuElement.ContextTextEntry( label = "Edit remotes", onClick = onEditRemotes ), diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StagedEntriesContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StagedEntriesContextMenu.kt deleted file mode 100644 index d082b19..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StagedEntriesContextMenu.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.jetpackduba.gitnuro.ui.context_menu - -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi -import com.jetpackduba.gitnuro.git.workspace.StatusEntry -import com.jetpackduba.gitnuro.git.workspace.StatusType - -@OptIn(ExperimentalFoundationApi::class) -fun stagedEntriesContextMenuItems( - diffEntry: StatusEntry, - onReset: () -> Unit, -): List { - return mutableListOf().apply { - if (diffEntry.statusType != StatusType.ADDED) { - add( - ContextMenuItem( - label = "Reset", - onClick = onReset, - ) - ) - } - } -} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StashesContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StashesContextMenu.kt index c11a1a6..4f2e81a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StashesContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StashesContextMenu.kt @@ -1,25 +1,26 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource -@OptIn(ExperimentalFoundationApi::class) fun stashesContextMenuItems( onApply: () -> Unit, onPop: () -> Unit, onDelete: () -> Unit, -): List { - return mutableListOf( - ContextMenuItem( +): List { + return listOf( + ContextMenuElement.ContextTextEntry( label = "Apply stash", + icon = { painterResource("apply_stash.svg") }, onClick = onApply ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Pop stash", + icon = { painterResource("apply_stash.svg") }, onClick = onPop ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Drop stash", + icon = { painterResource("delete.svg") }, onClick = onDelete ), ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StatusEntriesContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StatusEntriesContextMenu.kt index 41ccb99..0e80535 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StatusEntriesContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/StatusEntriesContextMenu.kt @@ -1,11 +1,9 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource import com.jetpackduba.gitnuro.git.workspace.StatusEntry import com.jetpackduba.gitnuro.git.workspace.StatusType -@OptIn(ExperimentalFoundationApi::class) fun statusEntriesContextMenuItems( statusEntry: StatusEntry, entryType: EntryType, @@ -13,26 +11,28 @@ fun statusEntriesContextMenuItems( onDelete: () -> Unit = {}, onBlame: () -> Unit, onHistory: () -> Unit, -): List { - return mutableListOf().apply { +): List { + return mutableListOf().apply { if (statusEntry.statusType != StatusType.ADDED) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Reset", + icon = { painterResource("undo.svg") }, onClick = onReset, ) ) if (statusEntry.statusType != StatusType.REMOVED) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Blame file", + icon = { painterResource("blame.svg") }, onClick = onBlame, ) ) add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "File history", onClick = onHistory, ) @@ -45,8 +45,9 @@ fun statusEntriesContextMenuItems( statusEntry.statusType != StatusType.REMOVED ) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Delete file", + icon = { painterResource("delete.svg") }, onClick = onDelete, ) ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt index c9abef4..330dbf8 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/SubmoduleContextMenu.kt @@ -1,19 +1,16 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem -import androidx.compose.foundation.ExperimentalFoundationApi import org.eclipse.jgit.submodule.SubmoduleStatus import org.eclipse.jgit.submodule.SubmoduleStatusType -@OptIn(ExperimentalFoundationApi::class) fun submoduleContextMenuItems( submoduleStatus: SubmoduleStatus, onInitializeModule: () -> Unit, -): List { - return mutableListOf().apply { +): List { + return mutableListOf().apply { if (submoduleStatus.type == SubmoduleStatusType.UNINITIALIZED) { add( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Initialize submodule", onClick = onInitializeModule ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/TagContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/TagContextMenu.kt index 34ff54d..aef5441 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/TagContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/TagContextMenu.kt @@ -1,20 +1,21 @@ package com.jetpackduba.gitnuro.ui.context_menu -import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.res.painterResource -@OptIn(ExperimentalFoundationApi::class) fun tagContextMenuItems( onCheckoutTag: () -> Unit, onDeleteTag: () -> Unit, -): List { +): List { return mutableListOf( - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Checkout tag", + icon = { painterResource("start.svg") }, onClick = onCheckoutTag ), - ContextMenuItem( + ContextMenuElement.ContextTextEntry( label = "Delete tag", + icon = { painterResource("delete.svg") }, onClick = onDeleteTag ) ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt index 53feec9..bf29ece 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt @@ -38,19 +38,14 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.jetpackduba.gitnuro.app.extensions.* import com.jetpackduba.gitnuro.git.workspace.StatusSummary import com.jetpackduba.gitnuro.git.graph.GraphCommitList import com.jetpackduba.gitnuro.git.graph.GraphNode import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding -import com.jetpackduba.gitnuro.app.theme.* import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.components.AvatarImage import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn -import com.jetpackduba.gitnuro.ui.context_menu.branchContextMenuItems -import com.jetpackduba.gitnuro.ui.context_menu.logContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.tagContextMenuItems import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog import com.jetpackduba.gitnuro.ui.dialogs.ResetBranchDialog @@ -59,6 +54,7 @@ import com.jetpackduba.gitnuro.viewmodels.LogStatus import com.jetpackduba.gitnuro.viewmodels.LogViewModel import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.theme.* +import com.jetpackduba.gitnuro.ui.context_menu.* import kotlinx.coroutines.launch import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.RepositoryState @@ -750,7 +746,7 @@ fun CommitLine( onRevCommitSelected: () -> Unit, onRebaseInteractive: () -> Unit, ) { - ContextMenuArea( + ContextMenu( items = { logContextMenu( onCheckoutCommit = { logViewModel.checkoutCommit(graphNode) }, @@ -1122,7 +1118,7 @@ fun TagChip( ) } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable fun RefChip( modifier: Modifier = Modifier, @@ -1130,7 +1126,7 @@ fun RefChip( icon: String, color: Color, onCheckoutRef: () -> Unit, - contextMenuItemsList: () -> List, + contextMenuItemsList: () -> List, endingContent: @Composable () -> Unit = {}, ) { Box( @@ -1141,7 +1137,7 @@ fun RefChip( .combinedClickable(onDoubleClick = onCheckoutRef, onClick = {}) .pointerHoverIcon(PointerIconDefaults.Hand) ) { - ContextMenuArea( + ContextMenu( items = contextMenuItemsList ) { Row( diff --git a/src/main/resources/blame.svg b/src/main/resources/blame.svg new file mode 100644 index 0000000..543143c --- /dev/null +++ b/src/main/resources/blame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/branch.svg b/src/main/resources/branch.svg index ef262d5..8dc060b 100644 --- a/src/main/resources/branch.svg +++ b/src/main/resources/branch.svg @@ -1,4 +1,7 @@ - - - - \ No newline at end of file + + + + + + + diff --git a/src/main/resources/cloud.svg b/src/main/resources/cloud.svg index bd9e5d2..e51022c 100644 --- a/src/main/resources/cloud.svg +++ b/src/main/resources/cloud.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/resources/delete.svg b/src/main/resources/delete.svg new file mode 100644 index 0000000..69a6835 --- /dev/null +++ b/src/main/resources/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/revert.svg b/src/main/resources/revert.svg new file mode 100644 index 0000000..432583a --- /dev/null +++ b/src/main/resources/revert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/start.svg b/src/main/resources/start.svg new file mode 100644 index 0000000..57cfaf3 --- /dev/null +++ b/src/main/resources/start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/tag.svg b/src/main/resources/tag.svg index 8c69c4b..67e58c6 100644 --- a/src/main/resources/tag.svg +++ b/src/main/resources/tag.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/resources/topic.svg b/src/main/resources/topic.svg index 160eae0..20de9ac 100644 --- a/src/main/resources/topic.svg +++ b/src/main/resources/topic.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/resources/undo.svg b/src/main/resources/undo.svg new file mode 100644 index 0000000..5375947 --- /dev/null +++ b/src/main/resources/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file