Class: Clevic::TableModel

Inherits:
Qt::AbstractTableModel
  • Object
show all
Includes:
Emitter, QtFlags
Defined in:
lib/clevic/table_model.rb,
lib/clevic/qt/table_model.rb,
lib/clevic/swing/table_model.rb

Overview

An instance of Clevic::TableModel is constructed by Clevic::ModelBuilder from the UI definition in a Clevic::View, or from the default Clevic::View created by including the Clevic::Record module in a Sequel::Model subclass.

Defined Under Namespace

Classes: DataChange

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Emitter

included

Constructor Details

#initializeTableModel

Returns a new instance of TableModel.



31
32
33
# File 'lib/clevic/qt/table_model.rb', line 31

def initialize( parent = nil )
  super
end

Instance Attribute Details

#auto_newObject

should this model create a new empty record by default?



26
27
28
# File 'lib/clevic/table_model.rb', line 26

def auto_new
  @auto_new
end

#builderObject

Returns the value of attribute builder.



31
32
33
# File 'lib/clevic/table_model.rb', line 31

def builder
  @builder
end

#collectionObject Also known as: cache_table

the CacheTable of Clevic::Record or Sequel::Model objects



16
17
18
# File 'lib/clevic/table_model.rb', line 16

def collection
  @collection
end

#entity_viewObject

Returns the value of attribute entity_view.



30
31
32
# File 'lib/clevic/table_model.rb', line 30

def entity_view
  @entity_view
end

#fieldsObject

the collection of Clevic::Field objects



20
21
22
# File 'lib/clevic/table_model.rb', line 20

def fields
  @fields
end

#oneObject

If somre or all the entities in the collection is related to a single entity somewhere else, this is it. Not sure if this is the right way to do it, but try anyway.



36
37
38
# File 'lib/clevic/table_model.rb', line 36

def one
  @one
end

#read_onlyObject

Returns the value of attribute read_only.



22
23
24
# File 'lib/clevic/table_model.rb', line 22

def read_only
  @read_only
end

Instance Method Details

#add_new_itemObject

add a new item, and set defaults from the Clevic::View add_new_item_start and add_new_item_end are provided by the GUI framework-specific class



77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/clevic/table_model.rb', line 77

def add_new_item
  add_new_item_start

  entity = entity_class.new

  # set default values without triggering changed
  fields.each do |f|
    f.set_default_for( entity ) unless f.default.nil?
  end

  collection << entity

  add_new_item_end
end

#add_new_item_endObject



40
41
42
43
# File 'lib/clevic/qt/table_model.rb', line 40

def add_new_item_end
  # notify listeners that the model has changed
  end_insert_rows
end

#add_new_item_startObject

add a new item, and set defaults from the Clevic::View



36
37
38
# File 'lib/clevic/qt/table_model.rb', line 36

def add_new_item_start
  begin_insert_rows( Qt::ModelIndex.invalid, row_count, row_count )
end

#attributesObject



64
65
66
# File 'lib/clevic/table_model.rb', line 64

def attributes
  @attributes ||= fields.map {|x| x.attribute }
end

#auto_new?Boolean

Returns:

  • (Boolean)


27
# File 'lib/clevic/table_model.rb', line 27

def auto_new?; auto_new; end

#columnCount(parent = nil) ⇒ Object



67
68
69
# File 'lib/clevic/qt/table_model.rb', line 67

def columnCount( parent = nil )
  fields.size
end

#create_index(row, column) ⇒ Object



30
31
32
# File 'lib/clevic/swing/table_model.rb', line 30

def create_index( row, column )
  SwingTableIndex.new( self, row, column )
end

#data(index, role = qt_display_role) ⇒ Object

Provide data to UI.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/clevic/qt/table_model.rb', line 157

def data( index, role = qt_display_role )
  #~ puts "data for index: #{index.inspect}, field #{index.field.attribute.inspect} and role: #{const_as_string role}"
  begin
    case role
      when qt_display_role
        # boolean values generally don't have text next to them in this context
        # check this explicitly to avoid fetching the entity from
        # the model's collection (and maybe db) when we
        # definitely don't need to
        unless index.meta.type == :boolean
          value = index.display_value
        end

      when qt_edit_role
        # see comment for qt_display_role
        unless index.meta.type == :boolean
          value = index.edit_value
        end

      when qt_checkstate_role
        if index.meta.type == :boolean
          index.raw_value ? qt_checked : qt_unchecked
        end

      when qt_text_alignment_role
        case index.field.alignment
          when :left; qt_alignleft
          when :right; qt_alignright
          when :centre; qt_aligncenter
          when :justified; qt_alignjustified
        end

      # just here to make debug output quieter
      when qt_size_hint_role;

      # show field with a red background if there's an error
      when qt_background_role
        index.field.background_for( index.entity ) || Qt::Color.new( 'red' ) if index.has_errors?

      when qt_font_role;

      when qt_foreground_role
        index.field.foreground_for( index.entity ) ||
        if index.field.read_only? || read_only?
          Qt::Color.new( 'dimgray' )
        end

      when qt_decoration_role;
        index.field.decoration_for( index.entity )

      when qt_tooltip_role
        index.tooltip

      else
        puts "data index: #{index}, role: #{const_as_string(role)}" if $options[:debug]
        nil
    # return the variant
    end.to_variant

  rescue Exception => e
    # this can generate a lot of errors from view code, so don't emit data_error every one
    puts "#{entity_view.class.name}.#{index.field.id}: #{index.inspect} for role: #{const_as_string role} #{value.inspect} #{index.entity.inspect}"
    puts e.message
    puts e.backtrace
    nil.to_variant
  end
end

#data_changed(*args, &block) ⇒ Object

A rubyish way of doing dataChanged

  • if args has one element, it’s either a single ModelIndex or something that understands top_left and bottom_right. These will be turned into a ModelIndex by calling create_index

  • if args has two element, assume it’s a two ModelIndex instances

  • otherwise create a new DataChange and pass it to the block.



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/clevic/qt/table_model.rb', line 303

def data_changed( *args, &block )
  case args.size
    when 1
      arg = args.first
      if ( arg.respond_to?( :top_left ) && arg.respond_to?( :bottom_right ) ) || arg.is_a?( Qt::ItemSelectionRange )
        # object is a DataChange, or a SelectionRange
        top_left_index = create_index( arg.top_left.row, arg.top_left.column )
        bottom_right_index = create_index( arg.bottom_right.row, arg.bottom_right.column )
        emit dataChanged( top_left_index, bottom_right_index )
      else
        # assume it's a ModelIndex
        emit dataChanged( arg, arg )
      end

    when 2
      emit dataChanged( args.first, args.last )

    else
      unless block.nil?
        change = DataChange.new
        block.call( change )
        # recursive call
        data_changed( change )
      end
  end
end

#emit_data_error(index, data, string) ⇒ Object



27
28
29
# File 'lib/clevic/qt/table_model.rb', line 27

def emit_data_error( index, data, string )
  emit data_error( index, data.to_variant, string )
end

#entity_classObject



38
39
40
# File 'lib/clevic/table_model.rb', line 38

def entity_class
  entity_view.entity_class
end

#field_column(field) ⇒ Object

field is a symbol or string referring to a column. returns the index of that field.



52
53
54
# File 'lib/clevic/table_model.rb', line 52

def field_column( field )
  fields.each_with_index {|x,i| return i if x.id == field.to_sym }
end

#field_for_index(model_index) ⇒ Object



56
57
58
# File 'lib/clevic/table_model.rb', line 56

def field_for_index( model_index )
  fields[model_index.column]
end

#flags(model_index) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/clevic/qt/table_model.rb', line 76

def flags( model_index )
  retval = super

  # sometimes this actually happens.
  # TODO probably a bug in the combo editor exit code
  return retval if model_index.column >= columnCount

  # TODO don't return IsEditable if the model is read-only
  if model_index.meta.type == :boolean
    retval = item_boolean_flags
  end

  unless model_index.field.read_only? || read_only?
    retval |= qt_item_is_editable.to_i
  end
  retval
end

#getColumnClass(column_index) ⇒ Object

override TableModel method



78
79
80
81
82
83
84
85
86
# File 'lib/clevic/swing/table_model.rb', line 78

def getColumnClass( column_index )
  if fields[column_index].meta.type == :boolean || fields[column_index].delegate.is_a?( BooleanDelegate )
    # easiest way to display a checkbox
    java.lang.Boolean
  else
    # This will be a treated as a String value
    java.lang.Object
  end
end

#getColumnCountObject Also known as: column_count

override TableModel method



65
66
67
# File 'lib/clevic/swing/table_model.rb', line 65

def getColumnCount
  fields.size
end

#getColumnName(column_index) ⇒ Object

override TableModel method



73
74
75
# File 'lib/clevic/swing/table_model.rb', line 73

def getColumnName( column_index )
  fields[column_index].label
end

#getRowCountObject Also known as: row_count

override TableModel method



57
58
59
# File 'lib/clevic/swing/table_model.rb', line 57

def getRowCount
  collection.size
end

#getValueAt(row_index, column_index) ⇒ Object

Provide raw value to renderers



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/clevic/swing/table_model.rb', line 110

def getValueAt( row_index, column_index )
  index = create_index( row_index, column_index )

  #~ valuer = valuer_for( index )
  valuer = index

  if index.field.delegate.native
    valuer.display_value
  else
    valuer.attribute_value
  end

rescue
  puts $!.inspect
  puts $!.backtrace
  nil
end

#headerData(section, orientation, role) ⇒ Object

values for horizontal and vertical headers



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/clevic/qt/table_model.rb', line 95

def headerData( section, orientation, role )
  value =
  case role
    when qt_display_role
      case orientation
        when Qt::Horizontal
          labels[section]
        when Qt::Vertical
          # display record number. Object id is in tooltip.
          section+1
      end

    when qt_text_alignment_role
      case orientation
        when Qt::Vertical
          Qt::AlignRight | Qt::AlignVCenter
      end

    when Qt::SizeHintRole
      # anything other than nil here makes the headers disappear.
      nil

    when qt_tooltip_role
      case orientation
        when Qt::Horizontal
          fields[section].tooltip

        when Qt::Vertical
          case
            when !collection[section].errors.empty?
              'Invalid data'
            when collection[section].modified?
              'Unsaved changes'
            else
              if collection.cached_at?( section )
                collection[section].pk.inspect
              end
          end
      end

    when qt_background_role
      if orientation == Qt::Vertical
        item = collection[section]
        unless item.nil?
          case
            when !item.errors.empty?
              Qt::Color.new( 'orange' )
            when item.changed?
              Qt::Color.new( 'yellow' )
            end
        end
      end

    else
      #~ puts "headerData section: #{section}, role: #{const_as_string(role)}" if $options[:debug]
      nil
  end

  return value.to_variant
end

#isCellEditable(row_index, column_index) ⇒ Object



88
89
90
91
# File 'lib/clevic/swing/table_model.rb', line 88

def isCellEditable( row_index, column_index )
  index = create_index( row_index, column_index )
  !( index.field.read_only? || index.entity.andand.readonly? || read_only? )
end

#labelsObject



60
61
62
# File 'lib/clevic/table_model.rb', line 60

def labels
  @labels ||= fields.map {|x| x.label }
end

#read_only?Boolean

Returns:

  • (Boolean)


23
# File 'lib/clevic/table_model.rb', line 23

def read_only?; read_only; end

#reload_data(dataset = nil, &dataset_block) ⇒ Object



145
146
147
148
149
150
# File 'lib/clevic/table_model.rb', line 145

def reload_data( dataset = nil, &dataset_block )
  # renew cache. All records will be dropped and reloaded.
  self.collection = self.cache_table.renew( dataset, &dataset_block )
  # tell the UI we had a major data change
  reset
end

#remove_notify(rows, &block) ⇒ Object



45
46
47
48
49
50
# File 'lib/clevic/qt/table_model.rb', line 45

def remove_notify( rows, &block )
  begin_remove_rows( Qt::ModelIndex.invalid, rows.first, rows.last )
  # do the removal
  yield
  end_remove_rows
end

#remove_rows(rows) ⇒ Object

rows is a collection of integers specifying row indices to remove



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/clevic/table_model.rb', line 93

def remove_rows( rows )
  # don't delete rows twice
  # put the entities to be removed in a separate collection
  # I can't figure out why the collection fails when they're
  # removed directly
  rows_in_order = rows.uniq.sort
  removals = rows_in_order.map do |index|
    collection[index]
  end

  remove_notify( rows_in_order ) do
    removals.each do |to_remove, index|
      # destroy the db object, and its associated table row
      to_remove.destroy unless to_remove.nil? || to_remove.new?
    end
  end

  # because it's too bloody complicated to figure out which items
  # were deleted and then remove them from the collection. And there
  # most likely isn't a big hit for doing this, unless there's a lot
  # of cached calcuation in the entities.
  self.collection = collection.renew

  # create a new row if auto_new is on
  # should really be in a signal handler
  add_new_item if collection.empty? && auto_new?
end

#resetObject

Tell the UI we had a major data change



51
52
53
54
# File 'lib/clevic/swing/table_model.rb', line 51

def reset
  # could also use fireTableStructureChanged(), but it doesn't seem necessary
  fireTableDataChanged
end

#rowCount(parent = nil) ⇒ Object



58
59
60
# File 'lib/clevic/qt/table_model.rb', line 58

def rowCount( parent = nil )
  collection.size
end

#save(index) ⇒ Object

save the model at the given index, if it’s dirty TODO Sequel uses modified?



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/clevic/table_model.rb', line 123

def save( index )
  item = collection[index.row]
  return false if item.nil?
  if item.changed?
    if item.valid?
      retval = item.save
      data_changed( index )
      retval
    else
      false
    end
  else
    # model not changed
    true
  end
rescue
  puts "Error saving #{item.inspect}"
  puts $!.message
  puts $!.backtrace
  emit_data_error( index, nil, $!.message )
end

#search(start_index, search_criteria) ⇒ Object

return a collection of indexes that match the search criteria at the moment this only returns the first index found TODO could handle dataset creation better



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/clevic/table_model.rb', line 155

def search( start_index, search_criteria )
  searcher = TableSearcher.new(
    cache_table.dataset,
    search_criteria,
    start_index.field
  )
  entity = searcher.search( start_index.entity )

  # return matched indexes
  unless entity.nil?
    found_row = collection.index_for_entity( entity )
    raise "found_row should find something" if found_row.nil?
    [ create_index( found_row, start_index.column, 0 ) ]
  else
    []
  end
end

#setData(index, variant, role = qt_edit_role) ⇒ Object

data sent from UI return true if conversion from variant was successful, or false if something went wrong.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/clevic/qt/table_model.rb', line 228

def setData( index, variant, role = qt_edit_role )
  if index.valid?
    case role
    when qt_edit_role
      # Don't allow the primary key to be changed
      return false if index.attribute == entity_class.primary_key.to_sym

      if ( index.column < 0 || index.column >= column_count )
        raise "invalid column #{index.column}"
      end

      begin
        case
          when variant.class.name == 'Qt::Date'
            index.attribute_value = Date.new( variant.year, variant.month, variant.day )

          when variant.class.name == 'Qt::Time'
            index.attribute_value = Time.new( variant.hour, variant.min, variant.sec )

          else
            index.edit_value = variant.value
        end

        # value conversion was successful
        data_changed( index )
        true
      rescue Exception => e
        puts e.backtrace.join( "\n" )
        puts e.message
        emit data_error( index, variant, e.message )
        # value conversion was not successful
        false
      end

    when qt_checkstate_role
      if index.meta.type == :boolean
        index.attribute_value = !index.attribute_value
        true
      else
        false
      end

    # user-defined role
    # TODO this only works with single-dotted paths
    when qt_paste_role
      puts "WARNING Qt::PasteRole Deprecated"
      if index.meta.type == :association
        field = index.field
        candidates = field.related_class.find( :all, :conditions => [ "#{field.attribute_path[1]} = ?", variant.value ] )
        case candidates.size
          when 0; puts "No match for #{variant.value}"
          when 1; index.attribute_value = candidates[0]
          else; puts "Too many for #{variant.value}"
        end
      else
        index.attribute_value = variant.value
      end
      true

    else
      puts "role: #{role.inspect}"
      true

    end
  else
    false
  end
end

#setValueAt(value, row_index, column_index) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/clevic/swing/table_model.rb', line 128

def setValueAt( value, row_index, column_index )
  index = create_index( row_index, column_index )
  #~ puts "setting index: #{index.inspect} to #{value.inspect}"
  #~ valuer = valuer_for( index )
  valuer = index

  # Don't allow the primary key to be changed
  return if index.attribute == entity_class.primary_key.to_sym

  # translate the value from the ui to something that
  # the DB entity will understand
  begin
    if value.is_a?( java.util.Date )
      valuer.attribute_value =
      case value
      # more specific descendant first
      when java.util.Time
        Time.new( value.hour, value.min, value.sec )

      when java.util.Date
        Date.new( value.year, value.month, value.day )

      else
        raise "don't know how to convert a #{value.class.name}:#{value.inspect}"
      end
    else
      valuer.edit_value = value
    end

    valuer.entity.save

    data_changed( index )
  rescue Exception => e
    puts "#{__FILE__}:#{__LINE__}:e.message: #{e.message}"
    puts e.backtrace
    emit_data_error( index, value, e.message )
  end
end

#update_vertical_header(index) ⇒ Object

save the AR model at the given index, if it’s dirty



53
54
55
56
# File 'lib/clevic/qt/table_model.rb', line 53

def update_vertical_header( index )
  raise "preferably use data_changed here, if possible"
  emit headerDataChanged( Qt::Vertical, index.row, index.row )
end

#valuer_for(index) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/clevic/swing/table_model.rb', line 93

def valuer_for( index )
  case
  # pull values from entity at index
  when index.field.entity_class == entity_class
    index

  # pull values from the Clevic::View class
  when entity_view.class.ancestors.include?( Clevic::View )
    #~ entity_view.entity = index.entity
    FieldValuer.valuer( index.field, entity_view )

  else
    raise "No valuer for #{index.inspect}"
  end
end