clevic 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,431 @@
1
+ require 'Qt4'
2
+ require 'date'
3
+
4
+ require 'qtext/flags.rb'
5
+ require 'qtext/extensions.rb'
6
+
7
+ require 'clevic/extensions.rb'
8
+ require 'clevic/model_column'
9
+
10
+ module Clevic
11
+
12
+ =begin rdoc
13
+ This table model allows an ActiveRecord or ActiveResource to be used as a
14
+ basis for a Qt::AbstractTableModel for viewing in a Qt::TableView.
15
+
16
+ Initial idea by Richard Dale and Silvio Fonseca.
17
+
18
+ * labels are the headings in the table view
19
+
20
+ * dots are the dotted attribute paths that specify how to get values from
21
+ the underlying ActiveRecord model
22
+
23
+ * attribute_paths is a collection of attribute symbols. It comes from
24
+ dots, and is split on /\./
25
+
26
+ * attributes are the first-level of the dots
27
+
28
+ * collection is the set of ActiveRecord model objects (also called entities)
29
+ =end
30
+ class TableModel < Qt::AbstractTableModel
31
+ include QtFlags
32
+
33
+ attr_accessor :collection, :dots, :attributes, :attribute_paths, :labels
34
+
35
+ signals(
36
+ # index where error occurred, value, message
37
+ 'data_error(QModelIndex,QVariant,QString)',
38
+ # top_left, bottom_right
39
+ 'dataChanged(const QModelIndex&,const QModelIndex&)'
40
+ )
41
+
42
+ def initialize( builder )
43
+ super()
44
+ @metadatas = []
45
+ @builder = builder
46
+ end
47
+
48
+ def hasChildren( *args )
49
+ puts 'hasChildren'
50
+ puts "args: #{args.inspect}"
51
+ super
52
+ end
53
+
54
+ def sort( col, order )
55
+ puts 'sort'
56
+ puts "col: #{col.inspect}"
57
+ #~ Qt::AscendingOrder
58
+ #~ Qt::DescendingOrder
59
+ puts "order: #{order.inspect}"
60
+ super
61
+ end
62
+
63
+ def match( start_index, role, search_value, hits, match_flags )
64
+ #~ Qt::MatchExactly 0 Performs QVariant-based matching.
65
+ #~ Qt::MatchFixedString 8 Performs string-based matching. String-based comparisons are case-insensitive unless the MatchCaseSensitive flag is also specified.
66
+ #~ Qt::MatchContains 1 The search term is contained in the item.
67
+ #~ Qt::MatchStartsWith 2 The search term matches the start of the item.
68
+ #~ Qt::MatchEndsWith 3 The search term matches the end of the item.
69
+ #~ Qt::MatchCaseSensitive 16 The search is case sensitive.
70
+ #~ Qt::MatchRegExp 4 Performs string-based matching using a regular expression as the search term.
71
+ #~ Qt::MatchWildcard 5 Performs string-based matching using a string with wildcards as the search term.
72
+ #~ 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.
73
+ super
74
+ end
75
+
76
+ def build_dots( dots, attrs, prefix="" )
77
+ attrs.inject( dots ) do |cols, a|
78
+ if a[1].respond_to? :attributes
79
+ build_keys(cols, a[1].attributes, prefix + a[0] + ".")
80
+ else
81
+ cols << prefix + a[0]
82
+ end
83
+ end
84
+ end
85
+
86
+ def model_class
87
+ @builder.model_class
88
+ end
89
+
90
+ # cache metadata (ActiveRecord#column_for_attribute) because it's not going
91
+ # to change over the lifetime of the table
92
+ # if the column is an attribute, create a ModelColumn
93
+ # TODO use ActiveRecord::Base.reflections instead
94
+ def metadata( column )
95
+ if @metadatas[column].nil?
96
+ meta = model_class.columns_hash[attributes[column].to_s]
97
+ if meta.nil?
98
+ meta = model_class.columns_hash[ "#{attributes[column]}_id" ]
99
+ if meta.nil?
100
+ return nil
101
+ else
102
+ @metadatas[column] = ModelColumn.new( attributes[column], :association, meta )
103
+ end
104
+ else
105
+ @metadatas[column] = meta
106
+ end
107
+ end
108
+ @metadatas[column]
109
+ end
110
+
111
+ def add_new_item
112
+ # 1 new row
113
+ begin_insert_rows( Qt::ModelIndex.invalid, row_count, row_count )
114
+ collection << model_class.new
115
+ end_insert_rows
116
+ end
117
+
118
+ # rows is a collection of integers specifying row indices to remove
119
+ # TODO call begin_remove and end_remove around the whole block
120
+ def remove_rows( rows )
121
+ # delete from the end to avoid holes affecting the indexing
122
+ rows.sort.reverse.each do |index|
123
+ # remove the item from the collection
124
+ begin_remove_rows( Qt::ModelIndex.invalid, index, index )
125
+ removed = collection.delete_at( index )
126
+ end_remove_rows
127
+ # destroy the db object, and its table row
128
+ removed.destroy
129
+ end
130
+ end
131
+
132
+ # save the AR model at the given index, if it's dirty
133
+ def save( index )
134
+ item = collection[index.row]
135
+ return false if item.nil?
136
+ if item.changed?
137
+ if item.valid?
138
+ item.save
139
+ else
140
+ false
141
+ end
142
+ else
143
+ # AR model not changed
144
+ true
145
+ end
146
+ end
147
+
148
+ def rowCount( parent = nil )
149
+ collection.size
150
+ end
151
+
152
+ def row_count
153
+ collection.size
154
+ end
155
+
156
+ def columnCount( parent = nil )
157
+ dots.size
158
+ end
159
+
160
+ def column_count
161
+ dots.size
162
+ end
163
+
164
+ def flags( model_index )
165
+ # TODO don't return IsEditable if the model is read-only
166
+ retval = qt_item_is_editable | super( model_index )
167
+ if model_index.metadata.type == :boolean
168
+ retval = item_boolean_flags
169
+ end
170
+ retval
171
+ end
172
+
173
+ def fetchMore( parent )
174
+ #~ puts "fetchMore"
175
+ #~ reload_data if canFetchMore( parent )
176
+ end
177
+
178
+ def canFetchMore( parent )
179
+ false
180
+ #~ puts "canFetchMore"
181
+ #~ puts "self.collection.size: #{self.collection.size.inspect}"
182
+ #~ puts "self.collection.sql_count: #{self.collection.sql_count.inspect}"
183
+ # Here, test for self.collection.size - new_records != self.collection.sql_count
184
+ # maintaining new_records will be the tricky part
185
+ #~ result = self.collection.size != self.collection.sql_count
186
+ #~ puts "result: #{result.inspect}"
187
+ #~ result
188
+ end
189
+
190
+ def reload_data( options = {} )
191
+ # renew cache
192
+ self.collection = self.collection.renew( options )
193
+ # tell the UI we had a major data change
194
+ reset
195
+ end
196
+
197
+ # values for horizontal and vertical headers
198
+ def headerData( section, orientation, role )
199
+ value =
200
+ case role
201
+ when qt_display_role
202
+ case orientation
203
+ when Qt::Horizontal
204
+ @labels[section]
205
+ when Qt::Vertical
206
+ # don't force a fetch from the db
207
+ if collection.cached_at?( section )
208
+ collection[section].id
209
+ else
210
+ section
211
+ end
212
+ end
213
+
214
+ when qt_text_alignment_role
215
+ case orientation
216
+ when Qt::Vertical
217
+ Qt::AlignRight | Qt::AlignVCenter
218
+ end
219
+
220
+ when Qt::SizeHintRole
221
+ # anything other than nil here makes the headers disappear.
222
+ nil
223
+
224
+ when qt_tooltip_role
225
+ if orientation == Qt::Horizontal
226
+ @builder.fields[section].tooltip
227
+ end
228
+
229
+ else
230
+ #~ puts "headerData section: #{section}, role: #{const_as_string(role)}" if $options[:debug]
231
+ nil
232
+ end
233
+
234
+ return value.to_variant
235
+ end
236
+
237
+ # Provide data to UI.
238
+ def data( index, role = qt_display_role )
239
+ #~ puts "data for index: #{index.inspect} and role: #{const_as_string role}"
240
+ begin
241
+ retval =
242
+ case role
243
+ when qt_display_role, qt_edit_role
244
+ # boolean values generally don't have text next to them in this context
245
+ # check explicitly to avoid fetching the entity from
246
+ # the model's collection when we don't need to
247
+ unless index.metadata.type == :boolean
248
+ begin
249
+ value = index.gui_value
250
+ unless value.nil?
251
+ field = @builder.fields[index.column]
252
+ field.do_format( value )
253
+ end
254
+ rescue Exception => e
255
+ puts e.backtrace
256
+ end
257
+ end
258
+
259
+ when qt_checkstate_role
260
+ if index.metadata.type == :boolean
261
+ index.gui_value ? qt_checked : qt_unchecked
262
+ end
263
+
264
+ when qt_text_alignment_role
265
+ @builder.fields[index.column].alignment
266
+
267
+ # these are just here to make debug output quieter
268
+ when qt_size_hint_role;
269
+ when qt_background_role;
270
+ when qt_font_role;
271
+ when qt_foreground_role;
272
+ when qt_decoration_role;
273
+
274
+ # provide a tooltip when an empty relational field is encountered
275
+ when qt_tooltip_role
276
+ if index.metadata.type == :association
277
+ @builder.fields[index.column].delegate.if_empty_message
278
+ end
279
+
280
+ else
281
+ puts "data index: #{index}, role: #{const_as_string(role)}" if $options[:debug]
282
+ nil
283
+ end
284
+
285
+ # return a variant
286
+ retval.to_variant
287
+ rescue Exception => e
288
+ puts e.backtrace.join( "\n" )
289
+ puts "#{index.inspect} #{value.inspect} #{index.entity.inspect} #{e.message}"
290
+ nil.to_variant
291
+ end
292
+ end
293
+
294
+ # data sent from UI
295
+ def setData( index, variant, role = qt_edit_role )
296
+ if index.valid?
297
+ case role
298
+ when qt_edit_role
299
+ # Don't allow the primary key to be changed
300
+ return false if index.attribute == :id
301
+
302
+ if ( index.column < 0 || index.column >= dots.size )
303
+ raise "invalid column #{index.column}"
304
+ end
305
+
306
+ type = index.metadata.type
307
+ value = variant.value
308
+
309
+ # translate the value from the ui to something that
310
+ # the AR model will understand
311
+ begin
312
+ index.gui_value =
313
+ case
314
+ when value.class.name == 'Qt::Date'
315
+ Date.new( value.year, value.month, value.day )
316
+
317
+ when value.class.name == 'Qt::Time'
318
+ Time.new( value.hour, value.min, value.sec )
319
+
320
+ # allow flexibility in entering dates. For example
321
+ # 16jun, 16-jun, 16 jun, 16 jun 2007 would be accepted here
322
+ # TODO need to be cleverer about which year to use
323
+ # for when you're entering 16dec and you're in the next
324
+ # year
325
+ when type == :date && value =~ %r{^(\d{1,2})[ /-]?(\w{3})$}
326
+ Date.parse( "#$1 #$2 #{Time.now.year.to_s}" )
327
+
328
+ # if a digit only is entered, fetch month and year from
329
+ # previous row
330
+ when type == :date && value =~ %r{^(\d{1,2})$}
331
+ previous_entity = collection[index.row - 1]
332
+ # year,month,day
333
+ Date.new( previous_entity.date.year, previous_entity.date.month, $1.to_i )
334
+
335
+ # this one is mostly to fix date strings that have come
336
+ # out of the db and been formatted
337
+ when type == :date && value =~ %r{^(\d{2})[ /-](\w{3})[ /-](\d{2})$}
338
+ Date.parse( "#$1 #$2 20#$3" )
339
+
340
+ # allow lots of flexibility in entering times
341
+ # 01:17, 0117, 117, 1 17, are all accepted
342
+ when type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
343
+ Time.parse( "#$1:#$2" )
344
+
345
+ else
346
+ value
347
+ end
348
+
349
+ emit dataChanged( index, index )
350
+ # value conversion was successful
351
+ true
352
+ rescue Exception => e
353
+ puts e.backtrace.join( "\n" )
354
+ puts e.message
355
+ emit data_error( index, variant, e.message )
356
+ # value conversion was not successful
357
+ false
358
+ end
359
+
360
+ when qt_checkstate_role
361
+ if index.metadata.type == :boolean
362
+ index.entity.toggle!( index.attribute )
363
+ true
364
+ else
365
+ false
366
+ end
367
+
368
+ # user-defined role
369
+ # TODO this only works with single-dotted paths
370
+ when qt_paste_role
371
+ if index.metadata.type == :association
372
+ field = @builder.fields[index.column]
373
+ association_class = field.class_name.constantize
374
+ candidates = association_class.find( :all, :conditions => [ "#{field.attribute_path[1]} = ?", variant.value ] )
375
+ case candidates.size
376
+ when 0; puts "No match for #{variant.value}"
377
+ when 1; index.attribute_value = candidates[0]
378
+ else; puts "Too many for #{variant.value}"
379
+ end
380
+ else
381
+ index.attribute_value = variant.value
382
+ end
383
+ true
384
+
385
+ else
386
+ puts "role: #{role.inspect}"
387
+ true
388
+
389
+ end
390
+ else
391
+ false
392
+ end
393
+ end
394
+
395
+ # return a set of indexes that match the search criteria
396
+ def search( start_index, search_criteria )
397
+ # get the search value parameter, in SQL format
398
+ search_value =
399
+ if search_criteria.whole_words?
400
+ "% #{search_criteria.search_text} %"
401
+ else
402
+ "%#{search_criteria.search_text}%"
403
+ end
404
+
405
+ # build up the conditions
406
+ bits = collection.build_sql_find( start_index.entity, search_criteria.direction )
407
+ conditions = "#{model_class.connection.quote_column_name( start_index.field_name )} ilike :search_value"
408
+ conditions += ( " and " + bits[:sql] ) unless search_criteria.from_start?
409
+ params = { :search_value => search_value }
410
+ params.merge!( bits[:params] ) unless search_criteria.from_start?
411
+ #~ puts "conditions: #{conditions.inspect}"
412
+ #~ puts "params: #{params.inspect}"
413
+ # find the first match
414
+ entity = model_class.find(
415
+ :first,
416
+ :conditions => [ conditions, params ],
417
+ :order => search_criteria.direction == :forwards ? collection.order : collection.reverse_order
418
+ )
419
+
420
+ # return matched indexes
421
+ if entity != nil
422
+ found_row = collection.index_for_entity( entity )
423
+ [ create_index( found_row, start_index.column ) ]
424
+ else
425
+ []
426
+ end
427
+ end
428
+
429
+ end
430
+
431
+ end #module
@@ -0,0 +1,479 @@
1
+ require 'rubygems'
2
+ require 'Qt4'
3
+ require 'fastercsv'
4
+ require 'clevic/model_builder.rb'
5
+
6
+ module Clevic
7
+
8
+ # The view class, implementing neat shortcuts and other pleasantness
9
+ class TableView < Qt::TableView
10
+ attr_reader :model_class, :builder
11
+ # whether the model is currently filtered
12
+ # TODO better in QAbstractSortFilter?
13
+ attr_accessor :filtered
14
+
15
+ # this is emitted when this object was to display something in the status bar
16
+ signals 'status_text(QString)'
17
+
18
+ def initialize( model_class, parent, *args )
19
+ super( parent )
20
+
21
+ # the AR entity class
22
+ @model_class = model_class
23
+
24
+ # see closeEditor
25
+ @index_override = false
26
+
27
+ # set some Qt things
28
+ self.horizontal_header.movable = false
29
+ # TODO might be useful to allow movable vertical rows,
30
+ # but need to change the shortcut ideas of next and previous rows
31
+ self.vertical_header.movable = false
32
+ self.sorting_enabled = false
33
+ @filtered = false
34
+
35
+ # turn off "Object#type deprecated" messages
36
+ $VERBOSE = nil
37
+ end
38
+
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 )
47
+ end
48
+ end
49
+ self
50
+ end
51
+
52
+ # alternative access for auto_size_column
53
+ def auto_size_attribute( attribute, sample )
54
+ col = model.attributes.index( attribute )
55
+ self.set_column_width( col, column_size( col, sample ).width )
56
+ end
57
+
58
+ # set the size of the column from the sample
59
+ def auto_size_column( col, sample )
60
+ self.set_column_width( col, column_size( col, sample ).width )
61
+ end
62
+
63
+ # set the size of the column from the string value of the data
64
+ # mostly copied from qheaderview.cpp:2301
65
+ def column_size( col, data )
66
+ opt = Qt::StyleOptionHeader.new
67
+
68
+ # fetch font size
69
+ fnt = font
70
+ #~ fnt.bold = true
71
+ opt.fontMetrics = Qt::FontMetrics.new( fnt )
72
+
73
+ # set data
74
+ opt.text = data.to_s
75
+
76
+ # icon size. Not needed
77
+ #~ variant = d->model->headerData(logicalIndex, d->orientation, Qt::DecorationRole);
78
+ #~ opt.icon = qvariant_cast<QIcon>(variant);
79
+ #~ if (opt.icon.isNull())
80
+ #~ opt.icon = qvariant_cast<QPixmap>(variant);
81
+
82
+ size = Qt::Size.new( 100, 30 )
83
+ # final parameter could be header section
84
+ style.sizeFromContents( Qt::Style::CT_HeaderSection, opt, size );
85
+ end
86
+
87
+ def relational_delegate( attribute, options )
88
+ col = model.attributes.index( attribute )
89
+ delegate = RelationalDelegate.new( self, model.columns[col], options )
90
+ set_item_delegate_for_column( col, delegate )
91
+ end
92
+
93
+ def delegate( attribute, delegate_class, options = nil )
94
+ col = model.attributes.index( attribute )
95
+ delegate = delegate_class.new( self, attribute, options )
96
+ set_item_delegate_for_column( col, delegate )
97
+ end
98
+
99
+ # is current_index on the last row?
100
+ def last_row?
101
+ current_index.row == model.row_count - 1
102
+ end
103
+
104
+ # is current_index on the bottom_right cell?
105
+ def last_cell?
106
+ current_index.row == model.row_count - 1 && current_index.column == model.column_count - 1
107
+ end
108
+
109
+ # make sure row size is correct
110
+ # show error messages for data
111
+ def setModel( model )
112
+ vertical_header.default_section_size = vertical_header.minimum_section_size
113
+ super
114
+
115
+ model.connect( SIGNAL( 'data_error(QModelIndex, QVariant, QString)' ) ) do |index,variant,msg|
116
+ error_message = Qt::ErrorMessage.new( self )
117
+ error_message.show_message( "Incorrect value '#{variant.value}' entered for field [#{index.attribute.to_s}].\nMessage was: #{msg}" )
118
+ error_message.show
119
+ end
120
+ end
121
+
122
+ # and override this because the Qt bindings don't call
123
+ # setModel otherwise
124
+ def model=( model )
125
+ setModel( model )
126
+ resize_columns
127
+ end
128
+
129
+ # resize all fields based on heuristics rather
130
+ # than iterating through the entire data model
131
+ def resize_columns
132
+ @builder.fields.each_with_index do |field, index|
133
+ auto_size_column( index, field.sample )
134
+ end
135
+ end
136
+
137
+ def moveCursor( cursor_action, modifiers )
138
+ # TODO use this as a preload indicator
139
+ super
140
+ end
141
+
142
+ # paste a CSV array to the index
143
+ # TODO make additional rows if we need them, or at least check for enough space
144
+ def paste_to_index( top_left_index, csv_arr )
145
+ csv_arr.each_with_index do |row,row_index|
146
+ row.each_with_index do |field, field_index|
147
+ cell_index = model.create_index( top_left_index.row + row_index, top_left_index.column + field_index )
148
+ model.setData( cell_index, field.to_variant, Qt::PasteRole )
149
+ end
150
+ # save records to db
151
+ model.save( model.create_index( top_left_index.row + row_index, 0 ) )
152
+ end
153
+
154
+ # make the gui refresh
155
+ bottom_right_index = model.create_index( top_left_index.row + csv_arr.size - 1, top_left_index.column + csv_arr[0].size - 1 )
156
+ emit model.dataChanged( top_left_index, bottom_right_index )
157
+ emit model.headerDataChanged( Qt::Vertical, top_left_index.row, top_left_index.row + csv_arr.size )
158
+ end
159
+
160
+ def delete_multiple_cells?
161
+ # go ahead with delete if there's only 1 cell, or the user says OK
162
+ delete_ok =
163
+ if selection_model.selected_indexes.size > 1
164
+ # confirmation message, until there are undos
165
+ msg = Qt::MessageBox.new(
166
+ Qt::MessageBox::Question,
167
+ 'Multiple Delete',
168
+ 'Are you sure you want to delete multiple cells?',
169
+ Qt::MessageBox::Yes | Qt::MessageBox::No,
170
+ self
171
+ )
172
+ msg.exec == Qt::MessageBox::Yes
173
+ else
174
+ true
175
+ end
176
+ end
177
+
178
+ def delete_cells
179
+ cells_deleted = false
180
+
181
+ # do delete
182
+ if delete_multiple_cells?
183
+ selection_model.selected_indexes.each do |index|
184
+ index.attribute_value = nil
185
+ cells_deleted = true
186
+ end
187
+ end
188
+
189
+ # deletes were done, so emit dataChanged
190
+ if cells_deleted
191
+ # emit data changed for all ranges
192
+ selection_model.selection.each do |selection_range|
193
+ emit dataChanged( selection_range.top_left, selection_range.bottom_right )
194
+ end
195
+ end
196
+ end
197
+
198
+ 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
+ begin
203
+ # call to model class for shortcuts
204
+ if model.model_class.respond_to?( :key_press_event )
205
+ begin
206
+ model_result = model.model_class.key_press_event( event, current_index, self )
207
+ return model_result if model_result != nil
208
+ rescue Exception => e
209
+ puts e.backtrace
210
+ error_message = Qt::ErrorMessage.new( self )
211
+ error_message.show_message( "Error in shortcut handler for #{model.model_class.name}: #{e.message}" )
212
+ error_message.show
213
+ end
214
+ end
215
+
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
336
+
337
+ # size is the same, so do the paste
338
+ paste_to_index( selected_index, arr )
339
+ end
340
+ return true
341
+
342
+ else
343
+ #~ puts event.inspect
344
+ end
345
+ super
346
+ rescue Exception => e
347
+ puts e.backtrace.join( "\n" )
348
+ puts e.message
349
+ error_message = Qt::ErrorMessage.new( self )
350
+ error_message.show_message( "Error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
351
+ error_message.show
352
+ end
353
+ end
354
+
355
+ # save the entity in the row of the given index
356
+ def save_row( index )
357
+ if index.valid?
358
+ saved = model.save( index )
359
+ if !saved
360
+ error_message = Qt::ErrorMessage.new( self )
361
+ msg = model.collection[index.row].errors.join("\n")
362
+ error_message.show_message( msg )
363
+ error_message.show
364
+ end
365
+ end
366
+ end
367
+
368
+ # save record whenever its row is exited
369
+ def currentChanged( current_index, previous_index )
370
+ @index_override = false
371
+ if current_index.row != previous_index.row
372
+ save_row( previous_index )
373
+ end
374
+ super
375
+ end
376
+
377
+ # this is to allow entity model UI handlers to tell the view
378
+ # where to move the current editing index to. If it's left blank
379
+ # default is based on the editing hint.
380
+ # see closeEditor
381
+ def override_next_index( model_index )
382
+ set_current_index( model_index )
383
+ @index_override = true
384
+ end
385
+
386
+ # call set_current_index with model_index unless override is true.
387
+ def set_current_unless_override( model_index )
388
+ if !@index_override
389
+ # move to next cell
390
+ # Qt seems to take care of tab wraparound
391
+ set_current_index( model_index )
392
+ end
393
+ @index_override = false
394
+ end
395
+
396
+ # override to prevent tab pressed from editing next field
397
+ # also takes into account that override_next_index may have been called
398
+ def closeEditor( editor, end_edit_hint )
399
+ case end_edit_hint
400
+ when Qt::AbstractItemDelegate.EditNextItem
401
+ super( editor, Qt::AbstractItemDelegate.NoHint )
402
+ set_current_unless_override( model.create_index( current_index.row, current_index.column + 1 ) )
403
+
404
+ when Qt::AbstractItemDelegate.EditPreviousItem
405
+ super( editor, Qt::AbstractItemDelegate.NoHint )
406
+ set_current_unless_override( model.create_index( current_index.row, current_index.column - 1 ) )
407
+
408
+ else
409
+ super
410
+ end
411
+ end
412
+
413
+ # If self.filter is false, use the data in the indexes to filter the data set;
414
+ # otherwise turn filtering off.
415
+ # Sets self.filter to true if filtering worked, false otherwise.
416
+ def filter_by_indexes( indexes )
417
+ save_entity = current_index.entity
418
+ save_index = current_index
419
+
420
+ unless self.filtered
421
+ # filter by current selection
422
+ # TODO handle a multiple-selection
423
+ if indexes.empty?
424
+ self.filtered = false
425
+ elsif indexes.size > 1
426
+ puts "Can't do multiple selection filters yet"
427
+ self.filtered = false
428
+ end
429
+
430
+ if indexes[0].entity.new_record?
431
+ emit status_text( "Can't filter on a new row" )
432
+ self.filtered = false
433
+ return
434
+ else
435
+ model.reload_data( :conditions => { indexes[0].field_name => indexes[0].field_value } )
436
+ self.filtered = true
437
+ end
438
+ else
439
+ # unfilter
440
+ model.reload_data( :conditions => {} )
441
+ self.filtered = false
442
+ end
443
+
444
+ # find the row for the saved entity
445
+ found_row = override_cursor( Qt::BusyCursor ) do
446
+ model.collection.index_for_entity( save_entity )
447
+ end
448
+
449
+ # create a new index and move to it
450
+ unless found_row.nil?
451
+ self.current_index = model.create_index( found_row, save_index.column )
452
+ end
453
+ end
454
+
455
+ # search_criteria must respond to:
456
+ # * search_text
457
+ # * whole_words?
458
+ # * direction ( :forward, :backward )
459
+ # * from_start?
460
+ #
461
+ # TODO formalise this
462
+ def search( search_criteria )
463
+ indexes = model.search( current_index, search_criteria )
464
+ if indexes.size > 0
465
+ emit status_text( "Found #{search_criteria.search_text} at row #{indexes[0].row}" )
466
+ selection_model.clear
467
+ self.current_index = indexes[0]
468
+ else
469
+ emit status_text( "No match found for #{search_criteria.search_text}" )
470
+ end
471
+ end
472
+
473
+ def itemDelegateForColumn( column )
474
+ puts "itemDelegateForColumn #{column}"
475
+ super
476
+ end
477
+ end
478
+
479
+ end