Does JavaFX 8.0 TableView have a sort error?

Java 8.0 x64, Win7 x64, Clojure, Emacs.

I am doing something in Clojure with TableView

where I am proxy

ing TableCell

, so I can display and edit arbitrary things in it. The values ​​represent the fields of the map inside the atom. The code is below. It uses a lot of useful functions and macros to make it easier, but you get the gist. The main thing is to control the graphic and text properties of the cell.

There is a keyboard handler that is attached to ComboBox

, so it knows when the user clicks ENTER

, etc. This handler is removed when defocusing from the cell, so we don't get many handlers into the object.

In this example, I have three columns: one for the field name (a simple factory cell that only shows text and is not editable), one for the value (fancy cell factory), and one for the type (simple factory cell). The result, using some example data, looks like this:

enter image description here

When I sort the table based on Value it seems to work fine:

Usually, when the keyboard manipulator starts up, it calls a cell function commitEdit

that calls its TableCell

superclass commitEdit

. The magic TableView

behind the scenes then calls the column handler onEditCommit

, which actually does the edit on the database. After the superclass returns, nothing remains commitEdit

in the cell commitEdit

. The cell is then updateItem

automatically called TableView

, which replaces ComboBox

with the cell's normal content.

PROBLEM

When I sort a table based on a column Field

one or more times, or a column Type

two or more times, and try to edit something with ComboBox

(in this case, a color picker), ComboBox

an extra click is required to get it, and the key ENTER

doesn't work, specifically as follows :

CAUSE

In the broken case, the superclass of the class TableCell

appears to return immediately and does not call the column handler onCommitEdit

, nor does the cell get called updateItem

, so the cell does not fall back to its normal unediting, i.e. without ComboBox

.

The normal and broken cases look like this: enter image description here

Here is the output of the debug text in the normal case and the broken case. enter image description here

It's strange that this problem sometimes appears with non-colored ones ComboBox

(the field sides

has an editor ComboBox

with numbers, for example).

So is this a bug in JavaFX TableView

? Or am I doing something wrong?

(defn add-handlers!
  "Adds common keyboard handler and focus listener to temporary editing graphic.
  graphic is typically textfield or combo-box. cell is tablecell which
  is being edited.  getterfn is function to get value from graphic so
  it can be commited to database."
  [graphic cell getterfn]
  (let [focus-listener (make-focus-change-listener cell getterfn)]
    (println "adding focus and keyboard listener")
    (add-listener! graphic :focused focus-listener)
    (.setOnKeyPressed graphic (eventhandler [e] ;; here "cell" still refers to the tablecell
                                            (condp = (.getCode e)
                                              KeyCode/ENTER (do (println "ENTER pressed.  Removing focus listener")
                                                                (remove-listener! graphic :focused focus-listener) ;; Prevent double-commit on defocus
                                                                (.commitEdit cell (getterfn)))
                                              KeyCode/ESCAPE (do (println "ESC pressed. Removing focus listener")
                                                                 (remove-listener! graphic :focused focus-listener) ;; Prevent double-commit on defocus
                                                                 (.cancelEdit cell)) ;; Removes textfield
                                              KeyCode/TAB (let [index (.. cell getTableRow getIndex)
                                                                next-column (get-next-column cell (not (.isShiftDown e)))]
                                                            (println "TAB pressed.  Removing focus listener")
                                                            (remove-listener! graphic :focused focus-listener) ;; Prevent double-commit on defocus
                                                            (.commitEdit cell (getterfn))
                                                            (.edit (.getTableView cell) index next-column))
                                              nil))))) ;; do nothing


(defn make-combobox
  "Implements dropdown combobox.  'cell' is fancy table cell in
  question.  'items' is list of things for dropdown, which can be
  anything that the dropdown can render and choose as the final item"
  [cell initvalue & [items]]
  (let [cmb (jfxnode ComboBox (observable items))
        cell-factory FANCY-LISTCELL-FACTORY
        blank-cell (.call cell-factory nil)]
    (doto cmb
      (add-handlers! cell #(.getValue cmb))
      (.setValue initvalue)
      (.setButtonCell blank-cell)
      (.setCellFactory cell-factory))))


(defn render-cell-with-item!
  "Puts correct item in cell graphic and/or text property based on item
  type.  Additional arguments for editing such as drop-down, are
  handled in the startEdit function; this function just renders the
  cell when called by updateItem or cancelEdit."
  [cell item]
  (cond
    (instance? Node item) (set-graphic-text! cell item nil) ;; for a graphic/Node item
    (instance? Boolean item) (let [[var full-accesspath] (calc-full-accesspath cell)
                                   cb (jfxnode CheckBox
                                               :text (str item)
                                               :selected item
                                               :disable (not (mutable? var)))]
                               (.setEditable cell false)
                               (set-graphic-text! cell cb nil)
                               (when (mutable? var)
                                 (uni-bind! (.selectedProperty cb) var full-accesspath)))
    (instance? clojure.lang.PersistentVector item) (set-graphic-text! cell (Label. "Put vector editor here") nil)
    (instance? Color item) (set-graphic-text! cell (make-color-box item) (color-map-inverse item))
    ;; All other types go here, presumably text types, so assume editable
    :else (set-graphic-text! cell nil (si/to-normstr item))))   ;; else set underlying text


(def FANCY-TABLECELL-FACTORY
  "The main callback interface which constructs the actual each cell
  for arbitrary types.  Assumes an editable cell for text representations."
  (callback [column]  
            (proxy [TableCell] []
              (updateItem [item empty]
                (proxy-super updateItem item empty)
                (when (not empty) 
                  (render-cell-with-item! this item)))

              (startEdit []
                (proxy-super startEdit)
                ;; Change to appropriate graphic when editing
                (println "in proxy startEdit.  Column commitHandler is" (.getOnEditCommit column))
                (let [item (apply access-db (calc-full-accesspath this))
                      options (get-field-options this)] ;; could be nil ...
                  (if-let [combo-items (:combo-items options)] ;; ... so put as argument to :combo-items
                    (let [cmb (make-combobox this item combo-items)]
                      (set-graphic-text! this cmb nil)
                      (.requestFocus cmb)
                      (.show cmb)) ;; This makes drop-down appear without clicking twice.
                    (when (textish? item)
                      (let [tf (make-textfield-editor this)]
                        (set-graphic-text! this tf nil) ;; just set tf as graphic; leave existing text alone
                        (.requestFocus tf)
                        (.selectAll tf))))))
              (cancelEdit []
                ;; CancelEdit gets called either by defocus or by ESC.
                ;; In any case, use the item currently in the database
                ;; for this cell and just render as in updateItem
                (proxy-super cancelEdit)
                (let [item (apply access-db (calc-full-accesspath this))]
                  (render-cell-with-item! this item)))
              (commitEdit [value]
                ;; Nothing to do here.  All commits happen either in the textField callback or in the column edit callback
                (println "in cell commitEdit, before super")
                (proxy-super commitEdit value)
                (println "in cell commitEdit, after super")))))


(defn inner-table-view*
  "Make inner table view for use by inspector-view and table-view"
  [var accesspath columns]
  (let [obslist (observable (var-snapshot var accesspath))]
    (jfxnode TableView
             :user-data {:var var ;; the actual var... 
                         :accesspath accesspath }  ;; ... and how to get to the displayed data
             :items obslist
             :columns columns
             :editable (mutable? var))))

(defn inspector-view
  "Takes plain map or atom/var/ref/agent of map and displays fields
  and values in JFX TableView. Compound values (ie maps, vectors,
  etc., for now are just displayed as their string value.  If access
  is supplied, assumes m is var/ref/atom and assigns appropriate
  linkage between m and view contents.  The topmost available var or
  map is assigned to the TableView, and the accessor for each field is
  assigned to each column."
  [var & {:keys [accesspath field-options]}]
  (let [ismutable (mutable? var)
        field-col (jfxnode TableColumn "Field"
                           :cell-value-factory CELL-VALUE-FACTORY
                           :cell-factory SIMPLE-TABLECELL-FACTORY
                           :user-data {:accessfn key } ;; label-only option not relevant yet
                           :editable false
                           :sortable true)
        value-col (jfxnode TableColumn "Value"
                           :cell-value-factory CELL-VALUE-FACTORY
                           :cell-factory FANCY-TABLECELL-FACTORY
                           :user-data {:accessfn val} ;; val is fn for accessing cell values from data item
                           :on-edit-start (eventhandler [e] (println "editing column " (.getOldValue e) (.getNewValue e)))
                           :on-edit-cancel (eventhandler [e] (println "canceling column with event" e))
                           :on-edit-commit (eventhandler [e] (do (println "column on-edit-commit handler calling column-commit") (column-commit e)))
                           :editable ismutable
                           :comparator columnComparator)
        type-col (jfxnode TableColumn "Type"
                          :cell-value-factory CELL-VALUE-FACTORY
                          :cell-factory SIMPLE-TABLECELL-FACTORY
                          :user-data {:accessfn #(type (val %))}
                          :editable false
                          :sortable true)
        cols [field-col value-col type-col]

        tv (inner-table-view* var accesspath cols)]
    ;; Add options to table userData.  This is for inspector-view
    ;; not table-view, so we don't put this in inner-table-view
    ;; function
    (let [userdata (.getUserData tv)
          newuserdata (conj userdata {:field-options field-options})]
      (.setUserData tv newuserdata))

    ;; Add watches, use tv instance as key so we can remove it later
    ;; This gets called each time db is changed.
    (if (mutable? var)
      (add-watch var tv (fn [k r o n] ;; Key Ref Old New
                          (println "Inside KRON with new var" n)
                          ;; Capture the existing sort order and type
                          ;; Taken from http://stackoverflow.com/questions/11096353/javafx-re-sorting-a-column-in-a-tableview
                          (let [sort-order (vec (.getSortOrder tv)) ;; need to remember ObservableList<TableColumn> and vectorize or it gets reset from underneath us
                                sort-types (map #(.getSortType %) sort-order)
                                sortables (map #(.isSortable %) sort-order)]

                            ;; Here we actually put the items into the tableview after the change
                            (.setItems tv (observable (var-snapshot var accesspath))) 

                            ;; Sort order is now empty up so we put back what was in it
                            (let [new-sort-order (.getSortOrder tv)] ;; get ObservableList<TableColumn>
                              (.setAll new-sort-order (into-array sort-order)) ;; reset the sort order based on what was there before

                              ;; Assign sorting to each column
                              (doseq [col sort-order, sort-type sort-types, sortable sortables]
                                (.setSortType col sort-type)
                                (.setSortable col sortable)))))))
    tv))

      

+3


source to share


1 answer


I found the problem, which was of course in my code.

Because JFX reuses cells, the cell's property editable

persists even if different content is displayed in the cell. In my case, I had a boolean member of my database which I selected as a checkbox. The checkbox itself was clickable, but the cell it was rendered in was not editable. When this cell was redisplayed after sorting with another item, the edit state was not preserved and screwed up the edit of the new item, which somehow caused the dropdown to not disappear properly. In fact, the error was found in elements not related to the combobox, for example, for editing text, etc.



So the solution was to explicitly set the editable cell property for each item type displayed.

+2


source







All Articles