clevic 0.8.0 → 0.11.1
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 +9 -0
- data/Manifest.txt +13 -10
- data/README.txt +6 -9
- data/Rakefile +35 -24
- data/TODO +29 -17
- data/bin/clevic +84 -37
- data/config/hoe.rb +7 -3
- data/lib/clevic.rb +2 -4
- data/lib/clevic/browser.rb +37 -49
- data/lib/clevic/cache_table.rb +55 -165
- data/lib/clevic/db_options.rb +32 -21
- data/lib/clevic/default_view.rb +66 -0
- data/lib/clevic/delegates.rb +51 -67
- data/lib/clevic/dirty.rb +101 -0
- data/lib/clevic/extensions.rb +24 -38
- data/lib/clevic/field.rb +400 -99
- data/lib/clevic/item_delegate.rb +32 -33
- data/lib/clevic/model_builder.rb +315 -148
- data/lib/clevic/order_attribute.rb +53 -0
- data/lib/clevic/record.rb +57 -57
- data/lib/clevic/search_dialog.rb +71 -67
- data/lib/clevic/sql_dialects.rb +33 -0
- data/lib/clevic/table_model.rb +73 -120
- data/lib/clevic/table_searcher.rb +165 -0
- data/lib/clevic/table_view.rb +140 -100
- data/lib/clevic/ui/.gitignore +1 -0
- data/lib/clevic/ui/browser_ui.rb +55 -56
- data/lib/clevic/ui/search_dialog_ui.rb +50 -51
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +89 -0
- data/models/accounts_models.rb +12 -9
- data/models/minimal_models.rb +4 -2
- data/models/times_models.rb +41 -25
- data/models/times_sqlite_models.rb +1 -145
- data/models/values_models.rb +15 -16
- data/test/test_cache_table.rb +138 -0
- data/test/test_helper.rb +131 -0
- data/test/test_model_index_extensions.rb +22 -0
- data/test/test_order_attribute.rb +62 -0
- data/test/test_sql_dialects.rb +77 -0
- data/test/test_table_searcher.rb +188 -0
- metadata +36 -20
- data/bin/import-times +0 -128
- data/config/jamis.rb +0 -589
- data/env.sh +0 -1
- data/lib/active_record/dirty.rb +0 -87
- data/lib/clevic/field_builder.rb +0 -42
- data/website/index.html +0 -170
- data/website/index.txt +0 -17
- data/website/screenshot.png +0 -0
- data/website/stylesheets/screen.css +0 -131
- data/website/template.html.erb +0 -41
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'clevic/sql_dialects.rb'
|
2
|
+
|
3
|
+
module Clevic
|
4
|
+
|
5
|
+
# TODO possibly use AR scopes for this?
|
6
|
+
class TableSearcher
|
7
|
+
attr_reader :entity_class, :order_attributes, :search_criteria, :field
|
8
|
+
|
9
|
+
# entity_class is a descendant of ActiveRecord::Base
|
10
|
+
# order_attributes is a collection of OrderAttribute objects
|
11
|
+
# - field is an instance of Clevic::Field
|
12
|
+
# - search_criteria responds to from_start?, direction, whole_words? and search_text
|
13
|
+
def initialize( entity_class, order_attributes, search_criteria, field )
|
14
|
+
raise "there must be at least one order_attribute" if order_attributes.nil? or order_attributes.empty?
|
15
|
+
raise "field must be specified" if field.nil?
|
16
|
+
raise "unknown order #{search_criteria.direction}" unless [:forwards, :backwards].include?( search_criteria.direction )
|
17
|
+
@entity_class = entity_class
|
18
|
+
@order_attributes = order_attributes
|
19
|
+
@search_criteria = search_criteria
|
20
|
+
@field = field
|
21
|
+
end
|
22
|
+
|
23
|
+
# start_entity is the entity to start from, ie any record found after it will qualify
|
24
|
+
def search( start_entity = nil )
|
25
|
+
search_field_name =
|
26
|
+
if field.is_association?
|
27
|
+
# for related tables
|
28
|
+
unless [String,Symbol].include?( field.display.class )
|
29
|
+
raise( "search field #{field.inspect} cannot have a complex display" )
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO this will only work with a path value with no dots
|
33
|
+
# otherwise the SQL gets complicated with joins etc
|
34
|
+
field.display
|
35
|
+
else
|
36
|
+
# for this table
|
37
|
+
entity_class.connection.quote_column_name( field.attribute.to_s )
|
38
|
+
end
|
39
|
+
|
40
|
+
# do the conditions for the search value
|
41
|
+
@conditions = search_clause( search_field_name )
|
42
|
+
|
43
|
+
# if we're not searching from the start, we need
|
44
|
+
# to find the next match. Which is complicated from an SQL point of view.
|
45
|
+
unless search_criteria.from_start?
|
46
|
+
raise "start_entity cannot be nil when from_start is false" if start_entity.nil?
|
47
|
+
# build up the ordering conditions
|
48
|
+
find_from!( start_entity )
|
49
|
+
end
|
50
|
+
|
51
|
+
# otherwise ActiveRecord thinks that the % in the string
|
52
|
+
# is for interpolations instead of treating it a the like wildcard
|
53
|
+
conditions_value =
|
54
|
+
if !@params.nil? and @params.size > 0
|
55
|
+
[ @conditions, @params ]
|
56
|
+
else
|
57
|
+
@conditions
|
58
|
+
end
|
59
|
+
|
60
|
+
# find the first match
|
61
|
+
entity_class.find(
|
62
|
+
:first,
|
63
|
+
:conditions => conditions_value,
|
64
|
+
:order => order,
|
65
|
+
:joins => ( field.meta.name if field.is_association? )
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
include SqlDialects
|
71
|
+
|
72
|
+
def quote_column( field_name )
|
73
|
+
entity_class.connection.quote_column_name( field_name )
|
74
|
+
end
|
75
|
+
|
76
|
+
def quote( value )
|
77
|
+
entity_class.connection.quote( value )
|
78
|
+
end
|
79
|
+
|
80
|
+
# recursively create a case statement to do the comparison
|
81
|
+
# because and ... and ... and filters on *each* one rather than
|
82
|
+
# consecutively.
|
83
|
+
# operator is either '<' or '>'
|
84
|
+
def build_recursive_comparison( operator, index = 0 )
|
85
|
+
# end recursion
|
86
|
+
return sql_boolean( false ) if index == order_attributes.size
|
87
|
+
|
88
|
+
# fetch the current attribute
|
89
|
+
attribute = order_attributes[index]
|
90
|
+
|
91
|
+
# build case statement, including recusion
|
92
|
+
st = <<-EOF
|
93
|
+
case
|
94
|
+
when #{entity_class.table_name}.#{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
|
95
|
+
when #{entity_class.table_name}.#{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
|
96
|
+
else #{sql_boolean false}
|
97
|
+
end
|
98
|
+
EOF
|
99
|
+
# indent
|
100
|
+
st.gsub!( /^/, ' ' * index )
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add the relevant conditions to use start_entity as the
|
104
|
+
# entity where the search starts, ie the first one after it is found
|
105
|
+
# start_entity is an AR model instance
|
106
|
+
# sets @params and @conditions
|
107
|
+
def find_from!( start_entity )
|
108
|
+
operator =
|
109
|
+
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 )}
|
127
|
+
|
128
|
+
@conditions += " and " + comparison_sql
|
129
|
+
end
|
130
|
+
|
131
|
+
# get the search value parameter, in SQL format
|
132
|
+
def search_clause( field_name )
|
133
|
+
if search_criteria.whole_words?
|
134
|
+
<<-EOF
|
135
|
+
(
|
136
|
+
#{field_name} #{like_operator} #{quote "% #{search_criteria.search_text} %"}
|
137
|
+
or
|
138
|
+
#{field_name} #{like_operator} #{quote "#{search_criteria.search_text} %"}
|
139
|
+
or
|
140
|
+
#{field_name} #{like_operator} #{quote "% #{search_criteria.search_text}"}
|
141
|
+
)
|
142
|
+
EOF
|
143
|
+
else
|
144
|
+
"#{field_name} #{like_operator} #{quote "%#{search_criteria.search_text}%"}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def ascending_order
|
149
|
+
order_attributes.map{|x| x.to_sql}.join(',')
|
150
|
+
end
|
151
|
+
|
152
|
+
def descending_order
|
153
|
+
order_attributes.map{|x| x.to_reverse_sql}.join(',')
|
154
|
+
end
|
155
|
+
|
156
|
+
def order
|
157
|
+
case search_criteria.direction
|
158
|
+
when :forwards; ascending_order
|
159
|
+
when :backwards; descending_order
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
data/lib/clevic/table_view.rb
CHANGED
@@ -6,41 +6,48 @@ require 'qtext/action_builder.rb'
|
|
6
6
|
|
7
7
|
module Clevic
|
8
8
|
|
9
|
-
# The view class
|
9
|
+
# The view class
|
10
10
|
class TableView < Qt::TableView
|
11
11
|
include ActionBuilder
|
12
12
|
|
13
|
-
attr_reader :model_class
|
14
13
|
# whether the model is currently filtered
|
15
14
|
# TODO better in QAbstractSortFilter?
|
16
15
|
attr_accessor :filtered
|
17
16
|
def filtered?; self.filtered; end
|
18
17
|
|
19
18
|
# status_text is emitted when this object was to display something in the status bar
|
19
|
+
# error_test is emitted when an error of some kind must be displayed to the user.
|
20
20
|
# filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
|
21
21
|
signals 'status_text(QString)', 'filter_status(bool)'
|
22
22
|
|
23
23
|
# model_builder_record is:
|
24
|
-
# -
|
25
|
-
# - an instance of
|
24
|
+
# - an ActiveRecord::Base subclass that includes Clevic::Record
|
25
|
+
# - an instance of Clevic::View
|
26
26
|
# - an instance of TableModel
|
27
|
-
def initialize(
|
27
|
+
def initialize( arg, parent = nil, &block )
|
28
28
|
# need the empty block here, otherwise Qt bindings grab &block
|
29
29
|
super( parent ) {}
|
30
30
|
|
31
|
-
# the model/
|
31
|
+
# the model/entity_class/builder
|
32
32
|
case
|
33
|
-
when
|
34
|
-
self.model =
|
33
|
+
when arg.kind_of?( TableModel )
|
34
|
+
self.model = arg
|
35
|
+
init_actions( arg.entity_view )
|
35
36
|
|
36
|
-
when model_builder_record.ancestors.include?( ActiveRecord::Base )
|
37
|
-
with_record( model_builder_record, &block )
|
38
|
-
|
39
|
-
when model_builder_record.kind_of?( Clevic::ModelBuilder )
|
40
|
-
with_builder( model_builder_record, &block )
|
41
|
-
|
42
37
|
else
|
43
|
-
|
38
|
+
# arg is a subclass of Clevic::View
|
39
|
+
model_builder = arg.define_ui
|
40
|
+
model_builder.exec_ui_block( &block )
|
41
|
+
|
42
|
+
# make sure the TableView has a fully-populated TableModel
|
43
|
+
# self.model is necessary to invoke the Qt layer
|
44
|
+
self.model = model_builder.build( self )
|
45
|
+
self.object_name = arg.widget_name
|
46
|
+
|
47
|
+
# connect data_changed signals for the entity_class to respond
|
48
|
+
connect_view_signals( arg )
|
49
|
+
|
50
|
+
init_actions( arg )
|
44
51
|
end
|
45
52
|
|
46
53
|
# see closeEditor
|
@@ -54,38 +61,24 @@ class TableView < Qt::TableView
|
|
54
61
|
self.sorting_enabled = false
|
55
62
|
@filtered = false
|
56
63
|
|
57
|
-
# turn off "Object#type deprecated" messages
|
58
|
-
$VERBOSE = nil
|
59
|
-
|
60
|
-
init_actions
|
61
64
|
self.context_menu_policy = Qt::ActionsContextMenu
|
62
65
|
end
|
63
66
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
# make sure the TableView has a fully-populated TableModel
|
68
|
-
self.model = model_builder.build( self )
|
69
|
-
|
70
|
-
# connect data_changed signals for the model_class to respond
|
71
|
-
connect_model_class_signals( model_class )
|
72
|
-
end
|
73
|
-
|
74
|
-
def with_record( model_class, &block )
|
75
|
-
builder = ModelBuilder.new( model_class )
|
76
|
-
with_builder( builder, &block )
|
67
|
+
def title
|
68
|
+
@title ||= model.entity_view.title
|
77
69
|
end
|
78
70
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
if model_class.respond_to?( :data_changed )
|
83
|
-
model.connect SIGNAL( 'dataChanged ( const QModelIndex &, const QModelIndex & )' ) do |top_left, bottom_right|
|
84
|
-
model_class.data_changed( top_left, bottom_right, self )
|
85
|
-
end
|
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 )
|
86
74
|
end
|
87
75
|
end
|
88
76
|
|
77
|
+
# find the row index for the given field
|
78
|
+
def field_column( field )
|
79
|
+
model.fields.each_with_index {|x,i| return i if x.attribute == field }
|
80
|
+
end
|
81
|
+
|
89
82
|
# return menu actions for the model, or an empty array if there aren't any
|
90
83
|
def model_actions
|
91
84
|
@model_actions ||= []
|
@@ -94,23 +87,27 @@ class TableView < Qt::TableView
|
|
94
87
|
# hook for the sanity_check_xxx methods
|
95
88
|
# called for the actions set up by ActionBuilder
|
96
89
|
# it just wraps the action block/method in a catch
|
97
|
-
# block for :insane
|
90
|
+
# block for :insane. Will also catch exceptions thrown in actions to make
|
91
|
+
# core application more robust to model & view errors.
|
98
92
|
def action_triggered( &block )
|
99
|
-
|
100
|
-
|
93
|
+
begin
|
94
|
+
catch :insane do
|
95
|
+
yield
|
96
|
+
end
|
97
|
+
rescue Exception => e
|
98
|
+
puts e.message
|
99
|
+
puts e.backtrace
|
101
100
|
end
|
102
101
|
end
|
103
102
|
|
104
|
-
def init_actions
|
103
|
+
def init_actions( entity_view )
|
105
104
|
# add model actions, if they're defined
|
106
|
-
|
107
|
-
|
108
|
-
model_class.actions( self, ab )
|
109
|
-
end
|
110
|
-
separator
|
105
|
+
list( :model ) do |ab|
|
106
|
+
entity_view.define_actions( self, ab )
|
111
107
|
end
|
108
|
+
separator
|
112
109
|
|
113
|
-
# list of actions
|
110
|
+
# list of actions in the edit menu
|
114
111
|
list( :edit ) do
|
115
112
|
#~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
|
116
113
|
action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
|
@@ -216,7 +213,7 @@ class TableView < Qt::TableView
|
|
216
213
|
def ditto
|
217
214
|
sanity_check_ditto
|
218
215
|
sanity_check_read_only
|
219
|
-
one_up_index =
|
216
|
+
one_up_index = current_index.choppy { |i| i.row -= 1 }
|
220
217
|
previous_value = one_up_index.attribute_value
|
221
218
|
if current_index.attribute_value != previous_value
|
222
219
|
current_index.attribute_value = previous_value
|
@@ -224,14 +221,24 @@ class TableView < Qt::TableView
|
|
224
221
|
end
|
225
222
|
end
|
226
223
|
|
224
|
+
# from and to are ModelIndex instances. Throws :insane if
|
225
|
+
# their fields don't have the same attribute_type.
|
226
|
+
def sanity_check_types( from, to )
|
227
|
+
unless from.field.attribute_type == to.field.attribute_type
|
228
|
+
emit status_text( 'Incompatible data' )
|
229
|
+
throw :insane
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
227
233
|
def ditto_right
|
228
234
|
sanity_check_ditto
|
229
235
|
sanity_check_read_only
|
230
|
-
|
236
|
+
if current_index.column >= model.column_count - 1
|
231
237
|
emit status_text( 'No column to the right' )
|
232
238
|
else
|
233
|
-
|
234
|
-
current_index
|
239
|
+
one_up_right = current_index.choppy {|i| i.row -= 1; i.column += 1 }
|
240
|
+
sanity_check_types( one_up_right, current_index )
|
241
|
+
current_index.attribute_value = one_up_right.attribute_value
|
235
242
|
emit model.dataChanged( current_index, current_index )
|
236
243
|
end
|
237
244
|
end
|
@@ -242,8 +249,9 @@ class TableView < Qt::TableView
|
|
242
249
|
unless current_index.column > 0
|
243
250
|
emit status_text( 'No column to the left' )
|
244
251
|
else
|
245
|
-
|
246
|
-
current_index
|
252
|
+
one_up_left = current_index.choppy { |i| i.row -= 1; i.column -= 1 }
|
253
|
+
sanity_check_types( one_up_left, current_index )
|
254
|
+
current_index.attribute_value = one_up_left.attribute_value
|
247
255
|
emit model.dataChanged( current_index, current_index )
|
248
256
|
end
|
249
257
|
end
|
@@ -260,16 +268,20 @@ class TableView < Qt::TableView
|
|
260
268
|
delegate.full_edit
|
261
269
|
end
|
262
270
|
|
271
|
+
# Add a new row and move to it, provided we're not in a read-only view.
|
263
272
|
def new_row
|
264
273
|
sanity_check_read_only_table
|
265
274
|
model.add_new_item
|
266
|
-
new_row_index = model.index( model.
|
275
|
+
new_row_index = model.index( model.row_count - 1, 0 )
|
267
276
|
currentChanged( new_row_index, current_index )
|
268
277
|
selection_model.clear
|
269
278
|
self.current_index = new_row_index
|
270
279
|
end
|
271
280
|
|
272
|
-
|
281
|
+
# Delete the current selection. If it's a set of rows, just delete
|
282
|
+
# them. If it's a rectangular selection, set the cells to nil.
|
283
|
+
# TODO make sure all affected rows are saved.
|
284
|
+
def delete_selection
|
273
285
|
sanity_check_read_only
|
274
286
|
|
275
287
|
# translate from ModelIndex objects to row indices
|
@@ -346,13 +358,13 @@ class TableView < Qt::TableView
|
|
346
358
|
|
347
359
|
# fetch font size
|
348
360
|
fnt = font
|
349
|
-
|
361
|
+
fnt.bold = true
|
350
362
|
opt.fontMetrics = Qt::FontMetrics.new( fnt )
|
351
363
|
|
352
|
-
# set data
|
364
|
+
# set data
|
353
365
|
opt.text = data.to_s
|
354
366
|
|
355
|
-
# icon size. Not needed
|
367
|
+
# icon size. Not needed
|
356
368
|
#~ variant = d->model->headerData(logicalIndex, d->orientation, Qt::DecorationRole);
|
357
369
|
#~ opt.icon = qvariant_cast<QIcon>(variant);
|
358
370
|
#~ if (opt.icon.isNull())
|
@@ -396,7 +408,7 @@ class TableView < Qt::TableView
|
|
396
408
|
super
|
397
409
|
|
398
410
|
# set delegates
|
399
|
-
self.item_delegate = Clevic::ItemDelegate.new( self )
|
411
|
+
#~ self.item_delegate = Clevic::ItemDelegate.new( self, field )
|
400
412
|
model.fields.each_with_index do |field, index|
|
401
413
|
set_item_delegate_for_column( index, field.delegate )
|
402
414
|
end
|
@@ -429,25 +441,40 @@ class TableView < Qt::TableView
|
|
429
441
|
super
|
430
442
|
end
|
431
443
|
|
432
|
-
#
|
433
|
-
|
444
|
+
# copied from actionpack
|
445
|
+
def pluralize(count, singular, plural = nil)
|
446
|
+
"#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
|
447
|
+
end
|
448
|
+
|
449
|
+
# Paste a CSV array to the index, replacing whatever is at that index
|
450
|
+
# and whatever is at other indices matching the size of the pasted
|
451
|
+
# csv array. Create new rows if there aren't enough.
|
434
452
|
def paste_to_index( top_left_index, csv_arr )
|
435
453
|
csv_arr.each_with_index do |row,row_index|
|
454
|
+
# append row if we need one
|
455
|
+
model.add_new_item if top_left_index.row + row_index >= model.row_count
|
456
|
+
|
436
457
|
row.each_with_index do |field, field_index|
|
437
|
-
|
438
|
-
|
458
|
+
unless top_left_index.column + field_index >= model.column_count
|
459
|
+
# do paste
|
460
|
+
cell_index = top_left_index.choppy {|i| i.row += row_index; i.column += field_index }
|
461
|
+
model.setData( cell_index, field.to_variant, Qt::PasteRole )
|
462
|
+
else
|
463
|
+
emit status_text( "#{pluralize( top_left_index.column + field_index, 'column' )} for pasting data is too large. Truncating." )
|
464
|
+
end
|
439
465
|
end
|
440
466
|
# save records to db
|
441
|
-
model.save(
|
467
|
+
model.save( top_left_index.choppy {|i| i.row += row_index; i.column = 0 } )
|
442
468
|
end
|
443
469
|
|
444
470
|
# make the gui refresh
|
445
|
-
bottom_right_index =
|
471
|
+
bottom_right_index = top_left_index.choppy {|i| i.row += csv_arr.size - 1; i.column += csv_arr[0].size - 1 }
|
446
472
|
emit model.dataChanged( top_left_index, bottom_right_index )
|
447
473
|
emit model.headerDataChanged( Qt::Vertical, top_left_index.row, top_left_index.row + csv_arr.size )
|
448
474
|
end
|
449
475
|
|
450
|
-
|
476
|
+
# ask the question in a dialog. If the user says yes, execute the block
|
477
|
+
def delete_multiple_cells?( question = 'Are you sure you want to delete multiple cells?', &block )
|
451
478
|
sanity_check_read_only
|
452
479
|
|
453
480
|
# go ahead with delete if there's only 1 cell, or the user says OK
|
@@ -457,7 +484,7 @@ class TableView < Qt::TableView
|
|
457
484
|
msg = Qt::MessageBox.new(
|
458
485
|
Qt::MessageBox::Question,
|
459
486
|
'Multiple Delete',
|
460
|
-
|
487
|
+
question,
|
461
488
|
Qt::MessageBox::Yes | Qt::MessageBox::No,
|
462
489
|
self
|
463
490
|
)
|
@@ -465,30 +492,37 @@ class TableView < Qt::TableView
|
|
465
492
|
else
|
466
493
|
true
|
467
494
|
end
|
468
|
-
end
|
469
495
|
|
496
|
+
yield if delete_ok
|
497
|
+
end
|
498
|
+
|
499
|
+
# Ask if multiple cell delete is OK, then replace contents
|
500
|
+
# of selected cells with nil.
|
470
501
|
def delete_cells
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
502
|
+
delete_multiple_cells? do
|
503
|
+
cells_deleted = false
|
504
|
+
|
505
|
+
# do delete
|
475
506
|
selection_model.selected_indexes.each do |index|
|
476
507
|
index.attribute_value = nil
|
477
508
|
cells_deleted = true
|
478
509
|
end
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
emit
|
510
|
+
|
511
|
+
# deletes were done, so emit dataChanged
|
512
|
+
if cells_deleted
|
513
|
+
# save affected rows
|
514
|
+
selection_model.selected_rows.each { |index| index.entity.save }
|
515
|
+
|
516
|
+
# emit data changed for all ranges
|
517
|
+
selection_model.selection.each do |selection_range|
|
518
|
+
emit dataChanged( selection_range.top_left, selection_range.bottom_right )
|
519
|
+
end
|
486
520
|
end
|
487
521
|
end
|
488
522
|
end
|
489
523
|
|
490
524
|
def delete_rows
|
491
|
-
|
525
|
+
delete_multiple_cells?( 'Are you sure you want to delete multiple rows?' ) do
|
492
526
|
model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
|
493
527
|
end
|
494
528
|
end
|
@@ -496,19 +530,18 @@ class TableView < Qt::TableView
|
|
496
530
|
# handle certain key combinations that aren't shortcuts
|
497
531
|
def keyPressEvent( event )
|
498
532
|
begin
|
499
|
-
# call to
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
error_message.show
|
509
|
-
end
|
533
|
+
# call to entity class for shortcuts
|
534
|
+
begin
|
535
|
+
view_result = model.entity_view.notify_key_press( self, event, current_index )
|
536
|
+
return view_result unless view_result.nil?
|
537
|
+
rescue Exception => e
|
538
|
+
puts e.backtrace
|
539
|
+
error_message = Qt::ErrorMessage.new( self )
|
540
|
+
error_message.show_message( "Error in shortcut handler for #{model.entity_view.name}: #{e.message}" )
|
541
|
+
error_message.show
|
510
542
|
end
|
511
543
|
|
544
|
+
# thrown by the sanity_check_xxx methods
|
512
545
|
catch :insane do
|
513
546
|
case
|
514
547
|
# on the last row, and down is pressed
|
@@ -525,7 +558,13 @@ class TableView < Qt::TableView
|
|
525
558
|
# TODO this is actually a shortcut
|
526
559
|
when event.ctrl? && event.return?
|
527
560
|
new_row
|
528
|
-
|
561
|
+
|
562
|
+
when event.delete?
|
563
|
+
if selection_model.selected_indexes.size > 1
|
564
|
+
delete_selection
|
565
|
+
return true
|
566
|
+
end
|
567
|
+
|
529
568
|
else
|
530
569
|
#~ puts event.inspect
|
531
570
|
end
|
@@ -624,15 +663,15 @@ class TableView < Qt::TableView
|
|
624
663
|
# override to prevent tab pressed from editing next field
|
625
664
|
# also takes into account that override_next_index may have been called
|
626
665
|
def closeEditor( editor, end_edit_hint )
|
627
|
-
puts "end_edit_hint: #{end_edit_hint.inspect}"
|
666
|
+
puts "end_edit_hint: #{end_edit_hint.inspect}" if $options[:debug]
|
628
667
|
case end_edit_hint
|
629
668
|
when Qt::AbstractItemDelegate.EditNextItem
|
630
669
|
super( editor, Qt::AbstractItemDelegate.NoHint )
|
631
|
-
set_current_unless_override(
|
670
|
+
set_current_unless_override( current_index.choppy { |i| i.column += 1 } )
|
632
671
|
|
633
672
|
when Qt::AbstractItemDelegate.EditPreviousItem
|
634
673
|
super( editor, Qt::AbstractItemDelegate.NoHint )
|
635
|
-
set_current_unless_override(
|
674
|
+
set_current_unless_override( current_index.choppy { |i| i.column -= 1 } )
|
636
675
|
|
637
676
|
else
|
638
677
|
super
|
@@ -651,6 +690,7 @@ class TableView < Qt::TableView
|
|
651
690
|
end
|
652
691
|
|
653
692
|
save_entity = current_index.entity
|
693
|
+
save_entity.save if save_entity.changed?
|
654
694
|
save_index = current_index
|
655
695
|
|
656
696
|
unless self.filtered
|
@@ -659,7 +699,7 @@ class TableView < Qt::TableView
|
|
659
699
|
if indexes.empty?
|
660
700
|
self.filtered = false
|
661
701
|
elsif indexes.size > 1
|
662
|
-
|
702
|
+
emit status_text( "Can't do multiple selection filters yet" )
|
663
703
|
self.filtered = false
|
664
704
|
end
|
665
705
|
|
@@ -686,7 +726,7 @@ class TableView < Qt::TableView
|
|
686
726
|
unless found_row.nil?
|
687
727
|
self.current_index = model.create_index( found_row, save_index.column )
|
688
728
|
if self.filtered?
|
689
|
-
emit status_text( "Filtered on #{current_index.
|
729
|
+
emit status_text( "Filtered on #{current_index.field.label} = #{current_index.gui_value}" )
|
690
730
|
else
|
691
731
|
emit status_text( nil )
|
692
732
|
end
|
@@ -699,7 +739,7 @@ class TableView < Qt::TableView
|
|
699
739
|
# * direction ( :forward, :backward )
|
700
740
|
# * from_start?
|
701
741
|
#
|
702
|
-
# TODO formalise this
|
742
|
+
# TODO formalise this?
|
703
743
|
def search( search_criteria )
|
704
744
|
indexes = model.search( current_index, search_criteria )
|
705
745
|
if indexes.size > 0
|