clevic 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,181 @@
1
+ require 'qtext/flags.rb'
2
+
3
+ module Clevic
4
+
5
+ =begin rdoc
6
+ This defines a field in the UI, and how it hooks up to a field in the DB.
7
+ =end
8
+ class Field
9
+ include QtFlags
10
+
11
+ attr_accessor :attribute, :path, :label, :delegate, :class_name, :alignment, :format, :tooltip
12
+ attr_writer :sample
13
+
14
+ # attribute is the symbol for the attribute on the model_class
15
+ def initialize( attribute, model_class, options )
16
+ # sanity checking
17
+ unless model_class.has_attribute?( attribute )
18
+ msg = <<EOF
19
+ #{attribute} not found in #{model_class.name}. Possibilities are:
20
+ #{model_class.attribute_names.join("\n")}
21
+ EOF
22
+ raise msg
23
+ end
24
+
25
+ # set values
26
+ @attribute = attribute
27
+ @model_class = model_class
28
+
29
+ options.each do |key,value|
30
+ self.send( "#{key}=", value ) if respond_to?( key )
31
+ end
32
+
33
+ # default the label
34
+ @label ||= attribute.to_s.humanize
35
+
36
+ # default formats
37
+ if @format.nil?
38
+ case meta.type
39
+ when :time; @format = '%H:%M'
40
+ when :date; @format = '%d-%h-%y'
41
+ when :datetime; @format = '%d-%h-%y %H:%M:%S'
42
+ when :decimal, :float; @format = "%.2f"
43
+ end
44
+ end
45
+
46
+ # default alignments
47
+ if @alignment.nil?
48
+ @alignment =
49
+ case meta.type
50
+ when :decimal, :integer, :float; qt_alignright
51
+ when :boolean; qt_aligncenter
52
+ end
53
+ end
54
+ end
55
+
56
+ # return true if it's a date, a time or a datetime
57
+ # cache result because the type won't change in the lifetime of the field
58
+ def is_date_time?
59
+ @is_date_time ||= [:time, :date, :datetime].include?( meta.type )
60
+ end
61
+
62
+ # return ActiveRecord::Base.columns_hash[attribute]
63
+ # in other words an ActiveRecord::ConnectionAdapters::Column object
64
+ def meta
65
+ @model_class.columns_hash[attribute.to_s] || @model_class.reflections[attribute]
66
+ end
67
+
68
+ # return the name of the field for this Field, quoted for the dbms
69
+ def quoted_field
70
+ @model_class.connection.quote_column_name( meta.name )
71
+ end
72
+
73
+ def column
74
+ [attribute.to_s, path].compact.join('.')
75
+ end
76
+
77
+ # return an array of the various attribute parts
78
+ def attribute_path
79
+ pieces = [ attribute.to_s ]
80
+ pieces.concat( path.split( /\./ ) ) unless path.nil?
81
+ pieces.map{|x| x.to_sym}
82
+ end
83
+
84
+ # format this value. Use strftime for date_time types, or % for everything else
85
+ def do_format( value )
86
+ if self.format != nil
87
+ if is_date_time?
88
+ value.strftime( format )
89
+ else
90
+ self.format % value
91
+ end
92
+ else
93
+ value
94
+ end
95
+ end
96
+
97
+ # return a sample for the field which can be used to size a column in the table
98
+ def sample
99
+ if @sample.nil?
100
+ self.sample =
101
+ case meta.type
102
+ # max width of 40 chars
103
+ when :string, :text
104
+ string_sample( 'n'*40 )
105
+
106
+ when :date, :time, :datetime
107
+ date_time_sample
108
+
109
+ when :numeric, :decimal, :integer, :float
110
+ numeric_sample
111
+
112
+ # TODO return a width, or something like that
113
+ when :boolean; 'W'
114
+
115
+ else
116
+ puts "#{@model_class.name}.#{attribute} is a #{meta.type}"
117
+ end
118
+
119
+ if $options[:debug]
120
+ puts "@sample for #{@model_class.name}.#{attribute} #{meta.type}: #{@sample.inspect}"
121
+ end
122
+ end
123
+ # if we don't know how to figure it out from the data, just return the label size
124
+ @sample || self.label
125
+ end
126
+
127
+ private
128
+
129
+ def format_result( result_set )
130
+ unless result_set.size == 0
131
+ obj = result_set[0][attribute]
132
+ unless obj.nil?
133
+ do_format( obj )
134
+ end
135
+ end
136
+ end
137
+
138
+ def string_sample( max_sample = nil )
139
+ result_set = @model_class.connection.execute <<-EOF
140
+ select distinct #{quoted_field}
141
+ from #{@model_class.table_name}
142
+ where
143
+ length( #{quoted_field} ) = (
144
+ select max( length( #{quoted_field} ) )
145
+ from #{@model_class.table_name}
146
+ )
147
+ EOF
148
+ unless result_set.entries.size == 0
149
+ result = result_set[0][0]
150
+ if max_sample.nil?
151
+ result
152
+ else
153
+ result.length < max_sample.length ? result : max_sample
154
+ end
155
+ end
156
+ end
157
+
158
+ def date_time_sample
159
+ result_set = @model_class.find_by_sql <<-EOF
160
+ select #{quoted_field}
161
+ from #{@model_class.table_name}
162
+ where #{quoted_field} is not null
163
+ limit 1
164
+ EOF
165
+ format_result( result_set )
166
+ end
167
+
168
+ def numeric_sample
169
+ # TODO Use precision from metadata, not for integers
170
+ # returns nil for floats. So it's probably not useful
171
+ #~ puts "meta.precision: #{meta.precision.inspect}"
172
+ result_set = @model_class.find_by_sql <<-EOF
173
+ select max( #{quoted_field} )
174
+ from #{@model_class.table_name}
175
+ EOF
176
+ format_result( result_set )
177
+ end
178
+
179
+ end
180
+
181
+ end
@@ -0,0 +1,62 @@
1
+ require 'Qt4'
2
+
3
+ module Qt
4
+ class KeyEvent
5
+ def inspect
6
+ "<Qt::KeyEvent text=#{text} key=#{key}"
7
+ end
8
+ end
9
+ end
10
+
11
+ module Clevic
12
+
13
+ class ItemDelegate < Qt::ItemDelegate
14
+
15
+ def initialize( parent )
16
+ super
17
+ end
18
+
19
+ # This catches the event that begins the edit process.
20
+ # Not used at the moment.
21
+ def editorEvent ( event, model, style_option_view_item, model_index )
22
+ puts "editorEvent"
23
+ puts "event: #{event.inspect}"
24
+ puts "model: #{model.inspect}"
25
+ puts "style_option_view_item: #{style_option_view_item.inspect}"
26
+ puts "model_index: #{model_index.inspect}"
27
+ super
28
+ end
29
+
30
+ def createEditor( parent_widget, style_option_view_item, model_index )
31
+ puts "model_index.metadata.type: #{model_index.metadata.type.inspect}"
32
+ if model_index.metadata.type == :date
33
+ # not going to work here because being triggered by
34
+ # an alphanumeric keystroke (as opposed to F4)
35
+ # will result in the calendar widget being opened.
36
+ #~ Qt::CalendarWidget.new( parent_widget )
37
+ super
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ #~ def setEditorData( editor, model_index )
44
+ #~ editor.value = model_index.gui_value
45
+ #~ end
46
+
47
+ #~ def setModelData( editor, abstract_item_model, model_index )
48
+ #~ model_index.gui_value = editor.value
49
+ #~ emit abstract_item_model.dataChanged( model_index, model_index )
50
+ #~ end
51
+
52
+ def updateEditorGeometry( editor, style_option_view_item, model_index )
53
+ # figure out where to put the editor widget, taking into
54
+ # account the sizes of the headers
55
+ rect = style_option_view_item.rect
56
+ rect.set_width( [editor.size_hint.width,rect.width].max )
57
+ rect.set_height( editor.size_hint.height )
58
+ editor.set_geometry( rect )
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,171 @@
1
+ require 'clevic/table_model.rb'
2
+ require 'clevic/delegates.rb'
3
+ require 'clevic/cache_table.rb'
4
+ require 'clevic/field.rb'
5
+
6
+ module Clevic
7
+
8
+ =begin rdoc
9
+ This is used to define a set of fields in a UI, any related tables,
10
+ restrictions on data entry, formatting and that kind of thing.
11
+
12
+ Optional specifiers are:
13
+ * :sample is used to size the columns. Will default to some hopefully sensible value from the db.
14
+ * :format is something that can be understood by strftime (for time and date
15
+ fields) or understood by % (for everything else)
16
+ * :alignment is one of Qt::TextAlignmentRole, ie Qt::AlignRight, Qt::AlignLeft, Qt::AlignCenter
17
+ * :set is the set of strings that are accepted by a RestrictedDelegate
18
+
19
+ In the case of relational fields, all other options are passed to ActiveRecord::Base#find
20
+
21
+ For example, a the UI for a model called Entry would be defined like this:
22
+
23
+ Clevic::TableView.new( Entry, parent ).create_model do
24
+ # :format is optional
25
+ plain :date, :format => '%d-%h-%y'
26
+ plain :start, :format => '%H:%M'
27
+ plain :amount, :format => '%.2f'
28
+ # :set is mandatory
29
+ restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
30
+ distinct :description, :conditions => 'now() - date <= interval( 1 year )'
31
+ relational :debit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
32
+ relational :credit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
33
+
34
+ # this is optional
35
+ records :order => 'date,start'
36
+
37
+ # could also be like this, where a..e are instances of Entry
38
+ records [ a,b,c,d,e ]
39
+ end
40
+ =end
41
+ class ModelBuilder
42
+ # The collection of Clevic::Field objects
43
+ attr_reader :fields
44
+
45
+ def initialize( table_view )
46
+ @table_view = table_view
47
+ @fields = []
48
+ end
49
+
50
+ # return the index of the named field
51
+ def index( field_name_sym )
52
+ retval = nil
53
+ fields.each_with_index{|x,i| retval = i if x.attribute == field_name_sym.to_sym }
54
+ retval
55
+ end
56
+
57
+ # the AR class for this table
58
+ def model_class
59
+ @table_view.model_class
60
+ end
61
+
62
+ # an ordinary field, edited in place with a text box
63
+ def plain( attribute, options = {} )
64
+ @fields << Clevic::Field.new( attribute.to_sym, model_class, options )
65
+ end
66
+
67
+ # edited with a combo box containing all previous entries in this field
68
+ def distinct( attribute, options = {} )
69
+ field = Clevic::Field.new( attribute.to_sym, model_class, options )
70
+ field.delegate = DistinctDelegate.new( @table_view, attribute, @table_view.model_class, options )
71
+ @fields << field
72
+ end
73
+
74
+ # edited with a combo box, but restricted to a specified set
75
+ def restricted( attribute, options = {} )
76
+ raise "restricted must have a set" unless options.has_key?( :set )
77
+ field = Clevic::Field.new( attribute.to_sym, model_class, options )
78
+ field.delegate = RestrictedDelegate.new( @table_view, attribute, @table_view.model_class, options )
79
+ @fields << field
80
+ end
81
+
82
+ # for foreign keys. Edited with a combo box using values from the specified
83
+ # path on the foreign key model object
84
+ def relational( attribute, path, options = {} )
85
+ unless options.has_key? :class_name
86
+ options[:class_name] = attribute.to_s.classify
87
+ end
88
+ field = Clevic::Field.new( attribute.to_sym, model_class, options )
89
+ field.path = path
90
+ field.delegate = RelationalDelegate.new( @table_view, field.attribute_path, options )
91
+ @fields << field
92
+ end
93
+
94
+ # add AR :include options for foreign keys, but it takes up too much memory,
95
+ # and actually takes longer to load a data set
96
+ def add_include_options
97
+ @fields.each do |field|
98
+ if field.delegate.class == RelationalDelegate
99
+ @options[:include] ||= []
100
+ @options[:include] << field.attribute
101
+ end
102
+ end
103
+ end
104
+
105
+ # mostly used in the create_model block, but may also be
106
+ # used as an accessor for records
107
+ def records( *args )
108
+ if args.size == 0
109
+ get_records
110
+ else
111
+ set_records( args[0] )
112
+ end
113
+ end
114
+
115
+ # This is intended to be called from the view class which instantiated
116
+ # this builder object.
117
+ def build
118
+ # build the model with all it's collections
119
+ # using @model here because otherwise the view's
120
+ # reference to this very same model is mysteriously
121
+ # set to nil
122
+ @model = Clevic::TableModel.new( self )
123
+ @model.object_name = @table_view.model_class.name
124
+ @model.dots = @fields.map {|x| x.column }
125
+ @model.labels = @fields.map {|x| x.label }
126
+ @model.attributes = @fields.map {|x| x.attribute }
127
+ @model.attribute_paths = @fields.map { |x| x.attribute_path }
128
+
129
+ # the data
130
+ @model.collection = records
131
+ # fill in an empty record
132
+ @model.collection << model_class.new if @model.collection.size == 0
133
+
134
+ # now set delegates
135
+ @table_view.item_delegate = Clevic::ItemDelegate.new( @table_view )
136
+ @fields.each_with_index do |field, index|
137
+ @table_view.set_item_delegate_for_column( index, field.delegate )
138
+ end
139
+
140
+ # give the built model back to the view class
141
+ # see above comment about @model
142
+ @table_view.model = @model
143
+ end
144
+
145
+ private
146
+
147
+ # The collection of model objects to display in a table
148
+ # arg can either be a Hash, in which case a new CacheTable
149
+ # is created, or it can be an array
150
+ # called by records( *args )
151
+ def set_records( arg )
152
+ if arg.class == Hash
153
+ # need to defer this until all fields are collected
154
+ @options = arg
155
+ else
156
+ @records = arg
157
+ end
158
+ end
159
+
160
+ # return a collection of records. Usually this will be a CacheTable.
161
+ # called by records( *args )
162
+ def get_records
163
+ if @records.nil?
164
+ #~ add_include_options
165
+ @records = CacheTable.new( model_class, @options )
166
+ end
167
+ @records
168
+ end
169
+ end
170
+
171
+ end
@@ -0,0 +1,23 @@
1
+ =begin rdoc
2
+ This responds to the same methods as ActiveRecord::ConnectionAdapter::Column.
3
+ It exists to pretend that the accessors generated by association methods
4
+ have a column type of :association, which lets us make a sensible paste
5
+ decision using PasteRole, and makes the field/attribute distinction possible
6
+ in ModelIndex.
7
+ =end
8
+ class ModelColumn
9
+ attr_accessor :primary, :scale, :sql_type, :name, :precision, :default, :limit, :type, :meta
10
+
11
+ def initialize( name, type, meta )
12
+ @name = name
13
+ @type = type
14
+ @meta = meta
15
+ end
16
+
17
+ # return the underlying field name, ie "#{attribute}_id" if
18
+ # it's an association
19
+ alias_method :old_name, :name
20
+ def name
21
+ @meta.name
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ require 'Qt4'
2
+ require 'clevic/ui/search_dialog_ui.rb'
3
+ require 'qtext/flags.rb'
4
+
5
+ class SearchDialog
6
+ include QtFlags
7
+ attr_reader :match_flags, :layout
8
+
9
+ def initialize
10
+ @layout = Ui_SearchDialog.new
11
+ @dialog = Qt::Dialog.new
12
+ @layout.setupUi( @dialog )
13
+ end
14
+
15
+ def from_start?
16
+ layout.from_start.value
17
+ end
18
+
19
+ def from_start=( value )
20
+ layout.from_start.value = value
21
+ end
22
+
23
+ def regex?
24
+ layout.regex.value
25
+ end
26
+
27
+ def whole_words?
28
+ layout.whole_words.value
29
+ end
30
+
31
+ def search_combo
32
+ layout.search_combo
33
+ end
34
+
35
+ def forwards?
36
+ @layout.forwards.checked?
37
+ end
38
+
39
+ def backwards?
40
+ @layout.backwards.checked?
41
+ end
42
+
43
+ # return either :backwards or :forwards
44
+ def direction
45
+ return :forwards if forwards?
46
+ return :backwards if backwards?
47
+ raise "direction not known"
48
+ end
49
+
50
+ def exec( text = '' )
51
+ search_combo.edit_text = text.to_s
52
+ search_combo.set_focus
53
+ retval = @dialog.exec
54
+
55
+ # remember previous searches
56
+ if search_combo.find_text( search_combo.current_text ) == -1
57
+ search_combo.add_item( search_combo.current_text )
58
+ end
59
+
60
+ #~ Qt::MatchExactly 0 Performs QVariant-based matching.
61
+ #~ Qt::MatchFixedString 8 Performs string-based matching. String-based comparisons are case-insensitive unless the MatchCaseSensitive flag is also specified.
62
+ #~ Qt::MatchContains 1 The search term is contained in the item.
63
+ #~ Qt::MatchStartsWith 2 The search term matches the start of the item.
64
+ #~ Qt::MatchEndsWith 3 The search term matches the end of the item.
65
+ #~ Qt::MatchCaseSensitive 16 The search is case sensitive.
66
+ #~ Qt::MatchRegExp 4 Performs string-based matching using a regular expression as the search term.
67
+ #~ Qt::MatchWildcard 5 Performs string-based matching using a string with wildcards as the search term.
68
+ #~ Qt::MatchWrap 32 Perform a search that wraps around, so that when the search reaches the last item in the model, it begins again at the first item and continues until all items have been examined.
69
+
70
+ retval
71
+ end
72
+
73
+ def search_text
74
+ search_combo.current_text
75
+ end
76
+
77
+ end