clevic 0.11.1 → 0.12.0
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.
- data/History.txt +21 -0
- data/Manifest.txt +4 -2
- data/Rakefile +32 -139
- data/TODO +12 -15
- data/bin/clevic +5 -0
- data/lib/clevic/browser.rb +1 -1
- data/lib/clevic/cache_table.rb +15 -34
- data/lib/clevic/default_view.rb +6 -0
- data/lib/clevic/delegates.rb +25 -19
- data/lib/clevic/extensions.rb +1 -1
- data/lib/clevic/field.rb +55 -5
- data/lib/clevic/filter_command.rb +51 -0
- data/lib/clevic/model_builder.rb +40 -23
- data/lib/clevic/table_model.rb +113 -8
- data/lib/clevic/table_view.rb +207 -92
- data/lib/clevic/text_delegate.rb +84 -0
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +28 -2
- data/models/accounts_models.rb +34 -40
- data/models/times_models.rb +69 -33
- data/tasks/clevic.rake +111 -0
- data/tasks/rdoc.rake +16 -0
- data/test/test_cache_table.rb +0 -23
- data/test/test_table_model.rb +61 -0
- data/test/test_table_searcher.rb +1 -1
- metadata +45 -10
- data/config/hoe.rb +0 -82
- data/config/requirements.rb +0 -15
    
        data/lib/clevic/extensions.rb
    CHANGED
    
    
    
        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 | 
            -
              #  | 
| 112 | 
            -
              #  | 
| 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 | 
            -
              #  | 
| 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( : | 
| 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
         | 
    
        data/lib/clevic/model_builder.rb
    CHANGED
    
    | @@ -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{| | 
| 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{| | 
| 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  | 
| 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  | 
| 354 | 
            +
                @fields[attribute] = field
         | 
| 344 355 | 
             
              end
         | 
| 345 | 
            -
             | 
| 346 | 
            -
              #  | 
| 347 | 
            -
               | 
| 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  | 
| 364 | 
            +
                  field.format ||= lambda{|x| field.set[x]}
         | 
| 356 365 | 
             
                end
         | 
| 357 366 |  | 
| 358 | 
            -
                field.delegate =  | 
| 359 | 
            -
                @fields  | 
| 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  | 
| 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 {| | 
| 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 | 
            -
                 | 
| 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 {| | 
| 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 | 
            -
                  @ | 
| 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, @ | 
| 570 | 
            +
                  @records = CacheTable.new( entity_class, @find_options )
         | 
| 554 571 | 
             
                end
         | 
| 555 572 | 
             
                @records
         | 
| 556 573 | 
             
              end
         | 
    
        data/lib/clevic/table_model.rb
    CHANGED
    
    | @@ -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 | 
            -
                 | 
| 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 | 
            -
                       | 
| 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 | 
            -
               | 
| 464 | 
            -
                 | 
| 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
         |