clevic 0.13.0.b3 → 0.13.0.b5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +21 -0
- data/Manifest.txt +91 -85
- data/README.txt +33 -18
- data/Rakefile +2 -3
- data/TODO +8 -14
- data/bin/clevic +18 -20
- data/lib/clevic.rb +7 -1
- data/lib/clevic/action_builder.rb +4 -1
- data/lib/clevic/ar_methods.rb +72 -57
- data/lib/clevic/attribute_list.rb +4 -0
- data/lib/clevic/cache_table.rb +43 -69
- data/lib/clevic/dataset_roller.rb +22 -0
- data/lib/clevic/delegate.rb +11 -5
- data/lib/clevic/delegates/combo_delegate.rb +156 -0
- data/lib/clevic/delegates/distinct_delegate.rb +48 -0
- data/lib/clevic/delegates/relational_delegate.rb +59 -0
- data/lib/clevic/delegates/set_delegate.rb +31 -0
- data/lib/clevic/field.rb +39 -55
- data/lib/clevic/field_valuer.rb +23 -10
- data/lib/clevic/filter_command.rb +22 -36
- data/lib/clevic/framework.rb +37 -0
- data/lib/clevic/generic_format.rb +5 -1
- data/lib/clevic/many_field.rb +28 -3
- data/lib/clevic/model_builder.rb +27 -32
- data/lib/clevic/ordered_dataset.rb +45 -0
- data/lib/clevic/qt.rb +4 -1
- data/lib/clevic/qt/action_builder.rb +9 -1
- data/lib/clevic/qt/browser.rb +1 -1
- data/lib/clevic/qt/clipboard.rb +3 -3
- data/lib/clevic/qt/combo_delegate.rb +25 -89
- data/lib/clevic/qt/delegate.rb +25 -0
- data/lib/clevic/qt/distinct_delegate.rb +5 -23
- data/lib/clevic/qt/extensions.rb +8 -1
- data/lib/clevic/qt/qt_combo_box.rb +58 -0
- data/lib/clevic/qt/relational_delegate.rb +18 -58
- data/lib/clevic/qt/set_delegate.rb +4 -34
- data/lib/clevic/qt/simplest_delegate.rb +19 -0
- data/lib/clevic/qt/table_model.rb +10 -10
- data/lib/clevic/qt/table_view.rb +7 -23
- data/lib/clevic/qt/text_delegate.rb +2 -2
- data/lib/clevic/qt/ui/browser_ui.rb +1 -1
- data/lib/clevic/qt/ui/search_dialog_ui.rb +1 -1
- data/lib/clevic/rails_models_loaders.rb +13 -0
- data/lib/clevic/record.rb +2 -2
- data/lib/clevic/sampler.rb +85 -39
- data/lib/clevic/sequel_ar_adapter.rb +1 -28
- data/lib/clevic/sequel_clevic.rb +68 -0
- data/lib/clevic/sequel_meta.rb +1 -13
- data/lib/clevic/subclasses.rb +18 -0
- data/lib/clevic/swing.rb +2 -1
- data/lib/clevic/swing/action.rb +27 -3
- data/lib/clevic/swing/action_builder.rb +0 -2
- data/lib/clevic/swing/browser.rb +1 -10
- data/lib/clevic/swing/combo_delegate.rb +45 -133
- data/lib/clevic/swing/delegate.rb +2 -0
- data/lib/clevic/swing/distinct_delegate.rb +2 -14
- data/lib/clevic/swing/relational_delegate.rb +2 -20
- data/lib/clevic/swing/set_delegate.rb +13 -28
- data/lib/clevic/swing/table_view.rb +1 -1
- data/lib/clevic/table_model.rb +3 -4
- data/lib/clevic/table_searcher.rb +10 -31
- data/lib/clevic/table_view.rb +97 -43
- data/lib/clevic/ui/browser_ui.rb +133 -0
- data/lib/clevic/ui/search_dialog_ui.rb +106 -0
- data/lib/clevic/version.rb +2 -2
- data/models/accounts_models.rb +24 -21
- data/models/times_models.rb +34 -28
- data/models/times_psql_models.rb +9 -3
- data/models/times_sqlite_models.rb +24 -1
- data/sql/times_sqlite.sql +3 -3
- data/tasks/clevic.rake +2 -2
- data/test/test_cache_table.rb +9 -19
- data/test/test_table_searcher.rb +2 -5
- metadata +95 -91
- data/lib/clevic/order_attribute.rb +0 -63
- data/lib/clevic/qt/boolean_delegate.rb +0 -8
- data/lib/clevic/qt/delegates.rb +0 -1
- data/lib/clevic/qt/item_delegate.rb +0 -66
- data/lib/clevic/sql_dialects.rb +0 -33
- data/tasks/website.rake +0 -25
- data/test/test_order_attribute.rb +0 -62
- data/test/test_sql_dialects.rb +0 -77
data/lib/clevic.rb
CHANGED
@@ -6,8 +6,14 @@ module Clevic
|
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
|
-
|
9
|
+
# TODO should this really be here?
|
10
|
+
# There are other inflection gems.
|
11
|
+
# JRuby-1.5.2 raises exception if this require has a .rb on the
|
12
|
+
require 'active_support/inflector'
|
13
|
+
|
14
|
+
require 'clevic/framework'
|
10
15
|
require 'clevic/sequel_length_validation.rb'
|
11
16
|
require 'clevic/record.rb'
|
12
17
|
require 'clevic/view.rb'
|
13
18
|
require 'clevic/sequel_meta.rb'
|
19
|
+
require 'clevic/sequel_clevic.rb'
|
@@ -84,7 +84,7 @@ module ActionBuilder
|
|
84
84
|
# Create and return a list of actions. The actions are grouped together,
|
85
85
|
# ie live together on the menu with a separator between groups.
|
86
86
|
# A method called "#{group_name}_actions" will be added to self, which will return the
|
87
|
-
# set of
|
87
|
+
# set of framework-specific Action instances created in the block.
|
88
88
|
def list( group_name, &block )
|
89
89
|
@group_name = group_name
|
90
90
|
group_names << group_name
|
@@ -160,6 +160,9 @@ protected
|
|
160
160
|
unless instance_methods.include?( :action_triggered )
|
161
161
|
def action_triggered( &someblock )
|
162
162
|
yield
|
163
|
+
rescue
|
164
|
+
puts $!.message
|
165
|
+
puts $!.backtrace
|
163
166
|
end
|
164
167
|
end
|
165
168
|
|
data/lib/clevic/ar_methods.rb
CHANGED
@@ -2,6 +2,71 @@ require 'active_support'
|
|
2
2
|
require 'active_support/core_ext/array/extract_options.rb'
|
3
3
|
|
4
4
|
module Sequel
|
5
|
+
class Dataset
|
6
|
+
# Basically, we're translating from AR's hash options
|
7
|
+
# to Sequel's method algebra, and returning the resulting
|
8
|
+
# dataset.
|
9
|
+
def translate( options )
|
10
|
+
# recursively send key-value pairs to self
|
11
|
+
# and return the result
|
12
|
+
options.inject( self ) do |dataset, (key, value)|
|
13
|
+
case key
|
14
|
+
when :limit; dataset.limit( value, nil )
|
15
|
+
when :offset
|
16
|
+
# workaround for Sequel's refusal to do offset without limit
|
17
|
+
# not sure we need :all for >= 3.13.0
|
18
|
+
dataset.limit( options[:limit] || :all, value )
|
19
|
+
|
20
|
+
when :order
|
21
|
+
orders = value.split(/, */ ).map do |x|
|
22
|
+
case x
|
23
|
+
when /^(\w+) +(asc|desc)$/i
|
24
|
+
$1.to_sym.send( $2 )
|
25
|
+
|
26
|
+
when /^\w+$/i
|
27
|
+
x.to_sym
|
28
|
+
|
29
|
+
else
|
30
|
+
x.lit
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
dataset.order( *orders )
|
35
|
+
|
36
|
+
when :conditions
|
37
|
+
# this translation is not adequate for all use cases of the AR api
|
38
|
+
# specifically where value contains a SQL expression
|
39
|
+
unless value.nil?
|
40
|
+
possible_literal =
|
41
|
+
if value.is_a?( String )
|
42
|
+
value.lit
|
43
|
+
else
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
dataset.filter( possible_literal )
|
48
|
+
end
|
49
|
+
|
50
|
+
when :include
|
51
|
+
# this is the class to join
|
52
|
+
joined_class = eval( model.reflections[value][:class_name] )
|
53
|
+
dataset.join_table(
|
54
|
+
:inner,
|
55
|
+
joined_class,
|
56
|
+
joined_class.primary_key => model.reflections[value][:key]
|
57
|
+
).select( model.table_name.* )
|
58
|
+
|
59
|
+
else
|
60
|
+
raise "#{key} not implemented"
|
61
|
+
# make sure at least it's unchanged, in case options is empty
|
62
|
+
end || dataset
|
63
|
+
end
|
64
|
+
|
65
|
+
rescue Exception => e
|
66
|
+
raise RuntimeError, "#{self} #{options.inspect} #{e.message}", caller(0)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
5
70
|
module Plugins
|
6
71
|
module ArMethods
|
7
72
|
# plugin :ar_methods calls this.
|
@@ -18,60 +83,10 @@ module Sequel
|
|
18
83
|
# Copy the necessary class instance variables to the subclass.
|
19
84
|
def inherited(subclass)
|
20
85
|
super
|
21
|
-
#~ store = @cache_store
|
22
|
-
#~ ttl = @cache_ttl
|
23
|
-
#~ cache_ignore_exceptions = @cache_ignore_exceptions
|
24
|
-
#~ subclass.instance_eval do
|
25
|
-
#~ @cache_store = store
|
26
|
-
#~ @cache_ttl = ttl
|
27
|
-
#~ @cache_ignore_exceptions = cache_ignore_exceptions
|
28
|
-
#~ end
|
29
86
|
end
|
30
87
|
|
31
|
-
def lit_if_string( arg )
|
32
|
-
if arg.is_a?( String )
|
33
|
-
arg.lit
|
34
|
-
else
|
35
|
-
arg
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Basically, we're translating from AR's hash options
|
40
|
-
# to Sequel's method algebra, and returning the resulting
|
41
|
-
# dataset.
|
42
88
|
def translate( options )
|
43
|
-
|
44
|
-
case key
|
45
|
-
when :limit; dataset.limit( value, nil )
|
46
|
-
when :offset
|
47
|
-
# workaround for Sequel's refusal to do offset without limit
|
48
|
-
dataset.limit( options[:limit] || :all, value )
|
49
|
-
|
50
|
-
when :order
|
51
|
-
dataset.order( lit_if_string( value ) )
|
52
|
-
|
53
|
-
when :conditions
|
54
|
-
# this is most likely not adequate for all use cases
|
55
|
-
# of the AR api
|
56
|
-
dataset.filter( lit_if_string( value ) ) unless value.nil?
|
57
|
-
|
58
|
-
when :include
|
59
|
-
# this is the class to joing
|
60
|
-
joined_class = eval( reflections[value][:class_name] )
|
61
|
-
dataset.join_table(
|
62
|
-
:inner,
|
63
|
-
joined_class,
|
64
|
-
joined_class.primary_key => reflections[value][:key]
|
65
|
-
).select( table_name.* )
|
66
|
-
|
67
|
-
else
|
68
|
-
raise "#{key} not implemented"
|
69
|
-
# make sure at least it's unchanged
|
70
|
-
end || dataset
|
71
|
-
end
|
72
|
-
|
73
|
-
rescue Exception => e
|
74
|
-
raise RuntimeError, "#{self.name} #{options.inspect} #{e.message}", caller(0)
|
89
|
+
dataset.translate( options )
|
75
90
|
end
|
76
91
|
|
77
92
|
def find_ar( *args )
|
@@ -82,19 +97,19 @@ module Sequel
|
|
82
97
|
|
83
98
|
case args.first
|
84
99
|
when :first
|
85
|
-
translate(options).first
|
100
|
+
dataset.translate(options).first
|
86
101
|
|
87
102
|
when :last
|
88
|
-
translate(options).last
|
103
|
+
dataset.translate(options).last
|
89
104
|
|
90
105
|
when :all
|
91
|
-
translate(options).all
|
106
|
+
dataset.translate(options).all
|
92
107
|
|
93
108
|
else
|
94
109
|
if args.size == 1
|
95
|
-
translate(options).filter( :id.qualify( table_name ) => args.first ).first
|
110
|
+
dataset.translate(options).filter( :id.qualify( table_name ) => args.first ).first
|
96
111
|
else
|
97
|
-
translate(options).filter( :id.qualify( table_name ) => args ).all
|
112
|
+
dataset.translate(options).filter( :id.qualify( table_name ) => args ).all
|
98
113
|
end
|
99
114
|
end
|
100
115
|
end
|
@@ -103,7 +118,7 @@ module Sequel
|
|
103
118
|
options = args.extract_options!
|
104
119
|
attribute = args.first
|
105
120
|
|
106
|
-
dataset = translate( options )
|
121
|
+
dataset = dataset.translate( options )
|
107
122
|
|
108
123
|
unless attribute.nil?
|
109
124
|
dataset = dataset.select( attribute )
|
@@ -1,4 +1,8 @@
|
|
1
1
|
module Clevic
|
2
|
+
|
3
|
+
# Provide a list of entries for a distinct field, ordered
|
4
|
+
# by either value order, or frequency order.
|
5
|
+
# TODO move this into the common DistinctDelegate class.
|
2
6
|
class AttributeList
|
3
7
|
def initialize( entity_class, attribute, attribute_value, find_options )
|
4
8
|
@entity_class = entity_class
|
data/lib/clevic/cache_table.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
require 'clevic/table_searcher
|
2
|
-
require 'clevic/
|
1
|
+
require 'clevic/table_searcher'
|
2
|
+
require 'clevic/ordered_dataset'
|
3
3
|
require 'bsearch'
|
4
4
|
|
5
|
+
module Clevic
|
6
|
+
|
5
7
|
=begin rdoc
|
6
8
|
Fetch rows from the db on demand, rather than all up front.
|
7
9
|
|
@@ -18,76 +20,34 @@ TODO drop rows when they haven't been accessed for a while
|
|
18
20
|
TODO how to handle a quickly-changing underlying table? invalidate cache
|
19
21
|
for each call?
|
20
22
|
|
21
|
-
TODO
|
23
|
+
TODO figure out how to handle situations where the character set ordering
|
24
|
+
in the db and in Ruby are different.
|
22
25
|
=end
|
23
26
|
class CacheTable < Array
|
27
|
+
|
28
|
+
include OrderedDataset
|
29
|
+
|
24
30
|
# the number of records loaded in one call to the db
|
25
31
|
attr_accessor :preload_count
|
26
|
-
attr_reader :
|
32
|
+
attr_reader :entity_class
|
27
33
|
|
28
|
-
def initialize( entity_class,
|
29
|
-
@preload_count =
|
30
|
-
# must be before sanitise_options
|
34
|
+
def initialize( entity_class, dataset = nil )
|
35
|
+
@preload_count = 30
|
31
36
|
@entity_class = entity_class
|
32
|
-
#
|
33
|
-
|
34
|
-
sanitise_options!
|
37
|
+
# defined in OrderAttributes
|
38
|
+
self.dataset = dataset || entity_class.dataset
|
35
39
|
|
36
40
|
# size the array and fill it with nils. They'll be filled
|
37
41
|
# in by the [] operator
|
38
|
-
|
39
|
-
super( @row_count )
|
42
|
+
super( sql_count )
|
40
43
|
end
|
41
44
|
|
42
45
|
# The count of the records according to the db, which may be different to
|
43
46
|
# the records in the cache
|
44
47
|
def sql_count
|
45
|
-
|
48
|
+
dataset.count
|
46
49
|
end
|
47
50
|
|
48
|
-
# Return the set of OrderAttribute objects for this collection.
|
49
|
-
# If no order attributes are specified, the primary key will be used.
|
50
|
-
# TODO what about compound primary keys?
|
51
|
-
def order_attributes
|
52
|
-
# This is sorted in @find_options[:order], so use that for the search
|
53
|
-
if @order_attributes.nil?
|
54
|
-
@order_attributes = find_options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@entity_class, x)}
|
55
|
-
|
56
|
-
# add the primary key if nothing is specified
|
57
|
-
# because we need an ordering of some kind otherwise
|
58
|
-
# index_for_entity will not work
|
59
|
-
unless @order_attributes.any? {|x| x.attribute.to_s == entity_class.primary_key.to_s }
|
60
|
-
@order_attributes << OrderAttribute.new( entity_class, entity_class.primary_key )
|
61
|
-
end
|
62
|
-
end
|
63
|
-
@order_attributes
|
64
|
-
end
|
65
|
-
|
66
|
-
# add an id to options[:order] if it's not in there
|
67
|
-
# make sure options[:conditions] uses db values rather than objects
|
68
|
-
# ie convert { :debit => DebitObject<...> } to { :debit_id => 5 }
|
69
|
-
def sanitise_options!
|
70
|
-
# make sure we have a string here, even if it's blank
|
71
|
-
# value would be a ,-separated list of order by fields (expressions?)
|
72
|
-
find_options[:order] ||= ''
|
73
|
-
|
74
|
-
# recreate the options[:order] entry to include default
|
75
|
-
find_options[:order] = order_attributes.map{|x| x.to_sql}.join(',')
|
76
|
-
|
77
|
-
# make sure objects are converted to ids
|
78
|
-
if find_options.has_key?( :conditions ) && find_options[:conditions].is_a?( Hash )
|
79
|
-
conditions = find_options[:conditions].map do |key,value|
|
80
|
-
metadata = entity_class.meta[key]
|
81
|
-
if metadata.association?
|
82
|
-
[metadata.key, value.send( value.primary_key )]
|
83
|
-
else
|
84
|
-
[key,value]
|
85
|
-
end
|
86
|
-
end
|
87
|
-
find_options[:conditions] = Hash[ *conditions.flatten ]
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
51
|
# Execute the block with the specified preload_count,
|
92
52
|
# and restore the existing one when done.
|
93
53
|
# Return the value of the block
|
@@ -104,14 +64,14 @@ class CacheTable < Array
|
|
104
64
|
# hits on the db
|
105
65
|
def fetch_entity( index )
|
106
66
|
# calculate negative indices for the SQL offset
|
107
|
-
offset = index < 0 ? index +
|
67
|
+
offset = index < 0 ? index + sql_count : index
|
108
68
|
|
109
69
|
# fetch self.preload_count records
|
110
|
-
records =
|
70
|
+
records = dataset.limit( preload_count, offset )
|
111
71
|
records.each_with_index {|x,i| self[i+index] = x if !cached_at?( i+index )}
|
112
72
|
|
113
73
|
# return the first one
|
114
|
-
records
|
74
|
+
records.first
|
115
75
|
end
|
116
76
|
|
117
77
|
# return the entity at the given index. Fetch it from the
|
@@ -120,11 +80,25 @@ class CacheTable < Array
|
|
120
80
|
super( index ) || fetch_entity( index )
|
121
81
|
end
|
122
82
|
|
123
|
-
#
|
124
|
-
#
|
125
|
-
|
126
|
-
|
127
|
-
|
83
|
+
# Make a new instance based on the current dataset.
|
84
|
+
# Unless new_dataset is specified, pass the dataset
|
85
|
+
# to the block, and use the return
|
86
|
+
# value from the block as the new dataset.
|
87
|
+
#
|
88
|
+
# This is so that filter of datasets can be based on the
|
89
|
+
# existing one, but it's easy to go back to previous data
|
90
|
+
# sets if necessary.
|
91
|
+
# TODO write tests for both cases.
|
92
|
+
def renew( new_dataset = nil, &block )
|
93
|
+
if new_dataset && block_given?
|
94
|
+
raise "Passing a new dataset and a modification block doesn't make sense."
|
95
|
+
end
|
96
|
+
|
97
|
+
if block_given?
|
98
|
+
self.class.new( entity_class, block.call( dataset ) )
|
99
|
+
else
|
100
|
+
self.class.new( entity_class, new_dataset || dataset )
|
101
|
+
end
|
128
102
|
end
|
129
103
|
|
130
104
|
# key is what we're searching for. candidate
|
@@ -165,14 +139,12 @@ class CacheTable < Array
|
|
165
139
|
bsearch do |candidate|
|
166
140
|
# find using all sort attributes
|
167
141
|
order_attributes.inject(0) do |result,attribute|
|
168
|
-
#
|
142
|
+
# result from the block should be in [-1,0,1],
|
169
143
|
# similar to candidate <=> entity
|
144
|
+
key, direction = attribute
|
170
145
|
if result == 0
|
171
|
-
# they're equal, so compare attribute values
|
172
|
-
method = attribute.attribute.to_sym
|
173
|
-
|
174
146
|
# compare taking ordering direction into account
|
175
|
-
retval = compare( entity.send(
|
147
|
+
retval = compare( entity.send( key ), candidate.send( key ), direction )
|
176
148
|
|
177
149
|
# exit now because we have a difference
|
178
150
|
next( retval ) if retval != 0
|
@@ -189,6 +161,8 @@ class CacheTable < Array
|
|
189
161
|
end
|
190
162
|
end
|
191
163
|
|
164
|
+
end
|
165
|
+
|
192
166
|
# This is part of Array in case the programmer wants to use
|
193
167
|
# a simple array instead of a CacheTable.
|
194
168
|
class Array
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Clevic
|
2
|
+
|
3
|
+
# Tricky, this. Keeps passing on the dataset and
|
4
|
+
# lets it build up, but keeps the result.
|
5
|
+
# Used in the UI block to make a nice syntax for specifying the dataset.
|
6
|
+
class DatasetRoller
|
7
|
+
def initialize( original_dataset )
|
8
|
+
@rolling_dataset = original_dataset
|
9
|
+
end
|
10
|
+
|
11
|
+
def dataset
|
12
|
+
@rolling_dataset
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(meth, *args, &block)
|
16
|
+
@rolling_dataset = @rolling_dataset.send( meth, *args, &block )
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
end
|
data/lib/clevic/delegate.rb
CHANGED
@@ -8,12 +8,21 @@ class Delegate
|
|
8
8
|
include FieldValuer
|
9
9
|
|
10
10
|
def initialize( field )
|
11
|
+
super()
|
11
12
|
@field = field
|
12
13
|
end
|
13
14
|
|
14
|
-
|
15
|
+
# This is the ORM entity instance for which this delegate
|
16
|
+
# is editing a single field. It needs to be the entire entity
|
17
|
+
# so we can set the edited field value on it.
|
18
|
+
attr_accessor :entity
|
15
19
|
|
20
|
+
# The parent widget of this delegate / this delegate's widget
|
21
|
+
attr_accessor :parent
|
22
|
+
|
23
|
+
# the Clevic::Field instance which this delegate edits.
|
16
24
|
attr_reader :field
|
25
|
+
|
17
26
|
def attribute
|
18
27
|
field.attribute
|
19
28
|
end
|
@@ -22,10 +31,7 @@ class Delegate
|
|
22
31
|
field.entity_class
|
23
32
|
end
|
24
33
|
|
25
|
-
|
26
|
-
field.find_options
|
27
|
-
end
|
28
|
-
|
34
|
+
# assume this is not a combo delegate. That will come later.
|
29
35
|
def is_combo?
|
30
36
|
false
|
31
37
|
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'andand'
|
2
|
+
|
3
|
+
module Clevic
|
4
|
+
|
5
|
+
=begin rdoc
|
6
|
+
Base class for other delegates using Combo boxes.
|
7
|
+
=end
|
8
|
+
# TODO this should be a module
|
9
|
+
class ComboDelegate
|
10
|
+
# Return the GUI component / widget that is displayed when editing.
|
11
|
+
# Usually this will be a combo box widget, but it can be a text editor
|
12
|
+
# in some cases.
|
13
|
+
# if editor is a combo it must support no_insert=( bool )
|
14
|
+
attr_reader :editor
|
15
|
+
|
16
|
+
# Return a string to be shown to the user.
|
17
|
+
# model_value is an item stored in the combo box model.
|
18
|
+
def display_for( model_value )
|
19
|
+
field.transform_attribute( model_value )
|
20
|
+
end
|
21
|
+
|
22
|
+
# Some GUIs (Qt) can just set this. Swing can't.
|
23
|
+
def configure_prefix
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO kinda redundant because all combos must be editable
|
27
|
+
# to support prefix matching
|
28
|
+
def configure_editable
|
29
|
+
editor.editable = true
|
30
|
+
end
|
31
|
+
|
32
|
+
# this will create the actual combo box widget
|
33
|
+
framework_responsibility :create_combo_box
|
34
|
+
|
35
|
+
# framework-specific code goes in here
|
36
|
+
# it's called right at the end of init_component, once all the other setup
|
37
|
+
# is done. Mainly it's so that event handlers can be attached
|
38
|
+
# to the combo box without having to deal with events triggered
|
39
|
+
# by setup code.
|
40
|
+
framework_responsibility :framework_setup
|
41
|
+
|
42
|
+
# This is called by the combo box to convert an item
|
43
|
+
# to something that the combo can insert into
|
44
|
+
# itself. Usually this will be a display value
|
45
|
+
# and a storage value.
|
46
|
+
framework_responsibility :item_to_editor
|
47
|
+
|
48
|
+
# This is called by the combo box when it needs to convert a
|
49
|
+
# storage value to an item, which is something that the delegate
|
50
|
+
# will understand.
|
51
|
+
framework_responsibility :editor_to_item
|
52
|
+
|
53
|
+
# Create a GUI widget and fill it with the possible values.
|
54
|
+
# *args will be passed as-is to framework_setup
|
55
|
+
# NOTE RIght now it's the framework's responsibility to call
|
56
|
+
# self.entity = some_entity
|
57
|
+
# There must be a good way to check that though.
|
58
|
+
def init_component( *args )
|
59
|
+
if needs_combo?
|
60
|
+
@editor = create_combo_box( *args )
|
61
|
+
@editor.delegate = self
|
62
|
+
|
63
|
+
# add all entries from population
|
64
|
+
population.each do |item|
|
65
|
+
editor << item
|
66
|
+
end
|
67
|
+
|
68
|
+
# create a nil entry if necessary
|
69
|
+
if allow_null? && !editor.include?( nil )
|
70
|
+
editor << nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# don't allow inserts if the delegate is restricted
|
74
|
+
editor.no_insert = restricted?
|
75
|
+
|
76
|
+
# set the correct value in the list
|
77
|
+
editor.selected_item = entity.nil? ? nil : attribute_value
|
78
|
+
|
79
|
+
# set up prefix matching when typing in the editor
|
80
|
+
configure_prefix
|
81
|
+
|
82
|
+
framework_setup( *args )
|
83
|
+
else
|
84
|
+
@editor =
|
85
|
+
if restricted?
|
86
|
+
show_message( empty_set_message )
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
# there is no data yet for the combo, and it's
|
90
|
+
# not restricted, so just edit with a simple text field.
|
91
|
+
line_editor( edit_value )
|
92
|
+
end
|
93
|
+
end
|
94
|
+
editor
|
95
|
+
end
|
96
|
+
|
97
|
+
# open the combo box, just like if F4 was pressed
|
98
|
+
framework_responsibility :full_edit
|
99
|
+
|
100
|
+
# show only the text editor part, not the drop-down
|
101
|
+
def minimal_edit
|
102
|
+
editor.hide_popup if is_combo?
|
103
|
+
end
|
104
|
+
|
105
|
+
# returns true if the editor allows values outside of a predefined
|
106
|
+
# range, false otherwise.
|
107
|
+
def restricted?
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# TODO fetch this from the model definition
|
112
|
+
def allow_null?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Subclasses should override this to prove a list of
|
117
|
+
# values to be used by the combo box. Values could
|
118
|
+
# be pretty much anything, depending on the delegate.
|
119
|
+
# For example, a RelationalDelegate will have a collection
|
120
|
+
# of entity objects, most other delegates will have collections
|
121
|
+
# of strings.
|
122
|
+
subclass_responsibility :population
|
123
|
+
|
124
|
+
# Return true if this delegate needs a combo, false otherwise
|
125
|
+
# ie if there are no values yet and it's not restricted, then a
|
126
|
+
# full combo doesn't make sense
|
127
|
+
subclass_responsibility :needs_combo?
|
128
|
+
|
129
|
+
# return true if this delegate has/needs a combo widget
|
130
|
+
# or false if it's a plain text field.
|
131
|
+
framework_responsibility :is_combo?
|
132
|
+
|
133
|
+
# return true if this field has no data (needs_combo? is false)
|
134
|
+
# and is at the same time restricted (ie needs data from somewhere else)
|
135
|
+
def empty_set?
|
136
|
+
!needs_combo? && restricted?
|
137
|
+
end
|
138
|
+
|
139
|
+
# the message to display if the set is empty, and
|
140
|
+
# the delegate is restricted to a predefined set.
|
141
|
+
subclass_responsibility :empty_set_message
|
142
|
+
|
143
|
+
# if this delegate has an empty set, return the message, otherwise
|
144
|
+
# return nil.
|
145
|
+
def if_empty_message
|
146
|
+
empty_set_message if empty_set?
|
147
|
+
end
|
148
|
+
|
149
|
+
# the value represented by the combo, ie either
|
150
|
+
# the current attribute_value of the field
|
151
|
+
# this combo is editing, or an object that could
|
152
|
+
# be a new attribute_value
|
153
|
+
framework_responsibility :value
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|