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.
- data/History.txt +10 -0
- data/Manifest.txt +209 -30
- data/README.txt +16 -20
- data/Rakefile +8 -8
- data/TODO +6 -7
- data/bin/clevic +12 -73
- data/lib/clevic/action_builder.rb +168 -0
- data/lib/clevic/ar_methods.rb +120 -0
- data/lib/clevic/attribute_list.rb +56 -0
- data/lib/clevic/cache_table.rb +60 -37
- data/lib/clevic/default_view.rb +3 -16
- data/lib/clevic/delegate.rb +46 -0
- data/lib/clevic/emitter.rb +38 -0
- data/lib/clevic/extensions.rb +61 -114
- data/lib/clevic/field.rb +159 -228
- data/lib/clevic/field_valuer.rb +165 -0
- data/lib/clevic/filter_command.rb +2 -6
- data/lib/clevic/generic_format.rb +52 -0
- data/lib/clevic/{ui → icons}/icon.png +0 -0
- data/lib/clevic/many_field.rb +7 -0
- data/lib/clevic/model_builder.rb +234 -146
- data/lib/clevic/model_column.rb +61 -13
- data/lib/clevic/order_attribute.rb +10 -0
- data/lib/clevic/qt.rb +35 -0
- data/lib/clevic/qt/action_builder.rb +47 -0
- data/lib/clevic/qt/boolean_delegate.rb +8 -0
- data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
- data/lib/clevic/qt/clipboard.rb +35 -0
- data/lib/clevic/qt/combo_delegate.rb +198 -0
- data/lib/clevic/qt/delegates.rb +1 -0
- data/lib/clevic/qt/distinct_delegate.rb +35 -0
- data/lib/clevic/qt/extensions.rb +52 -0
- data/lib/clevic/qt/field.rb +18 -0
- data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
- data/lib/clevic/qt/relational_delegate.rb +87 -0
- data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
- data/lib/clevic/qt/set_delegate.rb +44 -0
- data/lib/clevic/qt/table_model.rb +331 -0
- data/lib/clevic/qt/table_view.rb +344 -0
- data/lib/clevic/qt/text_area_delegate.rb +8 -0
- data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
- data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
- data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
- data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
- data/lib/clevic/rails_models_loaders.rb +56 -0
- data/lib/clevic/record.rb +2 -17
- data/lib/clevic/sampler.rb +81 -0
- data/lib/clevic/sequel_ar_adapter.rb +215 -0
- data/lib/clevic/sequel_length_validation.rb +23 -0
- data/lib/clevic/sequel_meta.rb +65 -0
- data/lib/clevic/sequel_naked.rb +30 -0
- data/lib/clevic/swing.rb +38 -0
- data/lib/clevic/swing/action.rb +125 -0
- data/lib/clevic/swing/action_builder.rb +47 -0
- data/lib/clevic/swing/boolean_delegate.rb +26 -0
- data/lib/clevic/swing/browser.rb +282 -0
- data/lib/clevic/swing/cell_editor.rb +95 -0
- data/lib/clevic/swing/cell_renderer.rb +44 -0
- data/lib/clevic/swing/clipboard.rb +135 -0
- data/lib/clevic/swing/combo_delegate.rb +336 -0
- data/lib/clevic/swing/confirm_dialog.rb +57 -0
- data/lib/clevic/swing/delegate.rb +40 -0
- data/lib/clevic/swing/distinct_delegate.rb +30 -0
- data/lib/clevic/swing/extensions.rb +274 -0
- data/lib/clevic/swing/field.rb +35 -0
- data/lib/clevic/swing/relational_delegate.rb +48 -0
- data/lib/clevic/swing/row_header.rb +210 -0
- data/lib/clevic/swing/search_dialog.rb +230 -0
- data/lib/clevic/swing/selection_model.rb +90 -0
- data/lib/clevic/swing/set_delegate.rb +41 -0
- data/lib/clevic/swing/swing_table_index.rb +43 -0
- data/lib/clevic/swing/table_model.rb +200 -0
- data/lib/clevic/swing/table_view.rb +385 -0
- data/lib/clevic/swing/table_view_focus.rb +47 -0
- data/lib/clevic/swing/tag_delegate.rb +127 -0
- data/lib/clevic/swing/tag_editor.rb +101 -0
- data/lib/clevic/swing/text_area_delegate.rb +46 -0
- data/lib/clevic/swing/text_delegate.rb +31 -0
- data/lib/clevic/swing/ui/build.xml +74 -0
- data/lib/clevic/swing/ui/dist/README.TXT +33 -0
- data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
- data/lib/clevic/swing/ui/manifest.mf +3 -0
- data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
- data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
- data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
- data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
- data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
- data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
- data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
- data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
- data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
- data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
- data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
- data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
- data/lib/clevic/table_index.rb +100 -0
- data/lib/clevic/table_model.rb +54 -425
- data/lib/clevic/table_searcher.rb +113 -116
- data/lib/clevic/table_view.rb +171 -399
- data/lib/clevic/table_view_paste.rb +199 -0
- data/lib/clevic/version.rb +3 -2
- data/lib/clevic/view.rb +94 -43
- data/models/accounts_models.rb +13 -13
- data/models/minimal_models.rb +5 -9
- data/models/times_models.rb +19 -14
- data/models/times_psql_models.rb +10 -0
- data/models/times_sqlite_models.rb +1 -8
- data/models/values_models.rb +2 -8
- data/tasks/clevic.rake +1 -1
- data/tasks/rdoc.rake +1 -5
- data/tasks/website.rake +1 -1
- data/test/test_cache_table.rb +15 -29
- data/test/test_helper.rb +14 -83
- data/test/test_order_attribute.rb +1 -1
- data/test/test_table_model.rb +0 -21
- data/test/test_table_searcher.rb +67 -61
- metadata +262 -78
- data/lib/clevic.rb +0 -4
- data/lib/clevic/db_options.rb +0 -112
- 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
|
-
|
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 :
|
9
|
+
attr_reader :dataset, :search_criteria, :field
|
8
10
|
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
26
|
-
|
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
|
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.
|
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
|
-
|
55
|
+
field.attribute.to_sym
|
38
56
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
93
|
+
rv = find_from( rv, start_entity )
|
49
94
|
end
|
50
95
|
|
51
|
-
#
|
52
|
-
|
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
|
-
#
|
61
|
-
|
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
|
-
|
84
|
-
def build_recursive_comparison( operator, index = 0 )
|
106
|
+
def build_recursive_comparison( start_entity, index = 0 )
|
85
107
|
# end recursion
|
86
|
-
return
|
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
|
92
|
-
|
93
|
-
case
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
#
|
104
|
-
#
|
105
|
-
|
106
|
-
|
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
|
-
|
139
|
+
# 1 indexes >, -1 indexes <
|
140
|
+
['','>','<'][comparator_direction]
|
129
141
|
end
|
130
142
|
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
+
|
data/lib/clevic/table_view.rb
CHANGED
@@ -1,47 +1,36 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'Qt4'
|
3
1
|
require 'fastercsv'
|
4
|
-
require '
|
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
|
-
#
|
12
|
-
class 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
|
-
#
|
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
|
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.
|
24
|
+
when arg.is_a?( TableModel )
|
35
25
|
self.model = arg
|
36
26
|
init_actions( arg.entity_view )
|
37
27
|
|
38
|
-
|
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
|
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
|
-
|
55
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
69
|
+
catch :insane do
|
70
|
+
yield
|
71
|
+
end
|
72
|
+
|
97
73
|
rescue Exception => e
|
98
|
-
puts
|
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 =>
|
113
|
-
action :
|
114
|
-
action :
|
115
|
-
action :
|
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, '&
|
118
|
-
action :action_ditto_right, 'Ditto
|
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
|
116
|
+
# list of actions for search
|
137
117
|
list( :search ) do
|
138
|
-
action :action_find, '&Find', :shortcut =>
|
139
|
-
action :action_find_next, 'Find &Next', :shortcut =>
|
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 =
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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
|
-
|
154
|
+
emit_status_text( 'Can\'t copy into read-only field.' )
|
223
155
|
elsif current_index.entity.readonly?
|
224
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
294
|
-
|
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 =
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
327
|
-
result = @search_dialog.exec( current_index.gui_value )
|
271
|
+
result = search_dialog.exec( current_index.display_value )
|
328
272
|
|
329
|
-
|
330
|
-
case
|
331
|
-
when
|
332
|
-
|
333
|
-
|
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
|
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
|
-
|
289
|
+
emit_status_text( 'No previous find' )
|
345
290
|
else
|
346
|
-
|
347
|
-
save_from_start =
|
348
|
-
|
349
|
-
search(
|
350
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 |
|
551
|
-
|
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.
|
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?(
|
564
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
621
|
-
|
622
|
-
save_row(
|
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
|
-
|
634
|
-
msg =
|
635
|
-
|
636
|
-
|
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
|
-
|
645
|
-
|
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
|
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
|
-
|
755
|
-
|
517
|
+
emit_status_text( nil )
|
518
|
+
emit_filter_status( false )
|
756
519
|
end
|
757
520
|
|
758
521
|
when indexes.empty?
|
759
|
-
|
522
|
+
emit_status_text( "No field selected for filter" )
|
760
523
|
|
761
524
|
when !indexes.first.field.filterable?
|
762
|
-
|
525
|
+
emit_status_text( "Can't filter on #{indexes.first.field.label}" )
|
763
526
|
|
764
527
|
when indexes.size > 1
|
765
|
-
|
528
|
+
emit_status_text( "Can't do multiple selection filters yet" )
|
766
529
|
|
767
530
|
when indexes.first.entity.new_record?
|
768
|
-
|
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
|
-
|
537
|
+
emit_filter_status( filtered.doit )
|
775
538
|
end
|
776
539
|
# update status bar
|
777
|
-
|
778
|
-
|
779
|
-
|
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
|
-
|
788
|
-
unless entity.
|
789
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
849
|
-
|
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
|
-
|
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
|
-
|
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
|