require 'fastercsv' require 'clevic/swing/action_builder.rb' require 'clevic/swing/cell_editor.rb' require 'clevic/swing/cell_renderer.rb' require 'clevic/swing/row_header.rb' require 'clevic/swing/table_view_focus.rb' require 'clevic/model_builder.rb' require 'clevic/filter_command.rb' module Clevic # TODO make sure JTable doesn't grab Ctrl-C and do its own copy routine. # TODO make sure Delegates use the correct copy routines. class ClevicTable < javax.swing.JTable attr_accessor :table_view def processKeyBinding( key_stroke, key_event, condition, pressed ) # don't auto-start if it's a Ctrl, or Alt-modified key # or a function key. Hopefully this doesn't get checked # for every single keystroke while editing - those should # be captured by the cell editor. if key_event.alt? || key_event.ctrl? || key_event.meta? || key_event.fx? || key_event.del? || key_event.esc? put_client_property( "JTable.autoStartsEdit", false ) end # do what JTable normally does with keys super rescue Exception => e puts e.message puts e.backtrace table_view.model.emit_data_error( table_view.current_index, nil, e.message ) ensure put_client_property( "JTable.autoStartsEdit", true ) end # override to make things simpler def getCellEditor( row_index, column_index ) index = table_view.model.create_index( row_index, column_index ) # Basically, this is for boolean editing. Number of mouse # clicks and so on is horribly complicated, so just let the # code in javax.swing.whatever handle it. # It has to go here and not in CellEditor, otherwise # listeners and things are wrong. if data_class = index.field.delegate.native # use the default editor for this class of object getDefaultEditor( data_class ) else # use the Clevic CellEditor @cell_editor ||= CellEditor.new( self ) end rescue puts "#{__FILE__}:#{__LINE__}:$!.message: #{$!.message}" puts $!.backtrace puts index.entity.inspect nil end # for mouse events, only edit if the cell is # already selected provided it isn't a combo # box, in which case show the drop-down arrow, but # not the drop-down itself. def editCellAt( row, column, event = nil ) if event index = table_view.model.create_index(row,column) edit_ok = if event.is_a?( java.awt.event.MouseEvent ) && index.field.delegate.needs_pre_selection? # the table_view selection model is mine. The JTable one is not. # Maybe that's weird. table_view.selection_model.with do |sm| sm.single_cell? && sm.selected?( row, column ) end else true end # must call superclass here to do the edit rather than # just returning whether it should be edited. Java. tsk tsk. if edit_ok super else false end else # no event, so do whatever JTable does, which seems to work OK. super end end end # The view class class TableView < javax.swing.JScrollPane # arg is: # - an instance of Clevic::View # - an instance of TableModel def initialize( arg, &block ) super( @jtable = ClevicTable.new ) @jtable.table_view = self # seems like this MUST go after the super call (or maybe the # ClevicTable constructor), otherwise Swing throws an error # somewhere deep inside something. It's not clear right now. # This should theoretically close editors when focus is lost # saving whatever values are in there jtable.put_client_property( "terminateEditOnFocusLost", true ) # cell editors get focus immediately on editor start jtable.surrendersFocusOnKeystroke = true # no auto-resizing of columns jtable.auto_resize_mode = javax.swing.JTable::AUTO_RESIZE_OFF # selection of all kinds allowed jtable.selection_mode = javax.swing.ListSelectionModel::MULTIPLE_INTERVAL_SELECTION jtable.row_selection_allowed = true jtable.column_selection_allowed = true jtable.cell_selection_enabled = true # appearance jtable.font = Clevic.tahoma self.font = Clevic.tahoma # make sure grid shows, even on mac jtable.show_grid = true # because OSX sets this to the same color as the foreground. Duh. jtable.grid_color = java.awt.SystemColor.controlHighlight jtable.setDefaultRenderer( java.lang.Object, CellRenderer.new( self ) ) fix_input_map framework_init( arg, &block ) # this must go after framework_init, because it needs the actions # which are set up in there jtable.component_popup_menu = popup_menu # add the row header RowHeader.new( self ) # make sure focus goes to the right place self.focus_traversal_policy = TableViewFocus.new( self ) end class EmptyAction < javax.swing.AbstractAction def actionPerformed( action_event ); end end def empty_action @empty_action ||= EmptyAction.new end def add_map( key_string, action = empty_action ) map.put( javax.swing.KeyStroke.getKeyStroke( key_string ), action ) end def map @map ||= jtable.getInputMap( javax.swing.JComponent::WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ) end # This puts empty actions in the local keyboard map so that the # generic keyboard map doesn't catch them and prevent our menu actions # from being triggered # TODO I'm sure this isn't the right way to do this. def fix_input_map add_map 'ctrl pressed C' add_map 'ctrl pressed V' add_map 'meta pressed V' add_map 'ctrl pressed X' add_map 'pressed DEL' end def popup_menu @popup_menu ||= javax.swing.JPopupMenu.new.tap do |menu| model_actions.each do |action| menu << action.clone.tap{|a| a.shortcut = nil} end # now do the generic edit items edit_actions.each do |action| menu << action.clone.tap{|a| a.shortcut = nil} end menu.pack end end attr_reader :jtable def connect_view_signals( entity_view ) # pick up model changes and pass them to the Clevic::View object model.addTableModelListener do |table_model_event| begin # pass changed events to view definitions return unless table_model_event.updated? # unlikely to be useful to models, and in fact causes a very very long # calculation. So don't pass it on. return if table_model_event.all_rows? top_left = model.create_index( table_model_event.first_row, table_model_event.column ) bottom_right = model.create_index( table_model_event.last_row, table_model_event.column ) entity_view.notify_data_changed( self, top_left, bottom_right ) to_next_index rescue Exception => e show_error e.message puts e.backtrace end end end # kind-of override of requestFocus, but it will probably only # work from Ruby def request_focus @jtable.request_focus end # return a collection of collections of SwingTableIndex objects # indicating the indices of the current selection def selected_rows @jtable.selected_rows.map do |row_index| @jtable.selected_columns.map do |column_index| SwingTableIndex.new( model, row_index, column_index ) end end end # called from the framework-independent part to edit a cell def edit( table_index ) # TODO keyboard focus doesn't seem to be reassigned to combo # when editing is started this way. @jtable.editCellAt( table_index.row, table_index.column ) end def status_text_listeners @status_text_listeners ||= Set.new end # If msg is provided, yield to stored block. # If block is provided, store it for later. def emit_status_text( msg = nil, ¬ifier_block ) if block_given? status_text_listeners << notifier_block else status_text_listeners.each do |notify| notify.call( msg ) end end end def filter_status_listeners @filter_status_listeners ||= Set.new end # emit whether the view is filtered or not def emit_filter_status( bool = nil, ¬ifier_block ) if block_given? filter_status_listeners << notifier_block else filter_status_listeners.each do |notify| notify.call( bool ) end end end def confirm_dialog( question, title ) cd = ConfirmDialog.new do |dialog| dialog.parent = self dialog.question = question dialog.title = title dialog['Ok'] = :accept, :default dialog['Cancel'] = :reject end cd.show end # set the size of the column from the sample def auto_size_column( col, sample ) @jtable.column_model.column( col ).preferred_width = column_width( col, sample ) end # calculate the size of the column from the string value of the data def column_width( col, data ) @jtable.getFontMetrics( @jtable.font).stringWidth( data ) + 5 end def trim_middle( value, max = 40 ) if value && value.length > max "#{value[0..(max/2-2)]}...#{value[-(max/2-2)..-1]}" else value end end # forward to @jtable # also handle model#emit_data_error def model=( model ) emitter_block = lambda do |index,value,message| show_error "#{index.rc} #{message}: #{trim_middle( value, 40 )}" end @jtable.model.remove_data_error( &emitter_block ) if @jtable.model.respond_to? :remove_data_error @jtable.model = model @jtable.model.emit_data_error( &emitter_block ) if @jtable.model.respond_to? :emit_data_error resize_columns end def model @jtable.model end def show_error( msg, title = "Error" ) @pane ||= javax.swing.JOptionPane.new( '', javax.swing.JOptionPane::ERROR_MESSAGE, javax.swing.JOptionPane::DEFAULT_OPTION ) @pane.message = msg @pane.create_dialog( self, title ).show end def selection_model SelectionModel.new( self ) end # move the cursor & selection to the specified table_index def current_index=( table_index ) @jtable.selection_model.clear_selection @jtable.setColumnSelectionInterval( table_index.column, table_index.column ) @jtable.setRowSelectionInterval( table_index.row, table_index.row ) # x position. Should be sum of widths of all columns up to the beginning of this one # ie not including this one, hence the -1 xpos = (0..table_index.column-1).inject(0) do |sum,column_index| sum + @jtable.column_model.getColumn( column_index ).width end rect = java.awt.Rectangle.new( xpos, # y position @jtable.row_height * table_index.row, # width of this column @jtable.column_model.getColumn( table_index.column ).width, # height @jtable.row_height ) @jtable.scrollRectToVisible( rect ) end # return a SwingTableIndex for the current cursor position # TODO optimise so we don't keep creating a new index, only if a selection # changed event has occurred def current_index model.create_index( @jtable.selected_row, @jtable.selected_column ) end def wait_cursor @wait_cursor ||= java.awt.Cursor.new( java.awt.Cursor::WAIT_CURSOR ) end # show a busy cursor, do the block, back to normal cursor # return value of block def busy_cursor( &block ) save_cursor = cursor self.cursor = wait_cursor rv = yield ensure self.cursor = save_cursor rv end # collect actions for the popup menu def add_action( action ) ( @context_actions ||= [] ) << action end end end