diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 7e1e2e2fd..41afd16ea 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -474,6 +474,7 @@ + diff --git a/src/nl/hannahsten/texifyidea/action/wizard/graphic/InsertGraphicWizardDialogWrapper.kt b/src/nl/hannahsten/texifyidea/action/wizard/graphic/InsertGraphicWizardDialogWrapper.kt index 7dd3192ea..894e9811f 100644 --- a/src/nl/hannahsten/texifyidea/action/wizard/graphic/InsertGraphicWizardDialogWrapper.kt +++ b/src/nl/hannahsten/texifyidea/action/wizard/graphic/InsertGraphicWizardDialogWrapper.kt @@ -7,34 +7,23 @@ import com.intellij.openapi.ui.TextBrowseFolderListener import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.ui.TitledSeparator import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextField import com.intellij.ui.components.fields.ExpandableTextField import com.intellij.ui.components.panels.HorizontalLayout import nl.hannahsten.texifyidea.lang.graphic.CaptionLocation import nl.hannahsten.texifyidea.lang.graphic.FigureLocation -import nl.hannahsten.texifyidea.util.Magic -import nl.hannahsten.texifyidea.util.addKeyReleasedListener -import nl.hannahsten.texifyidea.util.addTextChangeListener -import nl.hannahsten.texifyidea.util.setInputFilter -import java.awt.BorderLayout +import nl.hannahsten.texifyidea.util.* import java.awt.Dimension -import javax.swing.* -import javax.swing.border.EmptyBorder +import javax.swing.Box +import javax.swing.BoxLayout +import javax.swing.JPanel +import javax.swing.JTextField /** * @author Hannah Schellekens */ open class InsertGraphicWizardDialogWrapper(val initialFilePath: String = "") : DialogWrapper(true) { - companion object { - - /** - * The amount of pixels between the left pane border and the contorls. - */ - private const val CONTROL_LEFT_PADDING = 16 - } - /** * Stores the path to the graphics file. */ @@ -248,39 +237,6 @@ open class InsertGraphicWizardDialogWrapper(val initialFilePath: String = "") : add(Box.createRigidArea(Dimension(0, margin))) } - /** - * Adds a component to the panel with a label before it. - * - * @param component - * The component to add to the panel. - * @param description - * The label to put before the component. - * @param labelWidth - * The fixed label width, or `null` to use the label's inherent size. - */ - private fun JPanel.addLabeledComponent(component: JComponent, description: String, labelWidth: Int? = null): JPanel { - // Uses a border layout with West for the label and Center for the control itself. - // East is reserved for suffix elements. - val pane = JPanel(BorderLayout()).apply { - val label = JBLabel(description).apply { - // Left padding. - border = EmptyBorder(0, CONTROL_LEFT_PADDING, 0, 0) - - // Custom width if specified. - labelWidth?.let { - preferredSize = Dimension(it, height) - } - - // Align top. - alignmentY = 0.0f - } - add(label, BorderLayout.WEST) - add(component, BorderLayout.CENTER) - } - add(pane) - return pane - } - private fun updateFigureControlsState() { val enabled = checkPlaceInFigure.isSelected diff --git a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/ColumnType.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/ColumnType.kt similarity index 74% rename from src/nl/hannahsten/texifyidea/ui/tablecreationdialog/ColumnType.kt rename to src/nl/hannahsten/texifyidea/action/wizard/table/ColumnType.kt index 367b3f2fb..0abe8179c 100644 --- a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/ColumnType.kt +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/ColumnType.kt @@ -1,4 +1,4 @@ -package nl.hannahsten.texifyidea.ui.tablecreationdialog +package nl.hannahsten.texifyidea.action.wizard.table /** * @author Abby Berkers diff --git a/src/nl/hannahsten/texifyidea/action/wizard/table/LatexTableWizardAction.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/LatexTableWizardAction.kt index ed6d8d6ed..dc401b000 100644 --- a/src/nl/hannahsten/texifyidea/action/wizard/table/LatexTableWizardAction.kt +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/LatexTableWizardAction.kt @@ -4,10 +4,10 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import nl.hannahsten.texifyidea.action.insert.InsertTable import nl.hannahsten.texifyidea.lang.LatexPackage -import nl.hannahsten.texifyidea.ui.tablecreationdialog.ColumnType -import nl.hannahsten.texifyidea.ui.tablecreationdialog.TableCreationDialogWrapper import nl.hannahsten.texifyidea.util.caretOffset import nl.hannahsten.texifyidea.util.currentTextEditor import nl.hannahsten.texifyidea.util.files.psiFile @@ -23,17 +23,16 @@ import java.util.* */ class LatexTableWizardAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return - val project = e.getData(PlatformDataKeys.PROJECT) - val editor = project?.currentTextEditor() ?: return + fun executeAction(file: VirtualFile, project: Project, defaultDialogWrapper: TableCreationDialogWrapper? = null) { + val editor = project.currentTextEditor() ?: return val document = editor.editor.document // Get the indentation from the current line. val indent = document.lineIndentationByOffset(editor.editor.caretOffset()) // Create the dialog. - val dialogWrapper = TableCreationDialogWrapper() + val dialogWrapper = defaultDialogWrapper ?: TableCreationDialogWrapper() + // If the user pressed OK, do stuff. if (dialogWrapper.showAndGet()) { @@ -46,20 +45,28 @@ class LatexTableWizardAction : AnAction() { // Insert the booktabs package. WriteCommandAction.runWriteCommandAction( project, - "Insert table", + "Insert Table", "LaTeX", - Runnable { file.psiFile(project)!!.insertUsepackage(LatexPackage.BOOKTABS) }, + Runnable { file.psiFile(project)?.insertUsepackage(LatexPackage.BOOKTABS) }, file.psiFile(project) ) } } + override fun actionPerformed(e: AnActionEvent) { + val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return + val project = e.getData(PlatformDataKeys.PROJECT) ?: return + executeAction(file, project) + } + /** * Convert the table information to a latex table that can be inserted into the file. * * @param tableInformation - * @param lineIndent is the indentation of the current line, to be used on each new line. - * @param tabIndent is the continuation indent. + * @param lineIndent + * The indentation of the current line, to be used on each new line. + * @param tabIndent + * The continuation indent. */ private fun convertTableToLatex(tableInformation: TableInformation, lineIndent: String, tabIndent: String = " "): String { /** diff --git a/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationDialogWrapper.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationDialogWrapper.kt new file mode 100644 index 000000000..6b28ecbeb --- /dev/null +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationDialogWrapper.kt @@ -0,0 +1,300 @@ +package nl.hannahsten.texifyidea.action.wizard.table + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.KeyboardShortcut +import com.intellij.openapi.actionSystem.ShortcutSet +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.AnActionButton +import com.intellij.ui.LayeredIcon +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.panels.VerticalLayout +import com.intellij.ui.scale.JBUIScale +import com.intellij.ui.table.JBTable +import com.intellij.util.IconUtil +import nl.hannahsten.texifyidea.util.addLabeledComponent +import java.awt.* +import java.awt.event.ActionEvent +import java.awt.event.KeyEvent +import javax.swing.* + +/** + * Wrapper that contains the table creation dialog. + * + * @author Abby Berkers + */ +open class TableCreationDialogWrapper( + initialColumnTypes: List? = null, + initialTableModel: TableCreationTableModel? = null +) : DialogWrapper(true) { + + /** + * Types of the columns of the table, see [ColumnType], always start with an empty table. + */ + private val columnTypes = initialColumnTypes?.toMutableList() ?: mutableListOf() + + /** + * The model of the table, always start with an empty table. + */ + private val tableModel = initialTableModel ?: TableCreationTableModel() + + /** + * The table component that shows the table. + */ + private val table = JBTable(tableModel).apply { + addTabCreatesNewRowAction() + addEnterCreatesNewRowAction() + } + + /** + * The text field that contains the caption for the table. + */ + private val txtCaption = JBTextField() + + /** + * The text field that contains the label for the table. It has a default value "tab:" to encourage usage + * of label conventions. + */ + private val txtReference = JBTextField("tab:") + + /** + * Information about the table that is needed to convert it to latex. + */ + var tableInformation = TableInformation(tableModel, columnTypes, "", "") + private set + + init { + // Initialise the dialog, otherwise it shows as a line (i.e., infinitely small) and without any of the elements. + init() + title = "Insert table" + } + + /** + * Add a table column. + * + * @param title of the column. + * @param columnType is the column type of the column. + */ + private fun addTableColumn(title: String, columnType: ColumnType) { + // Add the column to the table, with an empty cell for each row (instead of the default null). + tableModel.addColumn(title, (0 until tableModel.rowCount).map { "" }.toTypedArray()) + + // Add the column type to the list of column types. + columnTypes.add(columnType) + + // If table is currently empty, add one row to this new column. + if (tableModel.columnCount == 1) { + tableModel.addRow(arrayOf("")) + } + } + + /** + * Edit the table column, i.e., udpate the header title and the column type. + * + * @param title is the new title of the header. + * @param columnType is the index of the column type. + * @param columnIndex is the index of the edited column in the table, starting at 0. + */ + private fun editTableColumn(title: String, columnType: ColumnType, columnIndex: Int) { + tableModel.setHeaderName(title, columnIndex) + + // Edit the column type of the edited column. + columnTypes[columnIndex] = columnType + + tableModel.fireTableStructureChanged() + } + + override fun createCenterPanel(): JPanel = JPanel(BorderLayout(8, 8)).apply { + // Put help text below table + add(createTablePanelContainer(), BorderLayout.CENTER) + + // Create labels. + add(JPanel(VerticalLayout(8)).apply { + addLabeledComponent(txtCaption, "Caption:", labelWidth = 64, leftPadding = 0) + addLabeledComponent(txtReference, "Label:", labelWidth = 64, leftPadding = 0) + }, BorderLayout.SOUTH) + } + + /** + * Generates table and the toolbaar buttons. + */ + private fun createToolbarDecorator() = ToolbarDecorator.createDecorator(table) + .setAddAction { + TableCreationEditColumnDialog( + { title, columnType, _ -> addTableColumn(title, columnType) }, + tableModel.columnCount + ) + } + .setAddActionName("Add Column") + .setAddIcon(addText(IconUtil.getAddIcon(), "C")) + .addExtraAction(getRemoveColumnActionButton()) + .addExtraAction(getEditColumnActionButton()) + .addExtraAction(getAddRowActionButton()) + .addExtraAction(getRemoveRowActionButton().apply { + shortcut = ShortcutSet { + arrayOf(KeyboardShortcut(KeyStroke.getKeyStroke("DELETE"), null)) + } + }) + .createPanel() + + /** + * Panel containing the table and its controls. + */ + private fun createTablePanel() = JPanel(BorderLayout()).apply { + add(createToolbarDecorator(), BorderLayout.CENTER) + } + + /** + * Creates a hint label describing the table controls. + */ + private fun createHelpText() = JBLabel().apply { + text = "Press tab to go to the next cell or row, press enter to go to the next row." + foreground = Color.GRAY + } + + /** + * The panel containing everything related to the table. + */ + private fun createTablePanelContainer() = JPanel(BorderLayout()).apply { + minimumSize = Dimension(480, 320) + + add(createTablePanel(), BorderLayout.CENTER) + add(createHelpText(), BorderLayout.SOUTH) + } + + /** + * See [IconUtil.addText]. + */ + fun addText(base: Icon, text: String, scale: Float = 7f): Icon = LayeredIcon(2).apply { + setIcon(base, 0, SwingConstants.NORTH_WEST) + setIcon(IconUtil.textToIcon(text, JLabel(), JBUIScale.scale(scale)), 1, SwingConstants.SOUTH_EAST) + } + + /** + * Sets the action when pressing TAB on the last cell in the last row to create a new (empty) row and set the + * selection on the first cell of the new row. + */ + private fun JTable.addTabCreatesNewRowAction() { + // Get the key stroke for pressing TAB. + val keyStroke = KeyStroke.getKeyStroke("TAB") + val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStroke) + + // Get the action that currently is under the TAB key. + val action = actionMap[actionKey] + val actionWrapper = object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + val table = this@addTabCreatesNewRowAction + + // When we're in the last column of the last row, add a new row before calling the usual action. + if (table.selectionModel.leadSelectionIndex == table.rowCount - 1 && + table.columnModel.selectionModel.leadSelectionIndex == table.columnCount - 1 + ) { + tableModel.addEmptyRow() + } + + // Perform the usual action. + action.actionPerformed(e) + } + } + + // Map the new action to the TAB key. + actionMap.put(actionKey, actionWrapper) + } + + /** + * Sets the action when pressing ENTER to create a new (empty) row and set the + * selection on the first cell of the new row. + * + * See [addTabCreatesNewRowAction] + */ + private fun JTable.addEnterCreatesNewRowAction() { + val keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0) + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, "enter") + + val actionWrapper = object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + tableModel.addEmptyRow() + + val keyStrokeTab = KeyStroke.getKeyStroke("TAB") + val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStrokeTab) + // Get the action to go to the next cell + val nextCellAction = actionMap[actionKey] + + // Skip the rest of the cells in the row + val table = this@addEnterCreatesNewRowAction + for (i in table.columnModel.selectionModel.leadSelectionIndex until table.columnCount) { + nextCellAction.actionPerformed(e) + } + } + } + + actionMap.put("enter", actionWrapper) + } + + /** + * Saves all data in [tableInformation]. + * Every input is always valid: this will mean empty icons. + */ + override fun doValidate(): ValidationInfo? { + tableInformation = TableInformation( + tableModel, + columnTypes, + txtCaption.text.trim(), + txtReference.text.trim() + ) + return null + } + + private fun getEditColumnActionButton(): AnActionButton { + return object : AnActionButton("Edit column header", addText(IconUtil.getEditIcon(), "C")) { + + override fun isEnabled() = table.columnCount > 0 + + override fun actionPerformed(e: AnActionEvent) { + if (table.selectedColumn >= 0) { + TableCreationEditColumnDialog( + { title, columnType, columnIndex -> editTableColumn(title, columnType, columnIndex) }, + table.selectedColumn, + table.getColumnName(table.selectedColumn), + columnTypes[table.selectedColumn] + ) + } + } + } + } + + private fun getAddRowActionButton(): AnActionButton { + return object : AnActionButton("Add Row", addText(IconUtil.getAddIcon(), "R")) { + + override fun isEnabled() = table.columnCount > 0 + + override fun actionPerformed(e: AnActionEvent) { + tableModel.addEmptyRow() + } + } + } + + private fun getRemoveRowActionButton(): AnActionButton { + return object : AnActionButton("Remove Row", addText(IconUtil.getRemoveIcon(), "R")) { + + override fun isEnabled() = table.selectedRow > -1 + + override fun actionPerformed(e: AnActionEvent) { + tableModel.removeRow(table.selectedRow) + } + } + } + + private fun getRemoveColumnActionButton(): AnActionButton { + return object : AnActionButton("Remove Column", addText(IconUtil.getRemoveIcon(), "C")) { + + override fun isEnabled() = table.selectedColumn > -1 + + override fun actionPerformed(e: AnActionEvent) { + tableModel.removeColumn(table.selectedColumn) + } + } + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationEditColumnDialog.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationEditColumnDialog.kt new file mode 100644 index 000000000..3ad3029a4 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationEditColumnDialog.kt @@ -0,0 +1,74 @@ +package nl.hannahsten.texifyidea.action.wizard.table + +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.panels.VerticalLayout +import nl.hannahsten.texifyidea.util.addLabeledComponent +import javax.swing.JComboBox +import javax.swing.JPanel + +/** + * Dialog to add a new column to the table. + * + * @author Abby Berkers + */ +class TableCreationEditColumnDialog( + + /** + * The function to execute when clicking the OK button. + */ + private val onOkFunction: (title: String, columnType: ColumnType, columnIndex: Int) -> Unit, + + /** + * The index of the column being edited. + */ + private val editingColumn: Int, + + /** + * The name of the column that is being edited. Default is the empty string, the title of a column that does + * not yet exist. + */ + private val columnName: String = "", + + /** + * The [ColumnType] of the column that is being edited. Default is a text column. + */ + private val columnType: ColumnType = ColumnType.TEXT_COLUMN +) { + + init { + DialogBuilder().apply { + // Text field for the name of the column, with the old name of the editing column filled in. + val txtColumnName = JBTextField(columnName) + + // A combobox to select the column type. + val cboxColumnType = JComboBox(ColumnType.values().map { it.displayName }.toTypedArray()) + cboxColumnType.selectedIndex = ColumnType.values().indexOf(columnType) + + // Add UI elements. + val panel = JPanel(VerticalLayout(8)).apply { + addLabeledComponent(txtColumnName, "Column title:", labelWidth = 96, leftPadding = 0) + addLabeledComponent(cboxColumnType, "Column type:", labelWidth = 96, leftPadding = 0) + } + setCenterPanel(panel) + setPreferredFocusComponent(txtColumnName) + + addOkAction() + setOkOperation { + dialogWrapper.close(0) + } + + if (columnName.isBlank()) { + title("Add column") + } + else { + title("Edit column") + } + + if (show() == DialogWrapper.OK_EXIT_CODE) { + onOkFunction(txtColumnName.text, ColumnType.values()[cboxColumnType.selectedIndex], editingColumn) + } + } + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationTableModel.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationTableModel.kt similarity index 74% rename from src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationTableModel.kt rename to src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationTableModel.kt index 5fbe5ac37..d932dd7f2 100644 --- a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationTableModel.kt +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/TableCreationTableModel.kt @@ -1,4 +1,4 @@ -package nl.hannahsten.texifyidea.ui.tablecreationdialog +package nl.hannahsten.texifyidea.action.wizard.table import java.util.* import javax.swing.table.DefaultTableModel @@ -6,7 +6,11 @@ import javax.swing.table.DefaultTableModel /** * @author Abby Berkers */ -class TableCreationTableModel : DefaultTableModel() { +class TableCreationTableModel : DefaultTableModel { + + constructor(data: Vector>, columnNames: Vector) : super(data, columnNames) + + constructor() : this(Vector(), Vector()) /** * Remove a column and its header from the table. @@ -35,7 +39,7 @@ class TableCreationTableModel : DefaultTableModel() { * Adds an empty row to the table. */ fun addEmptyRow() { - val emptyRow = (0 until columnCount).map { "" }.toTypedArray() + val emptyRow = Array(columnCount) { "" } addRow(emptyRow) } diff --git a/src/nl/hannahsten/texifyidea/action/wizard/table/TableInformation.kt b/src/nl/hannahsten/texifyidea/action/wizard/table/TableInformation.kt index 14ca6da1d..4e69efb35 100644 --- a/src/nl/hannahsten/texifyidea/action/wizard/table/TableInformation.kt +++ b/src/nl/hannahsten/texifyidea/action/wizard/table/TableInformation.kt @@ -1,22 +1,30 @@ package nl.hannahsten.texifyidea.action.wizard.table -import nl.hannahsten.texifyidea.ui.tablecreationdialog.ColumnType -import nl.hannahsten.texifyidea.ui.tablecreationdialog.TableCreationTableModel - /** * Stores all information about a table. * - * @param tableModel contains all information about the contents of the table. That is, the column names and the table - * entries. - * @param columnTypes contains the type of each column. - * @param caption contains the caption to go along with the table. - * @param label contains the label that is to be used to reference to the table. - * * @author Abby Berkers */ data class TableInformation( + + /** + * Contains all information about the contents of the table. + * That is, the column names and the table entries. + */ val tableModel: TableCreationTableModel, + + /** + * Contains the type of each column. + */ val columnTypes: List, + + /** + * Contains the caption to go along with the table. + */ val caption: String, + + /** + * Contains the label that is to be used to reference the table. + */ val label: String ) \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/editor/TablePasteProvider.kt b/src/nl/hannahsten/texifyidea/editor/TablePasteProvider.kt new file mode 100644 index 000000000..7cc19d24f --- /dev/null +++ b/src/nl/hannahsten/texifyidea/editor/TablePasteProvider.kt @@ -0,0 +1,120 @@ +package nl.hannahsten.texifyidea.editor + +import com.intellij.ide.PasteProvider +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.editor.actions.PasteAction +import nl.hannahsten.texifyidea.action.wizard.table.ColumnType +import nl.hannahsten.texifyidea.action.wizard.table.LatexTableWizardAction +import nl.hannahsten.texifyidea.action.wizard.table.TableCreationDialogWrapper +import nl.hannahsten.texifyidea.action.wizard.table.TableCreationTableModel +import nl.hannahsten.texifyidea.file.LatexFileType +import nl.hannahsten.texifyidea.util.Clipboard +import nl.hannahsten.texifyidea.util.toVector +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.awt.datatransfer.DataFlavor +import java.util.* + +/** + * Pastes the table html into a Insert Table Wizard if applicable. + * + * @author Hannah Schellekens + */ +open class TablePasteProvider : PasteProvider { + + override fun performPaste(dataContext: DataContext) { + val file = dataContext.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return + val project = dataContext.getData(PlatformDataKeys.PROJECT) ?: return + val clipboardHtml = dataContext.transferableHtml() ?: return + val html = Clipboard.extractHtmlFromClipboard(clipboardHtml) ?: return + val tableDialog = Jsoup.parse(html).toTableDialogWrapper() ?: return + + LatexTableWizardAction().executeAction(file, project, tableDialog) + } + + override fun isPastePossible(dataContext: DataContext): Boolean { + val file = dataContext.getData(PlatformDataKeys.PSI_FILE) ?: return false + if (file.fileType != LatexFileType) return false + if (ShiftTracker.isShiftPressed()) return false + + val pasteData = dataContext.transferableHtml() ?: return false + return pasteData.contains("> and headers. + val header = rows.first().select("td, th").map { it.text() }.toVector() + val content: Vector> = rows.drop(1).map { tr -> + tr.select("td, th").map { td -> td.handleHtmlFormatting() as Any? }.toVector() + }.toVector() + + // Find the type of column automatically. + val contentRows = rows.drop(1) + val columnTypes = (0 until width).map { col -> + // Check if all contents of the column (except the header) can be converted to a number. + // When that's the case => it's a number column. All other cases, text. Ignoring the Math option + // as the table information is most probably something outside of a latex context. + if (contentRows.all { it.select("td, th").getOrNull(col)?.text()?.toDoubleOrNull() != null }) { + ColumnType.NUMBERS_COLUMN + } + else ColumnType.TEXT_COLUMN + } + + return TableCreationDialogWrapper( + columnTypes, + TableCreationTableModel(content, header) + ) + } + + /** + * Converts // tags to latex formatting commands. + * `this` is a HTML Element. + */ + private fun Element.handleHtmlFormatting(): String { + val prefix = StringBuilder() + val suffix = StringBuilder() + + if (select("b, strong").isNotEmpty()) { + prefix.append("\\textbf{") + suffix.append("}") + } + if (select("i, em").isNotEmpty()) { + prefix.append("\\textit{") + suffix.append("}") + } + if (select("u").isNotEmpty()) { + prefix.append("\\underline{") + suffix.append("}") + } + + return prefix + .append(text()) + .append(suffix.toString()) + .toString() + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationDialogWrapper.kt b/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationDialogWrapper.kt deleted file mode 100644 index 070c12c0d..000000000 --- a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationDialogWrapper.kt +++ /dev/null @@ -1,296 +0,0 @@ -package nl.hannahsten.texifyidea.ui.tablecreationdialog - -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.KeyboardShortcut -import com.intellij.openapi.actionSystem.ShortcutSet -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.AnActionButton -import com.intellij.ui.LayeredIcon -import com.intellij.ui.ToolbarDecorator -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField -import com.intellij.ui.scale.JBUIScale.scale -import com.intellij.ui.table.JBTable -import com.intellij.util.IconUtil -import nl.hannahsten.texifyidea.action.wizard.table.TableInformation -import java.awt.* -import java.awt.event.ActionEvent -import java.awt.event.KeyEvent -import javax.swing.* -import javax.swing.border.EmptyBorder - -/** - * Wrapper that contains the table creation dialog. It validates the form when clicking the OK button. - * - * @param columnTypes The types of the columns of the table, see [ColumnType], always start with an empty table. - * @param tableModel The model of the table, always start with an empty table. - * @param tableInformation Information about the table that is needed to convert it to latex. - * - * UI components that have to be validated when clicking the OK button, i.e., checking if the user entered something. - * @param table The JTable component that shows the table. - * @param caption The text field that contains the caption for the table. - * @param reference The text field that contains the label for the table. It has a default value "tab:" to encourage usage - * of label conventions. - * - * @author Abby Berkers - */ -class TableCreationDialogWrapper( - private val columnTypes: MutableList = emptyList().toMutableList(), - private val tableModel: TableCreationTableModel = TableCreationTableModel(), - var tableInformation: TableInformation = TableInformation(tableModel, columnTypes, "", ""), - // Components that have to be validated when clicking the OK button. - private val table: JTable = JBTable(tableModel), - private val caption: JBTextField = JBTextField(), - private val reference: JBTextField = JBTextField("tab:") -) : - DialogWrapper(true) { - - init { - // Initialise the dialog, otherwise it shows as a line (i.e., infinitely small) and without any of the elements. - init() - title = "Insert table" - } - - /** - * Add a table column. - * - * @param title of the column. - * @param columnType is the column type of the column. - */ - @Suppress("KDocUnresolvedReference") - private val addColumnFun = fun(title: String, columnType: ColumnType, _: Int) { - // Add the column to the table, with an empty cell for each row (instead of the default null). - tableModel.addColumn(title, (0 until tableModel.rowCount).map { "" }.toTypedArray()) - // Add the column type to the list of column types. - columnTypes.add(columnType) - // If table is currently empty, add one row to this new column. - if (tableModel.columnCount == 1) tableModel.addRow(arrayOf("")) - } - - /** - * Edit the table column, i.e., udpate the header title and the column type. - * - * @param title is the new title of the header. - * @param columnType is the index of the column type. - * @param columnIndex is the index of the edited column in the table, starting at 0. - */ - @Suppress("KDocUnresolvedReference", "KDocUnresolvedReference") - private val editColumnFun = fun(title: String, columnType: ColumnType, columnIndex: Int) { - tableModel.setHeaderName(title, columnIndex) - // Edit the column type of the edited column. - columnTypes[columnIndex] = columnType - tableModel.fireTableStructureChanged() - } - - private fun getEditColumnActionButton(): AnActionButton = object : AnActionButton("Edit column header", addText(IconUtil.getEditIcon(), "C")) { - - override fun isEnabled() = table.columnCount > 0 - - override fun actionPerformed(e: AnActionEvent) { - if (table.selectedColumn >= 0) { - TableCreationEditColumnDialog( - editColumnFun, - table.selectedColumn, - table.getColumnName(table.selectedColumn), - columnTypes[table.selectedColumn] - ) - } - } - } - - private fun getAddRowActionButton() = object : AnActionButton("Add Row", addText(IconUtil.getAddIcon(), "R")) { - - override fun isEnabled() = table.columnCount > 0 - - override fun actionPerformed(e: AnActionEvent) { - tableModel.addEmptyRow() - } - } - - private fun getRemoveRowActionButton() = object : AnActionButton("Remove Row", addText(IconUtil.getRemoveIcon(), "R")) { - - override fun isEnabled() = table.selectedRow > -1 - - override fun actionPerformed(e: AnActionEvent) { - tableModel.removeRow(table.selectedRow) - } - } - - private fun getRemoveColumnActionButton() = object : AnActionButton("Remove Column", addText(IconUtil.getRemoveIcon(), "C")) { - - override fun isEnabled() = table.selectedColumn > -1 - - override fun actionPerformed(e: AnActionEvent) { - tableModel.removeColumn(table.selectedColumn) - } - } - - override fun createCenterPanel(): JPanel { - - // Decorator that contains the add/remove/edit buttons. - val decorator = ToolbarDecorator.createDecorator(table) - .setAddAction { - TableCreationEditColumnDialog(addColumnFun, tableModel.columnCount) - } - .setAddActionName("Add Column") - .setAddIcon(addText(IconUtil.getAddIcon(), "C")) - .addExtraAction(getRemoveColumnActionButton()) - .addExtraAction(getEditColumnActionButton()) - .addExtraAction(getAddRowActionButton()) - .addExtraAction(getRemoveRowActionButton().apply { shortcut = ShortcutSet { arrayOf(KeyboardShortcut(KeyStroke.getKeyStroke("DELETE"), null)) } }) - .createPanel() - - table.addTabCreatesNewRowAction() - table.addEnterCreatesNewRowAction() - - val captionLabel = JBLabel("Caption:") - captionLabel.labelFor = caption - - val referenceLabel = JBLabel("Label:") - referenceLabel.labelFor = reference - - // Add all elements to the panel view. - val panel = JPanel() - panel.apply { - // Add some air around the elements. - border = EmptyBorder(8, 8, 8, 8) - layout = BoxLayout(this, BoxLayout.Y_AXIS) - - // Create a panel for the table and its decorator. - val tablePanel = JPanel() - tablePanel.apply { - layout = BorderLayout() - add(JScrollPane(table), BorderLayout.WEST) - add(decorator, BorderLayout.EAST) - } - - // Help text - val helpText = JBLabel("Press tab to go to the next cell or row, press enter to go to the next row.") - helpText.foreground = Color.GRAY - - // Put help text below table - val tablePanelContainer = JPanel(GridBagLayout()) - val constraints = GridBagConstraints() - constraints.gridx = 0 - constraints.gridy = GridBagConstraints.RELATIVE - tablePanelContainer.apply { - add(tablePanel, constraints) - add(helpText, constraints) - } - add(tablePanelContainer) - - // Create a panel for the caption box and its label. - val captionPanel = JPanel() - captionPanel.apply { - layout = BoxLayout(this, BoxLayout.X_AXIS) - captionLabel.preferredSize = Dimension(80, captionLabel.height) - add(captionLabel) - add(caption) - } - - // Create a panel for the label/reference box and its label. - val referencePanel = JPanel() - referencePanel.apply { - layout = BoxLayout(this, BoxLayout.X_AXIS) - referenceLabel.preferredSize = Dimension(80, referenceLabel.height) - add(referenceLabel) - add(reference) - } - - // Actually add all the panels to the main panel. - // Add some air between components. - add(Box.createRigidArea(Dimension(0, 8))) - add(captionPanel) - add(Box.createRigidArea(Dimension(0, 8))) - add(referencePanel) - } - - return panel - } - - /** - * See [IconUtil.addText]. - */ - fun addText(base: Icon, text: String, scale: Float = 7f): Icon? { - val icon = LayeredIcon(2) - icon.setIcon(base, 0, SwingConstants.NORTH_WEST) - icon.setIcon(IconUtil.textToIcon(text, JLabel(), scale(scale)), 1, SwingConstants.SOUTH_EAST) - return icon - } - - /** - * When clicking OK, the wrapper will validate the form. This means that the table should at least have a header, - * there is some text in the caption text field, and the label text field contains more than just "tab:" (or no - * "tab:" at all, but then it should not be empty). - */ - override fun doValidate(): ValidationInfo? { - return if (tableModel.getColumnNames().size == 0) ValidationInfo("Table cannot be empty.", table) - else if (caption.text.isEmpty()) ValidationInfo("Caption cannot be empty.", caption) - else if (reference.text.isEmpty() || reference.text == "tab:") ValidationInfo("Label cannot be empty", reference) - else { - // 'Save' the current values in the form. - tableInformation = TableInformation(tableModel, columnTypes, caption.text, reference.text) - return null - } - } - - /** - * Sets the action when pressing TAB on the last cell in the last row to create a new (empty) row and set the - * selection on the first cell of the new row. - */ - private fun JTable.addTabCreatesNewRowAction() { - // Get the key stroke for pressing TAB. - val keyStroke = KeyStroke.getKeyStroke("TAB") - val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStroke) - // Get the action that currently is under the TAB key. - val action = actionMap[actionKey] - - val actionWrapper = object : AbstractAction() { - override fun actionPerformed(e: ActionEvent?) { - val table = this@addTabCreatesNewRowAction - // When we're in the last column of the last row, add a new row before calling the usual action. - if (table.selectionModel.leadSelectionIndex == table.rowCount - 1 && - table.columnModel.selectionModel.leadSelectionIndex == table.columnCount - 1 - ) { - tableModel.addEmptyRow() - } - // Perform the usual action. - action.actionPerformed(e) - } - } - - // Map the new action to the TAB key. - actionMap.put(actionKey, actionWrapper) - } - - /** - * Sets the action when pressing ENTER to create a new (empty) row and set the - * selection on the first cell of the new row. - * - * See [addTabCreatesNewRowAction] - */ - private fun JTable.addEnterCreatesNewRowAction() { - val keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0) - getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, "enter") - - val actionWrapper = object : AbstractAction() { - override fun actionPerformed(e: ActionEvent?) { - tableModel.addEmptyRow() - - val keyStrokeTab = KeyStroke.getKeyStroke("TAB") - val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStrokeTab) - // Get the action to go to the next cell - val nextCellAction = actionMap[actionKey] - - // Skip the rest of the cells in the row - val table = this@addEnterCreatesNewRowAction - for (i in table.columnModel.selectionModel.leadSelectionIndex until table.columnCount) { - nextCellAction.actionPerformed(e) - } - } - } - - actionMap.put("enter", actionWrapper) - } -} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationEditColumnDialog.kt b/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationEditColumnDialog.kt deleted file mode 100644 index a818fa509..000000000 --- a/src/nl/hannahsten/texifyidea/ui/tablecreationdialog/TableCreationEditColumnDialog.kt +++ /dev/null @@ -1,69 +0,0 @@ -package nl.hannahsten.texifyidea.ui.tablecreationdialog - -import com.intellij.openapi.ui.DialogBuilder -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField -import javax.swing.JComboBox -import javax.swing.JPanel - -/** - * Dialog to add a new column to the table. - * - * @param onOkFunction The function to execute when clicking the OK button. - * @param editingColumn The index of the column being edited. - * @param columnName The name of the column that is being edited. Default is the empty string, the title of a column that does - * not yet exist. - * @param columnType The [ColumnType] of the column that is being edited. Default is a text column. - * - * @author Abby Berkers - */ -class TableCreationEditColumnDialog( - private val onOkFunction: (String, ColumnType, Int) -> Unit, - private val editingColumn: Int, - private val columnName: String = "", - private val columnType: ColumnType = ColumnType.TEXT_COLUMN -) { - - init { - DialogBuilder().apply { - // Text field for the name of the column, with the old name of the editing column filled in. - val columnNameField = JBTextField(columnName) - val columnNameLabel = JBLabel("Column name") - columnNameLabel.labelFor = columnNameField - - // A combobox to select the column type. - val columnTypeComboBox = JComboBox(ColumnType.values().map { it.displayName }.toTypedArray()) - // Select the old type of the editing column. - columnTypeComboBox.selectedIndex = ColumnType.values().indexOf(columnType) - val columnTypeLabel = JBLabel("Column type") - columnTypeLabel.labelFor = columnTypeComboBox - - // Add UI elements. - val panel = JPanel().apply { - add(columnNameLabel) - add(columnNameField) - add(columnTypeLabel) - add(columnTypeComboBox) - } - setCenterPanel(panel) - setPreferredFocusComponent(columnNameField) - - addOkAction() - setOkOperation { - dialogWrapper.close(0) - } - - if (columnName.isBlank()) { - title("Add column") - } - else { - title("Edit column") - } - - if (show() == DialogWrapper.OK_EXIT_CODE) { - onOkFunction(columnNameField.text, ColumnType.values()[columnTypeComboBox.selectedIndex], editingColumn) - } - } - } -} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/Clipboard.kt b/src/nl/hannahsten/texifyidea/util/Clipboard.kt new file mode 100644 index 000000000..05c8cc1cc --- /dev/null +++ b/src/nl/hannahsten/texifyidea/util/Clipboard.kt @@ -0,0 +1,20 @@ +package nl.hannahsten.texifyidea.util + +/** + * @author Hannah Schellekens + */ +object Clipboard { + + /** + * Takes the complete clipboard contents (must have an html data flavor) and extracts the html (thus dropping + * the header). + * + * @return null when it could not find html + */ + @JvmStatic + fun extractHtmlFromClipboard(clipboardContents: String): String? { + return clipboardContents.indexOf("= 0 } + ?.let { clipboardContents.substring(it) } + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/Collections.kt b/src/nl/hannahsten/texifyidea/util/Collections.kt index 3f080ce27..60d7ecc58 100644 --- a/src/nl/hannahsten/texifyidea/util/Collections.kt +++ b/src/nl/hannahsten/texifyidea/util/Collections.kt @@ -147,4 +147,9 @@ fun Stream.set(): Set = this.mutableSet() /** * Collects stream to [MutableSet] */ -fun Stream.mutableSet(): MutableSet = this.collect(Collectors.toSet()) \ No newline at end of file +fun Stream.mutableSet(): MutableSet = this.collect(Collectors.toSet()) + +/** + * Converts the collection to a vector. + */ +fun Collection.toVector() = Vector(this) \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/UserInterface.kt b/src/nl/hannahsten/texifyidea/util/UserInterface.kt index 54bfb1764..1db531e70 100644 --- a/src/nl/hannahsten/texifyidea/util/UserInterface.kt +++ b/src/nl/hannahsten/texifyidea/util/UserInterface.kt @@ -6,9 +6,15 @@ import com.intellij.openapi.ui.popup.Balloon import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.wm.WindowManager import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import java.awt.BorderLayout +import java.awt.Dimension import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.KeyListener +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.border.EmptyBorder import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener import javax.swing.text.JTextComponent @@ -130,4 +136,42 @@ fun JTextComponent.setInputFilter(allowedCharacters: Set) = addKeyTypedLis if (it.keyChar !in allowedCharacters) { it.consume() } +} + +/** + * Adds a component to the panel with a label before it. + * + * @param component + * The component to add to the panel. + * @param description + * The label to put before the component. + * @param labelWidth + * The fixed label width, or `null` to use the label's inherent size. + */ +fun JPanel.addLabeledComponent( + component: JComponent, + description: String, + labelWidth: Int? = null, + leftPadding: Int = 16 +): JPanel { + // Uses a border layout with West for the label and Center for the control itself. + // East is reserved for suffix elements. + val pane = JPanel(BorderLayout()).apply { + val label = JBLabel(description).apply { + // Left padding. + border = EmptyBorder(0, leftPadding, 0, 0) + + // Custom width if specified. + labelWidth?.let { + preferredSize = Dimension(it, height) + } + + // Align top. + alignmentY = 0.0f + } + add(label, BorderLayout.WEST) + add(component, BorderLayout.CENTER) + } + add(pane) + return pane } \ No newline at end of file