clevic 0.8.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +13 -10
  3. data/README.txt +6 -9
  4. data/Rakefile +35 -24
  5. data/TODO +29 -17
  6. data/bin/clevic +84 -37
  7. data/config/hoe.rb +7 -3
  8. data/lib/clevic.rb +2 -4
  9. data/lib/clevic/browser.rb +37 -49
  10. data/lib/clevic/cache_table.rb +55 -165
  11. data/lib/clevic/db_options.rb +32 -21
  12. data/lib/clevic/default_view.rb +66 -0
  13. data/lib/clevic/delegates.rb +51 -67
  14. data/lib/clevic/dirty.rb +101 -0
  15. data/lib/clevic/extensions.rb +24 -38
  16. data/lib/clevic/field.rb +400 -99
  17. data/lib/clevic/item_delegate.rb +32 -33
  18. data/lib/clevic/model_builder.rb +315 -148
  19. data/lib/clevic/order_attribute.rb +53 -0
  20. data/lib/clevic/record.rb +57 -57
  21. data/lib/clevic/search_dialog.rb +71 -67
  22. data/lib/clevic/sql_dialects.rb +33 -0
  23. data/lib/clevic/table_model.rb +73 -120
  24. data/lib/clevic/table_searcher.rb +165 -0
  25. data/lib/clevic/table_view.rb +140 -100
  26. data/lib/clevic/ui/.gitignore +1 -0
  27. data/lib/clevic/ui/browser_ui.rb +55 -56
  28. data/lib/clevic/ui/search_dialog_ui.rb +50 -51
  29. data/lib/clevic/version.rb +2 -2
  30. data/lib/clevic/view.rb +89 -0
  31. data/models/accounts_models.rb +12 -9
  32. data/models/minimal_models.rb +4 -2
  33. data/models/times_models.rb +41 -25
  34. data/models/times_sqlite_models.rb +1 -145
  35. data/models/values_models.rb +15 -16
  36. data/test/test_cache_table.rb +138 -0
  37. data/test/test_helper.rb +131 -0
  38. data/test/test_model_index_extensions.rb +22 -0
  39. data/test/test_order_attribute.rb +62 -0
  40. data/test/test_sql_dialects.rb +77 -0
  41. data/test/test_table_searcher.rb +188 -0
  42. metadata +36 -20
  43. data/bin/import-times +0 -128
  44. data/config/jamis.rb +0 -589
  45. data/env.sh +0 -1
  46. data/lib/active_record/dirty.rb +0 -87
  47. data/lib/clevic/field_builder.rb +0 -42
  48. data/website/index.html +0 -170
  49. data/website/index.txt +0 -17
  50. data/website/screenshot.png +0 -0
  51. data/website/stylesheets/screen.css +0 -131
  52. data/website/template.html.erb +0 -41
@@ -0,0 +1,165 @@
1
+ require 'clevic/sql_dialects.rb'
2
+
3
+ module Clevic
4
+
5
+ # TODO possibly use AR scopes for this?
6
+ class TableSearcher
7
+ attr_reader :entity_class, :order_attributes, :search_criteria, :field
8
+
9
+ # entity_class is a descendant of ActiveRecord::Base
10
+ # order_attributes is a collection of OrderAttribute objects
11
+ # - field is an instance of Clevic::Field
12
+ # - search_criteria responds to from_start?, direction, whole_words? and search_text
13
+ def initialize( entity_class, order_attributes, search_criteria, field )
14
+ raise "there must be at least one order_attribute" if order_attributes.nil? or order_attributes.empty?
15
+ raise "field must be specified" if field.nil?
16
+ raise "unknown order #{search_criteria.direction}" unless [:forwards, :backwards].include?( search_criteria.direction )
17
+ @entity_class = entity_class
18
+ @order_attributes = order_attributes
19
+ @search_criteria = search_criteria
20
+ @field = field
21
+ end
22
+
23
+ # start_entity is the entity to start from, ie any record found after it will qualify
24
+ def search( start_entity = nil )
25
+ search_field_name =
26
+ if field.is_association?
27
+ # for related tables
28
+ unless [String,Symbol].include?( field.display.class )
29
+ raise( "search field #{field.inspect} cannot have a complex display" )
30
+ end
31
+
32
+ # TODO this will only work with a path value with no dots
33
+ # otherwise the SQL gets complicated with joins etc
34
+ field.display
35
+ else
36
+ # for this table
37
+ entity_class.connection.quote_column_name( field.attribute.to_s )
38
+ end
39
+
40
+ # do the conditions for the search value
41
+ @conditions = search_clause( search_field_name )
42
+
43
+ # if we're not searching from the start, we need
44
+ # to find the next match. Which is complicated from an SQL point of view.
45
+ unless search_criteria.from_start?
46
+ raise "start_entity cannot be nil when from_start is false" if start_entity.nil?
47
+ # build up the ordering conditions
48
+ find_from!( start_entity )
49
+ end
50
+
51
+ # otherwise ActiveRecord thinks that the % in the string
52
+ # is for interpolations instead of treating it a the like wildcard
53
+ conditions_value =
54
+ if !@params.nil? and @params.size > 0
55
+ [ @conditions, @params ]
56
+ else
57
+ @conditions
58
+ end
59
+
60
+ # find the first match
61
+ entity_class.find(
62
+ :first,
63
+ :conditions => conditions_value,
64
+ :order => order,
65
+ :joins => ( field.meta.name if field.is_association? )
66
+ )
67
+ end
68
+
69
+ protected
70
+ include SqlDialects
71
+
72
+ def quote_column( field_name )
73
+ entity_class.connection.quote_column_name( field_name )
74
+ end
75
+
76
+ def quote( value )
77
+ entity_class.connection.quote( value )
78
+ end
79
+
80
+ # recursively create a case statement to do the comparison
81
+ # because and ... and ... and filters on *each* one rather than
82
+ # consecutively.
83
+ # operator is either '<' or '>'
84
+ def build_recursive_comparison( operator, index = 0 )
85
+ # end recursion
86
+ return sql_boolean( false ) if index == order_attributes.size
87
+
88
+ # fetch the current attribute
89
+ attribute = order_attributes[index]
90
+
91
+ # build case statement, including recusion
92
+ st = <<-EOF
93
+ case
94
+ when #{entity_class.table_name}.#{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
95
+ when #{entity_class.table_name}.#{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
96
+ else #{sql_boolean false}
97
+ end
98
+ EOF
99
+ # indent
100
+ st.gsub!( /^/, ' ' * index )
101
+ end
102
+
103
+ # Add the relevant conditions to use start_entity as the
104
+ # entity where the search starts, ie the first one after it is found
105
+ # start_entity is an AR model instance
106
+ # sets @params and @conditions
107
+ def find_from!( start_entity )
108
+ operator =
109
+ case search_criteria.direction
110
+ when :forwards; '>'
111
+ when :backwards; '<'
112
+ end
113
+
114
+ # build the sql comparison where clause fragment
115
+ comparison_sql = build_recursive_comparison( operator )
116
+
117
+ # only Postgres seems to understand real booleans
118
+ # everything else needs the big case statement to be compared
119
+ # to something
120
+ unless entity_class.connection.adapter_name == 'PostgreSQL'
121
+ comparison_sql += " = #{sql_boolean true}"
122
+ end
123
+
124
+ # build parameter values
125
+ @params ||= {}
126
+ order_attributes.each {|x| @params[x.to_sym] = start_entity.send( x.attribute )}
127
+
128
+ @conditions += " and " + comparison_sql
129
+ end
130
+
131
+ # get the search value parameter, in SQL format
132
+ def search_clause( field_name )
133
+ if search_criteria.whole_words?
134
+ <<-EOF
135
+ (
136
+ #{field_name} #{like_operator} #{quote "% #{search_criteria.search_text} %"}
137
+ or
138
+ #{field_name} #{like_operator} #{quote "#{search_criteria.search_text} %"}
139
+ or
140
+ #{field_name} #{like_operator} #{quote "% #{search_criteria.search_text}"}
141
+ )
142
+ EOF
143
+ else
144
+ "#{field_name} #{like_operator} #{quote "%#{search_criteria.search_text}%"}"
145
+ end
146
+ end
147
+
148
+ def ascending_order
149
+ order_attributes.map{|x| x.to_sql}.join(',')
150
+ end
151
+
152
+ def descending_order
153
+ order_attributes.map{|x| x.to_reverse_sql}.join(',')
154
+ end
155
+
156
+ def order
157
+ case search_criteria.direction
158
+ when :forwards; ascending_order
159
+ when :backwards; descending_order
160
+ end
161
+ end
162
+
163
+ end
164
+
165
+ end
@@ -6,41 +6,48 @@ require 'qtext/action_builder.rb'
6
6
 
7
7
  module Clevic
8
8
 
9
- # The view class, implementing neat shortcuts and other pleasantness
9
+ # The view class
10
10
  class TableView < Qt::TableView
11
11
  include ActionBuilder
12
12
 
13
- attr_reader :model_class
14
13
  # whether the model is currently filtered
15
14
  # TODO better in QAbstractSortFilter?
16
15
  attr_accessor :filtered
17
16
  def filtered?; self.filtered; end
18
17
 
19
18
  # status_text is emitted when this object was to display something in the status bar
19
+ # error_test is emitted when an error of some kind must be displayed to the user.
20
20
  # filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
21
21
  signals 'status_text(QString)', 'filter_status(bool)'
22
22
 
23
23
  # model_builder_record is:
24
- # - a subclass of Clevic::Record or ActiveRecord::Base
25
- # - an instance of ModelBuilder
24
+ # - an ActiveRecord::Base subclass that includes Clevic::Record
25
+ # - an instance of Clevic::View
26
26
  # - an instance of TableModel
27
- def initialize( model_builder_record, parent, &block )
27
+ def initialize( arg, parent = nil, &block )
28
28
  # need the empty block here, otherwise Qt bindings grab &block
29
29
  super( parent ) {}
30
30
 
31
- # the model/model_class/builder
31
+ # the model/entity_class/builder
32
32
  case
33
- when model_builder_record.kind_of?( TableModel )
34
- self.model = model_builder_record
33
+ when arg.kind_of?( TableModel )
34
+ self.model = arg
35
+ init_actions( arg.entity_view )
35
36
 
36
- when model_builder_record.ancestors.include?( ActiveRecord::Base )
37
- with_record( model_builder_record, &block )
38
-
39
- when model_builder_record.kind_of?( Clevic::ModelBuilder )
40
- with_builder( model_builder_record, &block )
41
-
42
37
  else
43
- raise "Don't know what to do with #{model_builder_record}"
38
+ # arg is a subclass of Clevic::View
39
+ model_builder = arg.define_ui
40
+ model_builder.exec_ui_block( &block )
41
+
42
+ # make sure the TableView has a fully-populated TableModel
43
+ # self.model is necessary to invoke the Qt layer
44
+ self.model = model_builder.build( self )
45
+ self.object_name = arg.widget_name
46
+
47
+ # connect data_changed signals for the entity_class to respond
48
+ connect_view_signals( arg )
49
+
50
+ init_actions( arg )
44
51
  end
45
52
 
46
53
  # see closeEditor
@@ -54,38 +61,24 @@ class TableView < Qt::TableView
54
61
  self.sorting_enabled = false
55
62
  @filtered = false
56
63
 
57
- # turn off "Object#type deprecated" messages
58
- $VERBOSE = nil
59
-
60
- init_actions
61
64
  self.context_menu_policy = Qt::ActionsContextMenu
62
65
  end
63
66
 
64
- def with_builder( model_builder, &block )
65
- model_builder.instance_eval( &block ) unless block.nil?
66
-
67
- # make sure the TableView has a fully-populated TableModel
68
- self.model = model_builder.build( self )
69
-
70
- # connect data_changed signals for the model_class to respond
71
- connect_model_class_signals( model_class )
72
- end
73
-
74
- def with_record( model_class, &block )
75
- builder = ModelBuilder.new( model_class )
76
- with_builder( builder, &block )
67
+ def title
68
+ @title ||= model.entity_view.title
77
69
  end
78
70
 
79
- def connect_model_class_signals( model_class )
80
- # this is only here because model_class.data_changed needs the view.
81
- # Should probably fix that.
82
- if model_class.respond_to?( :data_changed )
83
- model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
84
- model_class.data_changed( top_left, bottom_right, self )
85
- end
71
+ def connect_view_signals( entity_view )
72
+ model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
73
+ entity_view.notify_data_changed( self, top_left, bottom_right )
86
74
  end
87
75
  end
88
76
 
77
+ # find the row index for the given field
78
+ def field_column( field )
79
+ model.fields.each_with_index {|x,i| return i if x.attribute == field }
80
+ end
81
+
89
82
  # return menu actions for the model, or an empty array if there aren't any
90
83
  def model_actions
91
84
  @model_actions ||= []
@@ -94,23 +87,27 @@ class TableView < Qt::TableView
94
87
  # hook for the sanity_check_xxx methods
95
88
  # called for the actions set up by ActionBuilder
96
89
  # it just wraps the action block/method in a catch
97
- # block for :insane
90
+ # block for :insane. Will also catch exceptions thrown in actions to make
91
+ # core application more robust to model & view errors.
98
92
  def action_triggered( &block )
99
- catch :insane do
100
- yield
93
+ begin
94
+ catch :insane do
95
+ yield
96
+ end
97
+ rescue Exception => e
98
+ puts e.message
99
+ puts e.backtrace
101
100
  end
102
101
  end
103
102
 
104
- def init_actions
103
+ def init_actions( entity_view )
105
104
  # add model actions, if they're defined
106
- if model_class.respond_to?( :actions )
107
- list( :model ) do |ab|
108
- model_class.actions( self, ab )
109
- end
110
- separator
105
+ list( :model ) do |ab|
106
+ entity_view.define_actions( self, ab )
111
107
  end
108
+ separator
112
109
 
113
- # list of actions called edit
110
+ # list of actions in the edit menu
114
111
  list( :edit ) do
115
112
  #~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
116
113
  action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
@@ -216,7 +213,7 @@ class TableView < Qt::TableView
216
213
  def ditto
217
214
  sanity_check_ditto
218
215
  sanity_check_read_only
219
- one_up_index = model.create_index( current_index.row - 1, current_index.column )
216
+ one_up_index = current_index.choppy { |i| i.row -= 1 }
220
217
  previous_value = one_up_index.attribute_value
221
218
  if current_index.attribute_value != previous_value
222
219
  current_index.attribute_value = previous_value
@@ -224,14 +221,24 @@ class TableView < Qt::TableView
224
221
  end
225
222
  end
226
223
 
224
+ # from and to are ModelIndex instances. Throws :insane if
225
+ # their fields don't have the same attribute_type.
226
+ def sanity_check_types( from, to )
227
+ unless from.field.attribute_type == to.field.attribute_type
228
+ emit status_text( 'Incompatible data' )
229
+ throw :insane
230
+ end
231
+ end
232
+
227
233
  def ditto_right
228
234
  sanity_check_ditto
229
235
  sanity_check_read_only
230
- unless current_index.column < model.column_count
236
+ if current_index.column >= model.column_count - 1
231
237
  emit status_text( 'No column to the right' )
232
238
  else
233
- one_up_right_index = model.create_index( current_index.row - 1, current_index.column + 1 )
234
- current_index.attribute_value = one_up_right_index.attribute_value
239
+ one_up_right = current_index.choppy {|i| i.row -= 1; i.column += 1 }
240
+ sanity_check_types( one_up_right, current_index )
241
+ current_index.attribute_value = one_up_right.attribute_value
235
242
  emit model.dataChanged( current_index, current_index )
236
243
  end
237
244
  end
@@ -242,8 +249,9 @@ class TableView < Qt::TableView
242
249
  unless current_index.column > 0
243
250
  emit status_text( 'No column to the left' )
244
251
  else
245
- one_up_left_index = model.create_index( current_index.row - 1, current_index.column - 1 )
246
- current_index.attribute_value = one_up_left_index.attribute_value
252
+ one_up_left = current_index.choppy { |i| i.row -= 1; i.column -= 1 }
253
+ sanity_check_types( one_up_left, current_index )
254
+ current_index.attribute_value = one_up_left.attribute_value
247
255
  emit model.dataChanged( current_index, current_index )
248
256
  end
249
257
  end
@@ -260,16 +268,20 @@ class TableView < Qt::TableView
260
268
  delegate.full_edit
261
269
  end
262
270
 
271
+ # Add a new row and move to it, provided we're not in a read-only view.
263
272
  def new_row
264
273
  sanity_check_read_only_table
265
274
  model.add_new_item
266
- new_row_index = model.index( model.collection.size - 1, 0 )
275
+ new_row_index = model.index( model.row_count - 1, 0 )
267
276
  currentChanged( new_row_index, current_index )
268
277
  selection_model.clear
269
278
  self.current_index = new_row_index
270
279
  end
271
280
 
272
- def deleted_selection
281
+ # Delete the current selection. If it's a set of rows, just delete
282
+ # them. If it's a rectangular selection, set the cells to nil.
283
+ # TODO make sure all affected rows are saved.
284
+ def delete_selection
273
285
  sanity_check_read_only
274
286
 
275
287
  # translate from ModelIndex objects to row indices
@@ -346,13 +358,13 @@ class TableView < Qt::TableView
346
358
 
347
359
  # fetch font size
348
360
  fnt = font
349
- #~ fnt.bold = true
361
+ fnt.bold = true
350
362
  opt.fontMetrics = Qt::FontMetrics.new( fnt )
351
363
 
352
- # set data
364
+ # set data
353
365
  opt.text = data.to_s
354
366
 
355
- # icon size. Not needed
367
+ # icon size. Not needed
356
368
  #~ variant = d->model->headerData(logicalIndex, d->orientation, Qt::DecorationRole);
357
369
  #~ opt.icon = qvariant_cast<QIcon>(variant);
358
370
  #~ if (opt.icon.isNull())
@@ -396,7 +408,7 @@ class TableView < Qt::TableView
396
408
  super
397
409
 
398
410
  # set delegates
399
- self.item_delegate = Clevic::ItemDelegate.new( self )
411
+ #~ self.item_delegate = Clevic::ItemDelegate.new( self, field )
400
412
  model.fields.each_with_index do |field, index|
401
413
  set_item_delegate_for_column( index, field.delegate )
402
414
  end
@@ -429,25 +441,40 @@ class TableView < Qt::TableView
429
441
  super
430
442
  end
431
443
 
432
- # paste a CSV array to the index
433
- # TODO make additional rows if we need them, or at least check for enough space
444
+ # copied from actionpack
445
+ def pluralize(count, singular, plural = nil)
446
+ "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
447
+ end
448
+
449
+ # Paste a CSV array to the index, replacing whatever is at that index
450
+ # and whatever is at other indices matching the size of the pasted
451
+ # csv array. Create new rows if there aren't enough.
434
452
  def paste_to_index( top_left_index, csv_arr )
435
453
  csv_arr.each_with_index do |row,row_index|
454
+ # append row if we need one
455
+ model.add_new_item if top_left_index.row + row_index >= model.row_count
456
+
436
457
  row.each_with_index do |field, field_index|
437
- cell_index = model.create_index( top_left_index.row + row_index, top_left_index.column + field_index )
438
- model.setData( cell_index, field.to_variant, Qt::PasteRole )
458
+ unless top_left_index.column + field_index >= model.column_count
459
+ # do paste
460
+ cell_index = top_left_index.choppy {|i| i.row += row_index; i.column += field_index }
461
+ model.setData( cell_index, field.to_variant, Qt::PasteRole )
462
+ else
463
+ emit status_text( "#{pluralize( top_left_index.column + field_index, 'column' )} for pasting data is too large. Truncating." )
464
+ end
439
465
  end
440
466
  # save records to db
441
- model.save( model.create_index( top_left_index.row + row_index, 0 ) )
467
+ model.save( top_left_index.choppy {|i| i.row += row_index; i.column = 0 } )
442
468
  end
443
469
 
444
470
  # make the gui refresh
445
- bottom_right_index = model.create_index( top_left_index.row + csv_arr.size - 1, top_left_index.column + csv_arr[0].size - 1 )
471
+ bottom_right_index = top_left_index.choppy {|i| i.row += csv_arr.size - 1; i.column += csv_arr[0].size - 1 }
446
472
  emit model.dataChanged( top_left_index, bottom_right_index )
447
473
  emit model.headerDataChanged( Qt::Vertical, top_left_index.row, top_left_index.row + csv_arr.size )
448
474
  end
449
475
 
450
- def delete_multiple_cells?
476
+ # ask the question in a dialog. If the user says yes, execute the block
477
+ def delete_multiple_cells?( question = 'Are you sure you want to delete multiple cells?', &block )
451
478
  sanity_check_read_only
452
479
 
453
480
  # go ahead with delete if there's only 1 cell, or the user says OK
@@ -457,7 +484,7 @@ class TableView < Qt::TableView
457
484
  msg = Qt::MessageBox.new(
458
485
  Qt::MessageBox::Question,
459
486
  'Multiple Delete',
460
- 'Are you sure you want to delete multiple cells?',
487
+ question,
461
488
  Qt::MessageBox::Yes | Qt::MessageBox::No,
462
489
  self
463
490
  )
@@ -465,30 +492,37 @@ class TableView < Qt::TableView
465
492
  else
466
493
  true
467
494
  end
468
- end
469
495
 
496
+ yield if delete_ok
497
+ end
498
+
499
+ # Ask if multiple cell delete is OK, then replace contents
500
+ # of selected cells with nil.
470
501
  def delete_cells
471
- cells_deleted = false
472
-
473
- # do delete
474
- if delete_multiple_cells?
502
+ delete_multiple_cells? do
503
+ cells_deleted = false
504
+
505
+ # do delete
475
506
  selection_model.selected_indexes.each do |index|
476
507
  index.attribute_value = nil
477
508
  cells_deleted = true
478
509
  end
479
- end
480
-
481
- # deletes were done, so emit dataChanged
482
- if cells_deleted
483
- # emit data changed for all ranges
484
- selection_model.selection.each do |selection_range|
485
- emit dataChanged( selection_range.top_left, selection_range.bottom_right )
510
+
511
+ # deletes were done, so emit dataChanged
512
+ if cells_deleted
513
+ # save affected rows
514
+ selection_model.selected_rows.each { |index| index.entity.save }
515
+
516
+ # emit data changed for all ranges
517
+ selection_model.selection.each do |selection_range|
518
+ emit dataChanged( selection_range.top_left, selection_range.bottom_right )
519
+ end
486
520
  end
487
521
  end
488
522
  end
489
523
 
490
524
  def delete_rows
491
- if delete_multiple_cells?
525
+ delete_multiple_cells?( 'Are you sure you want to delete multiple rows?' ) do
492
526
  model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
493
527
  end
494
528
  end
@@ -496,19 +530,18 @@ class TableView < Qt::TableView
496
530
  # handle certain key combinations that aren't shortcuts
497
531
  def keyPressEvent( event )
498
532
  begin
499
- # call to model class for shortcuts
500
- if model.model_class.respond_to?( :key_press_event )
501
- begin
502
- model_result = model.model_class.key_press_event( event, current_index, self )
503
- return model_result if model_result != nil
504
- rescue Exception => e
505
- puts e.backtrace
506
- error_message = Qt::ErrorMessage.new( self )
507
- error_message.show_message( "Error in shortcut handler for #{model.model_class.name}: #{e.message}" )
508
- error_message.show
509
- end
533
+ # call to entity class for shortcuts
534
+ begin
535
+ view_result = model.entity_view.notify_key_press( self, event, current_index )
536
+ return view_result unless view_result.nil?
537
+ rescue Exception => e
538
+ puts e.backtrace
539
+ error_message = Qt::ErrorMessage.new( self )
540
+ error_message.show_message( "Error in shortcut handler for #{model.entity_view.name}: #{e.message}" )
541
+ error_message.show
510
542
  end
511
543
 
544
+ # thrown by the sanity_check_xxx methods
512
545
  catch :insane do
513
546
  case
514
547
  # on the last row, and down is pressed
@@ -525,7 +558,13 @@ class TableView < Qt::TableView
525
558
  # TODO this is actually a shortcut
526
559
  when event.ctrl? && event.return?
527
560
  new_row
528
-
561
+
562
+ when event.delete?
563
+ if selection_model.selected_indexes.size > 1
564
+ delete_selection
565
+ return true
566
+ end
567
+
529
568
  else
530
569
  #~ puts event.inspect
531
570
  end
@@ -624,15 +663,15 @@ class TableView < Qt::TableView
624
663
  # override to prevent tab pressed from editing next field
625
664
  # also takes into account that override_next_index may have been called
626
665
  def closeEditor( editor, end_edit_hint )
627
- puts "end_edit_hint: #{end_edit_hint.inspect}"
666
+ puts "end_edit_hint: #{end_edit_hint.inspect}" if $options[:debug]
628
667
  case end_edit_hint
629
668
  when Qt::AbstractItemDelegate.EditNextItem
630
669
  super( editor, Qt::AbstractItemDelegate.NoHint )
631
- set_current_unless_override( model.create_index( current_index.row, current_index.column + 1 ) )
670
+ set_current_unless_override( current_index.choppy { |i| i.column += 1 } )
632
671
 
633
672
  when Qt::AbstractItemDelegate.EditPreviousItem
634
673
  super( editor, Qt::AbstractItemDelegate.NoHint )
635
- set_current_unless_override( model.create_index( current_index.row, current_index.column - 1 ) )
674
+ set_current_unless_override( current_index.choppy { |i| i.column -= 1 } )
636
675
 
637
676
  else
638
677
  super
@@ -651,6 +690,7 @@ class TableView < Qt::TableView
651
690
  end
652
691
 
653
692
  save_entity = current_index.entity
693
+ save_entity.save if save_entity.changed?
654
694
  save_index = current_index
655
695
 
656
696
  unless self.filtered
@@ -659,7 +699,7 @@ class TableView < Qt::TableView
659
699
  if indexes.empty?
660
700
  self.filtered = false
661
701
  elsif indexes.size > 1
662
- puts "Can't do multiple selection filters yet"
702
+ emit status_text( "Can't do multiple selection filters yet" )
663
703
  self.filtered = false
664
704
  end
665
705
 
@@ -686,7 +726,7 @@ class TableView < Qt::TableView
686
726
  unless found_row.nil?
687
727
  self.current_index = model.create_index( found_row, save_index.column )
688
728
  if self.filtered?
689
- emit status_text( "Filtered on #{current_index.field_name} = #{current_index.gui_value}" )
729
+ emit status_text( "Filtered on #{current_index.field.label} = #{current_index.gui_value}" )
690
730
  else
691
731
  emit status_text( nil )
692
732
  end
@@ -699,7 +739,7 @@ class TableView < Qt::TableView
699
739
  # * direction ( :forward, :backward )
700
740
  # * from_start?
701
741
  #
702
- # TODO formalise this
742
+ # TODO formalise this?
703
743
  def search( search_criteria )
704
744
  indexes = model.search( current_index, search_criteria )
705
745
  if indexes.size > 0