clevic 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -39,7 +39,7 @@ module Qt
39
39
 
40
40
  PasteRole = UserRole + 1
41
41
 
42
- class ItemDelegate
42
+ class AbstractItemDelegate
43
43
  # overridden in EntryDelegate subclasses
44
44
  def full_edit
45
45
  end
data/lib/clevic/field.rb CHANGED
@@ -108,17 +108,35 @@ class Field
108
108
  # - A symbol is treated as a method to be call on an entity
109
109
  property :tooltip
110
110
 
111
- # The set of allowed values for restricted fields. If it's a hash, the
112
- # keys will be stored in the db, and the values displayed in the UI.
111
+ # An Enumerable of allowed values for restricted fields. If each yields
112
+ # two values (like it does for a Hash), the
113
+ # first will be stored in the db, and the second displayed in the UI.
114
+ # If it's a proc, it must return an Enumerable as above.
113
115
  property :set
114
116
 
117
+ # When this is true, only the values in the combo may be entered.
118
+ # Otherwise the text-entry part of the combo can be used to enter
119
+ # non-listed values. Default is true if a set is explicitly specified.
120
+ # Otherwise depends on the field type.
121
+ property :restricted
122
+
115
123
  # Only for the distinct field type. The values will be sorted either with the
116
124
  # most used values first (:frequency => true) or in alphabetical order (:description => true).
117
125
  property :frequency, :description
118
126
 
119
- # Not implemented. Default value for this field for new records. Not sure how to populate it though.
127
+ # Default value for this field for new records.
128
+ # Can be a Proc or a value. A value will just be
129
+ # set, a proc will be executed with the entity as a parameter.
120
130
  property :default
121
131
 
132
+ # the property used for finding the field, ie by TableModel#field_column
133
+ # defaults to the attribute.
134
+ property :id
135
+
136
+ # called when the data in this field changes. Either a proc( clevic_view, table_view, model_index ) or a symbol
137
+ # for a method( view, model_index ) on the Clevic::View object. Both will take
138
+ property :notify_data_changed
139
+
122
140
  # properties for ActiveRecord options
123
141
  # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected
124
142
  # each element becomes a property.
@@ -175,6 +193,8 @@ EOF
175
193
 
176
194
  # instance variables
177
195
  @attribute = attribute
196
+ # default to attribute
197
+ @id = attribute
178
198
  @entity_class = entity_class
179
199
  @visible = true
180
200
 
@@ -239,7 +259,7 @@ EOF
239
259
  else
240
260
  @is_date_time ||=
241
261
  if display.nil?
242
- [:time, :date, :datetime].include?( meta.type )
262
+ [:time, :date, :datetime, :timestamp].include?( meta.type )
243
263
  else
244
264
  # it's a virtual field, so we need to use the value
245
265
  value.is_a?( Date ) || value.is_a?( Time )
@@ -374,7 +394,7 @@ EOF
374
394
 
375
395
  # Called by Clevic::TableModel to get the tooltip value
376
396
  def tooltip_for( entity )
377
- cache_value_for( :background, entity )
397
+ cache_value_for( :tooltip, entity )
378
398
  end
379
399
 
380
400
  # TODO Doesn't do anything useful yet.
@@ -403,6 +423,36 @@ EOF
403
423
  cache_value_for( :background, entity ) {|x| string_or_color(x)}
404
424
  end
405
425
 
426
+ def set_default_for( entity )
427
+ begin
428
+ entity[attribute] =
429
+ case default
430
+ when String
431
+ default
432
+ when Proc
433
+ default.call( entity )
434
+ end
435
+ rescue Exception => e
436
+ puts e.message
437
+ puts e.backtrace
438
+ end
439
+ end
440
+
441
+ def set_for( entity )
442
+ case set
443
+ when Proc
444
+ # the Proc should return an enumerable
445
+ set.call( entity )
446
+
447
+ when Symbol
448
+ entity.send( set )
449
+
450
+ else
451
+ # assume its an Enumerable
452
+ set
453
+ end
454
+ end
455
+
406
456
  protected
407
457
 
408
458
  # call the conversion_block with the value, or just return the
@@ -0,0 +1,51 @@
1
+ module Clevic
2
+ class FilterCommand
3
+ def initialize( table_view, filter_indexes, filter_conditions )
4
+ @table_view = table_view
5
+ @filter_conditions = filter_conditions
6
+ @filter_indexes = filter_indexes
7
+
8
+ # Better make the status message now, before the indexes become invalid
9
+ @status_message =
10
+ begin
11
+ "Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.gui_value}"
12
+ rescue
13
+ "Filtered"
14
+ end
15
+ end
16
+
17
+ # Do the filtering. Return true if successful, false otherwise.
18
+ def doit
19
+ begin
20
+ # store current AR conditions
21
+ @stored_conditions = @table_view.model.cache_table.options
22
+
23
+ # store auto_new
24
+ @auto_new = @table_view.model.auto_new
25
+
26
+ # reload cache table with new conditions
27
+ @table_view.model.auto_new = false
28
+ @table_view.model.reload_data( @filter_conditions )
29
+ rescue Exception => e
30
+ puts
31
+ puts e.message
32
+ puts e.backtrace
33
+ false
34
+ end
35
+ true
36
+ end
37
+
38
+ def undo
39
+ # restore auto_new
40
+ @table_view.model.auto_new = @auto_new
41
+
42
+ # reload cache table with stored AR conditions
43
+ @table_view.model.reload_data( @stored_conditions )
44
+ end
45
+
46
+ # return a message based on the conditions
47
+ def status_message
48
+ @status_message
49
+ end
50
+ end
51
+ end
@@ -1,7 +1,9 @@
1
1
  require 'activerecord'
2
+ require 'facets/dictionary'
2
3
 
3
4
  require 'clevic/table_model.rb'
4
5
  require 'clevic/delegates.rb'
6
+ require 'clevic/text_delegate.rb'
5
7
  require 'clevic/cache_table.rb'
6
8
  require 'clevic/field.rb'
7
9
 
@@ -273,11 +275,12 @@ class ModelBuilder
273
275
  @entity_view = entity_view
274
276
  @auto_new = true
275
277
  @read_only = false
276
- @fields = []
278
+ @fields = Dictionary.new
277
279
  exec_ui_block( &block )
278
280
  end
279
281
 
280
282
  attr_accessor :entity_view
283
+ attr_accessor :find_options
281
284
 
282
285
  # execute a block containing method calls understood by Clevic::ModelBuilder
283
286
  # arg can be something that responds to define_ui_block,
@@ -301,13 +304,13 @@ class ModelBuilder
301
304
  # The collection of Clevic::Field instances where visible == true.
302
305
  # the visible may go away.
303
306
  def fields
304
- @fields.reject{|x| !x.visible}
307
+ @fields.reject{|id,field| !field.visible}
305
308
  end
306
309
 
307
310
  # return the index of the named field in the collection of fields.
308
311
  def index( field_name_sym )
309
312
  retval = nil
310
- fields.each_with_index{|x,i| retval = i if x.attribute == field_name_sym.to_sym }
313
+ fields.each_with_index{|id,field,i| retval = i if field.attribute == field_name_sym.to_sym }
311
314
  retval
312
315
  end
313
316
 
@@ -332,7 +335,15 @@ class ModelBuilder
332
335
  # an ordinary field, edited in place with a text box
333
336
  def plain( attribute, options = {}, &block )
334
337
  read_only_default!( attribute, options )
335
- @fields << Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
338
+ @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
339
+ end
340
+
341
+ # an ordinary field like plain, except that a larger edit area can be used
342
+ def text( attribute, options = {}, &block )
343
+ read_only_default!( attribute, options )
344
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
345
+ field.delegate = TextDelegate.new( nil, field )
346
+ @fields[attribute] = field
336
347
  end
337
348
 
338
349
  # Returns a Clevic::Field with a DistinctDelegate, in other words
@@ -340,23 +351,27 @@ class ModelBuilder
340
351
  def distinct( attribute, options = {}, &block )
341
352
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
342
353
  field.delegate = DistinctDelegate.new( nil, field )
343
- @fields << field
354
+ @fields[attribute] = field
344
355
  end
345
-
346
- # Returns a Clevic::Field with a RestrictedDelegate,
347
- # a combo box, but restricted to a specified set, from the :set option.
348
- def restricted( attribute, options = {}, &block )
349
-
356
+
357
+ # a combo box with a set of supplied values
358
+ def combo( attribute, options = {}, &block )
350
359
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
351
- raise "field #{attribute} restricted must have a set" if field.set.nil?
352
360
 
353
- # TODO this really belongs in a separate 'map' field
361
+ # TODO this really belongs in a separate 'map' field?
362
+ # or maybe put it in SetDelegate?
354
363
  if field.set.is_a? Hash
355
- field.format = lambda{|x| field.set[x]}
364
+ field.format ||= lambda{|x| field.set[x]}
356
365
  end
357
366
 
358
- field.delegate = RestrictedDelegate.new( nil, field )
359
- @fields << field
367
+ field.delegate = SetDelegate.new( nil, field )
368
+ @fields[attribute] = field
369
+ end
370
+
371
+ # Returns a Clevic::Field with a restricted SetDelegate,
372
+ def restricted( attribute, options = {}, &block )
373
+ options[:restricted] = true
374
+ combo( attribute, options, &block )
360
375
  end
361
376
 
362
377
  # for foreign keys. Edited with a combo box using values from the specified
@@ -372,7 +387,7 @@ class ModelBuilder
372
387
  # check after all possible options have been collected
373
388
  raise ":display must be specified" if field.display.nil?
374
389
  field.delegate = RelationalDelegate.new( nil, field )
375
- @fields << field
390
+ @fields[attribute] = field
376
391
  end
377
392
 
378
393
  # mostly used in the new block to define the set of records
@@ -464,7 +479,7 @@ class ModelBuilder
464
479
 
465
480
  # return the named Clevic::Field object
466
481
  def field( attribute )
467
- @fields.find {|x| x.attribute == attribute }
482
+ @fields.find {|id,field| field.attribute == attribute }
468
483
  end
469
484
 
470
485
  # This takes all the information collected
@@ -475,14 +490,16 @@ class ModelBuilder
475
490
  # using @model here because otherwise the view's
476
491
  # reference to this very same model is garbage collected.
477
492
  @model = Clevic::TableModel.new( table_view )
478
- table_view.object_name = @object_name
493
+ @model.builder = self
479
494
  @model.entity_view = entity_view
480
- @model.fields = @fields
495
+ @model.fields = @fields.values
481
496
  @model.read_only = @read_only
482
497
  @model.auto_new = auto_new?
483
498
 
499
+ # setup model
500
+ table_view.object_name = @object_name
484
501
  # set parent for all delegates
485
- fields.each {|x| x.delegate.parent = table_view unless x.delegate.nil? }
502
+ fields.each {|id,field| field.delegate.parent = table_view unless field.delegate.nil? }
486
503
 
487
504
  # the data
488
505
  @model.collection = records
@@ -497,7 +514,7 @@ protected
497
514
  #--
498
515
  # TODO ActiveRecord-2.1 has smarter includes
499
516
  def add_include_options
500
- fields.each do |field|
517
+ fields.each do |id,field|
501
518
  if field.delegate.class == RelationalDelegate
502
519
  @options[:include] ||= []
503
520
  @options[:include] << field.attribute
@@ -539,7 +556,7 @@ protected
539
556
  def set_records( arg )
540
557
  if arg.class == Hash
541
558
  # need to defer this until all fields are collected
542
- @options = arg
559
+ @find_options = arg
543
560
  else
544
561
  @records = arg
545
562
  end
@@ -550,7 +567,7 @@ protected
550
567
  def get_records
551
568
  if @records.nil?
552
569
  #~ add_include_options
553
- @records = CacheTable.new( entity_class, @options.merge( :auto_new => auto_new? ) )
570
+ @records = CacheTable.new( entity_class, @find_options )
554
571
  end
555
572
  @records
556
573
  end
@@ -19,6 +19,7 @@ class TableModel < Qt::AbstractTableModel
19
19
 
20
20
  # the CacheTable of Clevic::Record or ActiveRecord::Base objects
21
21
  attr_reader :collection
22
+ alias_method :cache_table, :collection
22
23
 
23
24
  # the collection of Clevic::Field objects
24
25
  attr_reader :fields
@@ -29,8 +30,10 @@ class TableModel < Qt::AbstractTableModel
29
30
  # should this model create a new empty record by default?
30
31
  attr_accessor :auto_new
31
32
  def auto_new?; auto_new; end
33
+ def auto_new?; auto_new; end
32
34
 
33
35
  attr_accessor :entity_view
36
+ attr_accessor :builder
34
37
 
35
38
  def entity_class
36
39
  entity_view.entity_class
@@ -55,6 +58,16 @@ class TableModel < Qt::AbstractTableModel
55
58
  @attributes = nil
56
59
  end
57
60
 
61
+ # field is a symbol or string referring to a column.
62
+ # returns the index of that field.
63
+ def field_column( field )
64
+ fields.each_with_index {|x,i| return i if x.id == field.to_sym }
65
+ end
66
+
67
+ def field_for_index( model_index )
68
+ fields[model_index.column]
69
+ end
70
+
58
71
  def labels
59
72
  @labels ||= fields.map {|x| x.label }
60
73
  end
@@ -116,10 +129,19 @@ class TableModel < Qt::AbstractTableModel
116
129
  @metadatas[column]
117
130
  end
118
131
 
132
+ # add a new item, and set defaults from the Clevic::View
119
133
  def add_new_item
120
- # 1 new row
121
134
  begin_insert_rows( Qt::ModelIndex.invalid, row_count, row_count )
122
- collection << entity_class.new
135
+ # set default values without triggering changed
136
+ entity = entity_class.new
137
+ fields.each do |f|
138
+ unless f.default.nil?
139
+ f.set_default_for( entity )
140
+ end
141
+ end
142
+
143
+ collection << entity
144
+
123
145
  end_insert_rows
124
146
  end
125
147
 
@@ -136,6 +158,9 @@ class TableModel < Qt::AbstractTableModel
136
158
  # destroy the db object, and its associated table row
137
159
  removed.destroy
138
160
  end
161
+
162
+ # create a new row if auto_new is on
163
+ add_new_item if collection.empty? && auto_new?
139
164
  end
140
165
 
141
166
  # save the AR model at the given index, if it's dirty
@@ -144,7 +169,9 @@ class TableModel < Qt::AbstractTableModel
144
169
  return false if item.nil?
145
170
  if item.changed?
146
171
  if item.valid?
147
- item.save
172
+ retval = item.save
173
+ emit headerDataChanged( Qt::Vertical, index.row, index.row )
174
+ retval
148
175
  else
149
176
  false
150
177
  end
@@ -190,8 +217,8 @@ class TableModel < Qt::AbstractTableModel
190
217
  retval
191
218
  end
192
219
 
193
- def reload_data( options = {} )
194
- # renew cache
220
+ def reload_data( options = nil )
221
+ # renew cache. All records will be dropped and reloaded.
195
222
  self.collection = self.collection.renew( options )
196
223
  # tell the UI we had a major data change
197
224
  reset
@@ -318,6 +345,7 @@ class TableModel < Qt::AbstractTableModel
318
345
  index.errors.join("\n")
319
346
 
320
347
  # provide a tooltip when an empty relational field is encountered
348
+ # TODO should be part of field definition
321
349
  when index.metadata.type == :association
322
350
  index.field.delegate.if_empty_message
323
351
 
@@ -360,6 +388,7 @@ class TableModel < Qt::AbstractTableModel
360
388
 
361
389
  type = index.metadata.type
362
390
  value = variant.value
391
+ #~ puts "#{type.inspect} is #{value}"
363
392
 
364
393
  # translate the value from the ui to something that
365
394
  # the AR model will understand
@@ -396,12 +425,28 @@ class TableModel < Qt::AbstractTableModel
396
425
  # 01:17, 0117, 117, 1 17, are all accepted
397
426
  when type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
398
427
  Time.parse( "#$1:#$2" )
428
+
429
+ # remove thousand separators, allow for space and comma
430
+ # instead of . as a decimal separator
431
+ when type == :decimal
432
+ # do various transforms
433
+ value =
434
+ case
435
+ # accept a space or a comma instead of a . for floats
436
+ when value =~ /(.*?)(\d)[ ,](\d{2})$/
437
+ "#$1#$2.#$3"
438
+ else
439
+ value
440
+ end
399
441
 
442
+ # strip remaining commas
443
+ value.gsub( ',', '' )
444
+
400
445
  else
401
446
  value
402
447
  end
403
448
 
404
- emit dataChanged( index, index )
449
+ data_changed( index )
405
450
  # value conversion was successful
406
451
  true
407
452
  rescue Exception => e
@@ -460,8 +505,68 @@ class TableModel < Qt::AbstractTableModel
460
505
  end
461
506
  end
462
507
 
463
- def field_for_index( model_index )
464
- fields[model_index.column]
508
+ class DataChange
509
+ class ModelIndexProxy
510
+ attr_accessor :row
511
+ attr_accessor :column
512
+
513
+ def initialize( other = nil )
514
+ unless other.nil?
515
+ @row = other.row
516
+ @column = other.column
517
+ end
518
+ end
519
+ end
520
+
521
+ def top_left
522
+ @top_left ||= ModelIndexProxy.new
523
+ end
524
+
525
+ def bottom_right
526
+ @bottom_right ||= ModelIndexProxy.new
527
+ end
528
+
529
+ attr_writer :bottom_right
530
+ attr_writer :top_left
531
+
532
+ attr_reader :index
533
+ def index=( other )
534
+ self.top_left = ModelIndexProxy.new( other )
535
+ self.bottom_right = ModelIndexProxy.new( other )
536
+ end
537
+ end
538
+
539
+ # A rubyish way of doing dataChanged
540
+ # - if args has one element, it's either a single ModelIndex
541
+ # or something that understands top_left and bottom_right. These
542
+ # will be turned into a ModelIndex by calling create_index
543
+ # - if args has two element, assume it's a two ModelIndex instances
544
+ # - otherwise create a new DataChange and pass it to the block.
545
+ def data_changed( *args, &block )
546
+ case args.size
547
+ when 1
548
+ arg = args.first
549
+ if ( arg.respond_to?( :top_left ) && arg.respond_to?( :bottom_right ) ) || arg.is_a?( Qt::ItemSelectionRange )
550
+ # object is a DataChange, or a SelectionRange
551
+ top_left_index = create_index( arg.top_left.row, arg.top_left.column )
552
+ bottom_right_index = create_index( arg.bottom_right.row, arg.bottom_right.column )
553
+ emit dataChanged( top_left_index, bottom_right_index )
554
+ else
555
+ # assume it's a ModelIndex
556
+ emit dataChanged( arg, arg )
557
+ end
558
+
559
+ when 2
560
+ emit dataChanged( args.first, args.last )
561
+
562
+ else
563
+ unless block.nil?
564
+ change = DataChange.new
565
+ block.call( change )
566
+ # recursive call
567
+ data_changed( change )
568
+ end
569
+ end
465
570
  end
466
571
 
467
572
  end