Class: Clevic::TableView

Inherits:
Qt::TableView
  • Object
show all
Includes:
ActionBuilder
Defined in:
lib/clevic/table_view.rb,
lib/clevic/qt/table_view.rb,
lib/clevic/swing/table_view.rb,
lib/clevic/table_view_paste.rb

Overview

The view class

Defined Under Namespace

Classes: EmptyAction

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ActionBuilder

#action, #action_method_or_block, #build_actions, #create_action, #create_key_sequence, #group_names, included, #list, #separator

Constructor Details

#initialize(arg, &block) ⇒ TableView

arg is:

  • an instance of Clevic::View

  • an instance of TableModel



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/clevic/qt/table_view.rb', line 33

def initialize( arg, parent = nil, &block )
  # need the empty block here, otherwise Qt bindings grab &block
  super( parent ) {}

  framework_init( arg, &block )

  # see closeEditor
  @next_index = nil

  # set some Qt things
  self.horizontal_header.movable = false
  # TODO might be useful to allow movable vertical rows,
  # but need to change the shortcut ideas of next and previous rows
  self.vertical_header.movable = false
  self.vertical_header.default_alignment = Qt::AlignTop | Qt::AlignRight
  self.sorting_enabled = false

  # set fonts
  # TODO leave this here, but commented so we can see how to do it
  # properly later.
  #~ Qt::Font.new( font.family, font.point_size * 5 / 6 ).tap do |fnt|
    #~ self.font = fnt
    #~ self.horizontal_header.font = fnt
  #~ end 

  self.context_menu_policy = Qt::ActionsContextMenu
end

Instance Attribute Details

#before_edit_indexObject

Returns the value of attribute before_edit_index.



264
265
266
# File 'lib/clevic/qt/table_view.rb', line 264

def before_edit_index
  @before_edit_index
end

#filtersObject

the current stack of filter commands



19
20
21
# File 'lib/clevic/table_view.rb', line 19

def filters
  @filtered ||= []
end

#jtableObject (readonly)

Returns the value of attribute jtable.



195
196
197
# File 'lib/clevic/swing/table_view.rb', line 195

def jtable
  @jtable
end

#next_indexObject

set next_index for certain operations. Is only activated when to_next_index is called.



651
652
653
# File 'lib/clevic/table_view.rb', line 651

def next_index
  @next_index
end

#object_nameObject

Returns the value of attribute object_name.



56
57
58
# File 'lib/clevic/table_view.rb', line 56

def object_name
  @object_name
end

Instance Method Details

#action_triggered(&block) ⇒ Object

hook for the sanity_check_xxx methods called for the actions set up by ActionBuilder it just wraps the action block/method in a catch block for :insane. Will also catch exceptions thrown in actions to make core application more robust to model & view errors.



77
78
79
80
81
82
83
84
85
86
# File 'lib/clevic/table_view.rb', line 77

def action_triggered( &block )
  catch :insane do
    yield
  end

rescue Exception => e
  puts
  puts "#{model.entity_view.class.name}: #{e.message}"
  puts e.backtrace
end

#add_action(action) ⇒ Object

collect actions for the popup menu



384
385
386
# File 'lib/clevic/swing/table_view.rb', line 384

def add_action( action )
  ( @context_actions ||= [] ) << action
end

#add_map(key_string, action = empty_action) ⇒ Object



160
161
162
# File 'lib/clevic/swing/table_view.rb', line 160

def add_map( key_string, action = empty_action )
  map.put( javax.swing.KeyStroke.getKeyStroke( key_string ), action )
end

#auto_size_attribute(attribute, sample) ⇒ Object

alternative access for auto_size_column



331
332
333
# File 'lib/clevic/table_view.rb', line 331

def auto_size_attribute( attribute, sample )
  auto_size_column( model.attributes.index( attribute ), sample )
end

#auto_size_column(col, sample) ⇒ Object

set the size of the column from the sample



103
104
105
# File 'lib/clevic/qt/table_view.rb', line 103

def auto_size_column( col, sample )
  self.set_column_width( col, column_size( col, sample ).width )
end

#busy_cursor(&block) ⇒ Object

show a busy cursor, do the block, back to normal cursor return value of block



374
375
376
# File 'lib/clevic/swing/table_view.rb', line 374

def busy_cursor( &block )
  override_cursor( Qt::BusyCursor, &block )
end

#clipboardObject



134
135
136
137
# File 'lib/clevic/table_view.rb', line 134

def clipboard
  # Clipboard will be a framework-specific class
  @clipboard = Clipboard.new
end

#closeEditor(editor, end_edit_hint) ⇒ Object

override to prevent tab pressed from editing next field also takes into account that override_next_index may have been called



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/clevic/qt/table_view.rb', line 285

def closeEditor( editor, end_edit_hint )
  if $options[:debug]
    puts "end_edit_hint: #{Qt::AbstractItemDelegate.constants.find {|x| Qt::AbstractItemDelegate.const_get(x) == end_edit_hint } }"
    puts "next_index: #{next_index.inspect}"
  end

  subsequent_index =
  case end_edit_hint
    when Qt::AbstractItemDelegate.EditNextItem
      super( editor, Qt::AbstractItemDelegate.NoHint )
      before_edit_index.choppy { |i| i.column += 1 }

    when Qt::AbstractItemDelegate.EditPreviousItem
      super( editor, Qt::AbstractItemDelegate.NoHint )
      before_edit_index.choppy { |i| i.column -= 1 }

    else
      super
      nil
  end

  unless subsequent_index.nil?
    puts "subsequent_index: #{subsequent_index.inspect}" if $options[:debug]
    # TODO all this really does is reset next_index
    set_current_unless_override( next_index || subsequent_index || before_edit_index )
    self.before_edit_index = nil
  end
end

#column_size(col, data) ⇒ Object

set the size of the column from the string value of the data mostly copied from qheaderview.cpp:2301



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/clevic/qt/table_view.rb', line 113

def column_size( col, data )
  opt = Qt::StyleOptionHeader.new

  # fetch font size
  opt.fontMetrics = metrics
  opt.rect = opt.fontMetrics.bounding_rect( data.to_s )

  # set data
  opt.text = data.to_s

  opt.section =
  case
    when col == 0
      Qt::StyleOptionHeader::Beginning

    when col > 0 && col < model.fields.size - 1
      Qt::StyleOptionHeader::Middle

    when col == model.fields.size - 1
      Qt::StyleOptionHeader::End
  end

  size = Qt::Size.new( opt.fontMetrics.width( data.to_s ), opt.fontMetrics.height )

  # final parameter could be header section
  style.sizeFromContents( Qt::Style::CT_HeaderSection, opt, size )
end

#column_width(col, data) ⇒ Object

calculate the size of the column from the string value of the data



292
293
294
# File 'lib/clevic/swing/table_view.rb', line 292

def column_width( col, data )
  @jtable.getFontMetrics( @jtable.font ).stringWidth( data.to_s ) + 5
end

#commitData(editor) ⇒ Object

this is the only method that is called when an itemDelegate is open and the tabs are changed. Work around situation where an ItemDelegate is open when the surrouding tab is changed, but the right events don’t arrive.



239
240
241
242
243
244
245
246
# File 'lib/clevic/qt/table_view.rb', line 239

def commitData( editor )
  super
  save_current_row if @hiding
rescue
  puts $!.message
  puts $!.backtrace
  show_error "Error saving data from #{editor.inspect}: #{$!.message}"
end

#confirm_dialog(question, title) ⇒ Object

returns the Qt::MessageBox



182
183
184
185
186
187
188
189
190
191
192
# File 'lib/clevic/qt/table_view.rb', line 182

def confirm_dialog( question, title )
  msg = Qt::MessageBox.new(
    Qt::MessageBox::Question,
    title,
    question,
    Qt::MessageBox::Yes | Qt::MessageBox::No,
    self
  )
  msg.exec
  msg
end

#connect_view_signals(entity_view) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/clevic/qt/table_view.rb', line 61

def connect_view_signals( entity_view )
  model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
    begin
      entity_view.notify_data_changed( self, top_left, bottom_right )
    rescue Exception => e
      puts
      puts "#{model.entity_view.class.name}: #{e.message}"
      puts e.backtrace
    end
  end
end

#copy_current_selectionObject

copy current selection to clipboard as CSV TODO add text/csv, text/tab-separated-values, text/html as well as text/plain



141
142
143
# File 'lib/clevic/table_view.rb', line 141

def copy_current_selection
  clipboard.text = current_selection_csv
end

#current_indexObject

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



364
365
366
# File 'lib/clevic/swing/table_view.rb', line 364

def current_index
  model.create_index( @jtable.selected_row, @jtable.selected_column )
end

#current_index=(table_index) ⇒ Object

move the cursor & selection to the specified table_index



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/clevic/swing/table_view.rb', line 335

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

#current_selection_csvObject

return the current selection as csv



146
147
148
149
150
151
152
# File 'lib/clevic/table_view.rb', line 146

def current_selection_csv
  buffer = StringIO.new
  selected_rows.each do |row|
    buffer << row.map {|index| index.edit_value }.to_csv
  end
  buffer.string
end

#currentChanged(current_index, previous_index) ⇒ Object

save record whenever its row is exited make this work with framework



481
482
483
484
485
486
487
# File 'lib/clevic/table_view.rb', line 481

def currentChanged( current_index, previous_index )
  if previous_index.valid? && current_index.row != previous_index.row
    self.next_index = nil
    save_row( previous_index )
  end
  super
end

#delete_cellsObject

Ask if multiple cell delete is OK, then replace contents of selected cells with nil.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/clevic/table_view.rb', line 375

def delete_cells
  delete_multiple_cells? do
    cells_deleted = false

    # do delete
    selection_model.selected_indexes.each do |index|
      index.attribute_value = nil
      cells_deleted = true
    end

    # deletes were done, so call data_changed
    if cells_deleted
      # save affected rows
      selection_model.row_indexes.each do |row_index|
        save_row( model.create_index( row_index, 0 ) )
      end

      # emit data changed for all ranges
      selection_model.ranges.each do |selection_range|
        model.data_changed( selection_range )
      end
    end
  end
end

#delete_multiple_cells?(question = 'Are you sure you want to delete multiple cells?', &block) ⇒ Boolean

ask the question in a dialog. If the user says yes, execute the block

Returns:

  • (Boolean)


359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/clevic/table_view.rb', line 359

def delete_multiple_cells?( question = 'Are you sure you want to delete multiple cells?', &block )
  sanity_check_read_only

  # go ahead with delete if there's only 1 cell, or the user says OK
  delete_ok =
  if selection_model.selected_indexes.size > 1
    confirm_dialog( question, "Multiple Delete" ).accepted?
  else
    true
  end

  yield if delete_ok
end

#delete_rowsObject



400
401
402
403
404
405
406
407
408
409
410
# File 'lib/clevic/table_view.rb', line 400

def delete_rows
  delete_multiple_cells?( "Are you sure you want to delete #{selection_model.row_indexes.size} rows?" ) do
    begin
      model.remove_rows( selection_model.row_indexes )
    rescue
      puts $!.message
      puts $!.backtrace
      show_error $!.message
    end
  end
end

#delete_selectionObject

Delete the current selection. If it’s a set of rows, just delete them. If it’s a rectangular selection, set the cells to nil. TODO make sure all affected rows are saved.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/clevic/table_view.rb', line 254

def delete_selection
  busy_cursor do
    begin
      sanity_check_read_only

      # TODO translate from ModelIndex objects to row indices
      puts "#{__FILE__}:#{__LINE__}:implement vertical_header for delete_selection"
      #~ rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
      rows = []
      unless rows.empty?
        # header rows are selected, so delete them
        model.remove_rows( rows ) 
      else
        # otherwise various cells are selected, so delete the cells
        delete_cells
      end
    rescue
      show_error $!.message
    end
  end
end

#dittoObject



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/clevic/table_view.rb', line 180

def ditto
  sanity_check_ditto
  sanity_check_read_only
  selection_model.selected_indexes.each do |index|
    one_up_index = index.choppy { |i| i.row -= 1 }
    previous_value = one_up_index.attribute_value
    if index.attribute_value != previous_value
      index.attribute_value = previous_value
      model.data_changed( index )
    end
  end
end

#ditto_leftObject



215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/clevic/table_view.rb', line 215

def ditto_left
  sanity_check_ditto
  sanity_check_read_only
  unless current_index.column > 0
    emit_status_text( 'No column to the left' )
  else
    one_up_left = current_index.choppy { |i| i.row -= 1; i.column -= 1 }
    sanity_check_types( one_up_left, current_index )
    current_index.attribute_value = one_up_left.attribute_value
    model.data_changed( current_index )
  end
end

#ditto_rightObject



202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/clevic/table_view.rb', line 202

def ditto_right
  sanity_check_ditto
  sanity_check_read_only
  if current_index.column >= model.column_count - 1
    emit_status_text( 'No column to the right' )
  else
    one_up_right = current_index.choppy {|i| i.row -= 1; i.column += 1 }
    sanity_check_types( one_up_right, current_index )
    current_index.attribute_value = one_up_right.attribute_value
    model.data_changed( current_index )
  end
end

#edit(table_index) ⇒ Object

called from the framework-independent part to edit a cell



249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/clevic/qt/table_view.rb', line 249

def edit( model_index, trigger = nil, event = nil )
  self.before_edit_index = model_index
  #~ puts "edit model_index: #{model_index.inspect}"
  #~ puts "trigger: #{trigger.inspect}"
  #~ puts "event: #{event.inspect}"
  if trigger.nil? && event.nil?
    super( model_index )
  else
    super( model_index, trigger, event )
  end

  rescue Exception => e
    raise RuntimeError, "#{model.entity_view.class.name}.#{model_index.field.id}: #{e.message}", e.backtrace
end

#emit_filter_status(bool = nil, &notifier_block) ⇒ Object

emit whether the view is filtered or not



265
266
267
# File 'lib/clevic/swing/table_view.rb', line 265

def emit_filter_status( bool )
  emit filter_status_signal( bool )
end

#emit_status_text(msg = nil, &notifier_block) ⇒ Object

If msg is provided, yield to stored block. If block is provided, store it for later.



250
251
252
# File 'lib/clevic/swing/table_view.rb', line 250

def emit_status_text( string )
  emit status_text_signal( string )
end

#empty_actionObject



156
157
158
# File 'lib/clevic/swing/table_view.rb', line 156

def empty_action
  @empty_action ||= EmptyAction.new
end

#field_column(field) ⇒ Object

find the row index for the given field id (symbol)



63
64
65
# File 'lib/clevic/table_view.rb', line 63

def field_column( field )
  raise "use model.field_column( field )"
end

#filter_by_currentObject

toggle the filter, based on current selection.



490
491
492
# File 'lib/clevic/table_view.rb', line 490

def filter_by_current
  filter_by_indexes( selection_or_current )
end

#filter_by_dataset(message = nil, &dataset_block) ⇒ Object



544
545
546
547
548
549
550
551
552
553
# File 'lib/clevic/table_view.rb', line 544

def filter_by_dataset( message = nil, &dataset_block )
  # TODO clean this up and make it work AND for multiple columns, OR for multiple rows
  self.filters << FilterCommand.new( self, message, &dataset_block )

  # try to end up on the same entity, even after the filter
  restore_entity { filters.last.doit }

  # make sure status bar shows status
  update_filter_status_bar
end

#filter_by_indexes(indexes) ⇒ Object

Filter by the value in the current index. indexes is a collection of TableIndex instances



557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/clevic/table_view.rb', line 557

def filter_by_indexes( indexes )
  case
  when indexes.empty?
    emit_status_text( "No field selected for filter" )

  when !indexes.first.field.filterable?
    emit_status_text( "Can't filter on #{indexes.first.field.label}" )

  when indexes.size > 1
    emit_status_text( "Can't do multiple selection filters yet" )

  when indexes.first.entity.new_record?
    emit_status_text( "Can't filter on a new row" )

  else
    message = "#{indexes.first.field_name} = #{indexes.first.display_value}"
    filter_by_dataset( message ) do |dataset|
      indexes.first.field.with do |field|
        if field.association?
          dataset.filter( field.meta.keys => indexes.first.attribute_value.andand.pk )
        else
          dataset.filter( indexes.first.field_name.to_sym => indexes.first.attribute_value )
        end
      end
    end
  end
  filtered?
end

#filter_by_options(args) ⇒ Object

This is used by entity view classes. Keep it as a compatibility / deprecated option?



496
497
498
499
500
501
502
# File 'lib/clevic/table_view.rb', line 496

def filter_by_options( args )
  puts "#{self.class.name}#filter_by_options is deprecated. Use filter_by_dataset( message, &block ) instead."

  filter_by_dataset( "#{args.inspect}" ) do |dataset|
    dataset.translate( args )
  end
end

#filter_messageObject



533
534
535
# File 'lib/clevic/table_view.rb', line 533

def filter_message
  "Filter: " + filters.map( &:message ).join(' / ') unless filters.empty?
end

#filter_status_listenersObject



260
261
262
# File 'lib/clevic/swing/table_view.rb', line 260

def filter_status_listeners
  @filter_status_listeners ||= Set.new
end

#filtered?Boolean

Returns:

  • (Boolean)


24
# File 'lib/clevic/table_view.rb', line 24

def filtered?; !filters.empty?; end

#findObject

display a search dialog, and find the entered text



281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/clevic/table_view.rb', line 281

def find
  result = search_dialog.exec( current_index.display_value )

  busy_cursor do
    case
      when result.accepted?
        search( search_dialog )
      when result.rejected?
        puts "Don't search"
      else
        puts "unknown dialog result #{result}"
    end
  end
end

#find_nextObject



296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/clevic/table_view.rb', line 296

def find_next
  # yes, this must be an @ otherwise it lazy-creates
  # and will never be nil
  if @search_dialog.nil?
    emit_status_text( 'No previous find' )
  else
    busy_cursor do
      save_from_start = search_dialog.from_start?
      search_dialog.from_start = false
      search( search_dialog )
      search_dialog.from_start = save_from_start
    end
  end
end

#find_table_view(entity_model_or_view) ⇒ Object

find the TableView instance for the given entity_view or entity_model. Return nil if no match found. TODO doesn’t really belong here because TableView will not always be in a TabWidget context.



318
319
320
321
322
323
324
# File 'lib/clevic/qt/table_view.rb', line 318

def find_table_view( entity_model_or_view )
  parent.children.find do |x|
    if x.is_a? TableView
      x.model.entity_view.class == entity_model_or_view || x.model.entity_class == entity_model_or_view
    end
  end
end

#fix_input_mapObject

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.



172
173
174
175
176
177
178
# File 'lib/clevic/swing/table_view.rb', line 172

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

#focusOutEvent(event) ⇒ Object



229
230
231
232
# File 'lib/clevic/qt/table_view.rb', line 229

def focusOutEvent( event )
  super
  #~ save_current_row
end

#framework_init(arg, &block) ⇒ Object

Called from the gui-framework adapter code in this class arg is:

  • an instance of Clevic::View

  • an instance of TableModel



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/clevic/table_view.rb', line 30

def framework_init( arg, &block )
  # the model/entity_class/builder
  case 
    when arg.is_a?( TableModel )
      self.model = arg
      init_actions( arg.entity_view )

    when arg.is_a?( Clevic::View )
      model_builder = arg.define_ui
      model_builder.exec_ui_block( &block )

      # make sure the TableView has a fully-populated TableModel
      # self.model is necessary to invoke the GUI layer
      self.model = model_builder.build( self )
      self.object_name = arg.widget_name

      # connect data_changed signals for the entity_class to respond
      connect_view_signals( arg )

      init_actions( arg )

    else
      raise "Don't know what to do with #{arg.inspect}"
  end
end

#handle_key_press(event) ⇒ Object

handle certain key combinations that aren’t shortcuts TODO what is returned from here?



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/clevic/table_view.rb', line 414

def handle_key_press( event )
  begin
    # call to entity class for shortcuts
    begin
      view_result = model.entity_view.notify_key_press( self, event, current_index )
      return view_result unless view_result.nil?
    rescue Exception => e
      puts e.backtrace
      show_error( "Error in shortcut handler for #{model.entity_view.name}: #{e.message}" )
    end

    # thrown by the sanity_check_xxx methods
    catch :insane do
      case
      # on the last row, and down is pressed
      # add a new row
      when event.down? && last_row?
        new_row

      # on the right-bottom cell, and tab is pressed
      # then add a new row
      when event.tab? && last_cell?
        new_row

      # add new record and go to it
      # TODO this is actually a shortcut
      when event.ctrl? && event.return?
        new_row

      else
        #~ puts event.inspect
      end
    end
  rescue Exception => e
    puts e.backtrace
    puts e.message
    show_error( "handle_key_press #{__FILE__}:#{__LINE__} error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
  end
end

#hideEvent(event) ⇒ Object

work around situation where an ItemDelegate is open when the surrouding tab is changed, but the right events don’t arrive.



215
216
217
218
219
# File 'lib/clevic/qt/table_view.rb', line 215

def hideEvent( event )
  # can't call super here, for some reason. Qt binding says method not found.
  # super
  @hiding = true
end

#init_actions(entity_view) ⇒ Object

called from framework_init



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/clevic/table_view.rb', line 90

def init_actions( entity_view )
  # add model actions, if they're defined
  list( :model ) do |ab|
    entity_view.define_actions( self, ab )
    separator unless collect_actions.empty?
  end

  # list of actions in the edit menu
  list( :edit ) do
    action :action_save, '&Save', :shortcut => 'Ctrl+S', :method => :save_current_rows
    #~ action :action_cut, 'Cu&t', :shortcut => 'Ctrl+X', :method => :cut_current_selection
    action :action_copy, '&Copy', :shortcut => 'Ctrl+C', :method => :copy_current_selection
    action :action_paste, '&Paste', :shortcut => 'Ctrl+V', :method => :paste
    action :action_delete, '&Delete', :shortcut => 'Del', :method => :delete_selection
    separator
    action :action_ditto, 'D&itto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
    action :action_ditto_right, 'Ditto Ri&ght', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
    action :action_ditto_left, '&Ditto L&eft', :shortcut => 'Ctrl+[', :method => :ditto_left, :tool_tip => 'Copy field one to left from previous record'
    action :action_insert_date, 'Insert Date', :shortcut => 'Ctrl+;', :method => :insert_current_date
    action :action_open_editor, '&Open Editor', :shortcut => 'F4', :method => :open_editor
    separator
    action :action_row, 'New Ro&w', :shortcut => 'Ctrl+N', :method => :new_row
    action :action_refresh, '&Refresh', :shortcut => 'Ctrl+R', :method => :refresh
    action :action_delete_rows, 'Delete Rows', :shortcut => 'Ctrl+Delete', :method => :delete_rows

    if $options[:debug]
      action :action_dump, 'Du&mp', :shortcut => 'Ctrl+Shift+D' do
        puts model.collection[current_index.row].inspect
      end
    end
  end

  separator

  # list of actions for search
  list( :search ) do
    action :action_find, '&Find', :shortcut => 'Ctrl+F', :method => :find
    action :action_find_next, 'Find &Next', :shortcut => 'Ctrl+G', :method => :find_next
    action :action_filter, 'Fil&ter', :shortcut => 'Ctrl+L', :method => :filter_by_current
    action :action_unfilter, '&Un-Filter', :enabled => false, :shortcut => 'Ctrl+K', :method => :unfilter
    #~ action :action_highlight, '&Highlight', :visible => false, :shortcut => 'Ctrl+H'
  end
end

#insert_current_dateObject



228
229
230
231
232
# File 'lib/clevic/table_view.rb', line 228

def insert_current_date
  sanity_check_read_only
  current_index.attribute_value = Time.now
  model.data_changed( current_index )
end

#itemDelegate(model_index) ⇒ Object



97
98
99
100
# File 'lib/clevic/qt/table_view.rb', line 97

def itemDelegate( model_index )
  @pre_delegate_index = model_index
  super
end

#keyPressEvent(event) ⇒ Object



194
195
196
197
# File 'lib/clevic/qt/table_view.rb', line 194

def keyPressEvent( event )
  handle_key_press( event )
  super
end

#last_cell?Boolean

is current_index on the bottom_right cell?

Returns:

  • (Boolean)


341
342
343
# File 'lib/clevic/table_view.rb', line 341

def last_cell?
  current_index.row == model.row_count - 1 && current_index.column == model.column_count - 1
end

#last_row?Boolean

is current_index on the last row?

Returns:

  • (Boolean)


336
337
338
# File 'lib/clevic/table_view.rb', line 336

def last_row?
  current_index.row == model.row_count - 1
end

#mapObject



164
165
166
# File 'lib/clevic/swing/table_view.rb', line 164

def map
  @map ||= jtable.getInputMap( javax.swing.JComponent::WHEN_ANCESTOR_OF_FOCUSED_COMPONENT )
end

#metricsObject



107
108
109
# File 'lib/clevic/qt/table_view.rb', line 107

def metrics
  @metrics = Qt::FontMetrics.new( font )
end

#modelObject



316
317
318
# File 'lib/clevic/swing/table_view.rb', line 316

def model
  @jtable.model
end

#model=(model) ⇒ Object

forward to @jtable also handle model#emit_data_error



171
172
173
174
# File 'lib/clevic/qt/table_view.rb', line 171

def model=( model )
  setModel( model )
  resize_columns
end

#model_actionsObject

return menu actions for the model, or an empty array if there aren’t any



68
69
70
# File 'lib/clevic/table_view.rb', line 68

def model_actions
  @model_actions ||= []
end

#moveCursor(cursor_action, modifiers) ⇒ Object



176
177
178
179
# File 'lib/clevic/qt/table_view.rb', line 176

def moveCursor( cursor_action, modifiers )
  # TODO use this as a preload indicator
  super
end

#new_rowObject

Add a new row and move to it, provided we’re not in a read-only view.



244
245
246
247
248
249
# File 'lib/clevic/table_view.rb', line 244

def new_row
  sanity_check_read_only_table
  model.add_new_item
  selection_model.clear
  self.current_index = model.create_index( model.row_count - 1, 0 )
end

#next_index!(model_index) ⇒ Object

set and move to index. Leave index value in next_index so that it’s not overridden later. TODO All this next_index stuff is becoming a horrible hack.



279
280
281
# File 'lib/clevic/qt/table_view.rb', line 279

def next_index!( model_index )
  self.current_index = self.next_index = model_index
end

#open_editorObject



234
235
236
237
238
239
240
241
# File 'lib/clevic/table_view.rb', line 234

def open_editor
  # tell the table to edit here
  edit( current_index )

  # tell the editing component to do full edit, eg if it's a combo
  # box to open the list.
  current_index.field.delegate.full_edit
end

#override_next_index(model_index) ⇒ Object

This is to allow entity model UI handlers to tell the view whence to move the cursor when the current editor closes (see closeEditor). TODO not used?



657
658
659
# File 'lib/clevic/table_view.rb', line 657

def override_next_index( model_index )
  self.next_index = model_index
end

#pasteObject

get something from the clipboard and put it at the current selection intended to be called by action / keyboard / menu handlers



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/clevic/table_view_paste.rb', line 12

def paste
  busy_cursor do
    sanity_check_read_only

    # Try text/html then text/plain as tsv or csv
    # LATER maybe use the java-native-application at some point for
    # cut'n'paste internally?
    case
    when clipboard.html?
      paste_html
    when clipboard.text?
      paste_text
    else
      raise PasteError, "clipboard has neither text nor html, so can't paste"
    end
  end
rescue PasteError => e
  show_error e.message
end

#paste_array(arr) ⇒ Object

Paste array to either a single selection or a matching multiple selection TODO Check for rectangularness, ie csv_arr.map{|row| row.size}.uniq.size == 1



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/clevic/table_view_paste.rb', line 109

def paste_array( arr )
  if selection_model.single_cell?
    # only one cell selected, so paste like a spreadsheet
    selected_index = selection_model.selected_indexes.first
    if arr.size == 0 or ( arr.size == 1 and arr.first.size == 0 )
      # empty array, so just clear the current selection
      selected_index.attribute_value = nil
    else
      paste_to_index( selected_index, arr )
    end
  else
    if arr.size == 1 && arr.first.size == 1
      # single value to multiple selection
      paste_value_to_selection arr.first.first
    else
      if selection_model.ranges.size != 1
        raise PasteError, "Can't paste tabular data to multiple selection."
      end

      if selection_model.ranges.first.height != arr.size
        raise PasteError, "Height of paste area (#{selection_model.ranges.first.height}) doesn't match height of data (#{arr.size})."
      end

      if selection_model.ranges.first.width != arr.first.size
        raise PasteError, "Width of paste area (#{selection_model.ranges.first.width}) doesn't match width of data (#{arr.first.size})."
      end

      # size is the same, so do the paste
      paste_to_index( selected_index, arr )
    end
  end
end

#paste_htmlObject

Paste suitable html to the selection Check for presence of tr tags, and make sure there are no colspan or rowspan attributes on td tags.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/clevic/table_view_paste.rb', line 35

def paste_html
  emit_status_text "Fetching data."
  html = clipboard.html

  # This should really be factored out somewhere and tested thoroughly
  emit_status_text "Analysing data."
  doc =
  if html.is_a? Hpricot::Doc
    html
  else
    Hpricot.parse( html )
  end

  # call the plain text paste if we don't have tabular data
  if doc.search( "//tr" ).size == 0
    paste_text
  else
    # throw exception if there are [col|row]span > 1
    spans = doc.search( "//td[@rowspan > 1 || @colspan > 1]" )
    if spans.size > 0
      # make an itemised list of 
      cell_list = spans.map{|x| "- #{x.inner_text}"}.join("\n")
      raise PasteError, <<-EOF
Pasting will not work because source contains spanning cells.
If the source is a spreadsheet, you probably have merged cells
somewhere. Split them, and try copy and paste again.
Cells contain
#{cell_list}
      EOF
    end

    # run through the tabular data and convert to simple array
    emit_status_text "Pasting data."
    ary = ( doc / :tr ).map do |row|
      ( row / :td ).map do |cell|
        # trim leading and trailing \r\n\t

        # check for br
        unless cell.search( '//br' ).empty?
          # treat br as separate lines
          cell.search('//text()').map( &:to_s ).join("\n")
        else
          # otherwise just join text elements
          cell.search( '//text()' ).join('')
        end.gsub( /^[\r\n\t]*/, '').gsub( /[\r\n\t]*$/, '')
      end
    end

    paste_array ary
  end
end

#paste_textObject

LATER probably need a PasteParser or something, to figure out if a file is tsv or csv Try tsv first, because number formats often have embedded ‘,’. if tsv doesn’t work, try with csv and test for rectangularness otherwise assume it’s one string. TODO could also heuristically check paste selection area



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/clevic/table_view_paste.rb', line 93

def paste_text
  text = clipboard.text

  case text
    when /\t/
      paste_array( CSV.parse( text, :col_sep => "\t" ) )
    # assume multi-line text, or text with commas, is csv
    when /[,\n]/
      paste_array( CSV.parse( text, :col_sep => ',' ) )
    else
      paste_value_to_selection( text )
  end
end

#paste_to_index(top_left_index, csv_arr) ⇒ Object

Paste an array to the index, replacing whatever is at that index and whatever is at other indices matching the size of the pasted csv array. Create new rows if there aren’t enough.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/clevic/table_view_paste.rb', line 161

def paste_to_index( top_left_index, csv_arr )
  csv_arr_size = csv_arr.size
  csv_arr.each_with_index do |row,row_index|
    # append row if we need one
    model.add_new_item if top_left_index.row + row_index >= model.row_count

    row.each_with_index do |field, field_index|
      unless top_left_index.column + field_index >= model.column_count
        # do paste
        cell_index = top_left_index.choppy {|i| i.row += row_index; i.column += field_index }
        emit_status_text( "pasted #{row_index+1} of #{csv_arr_size}")
        begin
          cell_index.text_value = field
        rescue
          puts $!.message
          puts $!.backtrace
          show_error( $!.message )
        end
      else
        emit_status_text( "#{pluralize( top_left_index.column + field_index, 'column' )} for pasting data is too large. Truncating." )
      end
    end
    # save records to db via view, so we get error messages
    save_row( top_left_index.choppy {|i| i.row += row_index; i.column = 0 } )
  end

  # make the gui refresh
  model.data_changed do |change|
    change.top_left = top_left_index
    change.bottom_right = top_left_index.choppy do |i|
      i.row += csv_arr.size - 1
      i.column += csv_arr.first.size - 1
    end
  end
end

#paste_value_to_selection(value) ⇒ Object

set all indexes in the selection to the value



143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/clevic/table_view_paste.rb', line 143

def paste_value_to_selection( value )
  selection_model.selected_indexes.each do |index|
    index.text_value = value
    # save records to db via view, so we get error messages
    save_row( index )
  end

  # notify of changed data
  model.data_changed do |change|
    sorted = selection_model.selected_indexes.sort
    change.top_left = sorted.first
    change.bottom_right = sorted.last
  end
end

#pluralize(count, singular, plural = nil) ⇒ Object

copied from actionpack



354
355
356
# File 'lib/clevic/table_view.rb', line 354

def pluralize(count, singular, plural = nil)
  "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
end


180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/clevic/swing/table_view.rb', line 180

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

#raise_widgetObject

make this window visible if it’s in a TabWidget TODO doesn’t really belong here because TableView will not always be in a TabWidget context. Should emit a signal which is a request to raise



341
342
343
344
345
# File 'lib/clevic/qt/table_view.rb', line 341

def raise_widget
  # the tab's parent is a StackedWiget, and its parent is TabWidget
  tab_widget = parent.parent
  tab_widget.current_widget = self if tab_widget.class == Qt::TabWidget
end

#refreshObject

force a complete reload of the current tab’s data



312
313
314
315
316
317
318
# File 'lib/clevic/table_view.rb', line 312

def refresh
  busy_cursor do
    restore_entity do
      model.reload_data
    end
  end
end

#request_focusObject

kind-of override of requestFocus, but it will probably only work from Ruby



223
224
225
# File 'lib/clevic/swing/table_view.rb', line 223

def request_focus
  @jtable.request_focus
end

#resize_columnsObject

resize all fields based on heuristics rather than iterating through the entire data model



347
348
349
350
351
# File 'lib/clevic/table_view.rb', line 347

def resize_columns
  model.fields.each_with_index do |field, index|
    auto_size_column( index, field.sample )
  end
end

#restore_entity(&block) ⇒ Object

Save the current entity, do something, then restore the cursor position to the entity if possible. Return the result of the block.



507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/clevic/table_view.rb', line 507

def restore_entity( &block )
  save_entity = current_index.entity
  unless save_entity.nil?
    save_entity.save if save_entity.changed?
    save_index = current_index
  end

  retval = yield

  # find the entity if possible
  select_entity( save_entity, save_index.column ) unless save_entity.nil?

  retval
end

#sanity_check_dittoObject



154
155
156
157
158
159
# File 'lib/clevic/table_view.rb', line 154

def sanity_check_ditto
  if current_index.row == 0
    emit_status_text( 'No previous record to copy.' )
    throw :insane
  end
end

#sanity_check_read_onlyObject



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/clevic/table_view.rb', line 161

def sanity_check_read_only
  if current_index.field.read_only?
    emit_status_text( 'Can\'t copy into read-only field.' )
  elsif current_index.entity.readonly?
    emit_status_text( 'Can\'t copy into read-only record.' )
  else
    sanity_check_read_only_table
    return
  end
  throw :insane
end

#sanity_check_read_only_tableObject



173
174
175
176
177
178
# File 'lib/clevic/table_view.rb', line 173

def sanity_check_read_only_table
  if model.read_only?
    emit_status_text( 'Can\'t modify a read-only table.' )
    throw :insane
  end
end

#sanity_check_types(from, to) ⇒ Object

from and to are ModelIndex instances. Throws :insane if their fields don’t have the same attribute_type.



195
196
197
198
199
200
# File 'lib/clevic/table_view.rb', line 195

def sanity_check_types( from, to )
  unless from.field.meta.type == to.field.meta.type
    emit_status_text( 'Incompatible data' )
    throw :insane
  end
end

#save_current_rowsObject



454
455
456
457
458
# File 'lib/clevic/table_view.rb', line 454

def save_current_rows
  selection_model.row_indexes.each do |row_index|
    save_row( model.create_index( row_index, 0 ) )
  end
end

#save_row(index) ⇒ Object

save the entity in the row of the given index actually, model.save will check if the record is really changed before writing to DB.



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/clevic/table_view.rb', line 463

def save_row( index )
  if !index.nil? && index.valid? && index.entity
    saved = model.save( index )
    if !saved
      # construct error message(s)
      msg = index.entity.errors.map do |field, errors|
        abbr_value = trim_middle( index.entity.send(field) )
        "#{field} (#{abbr_value}) #{errors.join(',')}"
      end.join( "\n" )

      show_error( "Validation Errors: #{index.rc} #{msg}" )
    end
    saved
  end
end

#search(search_criteria) ⇒ Object

search_criteria must respond to:

  • search_text

  • whole_words?

  • direction ( :forward, :backward )

  • from_start?



620
621
622
623
624
625
626
627
628
629
# File 'lib/clevic/table_view.rb', line 620

def search( search_criteria )
  indexes = model.search( current_index, search_criteria )
  if indexes.size > 0
    emit_status_text( "Found #{search_criteria.search_text} at row #{indexes.first.row}" )
    selection_model.clear
    self.current_index = indexes.first
  else
    emit_status_text( "No match found for #{search_criteria.search_text}" )
  end
end

#search_dialogObject



276
277
278
# File 'lib/clevic/table_view.rb', line 276

def search_dialog
  @search_dialog ||= SearchDialog.new( self )
end

#select_entity(entity, column = nil) ⇒ Object

Move to the row for the given entity and the given column. If column is a symbol, field_column will be called to find the integer index.



589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
# File 'lib/clevic/table_view.rb', line 589

def select_entity( entity, column = nil )
  # sanity check that the entity can actually be found
  raise "entity is nil" if entity.nil?
  unless entity.is_a?( model.entity_class )
    raise "entity #{entity.class.name} does not match class #{model.entity_class.name}"
  end

  # find the row for the saved entity
  found_row = busy_cursor do
    model.collection.index_for_entity( entity )
  end

  # create a new index and move to it
  unless found_row.nil?
    if column.is_a? Symbol
      column = model.field_column( column )
    elsif column.nil?
      column = 0
    else
      raise "column #{column} does not exist" if column >= model.fields.size
    end
    selection_model.clear
    self.current_index = model.create_index( found_row, column || 0 )
  end
end

#selected_rowsObject

return a collection of collections of SwingTableIndex objects indicating the indices of the current selection



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/clevic/qt/table_view.rb', line 75

def selected_rows
  rows = []
  selection_model.selection.each do |selection_range|
    (selection_range.top..selection_range.bottom).each do |row|
      rows << (selection_range.top_left.column..selection_range.bottom_right.column).map do |col|
        model.create_index( row, col )
      end
    end
  end
  rows
end

#selected_rows_or_currentObject



326
327
328
# File 'lib/clevic/table_view.rb', line 326

def selected_rows_or_current
  indexes_or_current( selection_model.row_indexes.map{|row| model.create_index( row, 0 ) } )
end

#selection_modelObject



330
331
332
# File 'lib/clevic/swing/table_view.rb', line 330

def selection_model
  SelectionModel.new( self )
end

#selection_or_currentObject

return an array of the current selection, or the current index in an array if the selection is empty



322
323
324
# File 'lib/clevic/table_view.rb', line 322

def selection_or_current
  indexes_or_current( selection_model.selected_indexes )
end

#set_current_unless_override(model_index) ⇒ Object

Call set_current_index with next_index ( from override_next_index ) or model_index, in that order. Set next_index to nil afterwards.



663
664
665
666
# File 'lib/clevic/table_view.rb', line 663

def set_current_unless_override( model_index )
  set_current_index( @next_index || model_index )
  self.next_index = nil
end

#set_model_data(table_index, value) ⇒ Object



199
200
201
# File 'lib/clevic/qt/table_view.rb', line 199

def set_model_data( table_index, value )
  model.setData( table_index, value.to_variant, Qt::PasteRole )
end

#setModel(model) ⇒ Object

make sure row size is correct show error messages for data



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/clevic/qt/table_view.rb', line 143

def setModel( model )
  # must do this otherwise model gets garbage collected
  @model = model

  # make sure we get nice spacing
  vertical_header.default_section_size = metrics.height
  vertical_header.minimum_section_size = metrics.height
  super

  # set delegates
  model.fields.each_with_index do |field, index|
    set_item_delegate_for_column( index, field.delegate )
  end

  # data errors
  model.connect( SIGNAL( 'data_error(QModelIndex, QVariant, QString)' ) ) do |index,variant,msg|
    show_error( "Incorrect value '#{variant.value}' entered for field [#{index.attribute.to_s}].\nMessage was: #{msg}" )
  end
end

#show_error(msg, title = "Error") ⇒ Object

save the entity in the row of the given index actually, model.save will check if the record is really changed before writing to DB.



206
207
208
209
210
# File 'lib/clevic/qt/table_view.rb', line 206

def show_error( msg )
  error_message = Qt::ErrorMessage.new( self )
  error_message.show_message( msg )
  error_message.show
end

#showEvent(event) ⇒ Object

work around situation where an ItemDelegate is open when the surrouding tab is changed, but the right events don’t arrive.



224
225
226
227
# File 'lib/clevic/qt/table_view.rb', line 224

def showEvent( event )
  super
  @hiding = false
end

#status_text(msg) ⇒ Object



87
88
89
# File 'lib/clevic/qt/table_view.rb', line 87

def status_text( msg )
  emit status_text( msg )
end

#status_text_listenersObject



244
245
246
# File 'lib/clevic/swing/table_view.rb', line 244

def status_text_listeners
  @status_text_listeners ||= Set.new
end

#titleObject



58
59
60
# File 'lib/clevic/table_view.rb', line 58

def title
  @title ||= model.entity_view.title
end

#trim_middle(value, max = 40) ⇒ Object



296
297
298
299
300
301
302
# File 'lib/clevic/swing/table_view.rb', line 296

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

#unfilterObject



522
523
524
525
526
527
# File 'lib/clevic/table_view.rb', line 522

def unfilter
  restore_entity do
    filters.pop.undo
  end
  update_filter_status_bar
end

#unfilter_actionObject



529
530
531
# File 'lib/clevic/table_view.rb', line 529

def unfilter_action
  search_actions.find{|a| a.object_name == 'action_unfilter' }
end

#update_filter_status_barObject

update status bar with a message of all filters concatenated



538
539
540
541
542
# File 'lib/clevic/table_view.rb', line 538

def update_filter_status_bar
  emit_status_text( filter_message )
  emit_filter_status( filtered? )
  unfilter_action.enabled = filtered?
end

#wait_cursorObject



368
369
370
# File 'lib/clevic/swing/table_view.rb', line 368

def wait_cursor
  @wait_cursor ||= java.awt.Cursor.new( java.awt.Cursor::WAIT_CURSOR )
end

#with_table_view(entity_model_or_view) {|tv| ... } ⇒ Object

execute the block with the TableView instance currently handling the entity_model_or_view. Don’t execute the block if nothing is found. TODO doesn’t really belong here because TableView will not always be in a TabWidget context. TODO put it in a module and add the module when the tab widgets are being built.

Yields:

  • (tv)


333
334
335
336
# File 'lib/clevic/qt/table_view.rb', line 333

def with_table_view( entity_model_or_view, &block )
  tv = find_table_view( entity_model_or_view )
  yield( tv ) unless tv.nil?
end