clevic 0.12.0 → 0.13.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +209 -30
  3. data/README.txt +16 -20
  4. data/Rakefile +8 -8
  5. data/TODO +6 -7
  6. data/bin/clevic +12 -73
  7. data/lib/clevic/action_builder.rb +168 -0
  8. data/lib/clevic/ar_methods.rb +120 -0
  9. data/lib/clevic/attribute_list.rb +56 -0
  10. data/lib/clevic/cache_table.rb +60 -37
  11. data/lib/clevic/default_view.rb +3 -16
  12. data/lib/clevic/delegate.rb +46 -0
  13. data/lib/clevic/emitter.rb +38 -0
  14. data/lib/clevic/extensions.rb +61 -114
  15. data/lib/clevic/field.rb +159 -228
  16. data/lib/clevic/field_valuer.rb +165 -0
  17. data/lib/clevic/filter_command.rb +2 -6
  18. data/lib/clevic/generic_format.rb +52 -0
  19. data/lib/clevic/{ui → icons}/icon.png +0 -0
  20. data/lib/clevic/many_field.rb +7 -0
  21. data/lib/clevic/model_builder.rb +234 -146
  22. data/lib/clevic/model_column.rb +61 -13
  23. data/lib/clevic/order_attribute.rb +10 -0
  24. data/lib/clevic/qt.rb +35 -0
  25. data/lib/clevic/qt/action_builder.rb +47 -0
  26. data/lib/clevic/qt/boolean_delegate.rb +8 -0
  27. data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
  28. data/lib/clevic/qt/clipboard.rb +35 -0
  29. data/lib/clevic/qt/combo_delegate.rb +198 -0
  30. data/lib/clevic/qt/delegates.rb +1 -0
  31. data/lib/clevic/qt/distinct_delegate.rb +35 -0
  32. data/lib/clevic/qt/extensions.rb +52 -0
  33. data/lib/clevic/qt/field.rb +18 -0
  34. data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
  35. data/lib/clevic/qt/relational_delegate.rb +87 -0
  36. data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
  37. data/lib/clevic/qt/set_delegate.rb +44 -0
  38. data/lib/clevic/qt/table_model.rb +331 -0
  39. data/lib/clevic/qt/table_view.rb +344 -0
  40. data/lib/clevic/qt/text_area_delegate.rb +8 -0
  41. data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
  42. data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
  43. data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
  44. data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
  45. data/lib/clevic/rails_models_loaders.rb +56 -0
  46. data/lib/clevic/record.rb +2 -17
  47. data/lib/clevic/sampler.rb +81 -0
  48. data/lib/clevic/sequel_ar_adapter.rb +215 -0
  49. data/lib/clevic/sequel_length_validation.rb +23 -0
  50. data/lib/clevic/sequel_meta.rb +65 -0
  51. data/lib/clevic/sequel_naked.rb +30 -0
  52. data/lib/clevic/swing.rb +38 -0
  53. data/lib/clevic/swing/action.rb +125 -0
  54. data/lib/clevic/swing/action_builder.rb +47 -0
  55. data/lib/clevic/swing/boolean_delegate.rb +26 -0
  56. data/lib/clevic/swing/browser.rb +282 -0
  57. data/lib/clevic/swing/cell_editor.rb +95 -0
  58. data/lib/clevic/swing/cell_renderer.rb +44 -0
  59. data/lib/clevic/swing/clipboard.rb +135 -0
  60. data/lib/clevic/swing/combo_delegate.rb +336 -0
  61. data/lib/clevic/swing/confirm_dialog.rb +57 -0
  62. data/lib/clevic/swing/delegate.rb +40 -0
  63. data/lib/clevic/swing/distinct_delegate.rb +30 -0
  64. data/lib/clevic/swing/extensions.rb +274 -0
  65. data/lib/clevic/swing/field.rb +35 -0
  66. data/lib/clevic/swing/relational_delegate.rb +48 -0
  67. data/lib/clevic/swing/row_header.rb +210 -0
  68. data/lib/clevic/swing/search_dialog.rb +230 -0
  69. data/lib/clevic/swing/selection_model.rb +90 -0
  70. data/lib/clevic/swing/set_delegate.rb +41 -0
  71. data/lib/clevic/swing/swing_table_index.rb +43 -0
  72. data/lib/clevic/swing/table_model.rb +200 -0
  73. data/lib/clevic/swing/table_view.rb +385 -0
  74. data/lib/clevic/swing/table_view_focus.rb +47 -0
  75. data/lib/clevic/swing/tag_delegate.rb +127 -0
  76. data/lib/clevic/swing/tag_editor.rb +101 -0
  77. data/lib/clevic/swing/text_area_delegate.rb +46 -0
  78. data/lib/clevic/swing/text_delegate.rb +31 -0
  79. data/lib/clevic/swing/ui/build.xml +74 -0
  80. data/lib/clevic/swing/ui/dist/README.TXT +33 -0
  81. data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
  82. data/lib/clevic/swing/ui/manifest.mf +3 -0
  83. data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
  84. data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
  85. data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
  86. data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
  87. data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
  88. data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
  89. data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
  90. data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
  91. data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
  92. data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
  93. data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
  94. data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
  95. data/lib/clevic/table_index.rb +100 -0
  96. data/lib/clevic/table_model.rb +54 -425
  97. data/lib/clevic/table_searcher.rb +113 -116
  98. data/lib/clevic/table_view.rb +171 -399
  99. data/lib/clevic/table_view_paste.rb +199 -0
  100. data/lib/clevic/version.rb +3 -2
  101. data/lib/clevic/view.rb +94 -43
  102. data/models/accounts_models.rb +13 -13
  103. data/models/minimal_models.rb +5 -9
  104. data/models/times_models.rb +19 -14
  105. data/models/times_psql_models.rb +10 -0
  106. data/models/times_sqlite_models.rb +1 -8
  107. data/models/values_models.rb +2 -8
  108. data/tasks/clevic.rake +1 -1
  109. data/tasks/rdoc.rake +1 -5
  110. data/tasks/website.rake +1 -1
  111. data/test/test_cache_table.rb +15 -29
  112. data/test/test_helper.rb +14 -83
  113. data/test/test_order_attribute.rb +1 -1
  114. data/test/test_table_model.rb +0 -21
  115. data/test/test_table_searcher.rb +67 -61
  116. metadata +262 -78
  117. data/lib/clevic.rb +0 -4
  118. data/lib/clevic/db_options.rb +0 -112
  119. data/lib/clevic/delegates.rb +0 -386
@@ -1,165 +1,162 @@
1
- require 'clevic/sql_dialects.rb'
2
-
3
1
  module Clevic
4
2
 
5
- # TODO possibly use AR scopes for this?
3
+ =begin
4
+ Search for a record in the collection given a set of criteria. One of the
5
+ criteria will be a starting record, and the search method should return
6
+ the matching record next after this.
7
+ =end
6
8
  class TableSearcher
7
- attr_reader :entity_class, :order_attributes, :search_criteria, :field
9
+ attr_reader :dataset, :search_criteria, :field
8
10
 
9
- # entity_class is a descendant of ActiveRecord::Base
10
- # order_attributes is a collection of OrderAttribute objects
11
- # - field is an instance of Clevic::Field
12
- # - search_criteria responds to from_start?, direction, whole_words? and search_text
13
- def initialize( entity_class, order_attributes, search_criteria, field )
14
- raise "there must be at least one order_attribute" if order_attributes.nil? or order_attributes.empty?
11
+ # dataset is a Sequel::Dataset, which has an associated Sequel::Model
12
+ # field is an instance of Clevic::Field
13
+ # search_criteria responds to from_start?, direction, whole_words? and search_text
14
+ def initialize( dataset, search_criteria, field )
15
15
  raise "field must be specified" if field.nil?
16
16
  raise "unknown order #{search_criteria.direction}" unless [:forwards, :backwards].include?( search_criteria.direction )
17
- @entity_class = entity_class
18
- @order_attributes = order_attributes
17
+ raise "dataset has no model" unless dataset.respond_to?( :model )
18
+
19
+ # set default dataset ordering if it's not there
20
+ @dataset =
21
+ if dataset.opts[:order].nil?
22
+ dataset.order( dataset.model.primary_key )
23
+ else
24
+ dataset
25
+ end
26
+
19
27
  @search_criteria = search_criteria
20
28
  @field = field
21
29
  end
22
30
 
23
31
  # start_entity is the entity to start from, ie any record found after it will qualify
32
+ # return the first entity found that matches the criteria
24
33
  def search( start_entity = nil )
25
- search_field_name =
26
- if field.is_association?
34
+ search_dataset( start_entity ).first
35
+ end
36
+
37
+ protected
38
+ # return a Sequel expression for the name of the field to use as a comparison
39
+ def search_field_expression
40
+ if field.association?
27
41
  # for related tables
28
42
  unless [String,Symbol].include?( field.display.class )
29
- raise( "search field #{field.inspect} cannot have a complex display" )
43
+ raise( "search field #{field.inspect} cannot search lambda display" )
30
44
  end
31
45
 
46
+ raise "display not specified for #{field}" if field.display.nil?
47
+
32
48
  # TODO this will only work with a path value with no dots
33
49
  # otherwise the SQL gets complicated with joins etc
34
- field.display
50
+ field.related_class \
51
+ .filter( field.related_class.primary_key.qualify( field.related_class.table_name ) => field.meta.key.qualify( field.entity_class.table_name ) ) \
52
+ .select( field.display.to_sym )
35
53
  else
36
54
  # for this table
37
- entity_class.connection.quote_column_name( field.attribute.to_s )
55
+ field.attribute.to_sym
38
56
  end
39
-
40
- # do the conditions for the search value
41
- @conditions = search_clause( search_field_name )
57
+ end
58
+
59
+ # return an expression, or an array or expressions for representing search_criteria.search_text and whole_words?
60
+ def search_text_expression
61
+ if search_criteria.whole_words?
62
+ [
63
+ "% #{search_criteria.search_text} %",
64
+ "#{search_criteria.search_text} %",
65
+ "% #{search_criteria.search_text}",
66
+ search_criteria.search_text
67
+ ]
68
+ else
69
+ "%#{search_criteria.search_text}%"
70
+ end
71
+ end
72
+
73
+ # Add the relevant conditions to use start_entity as the
74
+ # entity where the search starts, ie the first one after it is found
75
+ # start_entity is a model instance
76
+ def find_from( dataset, start_entity )
77
+ expression = build_recursive_comparison( start_entity )
78
+ # need expression => true because most databases can't evaluate a
79
+ # pure boolean expression - they need something to compare it to.
80
+ dataset.filter( expression => true )
81
+ end
82
+
83
+ # return a dataset based on @dataset which filters on search_criteria
84
+ def search_dataset( start_entity )
85
+ likes = Array[*search_text_expression].map{|ste| Sequel::SQL::StringExpression.like(search_field_expression, ste, {:case_insensitive=>true})}
86
+ rv = @dataset.filter( Sequel::SQL::BooleanExpression.new(:OR, *likes ) )
42
87
 
43
88
  # if we're not searching from the start, we need
44
89
  # to find the next match. Which is complicated from an SQL point of view.
45
90
  unless search_criteria.from_start?
46
91
  raise "start_entity cannot be nil when from_start is false" if start_entity.nil?
47
92
  # build up the ordering conditions
48
- find_from!( start_entity )
93
+ rv = find_from( rv, start_entity )
49
94
  end
50
95
 
51
- # otherwise ActiveRecord thinks that the % in the string
52
- # is for interpolations instead of treating it a the like wildcard
53
- conditions_value =
54
- if !@params.nil? and @params.size > 0
55
- [ @conditions, @params ]
56
- else
57
- @conditions
58
- end
96
+ # reverse order by direction if necessary
97
+ rv = rv.reverse if search_criteria.direction == :backwards
59
98
 
60
- # find the first match
61
- entity_class.find(
62
- :first,
63
- :conditions => conditions_value,
64
- :order => order,
65
- :joins => ( field.meta.name if field.is_association? )
66
- )
67
- end
68
-
69
- protected
70
- include SqlDialects
71
-
72
- def quote_column( field_name )
73
- entity_class.connection.quote_column_name( field_name )
74
- end
75
-
76
- def quote( value )
77
- entity_class.connection.quote( value )
99
+ # return dataset
100
+ rv
78
101
  end
79
102
 
80
103
  # recursively create a case statement to do the comparison
81
104
  # because and ... and ... and filters on *each* one rather than
82
105
  # consecutively.
83
- # operator is either '<' or '>'
84
- def build_recursive_comparison( operator, index = 0 )
106
+ def build_recursive_comparison( start_entity, index = 0 )
85
107
  # end recursion
86
- return sql_boolean( false ) if index == order_attributes.size
108
+ return false if index == order_attributes.size
87
109
 
88
- # fetch the current attribute
89
- attribute = order_attributes[index]
110
+ # fetch the current order attribute and direction
111
+ attribute, direction = order_attributes[index]
112
+ value = start_entity.send( attribute )
90
113
 
91
- # build case statement, including recusion
92
- st = <<-EOF
93
- case
94
- when #{entity_class.table_name}.#{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
95
- when #{entity_class.table_name}.#{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
96
- else #{sql_boolean false}
97
- end
98
- EOF
99
- # indent
100
- st.gsub!( /^/, ' ' * index )
114
+ # build case statement using Sequel expressions, including recursion
115
+ # pseudo-SQL is
116
+ # case
117
+ # when attribute < value then true
118
+ # when attribute = value then #{build_recursive_comparison( operator, index+1 )}
119
+ # else false
120
+ # end
121
+
122
+ {
123
+ # if values are unequal, comparison levels end here
124
+ attribute.identifier.send( comparator(direction), value ) => true,
125
+ # if the values are equal, move on to the next level of comparison
126
+ { attribute => value } => build_recursive_comparison( start_entity, index+1 )
127
+ }.case( false ) # the else (default) clause, ie we don't want to see these records
101
128
  end
102
129
 
103
- # Add the relevant conditions to use start_entity as the
104
- # entity where the search starts, ie the first one after it is found
105
- # start_entity is an AR model instance
106
- # sets @params and @conditions
107
- def find_from!( start_entity )
108
- operator =
130
+ # return either > or < depending on both search_criteria.direction
131
+ # and local_direction
132
+ def comparator( local_direction = 1 )
133
+ comparator_direction =
109
134
  case search_criteria.direction
110
- when :forwards; '>'
111
- when :backwards; '<'
112
- end
113
-
114
- # build the sql comparison where clause fragment
115
- comparison_sql = build_recursive_comparison( operator )
116
-
117
- # only Postgres seems to understand real booleans
118
- # everything else needs the big case statement to be compared
119
- # to something
120
- unless entity_class.connection.adapter_name == 'PostgreSQL'
121
- comparison_sql += " = #{sql_boolean true}"
122
- end
123
-
124
- # build parameter values
125
- @params ||= {}
126
- order_attributes.each {|x| @params[x.to_sym] = start_entity.send( x.attribute )}
135
+ when :forwards; 1
136
+ when :backwards; -1
137
+ end * local_direction
127
138
 
128
- @conditions += " and " + comparison_sql
139
+ # 1 indexes >, -1 indexes <
140
+ ['','>','<'][comparator_direction]
129
141
  end
130
142
 
131
- # get the search value parameter, in SQL format
132
- def search_clause( field_name )
133
- if search_criteria.whole_words?
134
- <<-EOF
135
- (
136
- #{field_name} #{like_operator} #{quote "% #{search_criteria.search_text} %"}
137
- or
138
- #{field_name} #{like_operator} #{quote "#{search_criteria.search_text} %"}
139
- or
140
- #{field_name} #{like_operator} #{quote "% #{search_criteria.search_text}"}
141
- )
142
- EOF
143
- else
144
- "#{field_name} #{like_operator} #{quote "%#{search_criteria.search_text}%"}"
145
- end
146
- end
147
-
148
- def ascending_order
149
- order_attributes.map{|x| x.to_sql}.join(',')
150
- end
151
-
152
- def descending_order
153
- order_attributes.map{|x| x.to_reverse_sql}.join(',')
154
- end
155
-
156
- def order
157
- case search_criteria.direction
158
- when :forwards; ascending_order
159
- when :backwards; descending_order
143
+ # returns a collection of [ attribute, (1|-1) ]
144
+ # where 1 is forward/asc (>) and -1 is backward/desc (<)
145
+ def order_attributes
146
+ if @order_attributes.nil?
147
+ @order_attributes =
148
+ @dataset.opts[:order].map do |order_expr|
149
+ case order_expr
150
+ when Symbol; [ order_expr, 1 ]
151
+ when Sequel::SQL::OrderedExpression; [ order_expr.expression, order_expr.descending ? -1 : 1 ]
152
+ else
153
+ raise "unknown order_expr: #{order_expr.inspect}"
154
+ end
155
+ end
160
156
  end
157
+ @order_attributes
161
158
  end
162
-
163
159
  end
164
160
 
165
161
  end
162
+
@@ -1,47 +1,36 @@
1
- require 'rubygems'
2
- require 'Qt4'
3
1
  require 'fastercsv'
4
- require 'qtext/action_builder.rb'
2
+ require 'stringio'
5
3
 
6
4
  require 'clevic/model_builder.rb'
7
5
  require 'clevic/filter_command.rb'
8
6
 
9
7
  module Clevic
10
8
 
11
- # The view class
12
- class TableView < Qt::TableView
9
+ # Various methods common to view classes
10
+ class TableView
13
11
  include ActionBuilder
14
12
 
15
13
  # the current filter command
16
- # TODO better in QAbstractSortFilter?
17
14
  attr_accessor :filtered
18
15
  def filtered?; !@filtered.nil?; end
19
16
 
20
- # status_text is emitted when this object was to display something in the status bar
21
- # error_test is emitted when an error of some kind must be displayed to the user.
22
- # filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
23
- signals 'status_text(QString)', 'filter_status(bool)'
24
-
17
+ # Called from the gui-framework adapter code in this class
25
18
  # arg is:
26
19
  # - an instance of Clevic::View
27
20
  # - an instance of TableModel
28
- def initialize( arg, parent = nil, &block )
29
- # need the empty block here, otherwise Qt bindings grab &block
30
- super( parent ) {}
31
-
21
+ def framework_init( arg, &block )
32
22
  # the model/entity_class/builder
33
23
  case
34
- when arg.kind_of?( TableModel )
24
+ when arg.is_a?( TableModel )
35
25
  self.model = arg
36
26
  init_actions( arg.entity_view )
37
27
 
38
- else
39
- # arg is a subclass of Clevic::View
28
+ when arg.is_a?( Clevic::View )
40
29
  model_builder = arg.define_ui
41
30
  model_builder.exec_ui_block( &block )
42
31
 
43
32
  # make sure the TableView has a fully-populated TableModel
44
- # self.model is necessary to invoke the Qt layer
33
+ # self.model is necessary to invoke the GUI layer
45
34
  self.model = model_builder.build( self )
46
35
  self.object_name = arg.widget_name
47
36
 
@@ -49,31 +38,18 @@ class TableView < Qt::TableView
49
38
  connect_view_signals( arg )
50
39
 
51
40
  init_actions( arg )
52
- end
53
41
 
54
- # see closeEditor
55
- @next_index = nil
56
-
57
- # set some Qt things
58
- self.horizontal_header.movable = false
59
- # TODO might be useful to allow movable vertical rows,
60
- # but need to change the shortcut ideas of next and previous rows
61
- self.vertical_header.movable = false
62
- self.sorting_enabled = false
63
-
64
- self.context_menu_policy = Qt::ActionsContextMenu
42
+ else
43
+ raise "Don't know what to do with #{arg.inspect}"
44
+ end
65
45
  end
66
46
 
47
+ attr_accessor :object_name
48
+
67
49
  def title
68
50
  @title ||= model.entity_view.title
69
51
  end
70
52
 
71
- def connect_view_signals( entity_view )
72
- model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
73
- entity_view.notify_data_changed( self, top_left, bottom_right )
74
- end
75
- end
76
-
77
53
  # find the row index for the given field id (symbol)
78
54
  def field_column( field )
79
55
  raise "use model.field_column( field )"
@@ -90,32 +66,36 @@ class TableView < Qt::TableView
90
66
  # block for :insane. Will also catch exceptions thrown in actions to make
91
67
  # core application more robust to model & view errors.
92
68
  def action_triggered( &block )
93
- begin
94
- catch :insane do
95
- yield
96
- end
69
+ catch :insane do
70
+ yield
71
+ end
72
+
97
73
  rescue Exception => e
98
- puts e.message
74
+ puts
75
+ puts "#{model.entity_view.class.name}: #{e.message}"
99
76
  puts e.backtrace
100
- end
101
77
  end
102
78
 
79
+
80
+ # called from framework_init
103
81
  def init_actions( entity_view )
104
82
  # add model actions, if they're defined
105
83
  list( :model ) do |ab|
106
84
  entity_view.define_actions( self, ab )
107
- separator
85
+ separator unless collect_actions.empty?
108
86
  end
109
87
 
110
88
  # list of actions in the edit menu
111
89
  list( :edit ) do
112
- #~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
113
- action :action_copy, '&Save', :shortcut => Qt::KeySequence::Save, :method => :save_current_row
114
- action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
115
- action :action_paste, '&Paste', :shortcut => Qt::KeySequence::Paste, :method => :paste
90
+ #~ new_action :action_cut, 'Cu&t', :shortcut => 'Ctrl-X'
91
+ action :action_save, '&Save', :shortcut => 'Ctrl+S', :method => :save_current_rows
92
+ #~ action :action_cut, 'Cu&t', :shortcut => 'Ctrl+X', :method => :cut_current_selection
93
+ action :action_copy, '&Copy', :shortcut => 'Ctrl+C', :method => :copy_current_selection
94
+ action :action_paste, '&Paste', :shortcut => 'Ctrl+V', :method => :paste
95
+ action :action_delete, '&Delete', :shortcut => 'Del', :method => :delete_selection
116
96
  separator
117
- action :action_ditto, '&Ditto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
118
- action :action_ditto_right, 'Ditto R&ight', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
97
+ action :action_ditto, 'D&itto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
98
+ action :action_ditto_right, 'Ditto Ri&ght', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
119
99
  action :action_ditto_left, '&Ditto L&eft', :shortcut => 'Ctrl+[', :method => :ditto_left, :tool_tip => 'Copy field one to left from previous record'
120
100
  action :action_insert_date, 'Insert Date', :shortcut => 'Ctrl+;', :method => :insert_current_date
121
101
  action :action_open_editor, '&Open Editor', :shortcut => 'F4', :method => :open_editor
@@ -133,95 +113,47 @@ class TableView < Qt::TableView
133
113
 
134
114
  separator
135
115
 
136
- # list of actions called search
116
+ # list of actions for search
137
117
  list( :search ) do
138
- action :action_find, '&Find', :shortcut => Qt::KeySequence::Find, :method => :find
139
- action :action_find_next, 'Find &Next', :shortcut => Qt::KeySequence::FindNext, :method => :find_next
118
+ action :action_find, '&Find', :shortcut => 'Ctrl+F', :method => :find
119
+ action :action_find_next, 'Find &Next', :shortcut => 'Ctrl+G', :method => :find_next
140
120
  action :action_filter, 'Fil&ter', :checkable => true, :shortcut => 'Ctrl+L', :method => :filter_by_current
141
121
  action :action_highlight, '&Highlight', :visible => false, :shortcut => 'Ctrl+H'
142
122
  end
143
123
  end
144
124
 
125
+ def clipboard
126
+ # Clipboard will be a framework-specific class
127
+ @clipboard = Clipboard.new
128
+ end
129
+
130
+ # copy current selection to clipboard as CSV
131
+ # TODO add text/csv, text/tab-separated-values, text/html as well as text/plain
145
132
  def copy_current_selection
146
- text = String.new
147
- selection_model.selection.each do |selection_range|
148
- (selection_range.top..selection_range.bottom).each do |row|
149
- row_ary = Array.new
150
- selection_model.selected_indexes.each do |index|
151
- if index.row == row
152
- value = index.gui_value
153
- row_ary <<
154
- unless value.nil?
155
- index.field.do_format( value )
156
- end
157
- end
158
- end
159
- text << row_ary.to_csv
160
- end
161
- end
162
- Qt::Application::clipboard.text = text
133
+ clipboard.text = current_selection_csv
163
134
  end
164
135
 
165
- def paste
166
- sanity_check_read_only
167
-
168
- # remove trailing "\n" if there is one
169
- text = Qt::Application::clipboard.text.chomp
170
- arr = FasterCSV.parse( text )
171
-
172
- selection_model.selected_indexes.
173
- return true if selection_model.selection.size != 1
174
-
175
- selection_range = selection_model.selection.first
176
- selected_index = selection_model.selected_indexes.first
177
-
178
- if selection_model.selection.size == 1 && selection_range.single_cell?
179
- # only one cell selected, so paste like a spreadsheet
180
- if text.empty?
181
- # just clear the current selection
182
- model.setData( selected_index, nil.to_variant )
183
- else
184
- paste_to_index( selected_index, arr )
185
- end
186
- else
187
- if arr.size == 1 && arr.first.size == 1
188
- # only one value to paste, and multiple selection, so
189
- # set all selected indexes to the value
190
- value = arr.first.first
191
- selection_model.selected_indexes.each do |index|
192
- model.setData( index, value.to_variant, Qt::PasteRole )
193
- # save records to db
194
- model.save( index )
195
- end
196
-
197
- # notify of changed data
198
- model.data_changed do |change|
199
- sorted = selection_model.selected_indexes.sort
200
- change.top_left = sorted.first
201
- change.bottom_right = sorted.last
202
- end
203
- else
204
- return true if selection_range.height != arr.size
205
- return true if selection_range.width != arr.first.size
206
-
207
- # size is the same, so do the paste
208
- paste_to_index( selected_index, arr )
209
- end
136
+ # return the current selection as csv
137
+ def current_selection_csv
138
+ buffer = StringIO.new
139
+ selected_rows.each do |row|
140
+ buffer << row.map {|index| index.edit_value }.to_csv
210
141
  end
142
+ buffer.string
211
143
  end
212
144
 
213
145
  def sanity_check_ditto
214
146
  if current_index.row == 0
215
- emit status_text( 'No previous record to copy.' )
147
+ emit_status_text( 'No previous record to copy.' )
216
148
  throw :insane
217
149
  end
218
150
  end
219
151
 
220
152
  def sanity_check_read_only
221
153
  if current_index.field.read_only?
222
- emit status_text( 'Can\'t copy into read-only field.' )
154
+ emit_status_text( 'Can\'t copy into read-only field.' )
223
155
  elsif current_index.entity.readonly?
224
- emit status_text( 'Can\'t copy into read-only record.' )
156
+ emit_status_text( 'Can\'t copy into read-only record.' )
225
157
  else
226
158
  sanity_check_read_only_table
227
159
  return
@@ -231,7 +163,7 @@ class TableView < Qt::TableView
231
163
 
232
164
  def sanity_check_read_only_table
233
165
  if model.read_only?
234
- emit status_text( 'Can\'t modify a read-only table.' )
166
+ emit_status_text( 'Can\'t modify a read-only table.' )
235
167
  throw :insane
236
168
  end
237
169
  end
@@ -251,7 +183,7 @@ class TableView < Qt::TableView
251
183
  # their fields don't have the same attribute_type.
252
184
  def sanity_check_types( from, to )
253
185
  unless from.field.attribute_type == to.field.attribute_type
254
- emit status_text( 'Incompatible data' )
186
+ emit_status_text( 'Incompatible data' )
255
187
  throw :insane
256
188
  end
257
189
  end
@@ -260,7 +192,7 @@ class TableView < Qt::TableView
260
192
  sanity_check_ditto
261
193
  sanity_check_read_only
262
194
  if current_index.column >= model.column_count - 1
263
- emit status_text( 'No column to the right' )
195
+ emit_status_text( 'No column to the right' )
264
196
  else
265
197
  one_up_right = current_index.choppy {|i| i.row -= 1; i.column += 1 }
266
198
  sanity_check_types( one_up_right, current_index )
@@ -273,7 +205,7 @@ class TableView < Qt::TableView
273
205
  sanity_check_ditto
274
206
  sanity_check_read_only
275
207
  unless current_index.column > 0
276
- emit status_text( 'No column to the left' )
208
+ emit_status_text( 'No column to the left' )
277
209
  else
278
210
  one_up_left = current_index.choppy { |i| i.row -= 1; i.column -= 1 }
279
211
  sanity_check_types( one_up_left, current_index )
@@ -289,72 +221,85 @@ class TableView < Qt::TableView
289
221
  end
290
222
 
291
223
  def open_editor
224
+ # tell the table to edit here
292
225
  edit( current_index )
293
- delegate = item_delegate( current_index )
294
- delegate.full_edit
226
+
227
+ # tell the editing component to do full edit, eg if it's a combo
228
+ # box to open the list.
229
+ current_index.field.delegate.full_edit
295
230
  end
296
231
 
297
232
  # Add a new row and move to it, provided we're not in a read-only view.
298
233
  def new_row
299
234
  sanity_check_read_only_table
300
235
  model.add_new_item
301
- new_row_index = model.index( model.row_count - 1, 0 )
302
- currentChanged( new_row_index, current_index )
303
236
  selection_model.clear
304
- self.current_index = new_row_index
237
+ self.current_index = model.create_index( model.row_count - 1, 0 )
305
238
  end
306
239
 
307
240
  # Delete the current selection. If it's a set of rows, just delete
308
241
  # them. If it's a rectangular selection, set the cells to nil.
309
242
  # TODO make sure all affected rows are saved.
310
243
  def delete_selection
311
- sanity_check_read_only
312
-
313
- # translate from ModelIndex objects to row indices
314
- rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
315
- unless rows.empty?
316
- # header rows are selected, so delete them
317
- model.remove_rows( rows )
318
- else
319
- # otherwise various cells are selected, so delete the cells
320
- delete_cells
244
+ busy_cursor do
245
+ begin
246
+ sanity_check_read_only
247
+
248
+ # TODO translate from ModelIndex objects to row indices
249
+ puts "#{__FILE__}:#{__LINE__}:implement vertical_header for delete_selection"
250
+ #~ rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
251
+ rows = []
252
+ unless rows.empty?
253
+ # header rows are selected, so delete them
254
+ model.remove_rows( rows )
255
+ else
256
+ # otherwise various cells are selected, so delete the cells
257
+ delete_cells
258
+ end
259
+ rescue
260
+ show_error $!.message
261
+ end
321
262
  end
322
263
  end
323
264
 
265
+ def search_dialog
266
+ @search_dialog ||= SearchDialog.new( nil )
267
+ end
268
+
324
269
  # display a search dialog, and find the entered text
325
270
  def find
326
- @search_dialog ||= SearchDialog.new
327
- result = @search_dialog.exec( current_index.gui_value )
271
+ result = search_dialog.exec( current_index.display_value )
328
272
 
329
- override_cursor( Qt::BusyCursor ) do
330
- case result
331
- when Qt::Dialog::Accepted
332
- search_for = @search_dialog.search_text
333
- search( @search_dialog )
334
- when Qt::Dialog::Rejected
273
+ busy_cursor do
274
+ case
275
+ when result.accepted?
276
+ search( search_dialog )
277
+ when result.rejected?
335
278
  puts "Don't search"
336
279
  else
337
- puts "unknown dialog code #{result}"
280
+ puts "unknown dialog result #{result}"
338
281
  end
339
282
  end
340
283
  end
341
284
 
342
285
  def find_next
286
+ # yes, this must be an @ otherwise it lazy-creates
287
+ # and will never be nil
343
288
  if @search_dialog.nil?
344
- emit status_text( 'No previous find' )
289
+ emit_status_text( 'No previous find' )
345
290
  else
346
- override_cursor( Qt::BusyCursor ) do
347
- save_from_start = @search_dialog.from_start?
348
- @search_dialog.from_start = false
349
- search( @search_dialog )
350
- @search_dialog.from_start = save_from_start
291
+ busy_cursor do
292
+ save_from_start = search_dialog.from_start?
293
+ search_dialog.from_start = false
294
+ search( search_dialog )
295
+ search_dialog.from_start = save_from_start
351
296
  end
352
297
  end
353
298
  end
354
299
 
355
300
  # force a complete reload of the current tab's data
356
301
  def refresh
357
- override_cursor( Qt::BusyCursor ) do
302
+ busy_cursor do
358
303
  restore_entity do
359
304
  model.reload_data
360
305
  end
@@ -373,50 +318,7 @@ class TableView < Qt::TableView
373
318
 
374
319
  # alternative access for auto_size_column
375
320
  def auto_size_attribute( attribute, sample )
376
- col = model.attributes.index( attribute )
377
- self.set_column_width( col, column_size( col, sample ).width )
378
- end
379
-
380
- # set the size of the column from the sample
381
- def auto_size_column( col, sample )
382
- self.set_column_width( col, column_size( col, sample ).width )
383
- end
384
-
385
- # set the size of the column from the string value of the data
386
- # mostly copied from qheaderview.cpp:2301
387
- def column_size( col, data )
388
- opt = Qt::StyleOptionHeader.new
389
-
390
- # fetch font size
391
- fnt = font
392
- fnt.bold = true
393
- opt.fontMetrics = Qt::FontMetrics.new( fnt )
394
-
395
- # set data
396
- opt.text = data.to_s
397
-
398
- # icon size. Not needed
399
- #~ variant = d->model->headerData(logicalIndex, d->orientation, Qt::DecorationRole);
400
- #~ opt.icon = qvariant_cast<QIcon>(variant);
401
- #~ if (opt.icon.isNull())
402
- #~ opt.icon = qvariant_cast<QPixmap>(variant);
403
-
404
- size = Qt::Size.new( 100, 30 )
405
- # final parameter could be header section
406
- style.sizeFromContents( Qt::Style::CT_HeaderSection, opt, size );
407
- end
408
-
409
- # TODO is this even used?
410
- def relational_delegate( attribute, options )
411
- col = model.attributes.index( attribute )
412
- delegate = RelationalDelegate.new( self, model.columns[col], options )
413
- set_item_delegate_for_column( col, delegate )
414
- end
415
-
416
- def delegate( attribute, delegate_class, options = nil )
417
- col = model.attributes.index( attribute )
418
- delegate = delegate_class.new( self, attribute, options )
419
- set_item_delegate_for_column( col, delegate )
321
+ auto_size_column( model.attributes.index( attribute ), sample )
420
322
  end
421
323
 
422
324
  # is current_index on the last row?
@@ -428,37 +330,7 @@ class TableView < Qt::TableView
428
330
  def last_cell?
429
331
  current_index.row == model.row_count - 1 && current_index.column == model.column_count - 1
430
332
  end
431
-
432
- # make sure row size is correct
433
- # show error messages for data
434
- def setModel( model )
435
- # must do this otherwise model gets garbage collected
436
- @model = model
437
-
438
- # make sure we get nice spacing
439
- vertical_header.default_section_size = vertical_header.minimum_section_size
440
- super
441
-
442
- # set delegates
443
- model.fields.each_with_index do |field, index|
444
- set_item_delegate_for_column( index, field.delegate )
445
- end
446
-
447
- # data errors
448
- model.connect( SIGNAL( 'data_error(QModelIndex, QVariant, QString)' ) ) do |index,variant,msg|
449
- error_message = Qt::ErrorMessage.new( self )
450
- error_message.show_message( "Incorrect value '#{variant.value}' entered for field [#{index.attribute.to_s}].\nMessage was: #{msg}" )
451
- error_message.show
452
- end
453
- end
454
-
455
- # and override this because the Qt bindings don't call
456
- # setModel otherwise
457
- def model=( model )
458
- setModel( model )
459
- resize_columns
460
- end
461
-
333
+
462
334
  # resize all fields based on heuristics rather
463
335
  # than iterating through the entire data model
464
336
  def resize_columns
@@ -467,48 +339,11 @@ class TableView < Qt::TableView
467
339
  end
468
340
  end
469
341
 
470
- def moveCursor( cursor_action, modifiers )
471
- # TODO use this as a preload indicator
472
- super
473
- end
474
-
475
342
  # copied from actionpack
476
343
  def pluralize(count, singular, plural = nil)
477
344
  "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
478
345
  end
479
346
 
480
- # Paste a CSV array to the index, replacing whatever is at that index
481
- # and whatever is at other indices matching the size of the pasted
482
- # csv array. Create new rows if there aren't enough.
483
- def paste_to_index( top_left_index, csv_arr )
484
- csv_arr.each_with_index do |row,row_index|
485
- # append row if we need one
486
- model.add_new_item if top_left_index.row + row_index >= model.row_count
487
-
488
- row.each_with_index do |field, field_index|
489
- unless top_left_index.column + field_index >= model.column_count
490
- # do paste
491
- cell_index = top_left_index.choppy {|i| i.row += row_index; i.column += field_index }
492
- model.setData( cell_index, field.to_variant, Qt::PasteRole )
493
- else
494
- emit status_text( "#{pluralize( top_left_index.column + field_index, 'column' )} for pasting data is too large. Truncating." )
495
- end
496
- end
497
- # save records to db
498
- model.save( top_left_index.choppy {|i| i.row += row_index; i.column = 0 } )
499
- end
500
-
501
- # make the gui refresh
502
- model.data_changed do |change|
503
- change.top_left = top_left_index
504
- change.bottom_right = top_left_index.choppy do |i|
505
- i.row += csv_arr.size - 1
506
- i.column += csv_arr.first.size - 1
507
- end
508
- end
509
- emit model.headerDataChanged( Qt::Vertical, top_left_index.row, top_left_index.row + csv_arr.size )
510
- end
511
-
512
347
  # ask the question in a dialog. If the user says yes, execute the block
513
348
  def delete_multiple_cells?( question = 'Are you sure you want to delete multiple cells?', &block )
514
349
  sanity_check_read_only
@@ -516,15 +351,7 @@ class TableView < Qt::TableView
516
351
  # go ahead with delete if there's only 1 cell, or the user says OK
517
352
  delete_ok =
518
353
  if selection_model.selected_indexes.size > 1
519
- # confirmation message, until there are undos
520
- msg = Qt::MessageBox.new(
521
- Qt::MessageBox::Question,
522
- 'Multiple Delete',
523
- question,
524
- Qt::MessageBox::Yes | Qt::MessageBox::No,
525
- self
526
- )
527
- msg.exec == Qt::MessageBox::Yes
354
+ confirm_dialog( question, "Multiple Delete" ).accepted?
528
355
  else
529
356
  true
530
357
  end
@@ -547,12 +374,12 @@ class TableView < Qt::TableView
547
374
  # deletes were done, so call data_changed
548
375
  if cells_deleted
549
376
  # save affected rows
550
- selection_model.row_indexes.each do |index|
551
- index.entity.save
377
+ selection_model.row_indexes.each do |row_index|
378
+ save_row( model.create_index( row_index, 0 ) )
552
379
  end
553
380
 
554
381
  # emit data changed for all ranges
555
- selection_model.selection.each do |selection_range|
382
+ selection_model.ranges.each do |selection_range|
556
383
  model.data_changed( selection_range )
557
384
  end
558
385
  end
@@ -560,13 +387,20 @@ class TableView < Qt::TableView
560
387
  end
561
388
 
562
389
  def delete_rows
563
- delete_multiple_cells?( 'Are you sure you want to delete multiple rows?' ) do
564
- model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
390
+ delete_multiple_cells?( "Are you sure you want to delete #{selection_model.row_indexes.size} rows?" ) do
391
+ begin
392
+ model.remove_rows( selection_model.row_indexes )
393
+ rescue
394
+ puts $!.message
395
+ puts $!.backtrace
396
+ show_error $!.message
397
+ end
565
398
  end
566
399
  end
567
400
 
568
401
  # handle certain key combinations that aren't shortcuts
569
- def keyPressEvent( event )
402
+ # TODO what is returned from here?
403
+ def handle_key_press( event )
570
404
  begin
571
405
  # call to entity class for shortcuts
572
406
  begin
@@ -574,9 +408,7 @@ class TableView < Qt::TableView
574
408
  return view_result unless view_result.nil?
575
409
  rescue Exception => e
576
410
  puts e.backtrace
577
- error_message = Qt::ErrorMessage.new( self )
578
- error_message.show_message( "Error in shortcut handler for #{model.entity_view.name}: #{e.message}" )
579
- error_message.show
411
+ show_error( "Error in shortcut handler for #{model.entity_view.name}: #{e.message}" )
580
412
  end
581
413
 
582
414
  # thrown by the sanity_check_xxx methods
@@ -597,29 +429,20 @@ class TableView < Qt::TableView
597
429
  when event.ctrl? && event.return?
598
430
  new_row
599
431
 
600
- when event.delete?
601
- if selection_model.selected_indexes.size > 1
602
- delete_selection
603
- return true
604
- end
605
-
606
432
  else
607
433
  #~ puts event.inspect
608
434
  end
609
435
  end
610
- super
611
436
  rescue Exception => e
612
437
  puts e.backtrace
613
438
  puts e.message
614
- error_message = Qt::ErrorMessage.new( self )
615
- error_message.show_message( "Error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
616
- error_message.show
439
+ show_error( "handle_key_press #{__FILE__}:#{__LINE__} error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
617
440
  end
618
441
  end
619
442
 
620
- def save_current_row
621
- if !current_index.nil? && current_index.valid?
622
- save_row( current_index )
443
+ def save_current_rows
444
+ selection_model.row_indexes.each do |row_index|
445
+ save_row( model.create_index( row_index, 0 ) )
623
446
  end
624
447
  end
625
448
 
@@ -630,88 +453,28 @@ class TableView < Qt::TableView
630
453
  if !index.nil? && index.valid?
631
454
  saved = model.save( index )
632
455
  if !saved
633
- error_message = Qt::ErrorMessage.new( self )
634
- msg = model.collection[index.row].errors.to_a.join("\n")
635
- error_message.show_message( msg )
636
- error_message.show
456
+ # construct error message(s)
457
+ msg = index.entity.errors.map do |field, errors|
458
+ abbr_value = trim_middle( index.entity.send(field) )
459
+ "#{field} (#{abbr_value}) #{errors.join(',')}"
460
+ end.join( "\n" )
461
+
462
+ show_error( "#{index.rc} #{msg}", "Validation Errors" )
637
463
  end
638
464
  saved
639
465
  end
640
466
  end
641
467
 
642
468
  # save record whenever its row is exited
469
+ # make this work with framework
643
470
  def currentChanged( current_index, previous_index )
644
- @next_index = nil
645
- if current_index.row != previous_index.row
471
+ if previous_index.valid? && current_index.row != previous_index.row
472
+ self.next_index = nil
646
473
  save_row( previous_index )
647
474
  end
648
475
  super
649
476
  end
650
477
 
651
- # This is to allow entity model UI handlers to tell the view
652
- # whence to move the cursor when the current editor closes
653
- # (see closeEditor).
654
- def override_next_index( model_index )
655
- @next_index = model_index
656
- end
657
-
658
- # Call set_current_index with next_index ( from override_next_index )
659
- # or model_index, in that order. Set next_index to nil afterwards.
660
- def set_current_unless_override( model_index )
661
- set_current_index( @next_index || model_index )
662
- @next_index = nil
663
- end
664
-
665
- # work around situation where an ItemDelegate is open
666
- # when the surrouding tab is changed, but the right events
667
- # don't arrive.
668
- def hideEvent( event )
669
- # can't call super here, for some reason. Qt binding says method not found.
670
- # super
671
- @hiding = true
672
- end
673
-
674
- # work around situation where an ItemDelegate is open
675
- # when the surrouding tab is changed, but the right events
676
- # don't arrive.
677
- def showEvent( event )
678
- super
679
- @hiding = false
680
- end
681
-
682
- def focusOutEvent( event )
683
- super
684
- #~ save_current_row
685
- end
686
-
687
- # this is the only method that is called when an itemDelegate is open
688
- # and the tabs are changed.
689
- # Work around situation where an ItemDelegate is open
690
- # when the surrouding tab is changed, but the right events
691
- # don't arrive.
692
- def commitData( editor )
693
- super
694
- save_current_row if @hiding
695
- end
696
-
697
- # override to prevent tab pressed from editing next field
698
- # also takes into account that override_next_index may have been called
699
- def closeEditor( editor, end_edit_hint )
700
- puts "end_edit_hint: #{end_edit_hint.inspect}" if $options[:debug]
701
- case end_edit_hint
702
- when Qt::AbstractItemDelegate.EditNextItem
703
- super( editor, Qt::AbstractItemDelegate.NoHint )
704
- set_current_unless_override( current_index.choppy { |i| i.column += 1 } )
705
-
706
- when Qt::AbstractItemDelegate.EditPreviousItem
707
- super( editor, Qt::AbstractItemDelegate.NoHint )
708
- set_current_unless_override( current_index.choppy { |i| i.column -= 1 } )
709
-
710
- else
711
- super
712
- end
713
- end
714
-
715
478
  # toggle the filter, based on current selection.
716
479
  def filter_by_current( bool_filter )
717
480
  filter_by_indexes( selection_or_current )
@@ -742,7 +505,7 @@ class TableView < Qt::TableView
742
505
  end
743
506
 
744
507
  # Filter by the value in the current index.
745
- # indexes is a collection of Qt::ModelIndex
508
+ # indexes is a collection of TableIndex instances
746
509
  def filter_by_indexes( indexes )
747
510
  case
748
511
  when filtered?
@@ -751,32 +514,32 @@ class TableView < Qt::TableView
751
514
  filtered.undo
752
515
  self.filtered = nil
753
516
  # update status bar
754
- emit status_text( nil )
755
- emit filter_status( false )
517
+ emit_status_text( nil )
518
+ emit_filter_status( false )
756
519
  end
757
520
 
758
521
  when indexes.empty?
759
- emit status_text( "No field selected for filter" )
522
+ emit_status_text( "No field selected for filter" )
760
523
 
761
524
  when !indexes.first.field.filterable?
762
- emit status_text( "Can't filter on #{indexes.first.field.label}" )
525
+ emit_status_text( "Can't filter on #{indexes.first.field.label}" )
763
526
 
764
527
  when indexes.size > 1
765
- emit status_text( "Can't do multiple selection filters yet" )
528
+ emit_status_text( "Can't do multiple selection filters yet" )
766
529
 
767
530
  when indexes.first.entity.new_record?
768
- emit status_text( "Can't filter on a new row" )
531
+ emit_status_text( "Can't filter on a new row" )
769
532
 
770
533
  else
771
534
  self.filtered = FilterCommand.new( self, indexes, :conditions => { indexes.first.field_name => indexes.first.field_value } )
772
535
  # try to end up on the same entity, even after the filter
773
536
  restore_entity do
774
- emit filter_status( filtered.doit )
537
+ emit_filter_status( filtered.doit )
775
538
  end
776
539
  # update status bar
777
- emit status_text( filtered.status_message )
778
- end
779
- filtered?
540
+ emit_status_text( filtered.status_message )
541
+ end
542
+ filtered?
780
543
  end
781
544
 
782
545
  # Move to the row for the given entity and the given column.
@@ -784,13 +547,13 @@ class TableView < Qt::TableView
784
547
  # field_column will be called to find the integer index.
785
548
  def select_entity( entity, column = nil )
786
549
  # sanity check that the entity can actually be found
787
- Kernel.raise "entity is nil" if entity.nil?
788
- unless entity.class == model.entity_class
789
- Kernel.raise "entity #{entity.class.name} does not match class #{model.entity_class.name}"
550
+ raise "entity is nil" if entity.nil?
551
+ unless entity.is_a?( model.entity_class )
552
+ raise "entity #{entity.class.name} does not match class #{model.entity_class.name}"
790
553
  end
791
554
 
792
555
  # find the row for the saved entity
793
- found_row = override_cursor( Qt::BusyCursor ) do
556
+ found_row = busy_cursor do
794
557
  model.collection.index_for_entity( entity )
795
558
  end
796
559
 
@@ -807,16 +570,14 @@ class TableView < Qt::TableView
807
570
  # * whole_words?
808
571
  # * direction ( :forward, :backward )
809
572
  # * from_start?
810
- #
811
- # TODO formalise this?
812
573
  def search( search_criteria )
813
574
  indexes = model.search( current_index, search_criteria )
814
575
  if indexes.size > 0
815
- emit status_text( "Found #{search_criteria.search_text} at row #{indexes.first.row}" )
576
+ emit_status_text( "Found #{search_criteria.search_text} at row #{indexes.first.row}" )
816
577
  selection_model.clear
817
578
  self.current_index = indexes.first
818
579
  else
819
- emit status_text( "No match found for #{search_criteria.search_text}" )
580
+ emit_status_text( "No match found for #{search_criteria.search_text}" )
820
581
  end
821
582
  end
822
583
 
@@ -825,11 +586,7 @@ class TableView < Qt::TableView
825
586
  # TODO doesn't really belong here because TableView will not always
826
587
  # be in a TabWidget context.
827
588
  def find_table_view( entity_model_or_view )
828
- parent.children.find do |x|
829
- if x.is_a? TableView
830
- x.model.entity_view.class == entity_model_or_view || x.model.entity_class == entity_model_or_view
831
- end
832
- end
589
+ raise "framework responsibility"
833
590
  end
834
591
 
835
592
  # execute the block with the TableView instance
@@ -838,21 +595,27 @@ class TableView < Qt::TableView
838
595
  # TODO doesn't really belong here because TableView will not always
839
596
  # be in a TabWidget context.
840
597
  def with_table_view( entity_model_or_view, &block )
841
- tv = find_table_view( entity_model_or_view )
842
- yield( tv ) unless tv.nil?
598
+ raise "framework responsibility"
843
599
  end
844
600
 
845
601
  # make this window visible if it's in a TabWidget
846
602
  # TODO doesn't really belong here because TableView will not always
847
603
  # be in a TabWidget context.
848
- def raise
849
- # the tab's parent is a StackedWiget, and its parent is TabWidget
850
- tab_widget = parent.parent
851
- tab_widget.current_widget = self if tab_widget.class == Qt::TabWidget
604
+ def raise_widget
605
+ raise "framework responsibility"
852
606
  end
853
607
 
854
- protected
608
+ # set next_index for certain operations. Is only activated when
609
+ # to_next_index is called.
610
+ attr_accessor :next_index
855
611
 
612
+ protected
613
+
614
+ # show a busy cursor, do the block, back to normal cursor
615
+ # return value of block
616
+ # TODO implement generic way of indicating framework responsibility
617
+ # :busy_cursor
618
+
856
619
  # return either the set of indexes with all invalid indexes
857
620
  # remove, or the current selection.
858
621
  def indexes_or_current( indexes )
@@ -865,12 +628,21 @@ protected
865
628
 
866
629
  # strip out bad indexes, so other things don't have to check
867
630
  # can't use select because copying indexes causes an abort
868
- #~ retval.select{|x| x != nil && x.valid?}
631
+ # ie retval.select{|x| x != nil && x.valid?}
869
632
  retval.reject!{|x| x.nil? || !x.valid?}
870
633
  # retval needed here because reject! returns nil if nothing was rejected
871
634
  retval
872
635
  end
873
-
636
+
637
+ # move to next_index, if it's set
638
+ def to_next_index
639
+ if next_index
640
+ self.current_index = next_index
641
+ self.next_index = nil
642
+ end
643
+ end
874
644
  end
875
645
 
646
+ require 'clevic/table_view_paste.rb'
647
+
876
648
  end