diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt index 684a8d13..4274db1c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt @@ -17,6 +17,7 @@ import com.jetpackduba.gitnuro.ui.VerticalSplitPaneConfig import com.jetpackduba.gitnuro.updates.UpdatesRepository import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel import dagger.Component +import org.jetbrains.skiko.ClipboardManager import javax.inject.Singleton @Singleton @@ -50,4 +51,6 @@ interface AppComponent { fun updatesRepository(): UpdatesRepository fun credentialsCacheRepository(): CredentialsCacheRepository + + fun clipboardManager(): ClipboardManager } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/AppModule.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/AppModule.kt index 47facdfc..2252ab7f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/AppModule.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/AppModule.kt @@ -6,10 +6,14 @@ import dagger.Provides import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import org.jetbrains.skiko.ClipboardManager @Module class AppModule { @Provides @AppCoroutineScope fun provideAppScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @Provides + fun provideClipboardManager(): ClipboardManager = ClipboardManager() } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt index ca6090d4..b5dfcaa3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt @@ -240,7 +240,9 @@ fun LazyListScope.localBranches( onMergeBranch = { branchesViewModel.mergeBranch(branch) }, onRebaseBranch = { branchesViewModel.rebaseBranch(branch) }, onDeleteBranch = { branchesViewModel.deleteBranch(branch) }, - ) { onChangeDefaultUpstreamBranch(branch) } + onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(branch) }, + onCopyBranchNameToClipboard = { branchesViewModel.copyBranchNameToClipboard(branch) }, + ) } } } @@ -288,7 +290,7 @@ fun LazyListScope.remotes( onEditRemote = { val wrapper = remote.remoteInfo.remoteConfig.toRemoteWrapper() onShowAddEditRemoteDialog(wrapper) - }, + }, onDeleteRemote = { remotesViewModel.deleteRemote(remote.remoteInfo.remoteConfig.name) }, onRemoteClicked = { remotesViewModel.onRemoteClicked(remote) }, ) @@ -306,6 +308,7 @@ fun LazyListScope.remotes( onPullRemoteBranch = { remotesViewModel.pullFromRemoteBranch(remoteBranch) }, onRebaseRemoteBranch = { remotesViewModel.rebaseBranch(remoteBranch) }, onMergeRemoteBranch = { remotesViewModel.mergeBranch(remoteBranch) }, + onCopyBranchNameToClipboard = { remotesViewModel.copyBranchNameToClipboard(remoteBranch) } ) } } @@ -453,6 +456,7 @@ private fun Branch( onRebaseBranch: () -> Unit, onDeleteBranch: () -> Unit, onChangeDefaultUpstreamBranch: () -> Unit, + onCopyBranchNameToClipboard: () -> Unit, ) { val isCurrentBranch = currentBranch?.name == branch.name @@ -469,7 +473,8 @@ private fun Branch( onRebaseBranch = onRebaseBranch, onPushToRemoteBranch = {}, onPullFromRemoteBranch = {}, - onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch + onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch, + onCopyBranchNameToClipboard = onCopyBranchNameToClipboard ) } ) { @@ -531,6 +536,7 @@ private fun RemoteBranches( onPullRemoteBranch: () -> Unit, onRebaseRemoteBranch: () -> Unit, onMergeRemoteBranch: () -> Unit, + onCopyBranchNameToClipboard: () -> Unit ) { ContextMenu( items = { @@ -547,6 +553,7 @@ private fun RemoteBranches( onPushToRemoteBranch = onPushRemoteBranch, onPullFromRemoteBranch = onPullRemoteBranch, onChangeDefaultUpstreamBranch = {}, + onCopyBranchNameToClipboard = onCopyBranchNameToClipboard ) } ) { 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 96384dab..6375f3b2 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 @@ -4,7 +4,11 @@ import androidx.compose.ui.res.painterResource import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.extensions.isHead import com.jetpackduba.gitnuro.extensions.simpleLogName +import com.jetpackduba.gitnuro.extensions.simpleName +import com.jetpackduba.gitnuro.models.Notification +import com.jetpackduba.gitnuro.models.positiveNotification import org.eclipse.jgit.lib.Ref +import org.jetbrains.skiko.ClipboardManager fun branchContextMenuItems( branch: Ref, @@ -19,7 +23,9 @@ fun branchContextMenuItems( onPushToRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit, onChangeDefaultUpstreamBranch: () -> Unit, + onCopyBranchNameToClipboard: () -> Unit ): List { + return mutableListOf().apply { if (!isCurrentBranch) { add( @@ -90,8 +96,23 @@ fun branchContextMenuItems( ) } - if (lastOrNull() == ContextMenuElement.ContextSeparator) { - removeLast() - } + add( + ContextMenuElement.ContextTextEntry( + label = "Copy branch name", + icon = { painterResource(AppIcons.COPY) }, + onClick = { + onCopyBranchNameToClipboard() + } + ) + ) } +} + +internal fun copyBranchNameToClipboardAndGetNotification( + branch: Ref, + clipboardManager: ClipboardManager +): Notification { + val branchName = branch.simpleName + clipboardManager.setText(branchName) + return positiveNotification("\"${branchName}\" copied to clipboard") } \ No newline at end of file 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 276eb0e9..669cf75b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt @@ -287,6 +287,7 @@ private fun LogLoaded( onDeleteTag = { logViewModel.deleteTag(it) }, onPushToRemoteBranch = { logViewModel.pushToRemoteBranch(it) }, onPullFromRemoteBranch = { logViewModel.pullFromRemoteBranch(it) }, + onCopyBranchNameToClipboard = { ref -> logViewModel.copyBranchNameToClipboard(ref) } ) val density = LocalDensity.current.density @@ -505,6 +506,7 @@ fun CommitsList( onPushToRemoteBranch: (Ref) -> Unit, onPullFromRemoteBranch: (Ref) -> Unit, onShowLogDialog: (LogDialog) -> Unit, + onCopyBranchNameToClipboard: (Ref) -> Unit, graphWidth: Dp, horizontalScrollState: ScrollState, ) { @@ -584,6 +586,7 @@ fun CommitsList( onCherryPickCommit = { onCherryPickCommit(graphNode) }, onCheckoutRemoteBranch = onCheckoutRemoteBranch, onCheckoutRef = onCheckoutRef, + onCopyBranchNameToClipboard = onCopyBranchNameToClipboard, ) } @@ -824,6 +827,7 @@ private fun CommitLine( onCheckoutRemoteBranch: (Ref) -> Unit, onCheckoutRef: (Ref) -> Unit, onChangeDefaultUpstreamBranch: (Ref) -> Unit, + onCopyBranchNameToClipboard: (Ref) -> Unit, horizontalScrollState: ScrollState, ) { val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name @@ -918,6 +922,7 @@ private fun CommitLine( onPushRemoteBranch = { ref -> onPushToRemoteBranch(ref) }, onPullRemoteBranch = { ref -> onPullFromRemoteBranch(ref) }, onChangeDefaultUpstreamBranch = { ref -> onChangeDefaultUpstreamBranch(ref) }, + onCopyBranchNameToClipboard = { ref -> onCopyBranchNameToClipboard(ref) }, ) } } @@ -940,6 +945,7 @@ fun CommitMessage( onPushRemoteBranch: (ref: Ref) -> Unit, onPullRemoteBranch: (ref: Ref) -> Unit, onChangeDefaultUpstreamBranch: (ref: Ref) -> Unit, + onCopyBranchNameToClipboard: (ref: Ref) -> Unit, ) { Row( modifier = Modifier.fillMaxSize() @@ -981,6 +987,7 @@ fun CommitMessage( onPullRemoteBranch = { onPullRemoteBranch(ref) }, onPushRemoteBranch = { onPushRemoteBranch(ref) }, onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(ref) }, + onCopyBranchNameToClipboard = { onCopyBranchNameToClipboard(ref) }, ) } } @@ -1203,7 +1210,6 @@ fun UncommittedChangesGraphNode( } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun BranchChip( modifier: Modifier = Modifier, @@ -1218,6 +1224,7 @@ fun BranchChip( onPushRemoteBranch: () -> Unit, onPullRemoteBranch: () -> Unit, onChangeDefaultUpstreamBranch: () -> Unit, + onCopyBranchNameToClipboard: () -> Unit, color: Color, ) { val contextMenuItemsList = { @@ -1234,6 +1241,7 @@ fun BranchChip( onPushToRemoteBranch = onPushRemoteBranch, onPullFromRemoteBranch = onPullRemoteBranch, onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch, + onCopyBranchNameToClipboard = onCopyBranchNameToClipboard, ) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt index d25a5aed..768776b4 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/LogViewModel.kt @@ -22,6 +22,7 @@ import com.jetpackduba.gitnuro.git.workspace.StatusSummary import com.jetpackduba.gitnuro.models.positiveNotification import com.jetpackduba.gitnuro.repositories.AppSettingsRepository import com.jetpackduba.gitnuro.ui.SelectedItem +import com.jetpackduba.gitnuro.ui.context_menu.copyBranchNameToClipboardAndGetNotification import com.jetpackduba.gitnuro.ui.log.LogDialog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -30,6 +31,7 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.errors.CheckoutConflictException import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.revwalk.RevCommit +import org.jetbrains.skiko.ClipboardManager import javax.inject.Inject /** @@ -63,6 +65,7 @@ class LogViewModel @Inject constructor( private val tabState: TabState, private val appSettingsRepository: AppSettingsRepository, private val tabScope: CoroutineScope, + private val clipboardManager: ClipboardManager, sharedStashViewModel: SharedStashViewModel, sharedBranchesViewModel: SharedBranchesViewModel, sharedRemotesViewModel: SharedRemotesViewModel, @@ -136,7 +139,6 @@ class LogViewModel @Inject constructor( } } - private suspend fun loadLog(git: Git) = delayedStateChange( delayMs = LOG_MIN_TIME_IN_MS_TO_SHOW_LOAD, onDelayTriggered = { @@ -418,6 +420,16 @@ class LogViewModel @Inject constructor( _logStatus.value = LogStatus.Loaded(hasUncommittedChanges, log, currentBranch, statusSummary) } + + override fun copyBranchNameToClipboard(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.NONE, + taskType = TaskType.UNSPECIFIED + ) { + copyBranchNameToClipboardAndGetNotification( + branch, + clipboardManager + ) + } } sealed interface LogStatus { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedBranchesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedBranchesViewModel.kt index e618effc..457e5140 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedBranchesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedBranchesViewModel.kt @@ -11,8 +11,13 @@ import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase import com.jetpackduba.gitnuro.models.positiveNotification import com.jetpackduba.gitnuro.models.warningNotification import com.jetpackduba.gitnuro.repositories.AppSettingsRepository +import com.jetpackduba.gitnuro.ui.context_menu.copyBranchNameToClipboardAndGetNotification +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.eclipse.jgit.lib.Ref +import org.jetbrains.skiko.ClipboardManager import javax.inject.Inject interface ISharedBranchesViewModel { @@ -20,6 +25,7 @@ interface ISharedBranchesViewModel { fun deleteBranch(branch: Ref): Job fun checkoutRef(ref: Ref): Job fun rebaseBranch(ref: Ref): Job + fun copyBranchNameToClipboard(branch: Ref): Job } class SharedBranchesViewModel @Inject constructor( @@ -29,6 +35,7 @@ class SharedBranchesViewModel @Inject constructor( private val mergeBranchUseCase: MergeBranchUseCase, private val deleteBranchUseCase: DeleteBranchUseCase, private val checkoutRefUseCase: CheckoutRefUseCase, + private val clipboardManager: ClipboardManager ) : ISharedBranchesViewModel { override fun mergeBranch(ref: Ref) = tabState.safeProcessing( @@ -80,4 +87,14 @@ class SharedBranchesViewModel @Inject constructor( positiveNotification("\"${ref.simpleName}\" rebased") } } + + override fun copyBranchNameToClipboard(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.NONE, + taskType = TaskType.UNSPECIFIED + ) { + copyBranchNameToClipboardAndGetNotification( + branch, + clipboardManager + ) + } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedRemotesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedRemotesViewModel.kt index f5242390..7d6957cd 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedRemotesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SharedRemotesViewModel.kt @@ -10,8 +10,10 @@ import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCa import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase import com.jetpackduba.gitnuro.models.positiveNotification import com.jetpackduba.gitnuro.models.warningNotification +import com.jetpackduba.gitnuro.ui.context_menu.copyBranchNameToClipboardAndGetNotification import kotlinx.coroutines.Job import org.eclipse.jgit.lib.Ref +import org.jetbrains.skiko.ClipboardManager import javax.inject.Inject interface ISharedRemotesViewModel { @@ -19,6 +21,7 @@ interface ISharedRemotesViewModel { fun checkoutRemoteBranch(remoteBranch: Ref): Job fun pushToRemoteBranch(branch: Ref): Job fun pullFromRemoteBranch(branch: Ref): Job + fun copyBranchNameToClipboard(branch: Ref): Job } class SharedRemotesViewModel @Inject constructor( @@ -27,7 +30,9 @@ class SharedRemotesViewModel @Inject constructor( private val checkoutRefUseCase: CheckoutRefUseCase, private val pushToSpecificBranchUseCase: PushToSpecificBranchUseCase, private val pullFromSpecificBranchUseCase: PullFromSpecificBranchUseCase, + private val clipboardManager: ClipboardManager, ) : ISharedRemotesViewModel { + override fun deleteRemoteBranch(ref: Ref) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, title = "Deleting remote branch", @@ -76,4 +81,14 @@ class SharedRemotesViewModel @Inject constructor( positiveNotification("Pulled from \"${branch.simpleName}\"") } } + + override fun copyBranchNameToClipboard(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.NONE, + taskType = TaskType.UNSPECIFIED + ) { + copyBranchNameToClipboardAndGetNotification( + branch, + clipboardManager + ) + } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt index 31fef0cc..330a436b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt @@ -12,6 +12,7 @@ import com.jetpackduba.gitnuro.git.branches.GetRemoteBranchesUseCase import com.jetpackduba.gitnuro.git.remotes.* import com.jetpackduba.gitnuro.models.RemoteWrapper import com.jetpackduba.gitnuro.models.positiveNotification +import com.jetpackduba.gitnuro.ui.context_menu.copyBranchNameToClipboardAndGetNotification import com.jetpackduba.gitnuro.viewmodels.ISharedBranchesViewModel import com.jetpackduba.gitnuro.viewmodels.ISharedRemotesViewModel import com.jetpackduba.gitnuro.viewmodels.SharedBranchesViewModel @@ -26,6 +27,7 @@ import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.RemoteSetUrlCommand import org.eclipse.jgit.lib.Ref +import org.jetbrains.skiko.ClipboardManager class RemotesViewModel @AssistedInject constructor( private val tabState: TabState, @@ -37,6 +39,7 @@ class RemotesViewModel @AssistedInject constructor( private val updateRemoteUseCase: UpdateRemoteUseCase, private val deleteLocallyRemoteBranchesUseCase: DeleteLocallyRemoteBranchesUseCase, private val sharedBranchesViewModel: SharedBranchesViewModel, + private val clipboardManager: ClipboardManager, tabScope: CoroutineScope, sharedRemotesViewModel: SharedRemotesViewModel, @Assisted @@ -185,6 +188,16 @@ class RemotesViewModel @AssistedInject constructor( _remoteUpdated.emit(Unit) } + + override fun copyBranchNameToClipboard(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.NONE, + taskType = TaskType.UNSPECIFIED + ) { + copyBranchNameToClipboardAndGetNotification( + branch, + clipboardManager + ) + } } data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean)