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
@@ -0,0 +1,165 @@
1
+ module Clevic
2
+ # to be included in something that responds to entity and field
3
+ # used for getting values from the entity based on the definitions
4
+ # in the field.
5
+ module FieldValuer
6
+
7
+ # the value for this index
8
+ # used to be gui_value, but that wasn't right
9
+ def raw_value
10
+ field.value_for( entity )
11
+ end
12
+
13
+ def display_value
14
+ field.do_format( raw_value ) unless raw_value.nil?
15
+ end
16
+
17
+ def edit_value
18
+ field.do_edit_format( raw_value ) unless raw_value.nil?
19
+ end
20
+
21
+ # Set the value from an editable text representation
22
+ # of the value
23
+ def edit_value=( value )
24
+ # translate the value from the ui to something that
25
+ # the model will understand
26
+ self.attribute_value =
27
+ case
28
+ # allow flexibility in entering dates. For example
29
+ # 16jun, 16-jun, 16 jun, 16 jun 2007 would be accepted here
30
+ # TODO need to be cleverer about which year to use
31
+ # for when you're entering 16dec and you're in the next
32
+ # year
33
+ when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})[ /-]?(\w{3})$}
34
+ Date.parse( "#$1 #$2 #{Time.now.year.to_s}" )
35
+
36
+ # if a digit only is entered, fetch month and year from
37
+ # previous row
38
+ when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})$}
39
+ # year,month,day
40
+ prev_date = prev.attribute_value
41
+ if prev_date
42
+ Date.new( prev_date.year, prev_date.month, $1.to_i )
43
+ else
44
+ value
45
+ end
46
+
47
+ # this one is mostly to fix date strings that have come
48
+ # out of the db and been formatted
49
+ when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{2})[ /-](\w{3})[ /-](\d{2})$}
50
+ Date.parse( "#$1 #$2 20#$3" )
51
+
52
+ # allow lots of flexibility in entering times
53
+ # 01:17, 0117, 117, 1 17, are all accepted
54
+ when field.meta.type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
55
+ Time.parse( "#$1:#$2" )
56
+
57
+ # remove thousand separators, allow for space and comma
58
+ # instead of . as a decimal separator
59
+ when field.meta.type == :decimal
60
+ # do various transforms
61
+ case
62
+ # accept a space or a comma instead of a . for floats
63
+ when value =~ /(.*?)(\d)[ ,](\d{2})$/
64
+ "#$1#$2.#$3"
65
+ else
66
+ value
67
+ end.gsub( ',', '' ) # strip remaining commas
68
+
69
+ else
70
+ value
71
+ end
72
+ end
73
+
74
+ def find_related( attribute, value )
75
+ candidates = field.related_class.adaptor.find( :all, :conditions => {attribute => value} )
76
+ if candidates.size != 1
77
+ raise "#{candidates.size} != 1 candidates for #{value}: #{candidates.inspect}"
78
+ end
79
+ candidates.first
80
+ end
81
+
82
+ # set the field value from a value that could be
83
+ # a text representation a-la edit_value, or possible
84
+ # a name from a related entity
85
+ def text_value=( value )
86
+ case field.display
87
+ when Symbol
88
+ # we have a related class of some kind,
89
+ self.attribute_value = find_related( field.display, value )
90
+
91
+ when NilClass
92
+ self.edit_value = value
93
+
94
+ when String
95
+ # allow plain strings, but not dotted paths
96
+ if field.display['.']
97
+ raise "display (#{field.display}) is a dotted path"
98
+ else
99
+ self.attribute_value = find_related( field.display.to_sym, value )
100
+ end
101
+
102
+ else
103
+ raise "display is a not a symbol or nil: #{display.inspect}"
104
+ end
105
+ end
106
+
107
+ # fetch the value of the attribute, without following
108
+ # the full path. This will return a related entity for
109
+ # belongs_to or has_one relationships, or a plain value
110
+ # for model attributes
111
+ def attribute_value
112
+ entity.send( field.attribute )
113
+ end
114
+
115
+ # cache the writer method name since it's not likely to change
116
+ def writer
117
+ @writer ||= "#{field.attribute.to_s}="
118
+ end
119
+
120
+ # set the value of the attribute, without following the
121
+ # full path.
122
+ # TODO remove need to constantly recalculate the attribute writer
123
+ def attribute_value=( value )
124
+ entity.send( writer, value )
125
+ end
126
+
127
+ def tooltip
128
+ case
129
+ # show validation errors
130
+ when has_errors?
131
+ errors.join("\n")
132
+
133
+ # provide a tooltip when an empty relational field is encountered
134
+ # TODO should be part of field definition
135
+ when field.meta.type == :association
136
+ field.delegate.if_empty_message
137
+
138
+ # read-only field
139
+ when field.read_only?
140
+ field.tooltip_for( entity ) || 'Read-only'
141
+
142
+ else
143
+ field.tooltip_for( entity )
144
+ end
145
+ end
146
+
147
+ class Valuer
148
+ include FieldValuer
149
+ attr_accessor :field, :entity
150
+ def initialize( field, entity )
151
+ @field, @entity = field, entity
152
+ end
153
+ end
154
+
155
+ def self.valuer( field, entity )
156
+ Valuer.new(field,entity)
157
+ end
158
+ end
159
+
160
+ # Provides the minimum necessary for valuing, ie for irb testing
161
+ module SimpleFieldValuer
162
+ include FieldValuer
163
+ attr_accessor :field, :entity
164
+ end
165
+ end
@@ -7,18 +7,14 @@ module Clevic
7
7
 
8
8
  # Better make the status message now, before the indexes become invalid
9
9
  @status_message =
10
- begin
11
- "Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.gui_value}"
12
- rescue
13
- "Filtered"
14
- end
10
+ "Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.display_value}"
15
11
  end
16
12
 
17
13
  # Do the filtering. Return true if successful, false otherwise.
18
14
  def doit
19
15
  begin
20
16
  # store current AR conditions
21
- @stored_conditions = @table_view.model.cache_table.options
17
+ @stored_conditions = @table_view.model.cache_table.find_options
22
18
 
23
19
  # store auto_new
24
20
  @auto_new = @table_view.model.auto_new
@@ -0,0 +1,52 @@
1
+ module Clevic
2
+
3
+ #includers must provide meta and display
4
+ module GenericFormat
5
+ # Return true if the field is a date, datetime, time or timestamp.
6
+ # If display is nil, the value is calculated, so we need
7
+ # to check the value. Otherwise use the field metadata.
8
+ # Cache the result for the first non-nil value.
9
+ def is_date_time?( value )
10
+ if value.nil?
11
+ false
12
+ else
13
+ @is_date_time ||=
14
+ if display.nil?
15
+ [:time, :date, :datetime, :timestamp].include?( meta.type )
16
+ else
17
+ # it's a virtual field, so we need to use the value
18
+ value.is_a?( Date ) || value.is_a?( Time )
19
+ end
20
+ end
21
+ end
22
+
23
+ # apply format to value. Use strftime for date_time types, or % for everything else.
24
+ # If format is a proc, pass value to it.
25
+ def do_generic_format( format, value )
26
+ begin
27
+ unless format.nil?
28
+ if format.is_a? Proc
29
+ format.call( value )
30
+ else
31
+ if is_date_time?( value )
32
+ value.strftime( format )
33
+ else
34
+ format % value
35
+ end
36
+ end
37
+ else
38
+ value
39
+ end
40
+ rescue Exception => e
41
+ puts "format: #{format.inspect}"
42
+ puts "value.class: #{value.class.inspect}"
43
+ puts "value: #{value.inspect}"
44
+ puts e.message
45
+ puts e.backtrace
46
+ nil
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ module Clevic
2
+
3
+ # Field for the other end of a one-to-many, or maybe also a many-to-many.
4
+ class ManyField < Field
5
+ end
6
+
7
+ end
@@ -1,9 +1,7 @@
1
- require 'activerecord'
2
- require 'facets/dictionary'
1
+ # TODO not needed for 1.9
2
+ require 'hashery/orderedhash'
3
3
 
4
4
  require 'clevic/table_model.rb'
5
- require 'clevic/delegates.rb'
6
- require 'clevic/text_delegate.rb'
7
5
  require 'clevic/cache_table.rb'
8
6
  require 'clevic/field.rb'
9
7
 
@@ -21,99 +19,187 @@ you need it.
21
19
 
22
20
  To that end, there are 2 ways to define UIs:
23
21
 
24
- - an Embedded View as part of the ActiveRecord object (which is useful if you
22
+ - an Embedded View as part of the model (Sequel::Model) object (which is useful if you
25
23
  want minimal framework overhead). Just show me the data, dammit.
26
24
 
27
25
  - a Separate View in a separate class (which is useful when you want several
28
26
  diffent views of the same underlying table). I want a neato-nifty UI that does
29
27
  (relatively) complex things.
30
28
 
31
- I've tried to consistently refer to an instance of an ActiveRecord::Base subclass
32
- as an 'entity'.
29
+ I've tried to consistently refer to an instance of an Sequel::Model subclass as an 'entity'.
33
30
 
34
31
  ==Embedded View
35
32
  Minimal embedded definition is
36
33
 
37
- class Position < ActiveRecord::Base
34
+ class Position < Sequel::Model
38
35
  include Clevic::Record
39
36
  end
40
37
 
41
38
  which will build a fairly sensible default UI from the
42
39
  entity's metadata. Obviously you can use open classes to do
43
40
 
41
+ class Position < Sequel::Model
42
+ one_to_many :transactions
43
+ many_to_one :account
44
+ end
45
+
44
46
  class Position
45
47
  include Clevic::Record
46
48
  end
47
49
 
48
- where Position is previously defined to inherit from ActiveRecord::Base.
49
-
50
50
  A full-featured UI for an entity called Entry (part of an accounting database)
51
51
  could be defined like this:
52
52
 
53
- class Entry < ActiveRecord::Base
53
+ class Entry < Sequel::Model
54
+ belongs_to :invoice
55
+ belongs_to :activity
56
+ belongs_to :project
57
+
54
58
  include Clevic::Record
55
59
 
56
- # ActiveRecord foreign key definition
57
- belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
58
- # ActiveRecord foreign key definition
59
- belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
60
-
60
+ # spans of time more than 8 ours are coloured violet
61
+ # because they're often the result of typos.
62
+ def time_color
63
+ return if self.end.nil? || start.nil?
64
+ 'darkviolet' if self.end - start > 8.hours
65
+ end
66
+
67
+ # tooltip for spans of time > 8 hours
68
+ def time_tooltip
69
+ return if self.end.nil? || start.nil?
70
+ 'Time interval greater than 8 hours' if self.end - start > 8.hours
71
+ end
72
+
61
73
  define_ui do
62
- # :format and :edit_format are optional, in fact these are the defaults
63
- plain :date, :format => '%d-%h-%y', :edit_format => '%d-%h-%Y'
64
- plain :start, :format => '%H:%M'
65
- plain :amount, :format => '%.2f'
66
- # :set is mandatory for a restricted field
67
- restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
74
+ plain :date, :sample => '28-Dec-08'
68
75
 
69
- # alternately with a block for readability
70
- restricted :vat do
71
- label 'VAT'
72
- set %w{ yes no all }
73
- tooltip 'Is VAT included?'
76
+ # The project field
77
+ relational :project do |field|
78
+ field.display = 'project'
79
+ field.conditions = 'active = true'
80
+ field.order = 'lower(project)'
81
+
82
+ # handle data changed events. In this case,
83
+ # auto-fill-in the invoice field.
84
+ field.notify_data_changed do |entity_view, table_view, model_index|
85
+ if model_index.entity.invoice.nil?
86
+ entity_view.invoice_from_project( table_view, model_index ) do
87
+ # move here next if the invoice was changed
88
+ table_view.override_next_index model_index.choppy( :column => :start )
89
+ end
90
+ end
91
+ end
74
92
  end
75
93
 
76
- # distinct will retrieve from the table all other values for this field
77
- # and display them in the combo.
78
- distinct :description, :conditions => 'now() - date <= interval( 1 year )'
79
-
80
- # this is a read-only field
81
- plain :origin, :read_only => true
94
+ relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
82
95
 
83
- # :format is an attribute on the related
84
- # ActiveRecord entity, in this case an instance of Account
85
- # :order is an ActiveRecord option to find, defining the order in which related entries will be displayed.
86
- # :conditions is an ActiveRecord option to find, defining the subset of related entries to be displayed.
87
- relational :debit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
88
- relational :credit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
96
+ # call time_color method for foreground color value
97
+ plain :start, :foreground => :time_color, :tooltip => :time_tooltip
89
98
 
90
- # or like this to have an on-the-fly transform. item will be an instance of Account.
91
- # This also takes a block parameter
92
- relational :credit do |field|
93
- field.format = lambda {|item| item.name.downcase}
94
- field.conditions = 'active = true'
95
- field.order = 'lower(name)'
99
+ # another way to call time_color method for foreground color value
100
+ plain :end, :foreground => lambda{|x| x.time_color}, :tooltip => :time_tooltip
101
+
102
+ # multiline text
103
+ text :description, :sample => 'This is a long string designed to hold lots of data and description'
104
+
105
+ relational :activity do
106
+ display 'activity'
107
+ order 'lower(activity)'
108
+ sample 'Troubleshooting'
109
+ conditions 'active = true'
96
110
  end
97
111
 
98
- # this is a read-only display field from a related table
99
- # the Entry class should then define a method called currency
100
- # which returns an object that responds to 'short'.
101
- # You can also use a Proc for :display
102
- plain :currency, :display => 'short', :label => 'Currency'
112
+ distinct :module, :tooltip => 'Module or sub-project'
113
+ plain :charge, :tooltip => 'Is this time billable?'
114
+ distinct :person, :default => 'John', :tooltip => 'The person who did the work'
103
115
 
104
- # this is a read-only display field from a related table
105
- # the Entry class should then define a method called currency
106
- # which returns an object that responds to 'currency', which
107
- # returns an object that responds to 'rate'.
108
- # You can also use a Proc for :display
109
- plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
116
+ records :order => 'date, start, id'
117
+ end
118
+
119
+ def self.define_actions( view, action_builder )
120
+ action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
121
+ smart_copy( view )
122
+ end
110
123
 
111
- # this is optional. By default all records in id order will be displayed.
112
- records :order => 'date,start'
124
+ action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
125
+ invoice_from_project( view, view.current_index ) do
126
+ # execute the block if the invoice is changed
127
+
128
+ # save this before selection model is cleared
129
+ current_index = view.current_index
130
+ view.selection_model.clear
131
+ view.current_index = current_index.choppy( :column => :start )
132
+ end
133
+ end
134
+ end
135
+
136
+ # do a smart copy from the previous line
137
+ def self.smart_copy( view )
138
+ view.sanity_check_read_only
139
+ view.sanity_check_ditto
113
140
 
114
- # could also be like this, where a..e are instances of Entry
115
- records [ a,b,c,d,e ]
141
+ # need a reference to current_index here, because selection_model.clear will
142
+ # invalidate view.current_index. And anyway, its shorter and easier to read.
143
+ current_index = view.current_index
144
+ if current_index.row >= 1
145
+ # fetch previous item
146
+ previous_item = view.model.collection[current_index.row - 1]
147
+
148
+ # copy the relevant fields
149
+ current_index.entity.date = previous_item.date if current_index.entity.date.blank?
150
+ # depends on previous line
151
+ current_index.entity.start = previous_item.end if current_index.entity.date == previous_item.date
152
+
153
+ # copy rest of fields
154
+ [:project, :invoice, :activity, :module, :charge, :person].each do |attr|
155
+ current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
156
+ end
157
+
158
+ # tell view to update
159
+ view.model.data_changed do |change|
160
+ change.top_left = current_index.choppy( :column => 0 )
161
+ change.bottom_right = current_index.choppy( :column => view.model.fields.size - 1 )
162
+ end
163
+
164
+ # move to the first empty time field
165
+ next_field =
166
+ if current_index.entity.start.blank?
167
+ :start
168
+ else
169
+ :end
170
+ end
171
+
172
+ # next cursor location
173
+ view.selection_model.clear
174
+ view.current_index = current_index.choppy( :column => next_field )
175
+ end
176
+ end
177
+
178
+ # Auto-complete invoice number field from project.
179
+ # &block will be executed if an invoice was assigned
180
+ # If block takes one parameter, pass the new invoice.
181
+ def self.invoice_from_project( table_view, current_index, &block )
182
+ if current_index.entity.project != nil
183
+ # most recent entry, ordered in reverse
184
+ invoice = current_index.entity.project.latest_invoice
185
+ unless invoice.nil?
186
+ # make a reference to the invoice
187
+ current_index.entity.invoice = invoice
188
+
189
+ # update view from top_left to bottom_right
190
+ table_view.model.data_changed( current_index.choppy( :column => :invoice ) )
191
+
192
+ unless block.nil?
193
+ if block.arity == 1
194
+ block.call( invoice )
195
+ else
196
+ block.call
197
+ end
198
+ end
199
+ end
200
+ end
116
201
  end
202
+
117
203
  end
118
204
 
119
205
  == Separate View
@@ -121,7 +207,7 @@ could be defined like this:
121
207
  To define a separate ui class, do something like this:
122
208
  class Prospect < Clevic::View
123
209
 
124
- # This is the ActiveRecord::Base descendant
210
+ # This is the Sequel::Model descendant
125
211
  entity_class Position
126
212
 
127
213
  # This must return a ModelBuilder instance, which is made easier
@@ -136,6 +222,8 @@ To define a separate ui class, do something like this:
136
222
  # use the define_ui block from Position
137
223
  mb.exec_ui_block( Position )
138
224
 
225
+ # any other ModelBuilder code can go here too
226
+
139
227
  # use a different recordset
140
228
  mb.records :conditions => "status in ('prospect','open')", :order => 'date desc,code'
141
229
  end
@@ -174,6 +262,9 @@ can use DSL-style acessors (no assignment =) or assignment statement.
174
262
  plain
175
263
  is an ordinary editable field. Boolean values are displayed as checkboxes.
176
264
 
265
+ text
266
+ is a multiline editable field.
267
+
177
268
  relational
178
269
  displays a set of values pulled from a belongs_to (many-to-one) relationship.
179
270
  In other words all the possible related entities that this one could belong_to. Some
@@ -198,7 +289,7 @@ definition from somewhere else. It may go away and be replaced by remove.
198
289
  The attribute symbol is required, and is the first parameter after the field type. It must refer
199
290
  to a method already defined in the entity. In other words any of:
200
291
  - a db column
201
- - a relationship (belongs_to, has_many, etc)
292
+ - a relationship (one_to_many, etc)
202
293
  - a plain method that takes no parameters.
203
294
 
204
295
  will work. Named scopes might also work, but I haven't tried them yet.
@@ -223,7 +314,7 @@ See Clevic::Field properties for available options.
223
314
 
224
315
  === Menu Items
225
316
 
226
- You can define view/model specific actions ( an Action is Qt talk for menu items and shortcuts).
317
+ You can define view/model specific menu items.
227
318
  These will be added to the Edit menu, show up on context-click in the table
228
319
  display, and can have optional keyboard shortcuts:
229
320
 
@@ -242,17 +333,20 @@ display, and can have optional keyboard shortcuts:
242
333
 
243
334
  === Notifications
244
335
 
245
- The following method will be called whenever data is changed, ie a field edit is completed:
246
-
247
- def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
248
- end
249
-
250
336
  Key presses will be sent here:
251
337
 
338
+ # may also be defined as class methods on an entity class.
252
339
  def notify_key_press( table_view, key_press_event, current_model_index )
253
340
  end
254
341
 
255
- The above may also be defined as class methods on an entity class.
342
+ Fields have a property called notify_data_changed, which is called whenever
343
+ the field value changes. There is also an view method:
344
+
345
+ def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
346
+ end
347
+
348
+ But note that this will override the delegation to the field notify_data_changed
349
+ unless super is called.
256
350
 
257
351
  === Tab Order
258
352
 
@@ -271,11 +365,14 @@ class ModelBuilder
271
365
 
272
366
  # Create a definition for entity_view (subclass of Clevic::View).
273
367
  # Then execute block using self.instance_eval.
368
+ # entity_view must respond to entity_class, and if title is called, it
369
+ # must respond to title.
274
370
  def initialize( entity_view, &block )
275
371
  @entity_view = entity_view
276
372
  @auto_new = true
277
373
  @read_only = false
278
- @fields = Dictionary.new
374
+ # TODO not needed for 1.9
375
+ @fields = OrderedHash.new
279
376
  exec_ui_block( &block )
280
377
  end
281
378
 
@@ -304,7 +401,8 @@ class ModelBuilder
304
401
  # The collection of Clevic::Field instances where visible == true.
305
402
  # the visible may go away.
306
403
  def fields
307
- @fields.reject{|id,field| !field.visible}
404
+ #~ @fields.reject{|id,field| !field.visible}
405
+ @fields
308
406
  end
309
407
 
310
408
  # return the index of the named field in the collection of fields.
@@ -314,7 +412,7 @@ class ModelBuilder
314
412
  retval
315
413
  end
316
414
 
317
- # The ActiveRecord::Base subclass
415
+ # The ORM class
318
416
  def entity_class
319
417
  @entity_view.entity_class
320
418
  end
@@ -331,18 +429,29 @@ class ModelBuilder
331
429
 
332
430
  # should this table automatically show a new blank record?
333
431
  def auto_new?; @auto_new; end
432
+
433
+ # DSL for changing the title
434
+ def title( value )
435
+ entity_view.title = value
436
+ end
334
437
 
335
438
  # an ordinary field, edited in place with a text box
336
439
  def plain( attribute, options = {}, &block )
337
440
  read_only_default!( attribute, options )
338
- @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
441
+ field = @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
442
+ field.delegate =
443
+ if field.meta.type == :boolean
444
+ BooleanDelegate.new( field )
445
+ else
446
+ TextDelegate.new( field )
447
+ end
339
448
  end
340
449
 
341
450
  # an ordinary field like plain, except that a larger edit area can be used
342
451
  def text( attribute, options = {}, &block )
343
452
  read_only_default!( attribute, options )
344
453
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
345
- field.delegate = TextDelegate.new( nil, field )
454
+ field.delegate = TextAreaDelegate.new( field )
346
455
  @fields[attribute] = field
347
456
  end
348
457
 
@@ -350,7 +459,7 @@ class ModelBuilder
350
459
  # a combo box containing all values for this field from the table.
351
460
  def distinct( attribute, options = {}, &block )
352
461
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
353
- field.delegate = DistinctDelegate.new( nil, field )
462
+ field.delegate = DistinctDelegate.new( field )
354
463
  @fields[attribute] = field
355
464
  end
356
465
 
@@ -364,7 +473,7 @@ class ModelBuilder
364
473
  field.format ||= lambda{|x| field.set[x]}
365
474
  end
366
475
 
367
- field.delegate = SetDelegate.new( nil, field )
476
+ field.delegate = SetDelegate.new( field )
368
477
  @fields[attribute] = field
369
478
  end
370
479
 
@@ -373,23 +482,37 @@ class ModelBuilder
373
482
  options[:restricted] = true
374
483
  combo( attribute, options, &block )
375
484
  end
376
-
377
- # for foreign keys. Edited with a combo box using values from the specified
485
+
486
+ # For many_to_one relationships.
487
+ # Edited with a combo box using values from the specified
378
488
  # path on the foreign key model object
379
489
  # if options[:format] has a value, it's used either as a block
380
490
  # or as a dotted path
381
491
  def relational( attribute, options = {}, &block )
382
492
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
383
- if field.class_name.nil?
384
- field.class_name = entity_class.reflections[attribute].class_name || attribute.to_s.classify
385
- end
493
+ field.delegate = RelationalDelegate.new( field )
494
+ @fields[attribute] = field
495
+ end
496
+
497
+ def tags( attribute, options = {}, &block )
498
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
386
499
 
387
- # check after all possible options have been collected
388
- raise ":display must be specified" if field.display.nil?
389
- field.delegate = RelationalDelegate.new( nil, field )
500
+ # build a collection setter if necessary
501
+ unless entity_class.instance_methods.include? "#{attribute}="
502
+ raise NotImplementedError, "Need to build a collection setter for '#{attribute}='"
503
+ end
504
+
505
+ field.delegate = TagDelegate.new( field )
390
506
  @fields[attribute] = field
391
507
  end
392
508
 
509
+ # force a checkbox
510
+ def check( attribute, options = {}, &block )
511
+ read_only_default!( attribute, options )
512
+ field = @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
513
+ field.delegate = BooleanDelegate.new( field )
514
+ end
515
+
393
516
  # mostly used in the new block to define the set of records
394
517
  # for the TableModel, but may also be
395
518
  # used as an accessor for records.
@@ -411,7 +534,7 @@ class ModelBuilder
411
534
  # as editable in the table. Any belongs_to relations are used to build
412
535
  # combo boxes. Default ordering is the primary key.
413
536
  # Subscriber is already defined elsewhere as a subclass
414
- # of ActiveRecord::Base:
537
+ # of an ORM class ie Sequel::Model:
415
538
  # class Subscriber
416
539
  # include Clevic::Record
417
540
  # define_ui do
@@ -427,51 +550,26 @@ class ModelBuilder
427
550
  # * :name
428
551
  # * :title
429
552
  # * :username
553
+ # * :to_s
430
554
  def default_ui
431
- # combine reflections and attributes into one set
432
- reflections = entity_class.reflections.keys.map{|x| x.to_s}
433
- ui_columns = entity_class.columns.reject{|x| x.name == entity_class.primary_key }.map do |column|
434
- # TODO there must be a better way to do this
435
- att = column.name.gsub( /_id$/, '' )
436
- if reflections.include?( att )
437
- att
438
- else
439
- column.name
440
- end
441
- end
442
-
443
555
  # don't create an empty record, because sometimes there are
444
556
  # validations that will cause trouble
445
557
  auto_new false
446
558
 
447
559
  # build columns
448
- ui_columns.each do |column|
449
- if entity_class.reflections.has_key?( column.to_sym )
450
- begin
451
- reflection = entity_class.reflections[column.to_sym]
452
- if reflection.class == ActiveRecord::Reflection::AssociationReflection
453
- related_class = reflection.class_name.constantize
454
-
455
- # try to find a sensible display class. Default to to_s
456
- display_method =
457
- %w{#{entity_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
458
- related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
459
- end
460
-
461
- # set the display method
462
- relational column.to_sym, :display => display_method
463
- else
464
- plain column.to_sym
465
- end
466
- rescue
467
- puts $!.message
468
- puts $!.backtrace
469
- # just do a plain
470
- puts "Doing plain for #{entity_class}.#{column}"
471
- plain column.to_sym
560
+ entity_class.attributes.each do |column,model_column|
561
+ begin
562
+ if model_column.association?
563
+ relational column
564
+ else
565
+ plain column
472
566
  end
473
- else
474
- plain column.to_sym
567
+ rescue
568
+ puts $!.message
569
+ puts $!.backtrace
570
+ # just do a plain
571
+ puts "Doing plain for #{entity_class}.#{column}"
572
+ plain column
475
573
  end
476
574
  end
477
575
  records :order => entity_class.primary_key
@@ -484,22 +582,27 @@ class ModelBuilder
484
582
 
485
583
  # This takes all the information collected
486
584
  # by the other methods, and returns a new TableModel
487
- # with the given table_view as its parent.
488
- def build( table_view )
585
+ # with the given parent (usually a TableView) as its parent.
586
+ def build( parent )
489
587
  # build the model with all it's collections
490
588
  # using @model here because otherwise the view's
491
589
  # reference to this very same model is garbage collected.
492
- @model = Clevic::TableModel.new( table_view )
590
+ @model = Clevic::TableModel.new
493
591
  @model.builder = self
494
592
  @model.entity_view = entity_view
495
593
  @model.fields = @fields.values
496
594
  @model.read_only = @read_only
497
595
  @model.auto_new = auto_new?
498
596
 
499
- # setup model
500
- table_view.object_name = @object_name
501
- # set parent for all delegates
502
- fields.each {|id,field| field.delegate.parent = table_view unless field.delegate.nil? }
597
+ # set view name
598
+ parent.object_name = @object_name if parent.respond_to? :object_name
599
+
600
+ # set UI parent for all delegates
601
+ # and model for each field
602
+ fields.each do |id,field|
603
+ field.delegate.parent = parent unless field.delegate.nil?
604
+ field.model = @model
605
+ end
503
606
 
504
607
  # the data
505
608
  @model.collection = records
@@ -509,19 +612,6 @@ class ModelBuilder
509
612
 
510
613
  protected
511
614
 
512
- # Add ActiveRecord :include options for foreign keys, but it takes up too much memory,
513
- # and actually takes longer to load a data set.
514
- #--
515
- # TODO ActiveRecord-2.1 has smarter includes
516
- def add_include_options
517
- fields.each do |id,field|
518
- if field.delegate.class == RelationalDelegate
519
- @options[:include] ||= []
520
- @options[:include] << field.attribute
521
- end
522
- end
523
- end
524
-
525
615
  # set a sensible read-only value if it isn't already specified in options
526
616
  def read_only_default!( attribute, options )
527
617
  # sensible defaults for read-only-ness
@@ -537,8 +627,7 @@ protected
537
627
 
538
628
  when entity_class.reflections.include?( attribute )
539
629
  # one-to-one relationships can be edited. many-to-one certainly can't
540
- reflection = entity_class.reflections[attribute]
541
- reflection.macro != :has_one
630
+ entity_class.meta[attribute].type != :many_to_one
542
631
 
543
632
  when entity_class.instance_methods.include?( attribute.to_s )
544
633
  # read-only if there's no setter for the attribute
@@ -566,7 +655,6 @@ protected
566
655
  # Called by records( *args )
567
656
  def get_records
568
657
  if @records.nil?
569
- #~ add_include_options
570
658
  @records = CacheTable.new( entity_class, @find_options )
571
659
  end
572
660
  @records