blob: 4318cafb5f40bc6654c6d5d22787efec804316ef [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2i.nls.editor
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.ex.ActionButtonLook
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.keymap.KeymapUtil.getKeystrokeText
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextArea
import com.intellij.util.ui.JBDimension
import com.intellij.util.ui.PositionTracker
import org.eclipse.scout.sdk.core.s.nls.ITranslationEntry
import org.eclipse.scout.sdk.core.s.nls.TranslationStoreStack
import org.eclipse.scout.sdk.core.s.nls.TranslationValidator.*
import org.eclipse.scout.sdk.core.util.Strings
import org.eclipse.scout.sdk.s2i.EclipseScoutBundle.Companion.message
import org.eclipse.scout.sdk.s2i.nls.editor.NlsTableModel.Companion.KEY_COLUMN_INDEX
import org.eclipse.scout.sdk.s2i.nls.editor.NlsTableModel.Companion.NUM_ADDITIONAL_COLUMNS
import org.eclipse.scout.sdk.s2i.ui.TablePreservingSelection
import org.eclipse.scout.sdk.s2i.ui.TextAreaWithContentSize
import java.awt.*
import java.awt.event.*
import java.util.*
import java.util.function.Predicate
import javax.swing.*
import javax.swing.KeyStroke.getKeyStroke
import javax.swing.event.DocumentEvent
import javax.swing.event.TableModelEvent
import javax.swing.plaf.UIResource
import javax.swing.table.TableCellEditor
import javax.swing.table.TableCellRenderer
import javax.swing.table.TableRowSorter
import javax.swing.text.DefaultEditorKit
class NlsTable(stack: TranslationStoreStack, project: Project) : JBScrollPane() {
private val m_model: NlsTableModel = NlsTableModel(stack, project)
private val m_table: TablePreservingSelection
private val m_tableSorterFilter = TableRowSorter(m_model)
private val m_cellMargin = Insets(1, 4, 2, 2)
private val m_editStartEvent = EventObject(this)
private var m_balloon: Balloon? = null
private var m_balloonContent: JBLabel? = null
companion object {
const val TEXT_COLUMN_WIDTH = 350
const val KEY_COLUMN_WIDTH = 250
}
init {
m_table = TablePreservingSelection(m_model, { index -> m_model.translationForRow(index) }, { row -> m_model.rowForTranslation(row as ITranslationEntry) })
m_table.tableColumnsChangedCallback = { adjustView() }
m_table.tableChangedCallback = { adjustRowHeights(it) }
m_table.fillsViewportHeight = true
m_table.autoResizeMode = JTable.AUTO_RESIZE_OFF
m_table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
m_table.rowSelectionAllowed = true
m_table.columnSelectionAllowed = false
m_table.cellSelectionEnabled = true
m_table.setEnableAntialiasing(true)
m_table.putClientProperty("terminateEditOnFocusLost", true)
m_table.setShowGrid(true)
m_table.gridColor = JBColor.border()
m_table.tableHeader.reorderingAllowed = false
m_table.addKeyListener(TableKeyListener())
m_table.addPropertyChangeListener {
// reset row height on cell editor cancel
if ("tableCellEditor" == it.propertyName && it.newValue == null && it.oldValue != null) {
adjustEditingRowHeight()
hideBalloon()
}
}
m_table.rowSorter = m_tableSorterFilter
m_tableSorterFilter.sortKeys = listOf(RowSorter.SortKey(0, SortOrder.ASCENDING), RowSorter.SortKey(1, SortOrder.ASCENDING))
border = null
setViewportView(m_table)
}
private fun adjustView() {
val columnModel = m_table.columnModel
for (i in 0 until columnModel.columnCount) {
val column = columnModel.getColumn(i)
if (i == KEY_COLUMN_INDEX) {
column.preferredWidth = KEY_COLUMN_WIDTH
} else {
column.preferredWidth = TEXT_COLUMN_WIDTH
}
column.cellRenderer = MultiLineTextCellRenderer()
column.cellEditor = MultiLineTextCellEditor(i != KEY_COLUMN_INDEX)
}
adjustRowHeights()
}
private fun adjustRowHeights(e: TableModelEvent?) {
val event = e ?: return
if (event.type == TableModelEvent.DELETE) {
return
}
val fontHeight = fontHeight()
(event.firstRow..event.lastRow).forEach {
val viewIndex = m_table.convertRowIndexToView(it)
if (viewIndex >= 0) {
adjustRowHeight(viewIndex, fontHeight, null)
}
}
}
private fun adjustRowHeights() {
val fontHeight = fontHeight()
for (i in 0 until m_model.rowCount) {
val viewIndex = m_table.convertRowIndexToView(i)
if (viewIndex >= 0) {
adjustRowHeight(viewIndex, fontHeight)
}
}
}
private fun fontHeight() = getFontMetrics(m_table.font).height
private fun adjustRowHeight(rowIndex: Int, fontHeight: Int, additionalText: String? = null) {
val rowsRequired = maxLinesForRow(rowIndex, additionalText)
val height = (rowsRequired * fontHeight) + 6
m_table.setRowHeight(rowIndex, height)
}
private fun adjustEditingRowHeight(additionalText: String? = null) {
val editingRowIndexView = m_table.editingRow
if (editingRowIndexView >= 0) {
adjustRowHeight(editingRowIndexView, fontHeight(), additionalText)
}
}
private fun maxLinesForRow(rowIndex: Int, additionalText: String? = null): Int {
val entry = m_model.translationForRow(m_table.convertRowIndexToModel(rowIndex))
val add = additionalText ?: ""
return (sequenceOf(add) + entry.texts().values.asSequence())
.map { Strings.countMatches(it, "\n") + 1 }
.max() ?: 1
}
fun selectedLanguages() = m_table.selectedColumns
.filter { it >= NUM_ADDITIONAL_COLUMNS }
.map { m_table.convertColumnIndexToModel(it) }
.map { m_model.languageForColumn(it) }
fun selectedTranslations() = m_table.selectedRows
.map { m_table.convertRowIndexToModel(it) }
.map { m_model.translationForRow(it) }
fun visibleData(): List<List<String>> {
val numAdditionalRows = 1 // header row
val data = ArrayList<List<String>>(m_table.rowCount + numAdditionalRows)
// header
val headerRow = ArrayList<String>(m_table.columnCount)
headerRow.add(NlsTableModel.KEY_COLUMN_HEADER_NAME)
for (lang in m_model.languages()) {
headerRow.add(lang.locale().toString())
}
data.add(headerRow)
// data
for (row in 0 until m_table.rowCount) {
val dataRow = ArrayList<String>(m_table.columnCount)
for (col in 0 until m_table.columnCount) {
dataRow.add(m_table.getValueAt(row, col).toString())
}
data.add(dataRow)
}
return data
}
fun selectTranslation(translation: ITranslationEntry) {
val row = m_table.convertRowIndexToView(m_model.rowForTranslation(translation))
if (row >= 0) {
m_table.setRowSelectionInterval(row, row)
m_table.scrollToSelection()
}
}
fun setFilter(newFilter: Predicate<ITranslationEntry>?) {
val filter = object : RowFilter<NlsTableModel, Int>() {
override fun include(entry: Entry<out NlsTableModel, out Int>): Boolean {
return newFilter?.test(m_model.translationForRow(entry.identifier)) ?: true
}
}
m_tableSorterFilter.rowFilter = filter
val tableStructureChanged = m_model.setFilter(newFilter)
if (!tableStructureChanged) {
adjustView() // only if the structure did not change. Because on structure change it is done anyway
}
m_table.scrollToSelection()
}
private fun showBalloon(message: String, owner: Component, severity: MessageType) {
val existingBalloonContent = m_balloonContent
if (existingBalloonContent != null) {
existingBalloonContent.text = message
m_balloon?.revalidate() // adapt balloon size to new text
return
}
val lbl = JBLabel(message)
lbl.putClientProperty("html.disable", true)
val builder = JBPopupFactory.getInstance().createBalloonBuilder(lbl)
val balloon = builder
.setFillColor(severity.popupBackground)
.setBorderColor(severity.borderColor)
.setHideOnAction(false)
.setHideOnKeyOutside(false)
.setAnimationCycle(Registry.intValue("ide.tooltip.animationCycle"))
.setBlockClicksThroughBalloon(true)
.createBalloon()
balloon.show(object : PositionTracker<Balloon>(owner) {
override fun recalculateLocation(element: Balloon): RelativePoint {
return RelativePoint(owner, Point((owner.size.width * 0.75).toInt(), -4))
}
}, Balloon.Position.above)
balloon.addListener(object : JBPopupListener {
override fun onClosed(event: LightweightWindowEvent) {
hideBalloon()
}
})
m_balloon = balloon
m_balloonContent = lbl
}
private fun hideBalloon() {
m_balloon?.hide()
m_balloon = null
m_balloonContent = null
}
private inner class TableKeyListener : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_F2) {
val editStarted = m_table.editCellAt(m_table.selectedRow, m_table.selectedColumn, m_editStartEvent)
if (editStarted) {
e.consume()
val cellEditor = m_table.cellEditor
if (cellEditor is MultiLineTextCellEditor) {
cellEditor.focus()
}
}
}
}
}
private inner class MultiLineTextCellRenderer : TableCellRenderer {
override fun getTableCellRendererComponent(table: JTable, value: Any?, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
val text = value.toString()
val txt = TextAreaWithContentSize(m_table.font, text)
txt.margin = m_cellMargin
if (isSelected) {
txt.foreground = table.selectionForeground
txt.background = table.selectionBackground
} else {
var background = table.background
if (background == null || background is UIResource) {
val alternateColor = UIManager.getColor("Table.alternateRowColor")
if (alternateColor != null && row % 2 != 0) {
background = alternateColor
}
}
if (m_model.translationForRow(m_table.convertRowIndexToModel(row)).store().isEditable) {
txt.foreground = table.foreground
} else {
txt.foreground = UIManager.getColor("Button.disabledText")
}
txt.background = background
}
return txt
}
}
private inner class NewLineAction(private val txt: JBTextArea) : DumbAwareAction(null, message("insert.new.line.x",
getKeystrokeText(getKeyStroke(KeyEvent.VK_ENTER, InputEvent.ALT_DOWN_MASK))), AllIcons.Actions.SearchNewLine) {
init {
templatePresentation.hoveredIcon = AllIcons.Actions.SearchNewLineHover
}
override fun actionPerformed(e: AnActionEvent) {
DefaultEditorKit.InsertBreakAction().actionPerformed(ActionEvent(txt, 0, "action"))
}
}
private inner class MultiLineTextCellEditor(supportMultiLine: Boolean) : AbstractCellEditor(), TableCellEditor {
private val m_cellContentPanel = JBPanel<JBPanel<*>>()
private val m_txt = TextAreaWithContentSize(m_table.font)
private val m_scrollPane = JBScrollPane(m_txt, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED)
init {
val borderWidth = 1
m_cellContentPanel.border = BorderFactory.createLineBorder(JBColor.border(), borderWidth)
m_cellContentPanel.background = m_txt.background
m_scrollPane.border = null
m_txt.margin = Insets(m_cellMargin.top - borderWidth, m_cellMargin.left - borderWidth, m_cellMargin.bottom, m_cellMargin.right)
m_txt.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
if (supportMultiLine) {
adjustEditingRowHeight(m_txt.text)
}
val editingRowViewIndex = m_table.editingRow
val editingColumnViewIndex = m_table.editingColumn
if (editingRowViewIndex >= 0 && editingColumnViewIndex >= 0) {
validateEdit(m_txt.text, editingRowViewIndex, editingColumnViewIndex)
}
}
})
m_txt.addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (supportMultiLine && e.keyCode == KeyEvent.VK_ENTER && (e.isAltDown || e.isAltGraphDown)) {
m_txt.insert(System.lineSeparator(), m_txt.caretPosition)
e.consume()
} else if (e.keyCode == KeyEvent.VK_ENTER || e.keyCode == KeyEvent.VK_TAB) {
stopCellEditing()
e.consume()
}
}
})
if (!supportMultiLine) {
m_txt.document.putProperty("filterNewlines", true)
}
m_cellContentPanel.layout = GridBagLayout()
m_cellContentPanel.add(m_scrollPane, GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.BOTH, Insets(0, 0, 0, 0), 0, 0))
if (supportMultiLine) {
val newLineHelpButton = createButton(NewLineAction(m_txt))
m_cellContentPanel.add(newLineHelpButton, GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_END,
GridBagConstraints.NONE, Insets(0, 0, 0, 0), 0, 0))
}
}
fun focus() {
m_txt.requestFocus()
}
fun validateEdit(aValue: Any?, rowIndex: Int, columnIndex: Int) {
val table = m_table
val validationResult = m_model.validate(aValue, table.convertRowIndexToModel(rowIndex), table.convertColumnIndexToModel(columnIndex))
showValidationResult(validationResult)
}
private fun showValidationResult(result: Int) {
val severity = if (OK == result) {
MessageType.INFO
} else if (!isForbidden(result)) {
MessageType.WARNING
} else {
MessageType.ERROR
}
val borderColor = if (OK == result) JBColor.border() else severity.popupBackground
val msg = when (result) {
OK -> ""
DEFAULT_TRANSLATION_MISSING_ERROR -> message("default.text.mandatory")
DEFAULT_TRANSLATION_EMPTY_ERROR -> message("default.text.mandatory")
KEY_EMPTY_ERROR -> message("please.specify.key")
KEY_ALREADY_EXISTS_ERROR -> message("key.already.exists")
KEY_OVERRIDES_OTHER_STORE_WARNING -> message("key.would.override")
KEY_IS_OVERRIDDEN_BY_OTHER_STORE_WARNING -> message("key.would.be.overridden")
KEY_OVERRIDES_AND_IS_OVERRIDDEN_WARNING -> message("key.overrides.and.is.overridden")
else -> message("key.contains.invalid.chars")
}
if (Strings.isBlank(msg)) {
hideBalloon()
} else {
showBalloon(msg, m_txt, severity)
}
m_cellContentPanel.border = BorderFactory.createLineBorder(borderColor)
}
override fun getTableCellEditorComponent(table: JTable, value: Any?, isSelected: Boolean, row: Int, column: Int): Component {
m_txt.text = value.toString()
validateEdit(value, row, column)
return m_cellContentPanel
}
private fun createButton(action: AnAction): ActionButton {
val presentation = action.templatePresentation
val d = JBDimension(16, 16)
val button = object : ActionButton(action, presentation, ActionPlaces.UNKNOWN, d) {
override fun getDataContext(): DataContext {
return DataManager.getInstance().getDataContext(this)
}
}
button.setLook(ActionButtonLook.INPLACE_LOOK)
button.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
button.updateIcon()
return button
}
override fun isCellEditable(e: EventObject?): Boolean {
// edit mode only on our own event from the key listener or on double click
return e == m_editStartEvent || (e is MouseEvent && e.clickCount > 1)
}
override fun getCellEditorValue(): Any? = m_txt.text
}
}