Implemented custom UI design for compose's context menus
This commit is contained in:
parent
343da198b9
commit
29c04dbad3
@ -2,44 +2,60 @@
|
||||
|
||||
package com.jetpackduba.gitnuro
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.LocalTextContextMenu
|
||||
import androidx.compose.foundation.text.TextContextMenu
|
||||
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.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.input.InputMode
|
||||
import androidx.compose.ui.input.InputModeManager
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.nativeKeyCode
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalInputModeManager
|
||||
import androidx.compose.ui.platform.LocalLocalization
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import androidx.compose.ui.window.*
|
||||
import com.jetpackduba.gitnuro.di.DaggerAppComponent
|
||||
import com.jetpackduba.gitnuro.extensions.preferenceValue
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import com.jetpackduba.gitnuro.extensions.toWindowPlacement
|
||||
import com.jetpackduba.gitnuro.git.AppGpgSigner
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import com.jetpackduba.gitnuro.theme.AppTheme
|
||||
import com.jetpackduba.gitnuro.theme.Theme
|
||||
import com.jetpackduba.gitnuro.theme.isDark
|
||||
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
||||
import com.jetpackduba.gitnuro.ui.AppTab
|
||||
import com.jetpackduba.gitnuro.ui.TabsManager
|
||||
import com.jetpackduba.gitnuro.ui.components.RepositoriesTabPanel
|
||||
import com.jetpackduba.gitnuro.ui.components.TabInformation
|
||||
import com.jetpackduba.gitnuro.ui.components.emptyTabInformation
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.AppPopupMenu
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.Separator
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.TextEntry
|
||||
import org.eclipse.jgit.lib.GpgSigner
|
||||
import java.awt.event.KeyEvent
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
@ -70,6 +86,7 @@ class App {
|
||||
appComponent.inject(this)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun start(args: Array<String>) {
|
||||
tabsManager.appComponent = this.appComponent
|
||||
val windowPlacement = appSettings.windowPlacement.toWindowPlacement
|
||||
@ -117,12 +134,15 @@ class App {
|
||||
state = windowState,
|
||||
icon = painterResource(AppIcons.LOGO),
|
||||
) {
|
||||
val density = if (scale != -1f) {
|
||||
arrayOf(LocalDensity provides Density(scale, 1f))
|
||||
} else
|
||||
emptyArray()
|
||||
val compositionValues: MutableList<ProvidedValue<*>> = mutableListOf(LocalTextContextMenu provides AppPopupMenu())
|
||||
|
||||
CompositionLocalProvider(values = density) {
|
||||
if (scale != -1f) {
|
||||
compositionValues.add(LocalDensity provides Density(scale, 1f))
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
values = compositionValues.toTypedArray()
|
||||
) {
|
||||
AppTheme(
|
||||
selectedTheme = theme,
|
||||
customTheme = customTheme,
|
||||
@ -228,10 +248,9 @@ private fun TabContent(currentTab: TabInformation?) {
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (currentTab != null) {
|
||||
val density = arrayOf(LocalTabScope provides currentTab)
|
||||
val tabScope = arrayOf(LocalTabScope provides currentTab)
|
||||
|
||||
|
||||
CompositionLocalProvider(values = density) {
|
||||
CompositionLocalProvider(values = tabScope) {
|
||||
AppTab(currentTab.tabViewModel)
|
||||
}
|
||||
}
|
||||
@ -251,11 +270,3 @@ fun LoadingRepository(repoPath: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AboutIcon : Painter() {
|
||||
override val intrinsicSize = Size(256f, 256f)
|
||||
|
||||
override fun DrawScope.onDraw() {
|
||||
drawOval(Color(0xFFFFA500))
|
||||
}
|
||||
}
|
@ -14,6 +14,8 @@ object AppIcons {
|
||||
const val CLOUD = "cloud.svg"
|
||||
const val CODE = "code.svg"
|
||||
const val COMPUTER = "computer.svg"
|
||||
const val COPY = "copy.svg"
|
||||
const val CUT = "cut.svg"
|
||||
const val DELETE = "delete.svg"
|
||||
const val DOWNLOAD = "download.svg"
|
||||
const val DROPDOWN = "dropdown.svg"
|
||||
@ -31,6 +33,7 @@ object AppIcons {
|
||||
const val MESSAGE = "message.svg"
|
||||
const val MORE_VERT = "more_vert.svg"
|
||||
const val OPEN = "open.svg"
|
||||
const val PASTE = "paste.svg"
|
||||
const val PERSON = "person.svg"
|
||||
const val REFRESH = "refresh.svg"
|
||||
const val REMOVE = "remove.svg"
|
||||
|
@ -1,6 +1,113 @@
|
||||
package com.jetpackduba.gitnuro
|
||||
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.text.JPopupTextMenu
|
||||
import androidx.compose.foundation.text.LocalTextContextMenu
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.awt.ComposePanel
|
||||
import androidx.compose.ui.platform.LocalLocalization
|
||||
import java.awt.Color
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Graphics
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.KeyEvent.CTRL_DOWN_MASK
|
||||
import java.awt.event.KeyEvent.META_DOWN_MASK
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.KeyStroke.getKeyStroke
|
||||
import javax.swing.SwingUtilities
|
||||
import org.jetbrains.skiko.hostOs
|
||||
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val app = App()
|
||||
app.start(args)
|
||||
}
|
||||
}
|
||||
|
||||
//fun main() = SwingUtilities.invokeLater {
|
||||
// val panel = ComposePanel()
|
||||
// panel.setContent {
|
||||
// JPopupTextMenuProvider(panel) {
|
||||
// Column {
|
||||
// SelectionContainer {
|
||||
// Text("Hello, Compose!")
|
||||
// }
|
||||
//
|
||||
// var text by remember { mutableStateOf("") }
|
||||
//
|
||||
// TextField(text, { text = it })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val window = JFrame()
|
||||
// window.contentPane.add(panel)
|
||||
// window.size = Dimension(800, 600)
|
||||
// window.isVisible = true
|
||||
//}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun JPopupTextMenuProvider(owner: Component, content: @Composable () -> Unit) {
|
||||
val localization = LocalLocalization.current
|
||||
CompositionLocalProvider(
|
||||
LocalTextContextMenu provides JPopupTextMenu(owner) { textManager, items ->
|
||||
JPopupMenu().apply {
|
||||
textManager.cut?.also {
|
||||
add(
|
||||
swingItem(localization.cut,KeyEvent.VK_X, it)
|
||||
)
|
||||
}
|
||||
textManager.copy?.also {
|
||||
add(
|
||||
swingItem(localization.copy, KeyEvent.VK_C, it)
|
||||
)
|
||||
}
|
||||
textManager.paste?.also {
|
||||
add(
|
||||
swingItem(localization.paste, KeyEvent.VK_V, it)
|
||||
)
|
||||
}
|
||||
textManager.selectAll?.also {
|
||||
add(JPopupMenu.Separator())
|
||||
add(
|
||||
swingItem(localization.selectAll, KeyEvent.VK_A, it)
|
||||
)
|
||||
}
|
||||
|
||||
// Here we add other items that can be defined additionaly in the other places of the application via ContextMenuDataProvider
|
||||
for (item in items) {
|
||||
add(
|
||||
JMenuItem(item.label).apply {
|
||||
addActionListener { item.onClick() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
private fun swingItem(
|
||||
label: String,
|
||||
key: Int,
|
||||
onClick: () -> Unit
|
||||
) = JMenuItem(label).apply {
|
||||
accelerator = getKeyStroke(key, if (hostOs.isMacOS) META_DOWN_MASK else CTRL_DOWN_MASK)
|
||||
addActionListener { onClick() }
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ fun Tooltip(text: String?, content: @Composable () -> Unit) {
|
||||
tooltip = {
|
||||
if (text != null) {
|
||||
val border = if (MaterialTheme.colors.isDark) {
|
||||
BorderStroke(2.dp, MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.4f))
|
||||
BorderStroke(2.dp, MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.2f))
|
||||
} else
|
||||
null
|
||||
|
||||
|
@ -2,6 +2,8 @@ package com.jetpackduba.gitnuro.ui.context_menu
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.TextContextMenu
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
@ -10,27 +12,41 @@ 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.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.InputMode
|
||||
import androidx.compose.ui.input.InputModeManager
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalInputModeManager
|
||||
import androidx.compose.ui.platform.LocalLocalization
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.extensions.awaitFirstDownEvent
|
||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
|
||||
import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
||||
import com.jetpackduba.gitnuro.theme.isDark
|
||||
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import kotlin.math.abs
|
||||
|
||||
private var lastCheck: Long = 0
|
||||
private const val MIN_TIME_BETWEEN_POPUPS_IN_MS = 20
|
||||
private const val BORDER_RADIUS = 4
|
||||
|
||||
@Composable
|
||||
fun ContextMenu(items: () -> List<ContextMenuElement>, function: @Composable () -> Unit) {
|
||||
@ -155,12 +171,14 @@ fun showPopup(x: Int, y: Int, contextMenuElements: List<ContextMenuElement>, onD
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.shadow(4.dp)
|
||||
.width(300.dp)
|
||||
.shadow(8.dp)
|
||||
.clip(RoundedCornerShape(BORDER_RADIUS.dp))
|
||||
.background(MaterialTheme.colors.background)
|
||||
.width(IntrinsicSize.Max)
|
||||
.widthIn(min = 180.dp)
|
||||
.run {
|
||||
return@run if (!MaterialTheme.colors.isLight) {
|
||||
this.border(1.dp, MaterialTheme.colors.onBackground.copy(alpha = 0.2f))
|
||||
if (MaterialTheme.colors.isDark) {
|
||||
this.border(2.dp, MaterialTheme.colors.onBackground.copy(alpha = 0.2f), shape = RoundedCornerShape(BORDER_RADIUS.dp))
|
||||
} else
|
||||
this
|
||||
}
|
||||
@ -236,12 +254,149 @@ fun TextEntry(contextTextEntry: ContextMenuElement.ContextTextEntry, onDismissRe
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ContextMenuElement {
|
||||
data class ContextTextEntry(
|
||||
val label: String,
|
||||
sealed class ContextMenuElement(
|
||||
label: String,
|
||||
onClick: () -> Unit = {}
|
||||
) : ContextMenuItem(label, onClick) {
|
||||
class ContextTextEntry(
|
||||
label: String,
|
||||
val icon: @Composable (() -> Painter)? = null,
|
||||
val onClick: () -> Unit = {}
|
||||
) : ContextMenuElement
|
||||
onClick: () -> Unit = {}
|
||||
) : ContextMenuElement(label, onClick)
|
||||
|
||||
object ContextSeparator : ContextMenuElement
|
||||
object ContextSeparator : ContextMenuElement("", {})
|
||||
}
|
||||
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
class AppPopupMenu : TextContextMenu {
|
||||
@Composable
|
||||
override fun Area(
|
||||
textManager: TextContextMenu.TextManager,
|
||||
state: ContextMenuState,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val localization = LocalLocalization.current
|
||||
val items = {
|
||||
listOfNotNull(
|
||||
textManager.copy?.let {
|
||||
ContextMenuElement.ContextTextEntry(
|
||||
label = localization.copy,
|
||||
icon = { painterResource(AppIcons.COPY) },
|
||||
onClick = it
|
||||
)
|
||||
},
|
||||
textManager.cut?.let {
|
||||
ContextMenuElement.ContextTextEntry(
|
||||
label = localization.cut,
|
||||
icon = { painterResource(AppIcons.CUT) },
|
||||
onClick = it
|
||||
)
|
||||
},
|
||||
textManager.paste?.let {
|
||||
ContextMenuElement.ContextTextEntry(
|
||||
label = localization.paste,
|
||||
icon = { painterResource(AppIcons.PASTE) },
|
||||
onClick = it
|
||||
)
|
||||
},
|
||||
textManager.selectAll?.let {
|
||||
ContextMenuElement.ContextTextEntry(
|
||||
label = localization.selectAll,
|
||||
icon = null,
|
||||
onClick = it
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContextMenuRepresentation provides AppContextMenuRepresentation()
|
||||
) {
|
||||
ContextMenuArea(items, state, content = content)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class AppContextMenuRepresentation : ContextMenuRepresentation {
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
override fun Representation(state: ContextMenuState, items: () -> List<ContextMenuItem>) {
|
||||
LightDefaultContextMenuRepresentation
|
||||
val status = state.status
|
||||
if (status is ContextMenuState.Status.Open) {
|
||||
var focusManager: FocusManager? by mutableStateOf(null)
|
||||
var inputModeManager: InputModeManager? by mutableStateOf(null)
|
||||
|
||||
Popup(
|
||||
focusable = true,
|
||||
onDismissRequest = { state.status = ContextMenuState.Status.Closed },
|
||||
popupPositionProvider = rememberPopupPositionProviderAtPosition(
|
||||
positionPx = status.rect.center
|
||||
),
|
||||
onKeyEvent = {
|
||||
if (it.type == KeyEventType.KeyDown) {
|
||||
when (it.key.nativeKeyCode) {
|
||||
KeyEvent.VK_ESCAPE -> {
|
||||
state.status = ContextMenuState.Status.Closed
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.VK_DOWN -> {
|
||||
inputModeManager!!.requestInputMode(InputMode.Keyboard)
|
||||
focusManager!!.moveFocus(FocusDirection.Next)
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.VK_UP -> {
|
||||
inputModeManager!!.requestInputMode(InputMode.Keyboard)
|
||||
focusManager!!.moveFocus(FocusDirection.Previous)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
) {
|
||||
focusManager = LocalFocusManager.current
|
||||
inputModeManager = LocalInputModeManager.current
|
||||
|
||||
val border = if (MaterialTheme.colors.isDark) {
|
||||
BorderStroke(2.dp, MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.2f))
|
||||
} else
|
||||
null
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.shadow(8.dp)
|
||||
.clip(RoundedCornerShape(BORDER_RADIUS.dp))
|
||||
.background(MaterialTheme.colors.background)
|
||||
.width(IntrinsicSize.Max)
|
||||
.widthIn(min = 180.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.run {
|
||||
if (border != null)
|
||||
border(border, RoundedCornerShape(BORDER_RADIUS.dp))
|
||||
else
|
||||
this
|
||||
}
|
||||
|
||||
) {
|
||||
items().forEach { item ->
|
||||
when (item) {
|
||||
is ContextMenuElement.ContextTextEntry -> TextEntry(
|
||||
contextTextEntry = item,
|
||||
onDismissRequest = { state.status = ContextMenuState.Status.Closed }
|
||||
)
|
||||
|
||||
is ContextMenuElement.ContextSeparator -> Separator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -515,6 +515,7 @@ fun SplitDiffLine(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SplitDiffLineSide(
|
||||
modifier: Modifier,
|
||||
|
1
src/main/resources/copy.svg
Normal file
1
src/main/resources/copy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
After Width: | Height: | Size: 284 B |
1
src/main/resources/cut.svg
Normal file
1
src/main/resources/cut.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><circle cx="6" cy="18" fill="none" r="2"/><circle cx="12" cy="12" fill="none" r=".5"/><circle cx="6" cy="6" fill="none" r="2"/><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z"/></svg>
|
After Width: | Height: | Size: 694 B |
1
src/main/resources/paste.svg
Normal file
1
src/main/resources/paste.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 2h-4.18C14.4.84 13.3 0 12 0c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z"/></svg>
|
After Width: | Height: | Size: 357 B |
Loading…
Reference in New Issue
Block a user