clevic 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,40 @@
1
+ require 'active_record.rb'
2
+ require 'active_record/dirty.rb'
3
+
4
+ module Clevic
5
+
6
+ module Default
7
+ module ClassMethods
8
+ def define_ui_block; nil; end
9
+
10
+ def post_default_ui_block
11
+ @post_default_ui_block
12
+ end
13
+
14
+ def post_default_ui( &block )
15
+ @post_default_ui_block = block
16
+ end
17
+ end
18
+
19
+ def self.included(base)
20
+ base.extend(ClassMethods)
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ module ActiveRecord
27
+ class Base
28
+ include Clevic::Default
29
+ end
30
+ end
31
+
1
32
  module Clevic
2
33
 
3
34
  # The base class for all Clevic model and UI definitions.
4
35
  # minimal definition is like this
5
36
  # class User < Clevic::Record; end
6
- # This will automatically keep track of the order
37
+ # Record automatically keeps track of the order
7
38
  # in which models are defined, so that tabs can
8
39
  # be constructed in that order.
9
40
  class Record < ActiveRecord::Base
@@ -11,6 +42,13 @@ module Clevic
11
42
  self.abstract_class = true
12
43
  @@subclass_order = []
13
44
 
45
+ def self.define_ui_block
46
+ @define_ui_block
47
+ end
48
+
49
+ # keep track of the order in which subclasses are
50
+ # defined, so that can be used as the default ordering
51
+ # of the views.
14
52
  def self.inherited( subclass )
15
53
  @@subclass_order << subclass
16
54
  super
@@ -19,10 +57,15 @@ module Clevic
19
57
  def self.models
20
58
  @@subclass_order
21
59
  end
22
-
60
+
23
61
  def self.models=( array )
24
62
  @@subclass_order = array
25
63
  end
64
+
65
+ # use this to define UI blocks using the ModelBuilder DSL
66
+ def self.define_ui( &block )
67
+ @define_ui_block = block
68
+ end
26
69
  end
27
70
 
28
71
  end
@@ -13,8 +13,6 @@ module Clevic
13
13
  This table model allows an ActiveRecord or ActiveResource to be used as a
14
14
  basis for a Qt::AbstractTableModel for viewing in a Qt::TableView.
15
15
 
16
- Initial idea by Richard Dale and Silvio Fonseca.
17
-
18
16
  * labels are the headings in the table view
19
17
 
20
18
  * dots are the dotted attribute paths that specify how to get values from
@@ -30,8 +28,22 @@ Initial idea by Richard Dale and Silvio Fonseca.
30
28
  class TableModel < Qt::AbstractTableModel
31
29
  include QtFlags
32
30
 
33
- attr_accessor :collection, :dots, :attributes, :attribute_paths, :labels
31
+ # the CacheTable of Clevic::Record or ActiveRecord::Base objects
32
+ attr_reader :collection
33
+
34
+ # the actual class for the collection objects
35
+ attr_accessor :model_class
36
+
37
+ # the collection of Clevic::Field objects
38
+ attr_reader :fields
39
+
40
+ attr_accessor :read_only
41
+ def read_only?; read_only; end
34
42
 
43
+ # should this model create a new empty record by default?
44
+ attr_accessor :auto_new
45
+ def auto_new?; auto_new; end
46
+
35
47
  signals(
36
48
  # index where error occurred, value, message
37
49
  'data_error(QModelIndex,QVariant,QString)',
@@ -39,10 +51,44 @@ class TableModel < Qt::AbstractTableModel
39
51
  'dataChanged(const QModelIndex&,const QModelIndex&)'
40
52
  )
41
53
 
42
- def initialize( builder )
43
- super()
54
+ def initialize( parent = nil )
55
+ super
56
+ @metadatas = []
57
+ end
58
+
59
+ def fields=( arr )
60
+ @fields = arr
61
+
62
+ #reset these
44
63
  @metadatas = []
45
- @builder = builder
64
+ @dots = nil
65
+ @labels = nil
66
+ @attributes = nil
67
+ @attribute_paths = nil
68
+ end
69
+
70
+ def dots
71
+ @dots ||= fields.map {|x| x.column }
72
+ end
73
+
74
+ def labels
75
+ @labels ||= fields.map {|x| x.label }
76
+ end
77
+
78
+ def attributes
79
+ @attributes ||= fields.map {|x| x.attribute }
80
+ end
81
+
82
+ def attribute_paths
83
+ @attribute_paths ||= fields.map {|x| x.attribute_path }
84
+ end
85
+
86
+ def collection=( arr )
87
+ @collection = arr
88
+ # fill in an empty record for data entry
89
+ if collection.size == 0 && auto_new?
90
+ collection << model_class.new
91
+ end
46
92
  end
47
93
 
48
94
  def sort( col, order )
@@ -54,6 +100,7 @@ class TableModel < Qt::AbstractTableModel
54
100
  super
55
101
  end
56
102
 
103
+ # this is called for read-only tables.
57
104
  def match( start_index, role, search_value, hits, match_flags )
58
105
  #~ Qt::MatchExactly 0 Performs QVariant-based matching.
59
106
  #~ Qt::MatchFixedString 8 Performs string-based matching. String-based comparisons are case-insensitive unless the MatchCaseSensitive flag is also specified.
@@ -64,22 +111,19 @@ class TableModel < Qt::AbstractTableModel
64
111
  #~ Qt::MatchRegExp 4 Performs string-based matching using a regular expression as the search term.
65
112
  #~ Qt::MatchWildcard 5 Performs string-based matching using a string with wildcards as the search term.
66
113
  #~ Qt::MatchWrap 32 Perform a search that wraps around, so that when the search reaches the last item in the model, it begins again at the first item and continues until all items have been examined.
67
- super
114
+ #~ super
115
+ []
68
116
  end
69
117
 
70
- def build_dots( dots, attrs, prefix="" )
71
- attrs.inject( dots ) do |cols, a|
72
- if a[1].respond_to? :attributes
73
- build_keys(cols, a[1].attributes, prefix + a[0] + ".")
74
- else
75
- cols << prefix + a[0]
76
- end
77
- end
78
- end
79
-
80
- def model_class
81
- @builder.model_class
82
- end
118
+ #~ def build_dots( dots, attrs, prefix="" )
119
+ #~ attrs.inject( dots ) do |cols, a|
120
+ #~ if a[1].respond_to? :attributes
121
+ #~ build_keys(cols, a[1].attributes, prefix + a[0] + ".")
122
+ #~ else
123
+ #~ cols << prefix + a[0]
124
+ #~ end
125
+ #~ end
126
+ #~ end
83
127
 
84
128
  # cache metadata (ActiveRecord#column_for_attribute) because it's not going
85
129
  # to change over the lifetime of the table
@@ -161,7 +205,11 @@ class TableModel < Qt::AbstractTableModel
161
205
  if model_index.metadata.type == :boolean
162
206
  retval = item_boolean_flags
163
207
  end
164
- retval |= qt_item_is_editable.to_i unless model_index.field.read_only?
208
+
209
+ # read-only
210
+ unless model_index.field.read_only? || model_index.entity.readonly? || read_only?
211
+ retval |= qt_item_is_editable.to_i
212
+ end
165
213
  retval
166
214
  end
167
215
 
@@ -179,7 +227,7 @@ class TableModel < Qt::AbstractTableModel
179
227
  when qt_display_role
180
228
  case orientation
181
229
  when Qt::Horizontal
182
- @labels[section]
230
+ labels[section]
183
231
  when Qt::Vertical
184
232
  # don't force a fetch from the db
185
233
  if collection.cached_at?( section )
@@ -200,8 +248,27 @@ class TableModel < Qt::AbstractTableModel
200
248
  nil
201
249
 
202
250
  when qt_tooltip_role
203
- if orientation == Qt::Horizontal
204
- @builder.fields[section].tooltip
251
+ case orientation
252
+ when Qt::Horizontal
253
+ fields[section].tooltip
254
+
255
+ when Qt::Vertical
256
+ case
257
+ when !collection[section].errors.empty?
258
+ 'Invalid data'
259
+ when collection[section].changed?
260
+ 'Unsaved changes'
261
+ end
262
+ end
263
+
264
+ when qt_background_role
265
+ if orientation == Qt::Vertical
266
+ case
267
+ when !collection[section].errors.empty?
268
+ Qt::Color.new( 'orange' )
269
+ when collection[section].changed?
270
+ Qt::Color.new( 'yellow' )
271
+ end
205
272
  end
206
273
 
207
274
  else
@@ -240,22 +307,36 @@ class TableModel < Qt::AbstractTableModel
240
307
 
241
308
  when qt_text_alignment_role
242
309
  index.field.alignment
243
-
310
+
244
311
  # these are just here to make debug output quieter
245
312
  when qt_size_hint_role;
246
- when qt_background_role;
313
+
314
+ # show field with a red background if there's an error
315
+ when qt_background_role
316
+ Qt::Color.new( 'red' ) if index.has_errors?
317
+
247
318
  when qt_font_role;
248
319
  when qt_foreground_role
249
- Qt::Color.new( 'dimgray' ) if index.field.read_only?
320
+ if index.field.read_only? || index.entity.readonly? || read_only?
321
+ Qt::Color.new( 'dimgray' )
322
+ end
323
+
250
324
  when qt_decoration_role;
251
325
 
252
- # provide a tooltip when an empty relational field is encountered
253
326
  when qt_tooltip_role
254
- if index.metadata.type == :association
255
- index.field.delegate.if_empty_message
256
- end
257
-
258
- 'Read-only' if index.field.read_only?
327
+ case
328
+ # show ActiveRecord validation errors
329
+ when index.has_errors?
330
+ index.errors.join("\n")
331
+
332
+ # provide a tooltip when an empty relational field is encountered
333
+ when index.metadata.type == :association
334
+ index.field.delegate.if_empty_message
335
+
336
+ # read-only field
337
+ when index.field.read_only?
338
+ 'Read-only'
339
+ end
259
340
  else
260
341
  puts "data index: #{index}, role: #{const_as_string(role)}" if $options[:debug]
261
342
  nil
@@ -264,7 +345,7 @@ class TableModel < Qt::AbstractTableModel
264
345
  # return a variant
265
346
  retval.to_variant
266
347
  rescue Exception => e
267
- puts e.backtrace.join( "\n" )
348
+ puts e.backtrace
268
349
  puts "#{index.inspect} #{value.inspect} #{index.entity.inspect} #{e.message}"
269
350
  nil.to_variant
270
351
  end
@@ -276,7 +357,7 @@ class TableModel < Qt::AbstractTableModel
276
357
  case role
277
358
  when qt_edit_role
278
359
  # Don't allow the primary key to be changed
279
- return false if index.attribute == :id
360
+ return false if index.attribute == model_class.primary_key.to_sym
280
361
 
281
362
  if ( index.column < 0 || index.column >= dots.size )
282
363
  raise "invalid column #{index.column}"
@@ -338,7 +419,7 @@ class TableModel < Qt::AbstractTableModel
338
419
 
339
420
  when qt_checkstate_role
340
421
  if index.metadata.type == :boolean
341
- index.entity.toggle!( index.attribute )
422
+ index.entity.toggle( index.attribute )
342
423
  true
343
424
  else
344
425
  false
@@ -379,6 +460,7 @@ class TableModel < Qt::AbstractTableModel
379
460
  end
380
461
 
381
462
  # return a set of indexes that match the search criteria
463
+ # TODO this implementation is very un-ruby.
382
464
  def search( start_index, search_criteria )
383
465
  # get the search value parameter, in SQL format
384
466
  search_value =
@@ -388,17 +470,32 @@ class TableModel < Qt::AbstractTableModel
388
470
  "%#{search_criteria.search_text}%"
389
471
  end
390
472
 
391
- # build up the conditions
473
+ # build up the ordering conditions
392
474
  bits = collection.build_sql_find( start_index.entity, search_criteria.direction )
393
- conditions = "#{model_class.connection.quote_column_name( start_index.field_name )} #{like_operator} :search_value"
475
+
476
+ # do the conditions for the search value
477
+ conditions =
478
+ if start_index.field.is_association?
479
+ # for related tables
480
+ # TODO this will only work with a path value with no dots
481
+ "#{start_index.field.path} #{like_operator} :search_value"
482
+ else
483
+ # for this table
484
+ "#{model_class.connection.quote_column_name( start_index.field_name )} #{like_operator} :search_value"
485
+ end
486
+
487
+ # add ordering conditions
394
488
  conditions += ( " and " + bits[:sql] ) unless search_criteria.from_start?
489
+
395
490
  params = { :search_value => search_value }
396
491
  params.merge!( bits[:params] ) unless search_criteria.from_start?
492
+
397
493
  # find the first match
398
494
  entity = model_class.find(
399
495
  :first,
400
496
  :conditions => [ conditions, params ],
401
- :order => search_criteria.direction == :forwards ? collection.order : collection.reverse_order
497
+ :order => search_criteria.direction == :forwards ? collection.order : collection.reverse_order,
498
+ :joins => ( start_index.field.meta.name if start_index.field.is_association? )
402
499
  )
403
500
 
404
501
  # return matched indexes
@@ -411,7 +508,7 @@ class TableModel < Qt::AbstractTableModel
411
508
  end
412
509
 
413
510
  def field_for_index( model_index )
414
- @builder.fields[model_index.column]
511
+ fields[model_index.column]
415
512
  end
416
513
 
417
514
  end
@@ -2,25 +2,47 @@ require 'rubygems'
2
2
  require 'Qt4'
3
3
  require 'fastercsv'
4
4
  require 'clevic/model_builder.rb'
5
+ require 'qtext/action_builder.rb'
5
6
 
6
7
  module Clevic
7
8
 
8
9
  # The view class, implementing neat shortcuts and other pleasantness
9
10
  class TableView < Qt::TableView
10
- attr_reader :model_class, :builder
11
+ include ActionBuilder
12
+
13
+ attr_reader :model_class
11
14
  # whether the model is currently filtered
12
15
  # TODO better in QAbstractSortFilter?
13
16
  attr_accessor :filtered
17
+ def filtered?; self.filtered; end
14
18
 
15
- # this is emitted when this object was to display something in the status bar
16
- signals 'status_text(QString)'
19
+ # status_text is emitted when this object was to display something in the status bar
20
+ # filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
21
+ signals 'status_text(QString)', 'filter_status(bool)'
17
22
 
18
- def initialize( model_class, parent, *args )
19
- super( parent )
20
-
21
- # the AR entity class
22
- @model_class = model_class
23
+ # model_builder_record is:
24
+ # - a subclass of Clevic::Record or ActiveRecord::Base
25
+ # - an instance of ModelBuilder
26
+ # - an instance of TableModel
27
+ def initialize( model_builder_record, parent, &block )
28
+ # need the empty block here, otherwise Qt bindings grab &block
29
+ super( parent ) {}
23
30
 
31
+ # the model/model_class/builder
32
+ case
33
+ when model_builder_record.kind_of?( TableModel )
34
+ self.model = model_builder_record
35
+
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
+ else
43
+ raise "Don't know what to do with #{model_builder_record}"
44
+ end
45
+
24
46
  # see closeEditor
25
47
  @index_override = false
26
48
 
@@ -34,21 +56,278 @@ class TableView < Qt::TableView
34
56
 
35
57
  # turn off "Object#type deprecated" messages
36
58
  $VERBOSE = nil
59
+
60
+ init_actions
61
+ self.context_menu_policy = Qt::ActionsContextMenu
62
+ end
63
+
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 )
77
+ end
78
+
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
86
+ end
87
+ end
88
+
89
+ # return menu actions for the model, or an empty array if there aren't any
90
+ def model_actions
91
+ @model_actions ||= []
92
+ end
93
+
94
+ # hook for the sanity_check_xxx methods
95
+ # called for the actions set up by ActionBuilder
96
+ # it just wraps the action block/method in a catch
97
+ # block for :insane
98
+ def action_triggered( &block )
99
+ catch :insane do
100
+ yield
101
+ end
102
+ end
103
+
104
+ def init_actions
105
+ # 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
111
+ end
112
+
113
+ # list of actions called edit
114
+ list( :edit ) do
115
+ #~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
116
+ action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
117
+ action :action_paste, '&Paste', :shortcut => Qt::KeySequence::Paste, :method => :paste
118
+ separator
119
+ action :action_ditto, '&Ditto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
120
+ action :action_ditto_right, 'Ditto R&ight', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
121
+ action :action_ditto_left, '&Ditto L&eft', :shortcut => 'Ctrl+[', :method => :ditto_left, :tool_tip => 'Copy field one to left from previous record'
122
+ action :action_insert_date, 'Insert Date', :shortcut => 'Ctrl+;', :method => :insert_current_date
123
+ action :action_open_editor, '&Open Editor', :shortcut => 'F4', :method => :open_editor
124
+ separator
125
+ action :action_row, 'New Ro&w', :shortcut => 'Ctrl+N', :method => :new_row
126
+ action :action_refresh, '&Refresh', :shortcut => 'Ctrl+R', :method => :refresh
127
+ action :action_delete_rows, 'Delete Rows', :shortcut => 'Ctrl+Delete', :method => :delete_rows
128
+
129
+ if $options[:debug]
130
+ action :action_dump, 'D&ump', :shortcut => 'Ctrl+Shift+D' do
131
+ puts model.collection[current_index.row].inspect
132
+ end
133
+ end
134
+ end
135
+
136
+ separator
137
+
138
+ # list of actions called search
139
+ list( :search ) do
140
+ action :action_find, '&Find', :shortcut => Qt::KeySequence::Find, :method => :find
141
+ action :action_find_next, 'Find &Next', :shortcut => Qt::KeySequence::FindNext, :method => :find_next
142
+ action :action_filter, 'Fil&ter', :checkable => true, :shortcut => 'Ctrl+L', :method => :filter_by_current
143
+ action :action_highlight, '&Highlight', :visible => false, :shortcut => 'Ctrl+H'
144
+ end
37
145
  end
38
146
 
39
- def create_model( &block )
40
- raise "provide a block" unless block
41
- @builder = Clevic::ModelBuilder.new( self )
42
- @builder.instance_eval( &block )
43
- @builder.build
44
- model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
45
- if @model_class.respond_to?( :data_changed )
46
- @model_class.data_changed( top_left, bottom_right, self )
147
+ def copy_current_selection
148
+ text = String.new
149
+ selection_model.selection.each do |selection_range|
150
+ (selection_range.top..selection_range.bottom).each do |row|
151
+ row_ary = Array.new
152
+ selection_model.selected_indexes.each do |index|
153
+ row_ary << index.gui_value if index.row == row
154
+ end
155
+ text << row_ary.to_csv
47
156
  end
48
157
  end
49
- self
158
+ Qt::Application::clipboard.text = text
159
+ end
160
+
161
+ def paste
162
+ sanity_check_read_only
163
+
164
+ # remove trailing "\n" if there is one
165
+ text = Qt::Application::clipboard.text.chomp
166
+ arr = FasterCSV.parse( text )
167
+
168
+ return true if selection_model.selection.size != 1
169
+
170
+ selection_range = selection_model.selection[0]
171
+ selected_index = selection_model.selected_indexes[0]
172
+
173
+ if selection_range.single_cell?
174
+ # only one cell selected, so paste like a spreadsheet
175
+ if text.empty?
176
+ # just clear the current selection
177
+ model.setData( selected_index, nil.to_variant )
178
+ else
179
+ paste_to_index( selected_index, arr )
180
+ end
181
+ else
182
+ return true if selection_range.height != arr.size
183
+ return true if selection_range.width != arr[0].size
184
+
185
+ # size is the same, so do the paste
186
+ paste_to_index( selected_index, arr )
187
+ end
188
+ end
189
+
190
+ def sanity_check_ditto
191
+ if current_index.row == 0
192
+ emit status_text( 'No previous record to copy.' )
193
+ throw :insane
194
+ end
195
+ end
196
+
197
+ def sanity_check_read_only
198
+ if current_index.field.read_only?
199
+ emit status_text( 'Can\'t copy into read-only field.' )
200
+ elsif current_index.entity.readonly?
201
+ emit status_text( 'Can\'t copy into read-only record.' )
202
+ else
203
+ sanity_check_read_only_table
204
+ return
205
+ end
206
+ throw :insane
207
+ end
208
+
209
+ def sanity_check_read_only_table
210
+ if model.read_only?
211
+ emit status_text( 'Can\'t modify a read-only table.' )
212
+ throw :insane
213
+ end
214
+ end
215
+
216
+ def ditto
217
+ sanity_check_ditto
218
+ sanity_check_read_only
219
+ one_up_index = model.create_index( current_index.row - 1, current_index.column )
220
+ previous_value = one_up_index.attribute_value
221
+ if current_index.attribute_value != previous_value
222
+ current_index.attribute_value = previous_value
223
+ emit model.dataChanged( current_index, current_index )
224
+ end
225
+ end
226
+
227
+ def ditto_right
228
+ sanity_check_ditto
229
+ sanity_check_read_only
230
+ unless current_index.column < model.column_count
231
+ emit status_text( 'No column to the right' )
232
+ 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
235
+ emit model.dataChanged( current_index, current_index )
236
+ end
237
+ end
238
+
239
+ def ditto_left
240
+ sanity_check_ditto
241
+ sanity_check_read_only
242
+ unless current_index.column > 0
243
+ emit status_text( 'No column to the left' )
244
+ 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
247
+ emit model.dataChanged( current_index, current_index )
248
+ end
50
249
  end
250
+
251
+ def insert_current_date
252
+ sanity_check_read_only
253
+ current_index.attribute_value = Time.now
254
+ emit model.dataChanged( current_index, current_index )
255
+ end
256
+
257
+ def open_editor
258
+ edit( current_index )
259
+ delegate = item_delegate( current_index )
260
+ delegate.full_edit
261
+ end
262
+
263
+ def new_row
264
+ sanity_check_read_only_table
265
+ model.add_new_item
266
+ new_row_index = model.index( model.collection.size - 1, 0 )
267
+ currentChanged( new_row_index, current_index )
268
+ selection_model.clear
269
+ self.current_index = new_row_index
270
+ end
271
+
272
+ def deleted_selection
273
+ sanity_check_read_only
51
274
 
275
+ # translate from ModelIndex objects to row indices
276
+ rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
277
+ unless rows.empty?
278
+ # header rows are selected, so delete them
279
+ model.remove_rows( rows )
280
+ else
281
+ # otherwise various cells are selected, so delete the cells
282
+ delete_cells
283
+ end
284
+ end
285
+
286
+ # display a search dialog, and find the entered text
287
+ def find
288
+ @search_dialog ||= SearchDialog.new
289
+ result = @search_dialog.exec( current_index.gui_value )
290
+
291
+ override_cursor( Qt::BusyCursor ) do
292
+ case result
293
+ when Qt::Dialog::Accepted
294
+ search_for = @search_dialog.search_text
295
+ search( @search_dialog )
296
+ when Qt::Dialog::Rejected
297
+ puts "Don't search"
298
+ else
299
+ puts "unknown dialog code #{result}"
300
+ end
301
+ end
302
+ end
303
+
304
+ def find_next
305
+ if @search_dialog.nil?
306
+ emit status_text( 'No previous find' )
307
+ else
308
+ override_cursor( Qt::BusyCursor ) do
309
+ save_from_start = @search_dialog.from_start?
310
+ @search_dialog.from_start = false
311
+ search( @search_dialog )
312
+ @search_dialog.from_start = save_from_start
313
+ end
314
+ end
315
+ end
316
+
317
+ # force a complete reload of the current tab's data
318
+ def refresh
319
+ override_cursor( Qt::BusyCursor ) do
320
+ model.reload_data
321
+ end
322
+ end
323
+
324
+ # toggle the filter, based on current selection.
325
+ def filter_by_current( bool_filter )
326
+ # TODO if there's no selection, use the current index instead
327
+ filter_by_indexes( selection_model.selected_indexes )
328
+ emit filter_status( bool_filter )
329
+ end
330
+
52
331
  # alternative access for auto_size_column
53
332
  def auto_size_attribute( attribute, sample )
54
333
  col = model.attributes.index( attribute )
@@ -109,9 +388,20 @@ class TableView < Qt::TableView
109
388
  # make sure row size is correct
110
389
  # show error messages for data
111
390
  def setModel( model )
391
+ # must do this otherwise model gets garbage collected
392
+ @model = model
393
+
394
+ # make sure we get nice spacing
112
395
  vertical_header.default_section_size = vertical_header.minimum_section_size
113
396
  super
114
397
 
398
+ # set delegates
399
+ self.item_delegate = Clevic::ItemDelegate.new( self )
400
+ model.fields.each_with_index do |field, index|
401
+ set_item_delegate_for_column( index, field.delegate )
402
+ end
403
+
404
+ # data errors
115
405
  model.connect( SIGNAL( 'data_error(QModelIndex, QVariant, QString)' ) ) do |index,variant,msg|
116
406
  error_message = Qt::ErrorMessage.new( self )
117
407
  error_message.show_message( "Incorrect value '#{variant.value}' entered for field [#{index.attribute.to_s}].\nMessage was: #{msg}" )
@@ -129,7 +419,7 @@ class TableView < Qt::TableView
129
419
  # resize all fields based on heuristics rather
130
420
  # than iterating through the entire data model
131
421
  def resize_columns
132
- @builder.fields.each_with_index do |field, index|
422
+ model.fields.each_with_index do |field, index|
133
423
  auto_size_column( index, field.sample )
134
424
  end
135
425
  end
@@ -158,6 +448,8 @@ class TableView < Qt::TableView
158
448
  end
159
449
 
160
450
  def delete_multiple_cells?
451
+ sanity_check_read_only
452
+
161
453
  # go ahead with delete if there's only 1 cell, or the user says OK
162
454
  delete_ok =
163
455
  if selection_model.selected_indexes.size > 1
@@ -195,10 +487,14 @@ class TableView < Qt::TableView
195
487
  end
196
488
  end
197
489
 
490
+ def delete_rows
491
+ if delete_multiple_cells?
492
+ model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
493
+ end
494
+ end
495
+
496
+ # handle certain key combinations that aren't shortcuts
198
497
  def keyPressEvent( event )
199
- # for some reason, trying to call another method inside
200
- # the begin .. rescue block throws a superclass method not
201
- # found error. Weird.
202
498
  begin
203
499
  # call to model class for shortcuts
204
500
  if model.model_class.respond_to?( :key_press_event )
@@ -213,138 +509,30 @@ class TableView < Qt::TableView
213
509
  end
214
510
  end
215
511
 
216
- # now do all the usual shortcuts
217
- case
218
- # on the last row, and down is pressed
219
- # add a new row
220
- when event.down? && last_row?
221
- model.add_new_item
222
-
223
- # on the right-bottom cell, and tab is pressed
224
- # then add a new row
225
- when event.tab? && last_cell?
226
- model.add_new_item
227
-
228
- # delete the current row
229
- when event.ctrl? && event.delete?
230
- if delete_multiple_cells?
231
- model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
232
- end
233
-
234
- # copy the value from the row one above
235
- when event.ctrl? && event.apostrophe?
236
- if current_index.row > 0
237
- one_up_index = model.create_index( current_index.row - 1, current_index.column )
238
- previous_value = one_up_index.attribute_value
239
- if current_index.attribute_value != previous_value
240
- current_index.attribute_value = previous_value
241
- emit model.dataChanged( current_index, current_index )
242
- end
243
- end
244
-
245
- # copy the value from the previous row, one cell right
246
- when event.ctrl? && event.bracket_right?
247
- if current_index.row > 0 && current_index.column < model.column_count
248
- one_up_right_index = model.create_index( current_index.row - 1, current_index.column + 1 )
249
- current_index.attribute_value = one_up_right_index.attribute_value
250
- emit model.dataChanged( current_index, current_index )
251
- end
252
-
253
- # copy the value from the previous row, one cell left
254
- when event.ctrl? && event.bracket_left?
255
- if current_index.row > 0 && current_index.column > 0
256
- one_up_left_index = model.create_index( current_index.row - 1, current_index.column - 1 )
257
- current_index.attribute_value = one_up_left_index.attribute_value
258
- emit model.dataChanged( current_index, current_index )
259
- end
260
-
261
- # insert today's date in the current field
262
- when event.ctrl? && event.semicolon?
263
- current_index.attribute_value = Time.now
264
- emit model.dataChanged( current_index, current_index )
265
-
266
- # dump current record to stdout
267
- when event.ctrl? && event.d?
268
- puts model.collection[current_index.row].inspect
269
-
270
- # add new record and go to it
271
- when event.ctrl? && ( event.n? || event.return? )
272
- model.add_new_item
273
- new_row_index = model.index( model.collection.size - 1, 0 )
274
- currentChanged( new_row_index, current_index )
275
- selection_model.clear
276
- self.current_index = new_row_index
277
-
278
- # handle deletion of entire rows
279
- when event.delete?
280
- # translate from ModelIndex objects to row indices
281
- rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
282
- unless rows.empty?
283
- # header rows are selected, so delete them
284
- model.remove_rows( rows )
285
- # make sure no other handlers get this event
286
- return true
287
- else
288
- # otherwise various cells are selected, so delete the cells
289
- delete_cells
290
- # nobody else handles this
291
- return true
292
- end
293
-
294
- # f4 should open editor immediately
295
- when event.f4?
296
- edit( current_index, Qt::AbstractItemView::AllEditTriggers, event )
297
- delegate = item_delegate( current_index )
298
- delegate.full_edit
299
-
300
- # copy currently selected data in csv format
301
- when event.ctrl? && event.c?
302
- text = String.new
303
- selection_model.selection.each do |selection_range|
304
- (selection_range.top..selection_range.bottom).each do |row|
305
- row_ary = Array.new
306
- selection_model.selected_indexes.each do |index|
307
- row_ary << index.gui_value if index.row == row
308
- end
309
- text << row_ary.to_csv
310
- end
311
- end
312
- Qt::Application::clipboard.text = text
313
- return true
314
-
315
- when event.ctrl? && event.v?
316
- # remove trailing "\n" if there is one
317
- text = Qt::Application::clipboard.text.chomp
318
- arr = FasterCSV.parse( text )
319
-
320
- return true if selection_model.selection.size != 1
321
-
322
- selection_range = selection_model.selection[0]
323
- selected_index = selection_model.selected_indexes[0]
324
-
325
- if selection_range.single_cell?
326
- # only one cell selected, so paste like a spreadsheet
327
- if text.empty?
328
- # just clear the current selection
329
- model.setData( selected_index, nil.to_variant )
330
- else
331
- paste_to_index( selected_index, arr )
332
- end
333
- else
334
- return true if selection_range.height != arr.size
335
- return true if selection_range.width != arr[0].size
512
+ catch :insane do
513
+ case
514
+ # on the last row, and down is pressed
515
+ # add a new row
516
+ when event.down? && last_row?
517
+ new_row
336
518
 
337
- # size is the same, so do the paste
338
- paste_to_index( selected_index, arr )
519
+ # on the right-bottom cell, and tab is pressed
520
+ # then add a new row
521
+ when event.tab? && last_cell?
522
+ new_row
523
+
524
+ # add new record and go to it
525
+ # TODO this is actually a shortcut
526
+ when event.ctrl? && event.return?
527
+ new_row
528
+
529
+ else
530
+ #~ puts event.inspect
339
531
  end
340
- return true
341
-
342
- else
343
- #~ puts event.inspect
344
532
  end
345
533
  super
346
534
  rescue Exception => e
347
- puts e.backtrace.join( "\n" )
535
+ puts e.backtrace
348
536
  puts e.message
349
537
  error_message = Qt::ErrorMessage.new( self )
350
538
  error_message.show_message( "Error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
@@ -366,7 +554,7 @@ class TableView < Qt::TableView
366
554
  saved = model.save( index )
367
555
  if !saved
368
556
  error_message = Qt::ErrorMessage.new( self )
369
- msg = model.collection[index.row].errors.join("\n")
557
+ msg = model.collection[index.row].errors.to_a.join("\n")
370
558
  error_message.show_message( msg )
371
559
  error_message.show
372
560
  end
@@ -420,7 +608,7 @@ class TableView < Qt::TableView
420
608
 
421
609
  def focusOutEvent( event )
422
610
  super
423
- save_current_row
611
+ #~ save_current_row
424
612
  end
425
613
 
426
614
  # this is the only method that is called when an itemDelegate is open
@@ -455,6 +643,7 @@ class TableView < Qt::TableView
455
643
  # otherwise turn filtering off.
456
644
  # Sets self.filter to true if filtering worked, false otherwise.
457
645
  # indexes is a collection of Qt::ModelIndex
646
+ # TODO combine with filter_by_current
458
647
  def filter_by_indexes( indexes )
459
648
  unless indexes[0].field.filterable?
460
649
  emit status_text( "Can't filter on #{indexes[0].field.label}" )
@@ -496,6 +685,11 @@ class TableView < Qt::TableView
496
685
  # create a new index and move to it
497
686
  unless found_row.nil?
498
687
  self.current_index = model.create_index( found_row, save_index.column )
688
+ if self.filtered?
689
+ emit status_text( "Filtered on #{current_index.field_name} = #{current_index.gui_value}" )
690
+ else
691
+ emit status_text( nil )
692
+ end
499
693
  end
500
694
  end
501
695
 
@@ -521,6 +715,7 @@ class TableView < Qt::TableView
521
715
  puts "itemDelegateForColumn #{column}"
522
716
  super
523
717
  end
718
+
524
719
  end
525
720
 
526
721
  end