src/Tools/jEdit/src/keymap_merge.scala
author wenzelm
Mon Jan 09 20:26:59 2017 +0100 (2017-01-09)
changeset 64854 f5aa712e6250
parent 63772 4ad836f0b146
child 66923 914935f8a462
permissions -rw-r--r--
tuned signature;
     1 /*  Title:      Tools/jEdit/src/keymap_merge.scala
     2     Author:     Makarius
     3 
     4 Merge of Isabelle shortcuts vs. jEdit keymap.
     5 */
     6 
     7 package isabelle.jedit
     8 
     9 
    10 import isabelle._
    11 
    12 import java.lang.{Class, Boolean => JBoolean}
    13 import java.awt.{Color, Dimension, BorderLayout}
    14 import javax.swing.{JPanel, JTable, JScrollPane, JOptionPane}
    15 import javax.swing.table.AbstractTableModel
    16 
    17 import scala.collection.JavaConversions
    18 
    19 import org.gjt.sp.jedit.{jEdit, View, GUIUtilities}
    20 import org.jedit.keymap.{KeymapManager, Keymap}
    21 
    22 
    23 object Keymap_Merge
    24 {
    25   /** shortcuts **/
    26 
    27   private def is_shortcut(property: String): Boolean =
    28     (property.endsWith(".shortcut") || property.endsWith(".shortcut2")) &&
    29     !property.startsWith("options.shortcuts.")
    30 
    31   class Shortcut(val property: String, val binding: String)
    32   {
    33     override def toString: String = property + "=" + binding
    34 
    35     def primary: Boolean = property.endsWith(".shortcut")
    36 
    37     val action: String =
    38       Library.try_unsuffix(".shortcut", property) orElse
    39       Library.try_unsuffix(".shortcut2", property) getOrElse
    40       error("Bad shortcut property: " + quote(property))
    41 
    42     val label: String =
    43       GUIUtilities.prettifyMenuLabel(jEdit.getProperty(action + ".label", ""))
    44 
    45 
    46     /* ignore wrt. keymap */
    47 
    48     private def prop_ignore: String = property + ".ignore"
    49 
    50     def ignored_keymaps(): List[String] =
    51       Library.space_explode(',', jEdit.getProperty(prop_ignore, ""))
    52 
    53     def is_ignored(keymap_name: String): Boolean =
    54       ignored_keymaps().contains(keymap_name)
    55 
    56     def ignore(keymap_name: String): Unit =
    57       jEdit.setProperty(prop_ignore,
    58         Library.insert(keymap_name)(ignored_keymaps()).sorted.mkString(","))
    59 
    60     def set(keymap: Keymap): Unit = keymap.setShortcut(property, binding)
    61     def reset(keymap: Keymap): Unit = keymap.setShortcut(property, null)
    62   }
    63 
    64 
    65   /* content wrt. keymap */
    66 
    67   def convert_properties(props: java.util.Properties): List[Shortcut] =
    68     if (props == null) Nil
    69     else {
    70       var result = List.empty[Shortcut]
    71       for (entry <- JavaConversions.mapAsScalaMap(props)) {
    72         entry match {
    73           case (a: String, b: String) if is_shortcut(a) =>
    74             result ::= new Shortcut(a, b)
    75           case _ =>
    76         }
    77       }
    78       result.sortBy(_.property)
    79     }
    80 
    81   def get_shortcut_conflicts(keymap_name: String, keymap: Keymap): List[(Shortcut, List[Shortcut])] =
    82   {
    83     val keymap_shortcuts =
    84       if (keymap == null) Nil
    85       else convert_properties(Untyped.get[java.util.Properties](keymap, "props"))
    86 
    87     for (s <- convert_properties(jEdit.getProperties) if !s.is_ignored(keymap_name)) yield {
    88       val conflicts =
    89         keymap_shortcuts.filter(s1 =>
    90           s.property == s1.property && s.binding != s1.binding ||
    91           s.property != s1.property && s.binding == s1.binding && s1.binding != "")
    92       (s, conflicts)
    93     }
    94   }
    95 
    96 
    97 
    98   /** table **/
    99 
   100   private def conflict_color: Color =
   101     PIDE.options.color_value("error_color")
   102 
   103   private sealed case class Table_Entry(shortcut: Shortcut, head: Option[Int], tail: List[Int])
   104   {
   105     override def toString: String =
   106       if (head.isEmpty) "<html>" + HTML.output(shortcut.toString) + "</html>"
   107       else
   108         "<html><font style='color: #" + Color_Value.print(conflict_color) + ";'>" +
   109           HTML.output("--- " + shortcut.toString) +
   110         "</font></html>"
   111   }
   112 
   113   private class Table_Model(entries: List[Table_Entry]) extends AbstractTableModel
   114   {
   115     private val entries_count = entries.length
   116     private def has_entry(row: Int): Boolean = 0 <= row && row <= entries_count
   117     private def get_entry(row: Int): Option[Table_Entry] =
   118       if (has_entry(row)) Some(entries(row)) else None
   119 
   120     private val selected =
   121       Synchronized[Set[Int]](
   122         (for ((e, i) <- entries.iterator.zipWithIndex if e.head.isEmpty) yield i).toSet)
   123 
   124     private def is_selected(row: Int): Boolean =
   125       selected.value.contains(row)
   126 
   127     private def select(head: Int, tail: List[Int], b: Boolean): Unit =
   128       selected.change(set => if (b) set + head -- tail else set - head ++ tail)
   129 
   130     def apply(keymap_name: String, keymap: Keymap)
   131     {
   132       GUI_Thread.require {}
   133 
   134       for ((entry, row) <- entries.iterator.zipWithIndex if entry.head.isEmpty) {
   135         val b = is_selected(row)
   136         if (b) {
   137           entry.tail.foreach(i => entries(i).shortcut.reset(keymap))
   138           entry.shortcut.set(keymap)
   139         }
   140         else
   141           entry.shortcut.ignore(keymap_name)
   142       }
   143     }
   144 
   145     override def getColumnCount: Int = 2
   146 
   147     override def getColumnClass(i: Int): Class[_ <: Object] =
   148       if (i == 0) classOf[JBoolean] else classOf[Object]
   149 
   150     override def getColumnName(i: Int): String =
   151       if (i == 0) " " else if (i == 1) "Keyboard shortcut" else "???"
   152 
   153     override def getRowCount: Int = entries_count
   154 
   155     override def getValueAt(row: Int, column: Int): AnyRef =
   156     {
   157       get_entry(row) match {
   158         case Some(entry) if column == 0 => JBoolean.valueOf(is_selected(row))
   159         case Some(entry) if column == 1 => entry
   160         case _ => null
   161       }
   162     }
   163 
   164     override def isCellEditable(row: Int, column: Int): Boolean =
   165       has_entry(row) && column == 0
   166 
   167     override def setValueAt(value: AnyRef, row: Int, column: Int)
   168     {
   169       value match {
   170         case obj: JBoolean if has_entry(row) && column == 0 =>
   171           val b = obj.booleanValue
   172           val entry = entries(row)
   173           entry.head match {
   174             case None => select(row, entry.tail, b)
   175             case Some(head_row) =>
   176               val head_entry = entries(head_row)
   177               select(head_row, head_entry.tail, !b)
   178           }
   179           GUI_Thread.later { fireTableDataChanged() }
   180         case _ =>
   181       }
   182     }
   183   }
   184 
   185   private class Table(table_model: Table_Model) extends JPanel(new BorderLayout)
   186   {
   187     private val cell_size = GUIUtilities.defaultTableCellSize()
   188     private val table_size = new Dimension(cell_size.width * 2, cell_size.height * 15)
   189 
   190     val table = new JTable(table_model)
   191     table.setShowGrid(false)
   192     table.setIntercellSpacing(new Dimension(0, 0))
   193     table.setRowHeight(cell_size.height + 2)
   194     table.setPreferredScrollableViewportSize(table_size)
   195     table.setFillsViewportHeight(true)
   196     table.getTableHeader.setReorderingAllowed(false)
   197 
   198 		table.getColumnModel.getColumn(0).setPreferredWidth(30)
   199 		table.getColumnModel.getColumn(0).setMinWidth(30)
   200 		table.getColumnModel.getColumn(0).setMaxWidth(30)
   201 		table.getColumnModel.getColumn(0).setResizable(false)
   202 		table.getColumnModel.getColumn(1).setPreferredWidth(table_size.width - 30)
   203 
   204     val scroller = new JScrollPane(table)
   205 		scroller.getViewport.setBackground(table.getBackground)
   206 		scroller.setPreferredSize(table_size)
   207 
   208     add(scroller, BorderLayout.CENTER)
   209   }
   210 
   211 
   212 
   213   /** check with optional dialog **/
   214 
   215   def check_dialog(view: View)
   216   {
   217     GUI_Thread.require {}
   218 
   219     val keymap_manager = jEdit.getKeymapManager
   220     val keymap_name = jEdit.getProperty("keymap.current", KeymapManager.DEFAULT_KEYMAP_NAME)
   221     val keymap =
   222       keymap_manager.getKeymap(keymap_name) match {
   223         case null => keymap_manager.getKeymap(KeymapManager.DEFAULT_KEYMAP_NAME)
   224         case keymap => keymap
   225       }
   226 
   227     val all_shortcut_conflicts = get_shortcut_conflicts(keymap_name, keymap)
   228     val no_shortcut_conflicts = for ((s, cs) <- all_shortcut_conflicts if cs.isEmpty) yield s
   229     val shortcut_conflicts = all_shortcut_conflicts.filter(_._2.nonEmpty)
   230 
   231     val table_entries =
   232       for {
   233         ((shortcut, conflicts), i) <- shortcut_conflicts zip
   234           shortcut_conflicts.scanLeft(0)({ case (i, (_, cs)) => i + 1 + cs.length })
   235         entry <-
   236           Table_Entry(shortcut, None, ((i + 1) to (i + conflicts.length)).toList) ::
   237           conflicts.map(Table_Entry(_, Some(i), Nil))
   238       } yield entry
   239 
   240     val table_model = new Table_Model(table_entries)
   241 
   242     if (table_entries.nonEmpty &&
   243         GUI.confirm_dialog(view,
   244           "Pending Isabelle/jEdit keymap changes",
   245           JOptionPane.OK_CANCEL_OPTION,
   246           "The following Isabelle keymap changes are in conflict with the current",
   247           "jEdit keymap " + quote(keymap_name) + ":",
   248           new Table(table_model),
   249           "Selected shortcuts will be applied, unselected changes will be ignored.",
   250           "Results are stored in $JEDIT_SETTINGS/properties and $JEDIT_SETTINGS/keymaps/.") == 0)
   251     { table_model.apply(keymap_name, keymap) }
   252 
   253     no_shortcut_conflicts.foreach(_.set(keymap))
   254 
   255     keymap.save()
   256     keymap_manager.reload()
   257     jEdit.saveSettings()
   258   }
   259 }