clevic 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +12 -999
- data/Manifest.txt +1 -0
- data/README.txt +13 -10
- data/Rakefile +4 -3
- data/TODO +28 -13
- data/bin/clevic +64 -11
- data/config/hoe.rb +1 -1
- data/lib/clevic/browser.rb +86 -116
- data/lib/clevic/cache_table.rb +14 -4
- data/lib/clevic/db_options.rb +13 -8
- data/lib/clevic/delegates.rb +3 -1
- data/lib/clevic/extensions.rb +16 -0
- data/lib/clevic/field.rb +13 -3
- data/lib/clevic/field_builder.rb +42 -0
- data/lib/clevic/model_builder.rb +285 -92
- data/lib/clevic/record.rb +45 -2
- data/lib/clevic/table_model.rb +137 -40
- data/lib/clevic/table_view.rb +345 -150
- data/lib/clevic/ui/browser.ui +18 -73
- data/lib/clevic/ui/browser_ui.rb +57 -99
- data/lib/clevic/ui/search_dialog_ui.rb +51 -50
- data/lib/clevic/version.rb +1 -1
- data/models/accounts_models.rb +21 -26
- data/models/minimal_models.rb +2 -0
- data/models/times_models.rb +78 -79
- data/models/times_sqlite_models.rb +8 -8
- data/models/values_models.rb +7 -6
- data/website/index.html +24 -27
- metadata +4 -3
data/lib/clevic/record.rb
CHANGED
@@ -1,9 +1,40 @@
|
|
1
|
+
require 'active_record.rb'
|
2
|
+
require 'active_record/dirty.rb'
|
3
|
+
|
4
|
+
module Clevic
|
5
|
+
|
6
|
+
module Default
|
7
|
+
module ClassMethods
|
8
|
+
def define_ui_block; nil; end
|
9
|
+
|
10
|
+
def post_default_ui_block
|
11
|
+
@post_default_ui_block
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_default_ui( &block )
|
15
|
+
@post_default_ui_block = block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module ActiveRecord
|
27
|
+
class Base
|
28
|
+
include Clevic::Default
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
1
32
|
module Clevic
|
2
33
|
|
3
34
|
# The base class for all Clevic model and UI definitions.
|
4
35
|
# minimal definition is like this
|
5
36
|
# class User < Clevic::Record; end
|
6
|
-
#
|
37
|
+
# Record automatically keeps track of the order
|
7
38
|
# in which models are defined, so that tabs can
|
8
39
|
# be constructed in that order.
|
9
40
|
class Record < ActiveRecord::Base
|
@@ -11,6 +42,13 @@ module Clevic
|
|
11
42
|
self.abstract_class = true
|
12
43
|
@@subclass_order = []
|
13
44
|
|
45
|
+
def self.define_ui_block
|
46
|
+
@define_ui_block
|
47
|
+
end
|
48
|
+
|
49
|
+
# keep track of the order in which subclasses are
|
50
|
+
# defined, so that can be used as the default ordering
|
51
|
+
# of the views.
|
14
52
|
def self.inherited( subclass )
|
15
53
|
@@subclass_order << subclass
|
16
54
|
super
|
@@ -19,10 +57,15 @@ module Clevic
|
|
19
57
|
def self.models
|
20
58
|
@@subclass_order
|
21
59
|
end
|
22
|
-
|
60
|
+
|
23
61
|
def self.models=( array )
|
24
62
|
@@subclass_order = array
|
25
63
|
end
|
64
|
+
|
65
|
+
# use this to define UI blocks using the ModelBuilder DSL
|
66
|
+
def self.define_ui( &block )
|
67
|
+
@define_ui_block = block
|
68
|
+
end
|
26
69
|
end
|
27
70
|
|
28
71
|
end
|
data/lib/clevic/table_model.rb
CHANGED
@@ -13,8 +13,6 @@ module Clevic
|
|
13
13
|
This table model allows an ActiveRecord or ActiveResource to be used as a
|
14
14
|
basis for a Qt::AbstractTableModel for viewing in a Qt::TableView.
|
15
15
|
|
16
|
-
Initial idea by Richard Dale and Silvio Fonseca.
|
17
|
-
|
18
16
|
* labels are the headings in the table view
|
19
17
|
|
20
18
|
* dots are the dotted attribute paths that specify how to get values from
|
@@ -30,8 +28,22 @@ Initial idea by Richard Dale and Silvio Fonseca.
|
|
30
28
|
class TableModel < Qt::AbstractTableModel
|
31
29
|
include QtFlags
|
32
30
|
|
33
|
-
|
31
|
+
# the CacheTable of Clevic::Record or ActiveRecord::Base objects
|
32
|
+
attr_reader :collection
|
33
|
+
|
34
|
+
# the actual class for the collection objects
|
35
|
+
attr_accessor :model_class
|
36
|
+
|
37
|
+
# the collection of Clevic::Field objects
|
38
|
+
attr_reader :fields
|
39
|
+
|
40
|
+
attr_accessor :read_only
|
41
|
+
def read_only?; read_only; end
|
34
42
|
|
43
|
+
# should this model create a new empty record by default?
|
44
|
+
attr_accessor :auto_new
|
45
|
+
def auto_new?; auto_new; end
|
46
|
+
|
35
47
|
signals(
|
36
48
|
# index where error occurred, value, message
|
37
49
|
'data_error(QModelIndex,QVariant,QString)',
|
@@ -39,10 +51,44 @@ class TableModel < Qt::AbstractTableModel
|
|
39
51
|
'dataChanged(const QModelIndex&,const QModelIndex&)'
|
40
52
|
)
|
41
53
|
|
42
|
-
def initialize(
|
43
|
-
super
|
54
|
+
def initialize( parent = nil )
|
55
|
+
super
|
56
|
+
@metadatas = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def fields=( arr )
|
60
|
+
@fields = arr
|
61
|
+
|
62
|
+
#reset these
|
44
63
|
@metadatas = []
|
45
|
-
@
|
64
|
+
@dots = nil
|
65
|
+
@labels = nil
|
66
|
+
@attributes = nil
|
67
|
+
@attribute_paths = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def dots
|
71
|
+
@dots ||= fields.map {|x| x.column }
|
72
|
+
end
|
73
|
+
|
74
|
+
def labels
|
75
|
+
@labels ||= fields.map {|x| x.label }
|
76
|
+
end
|
77
|
+
|
78
|
+
def attributes
|
79
|
+
@attributes ||= fields.map {|x| x.attribute }
|
80
|
+
end
|
81
|
+
|
82
|
+
def attribute_paths
|
83
|
+
@attribute_paths ||= fields.map {|x| x.attribute_path }
|
84
|
+
end
|
85
|
+
|
86
|
+
def collection=( arr )
|
87
|
+
@collection = arr
|
88
|
+
# fill in an empty record for data entry
|
89
|
+
if collection.size == 0 && auto_new?
|
90
|
+
collection << model_class.new
|
91
|
+
end
|
46
92
|
end
|
47
93
|
|
48
94
|
def sort( col, order )
|
@@ -54,6 +100,7 @@ class TableModel < Qt::AbstractTableModel
|
|
54
100
|
super
|
55
101
|
end
|
56
102
|
|
103
|
+
# this is called for read-only tables.
|
57
104
|
def match( start_index, role, search_value, hits, match_flags )
|
58
105
|
#~ Qt::MatchExactly 0 Performs QVariant-based matching.
|
59
106
|
#~ Qt::MatchFixedString 8 Performs string-based matching. String-based comparisons are case-insensitive unless the MatchCaseSensitive flag is also specified.
|
@@ -64,22 +111,19 @@ class TableModel < Qt::AbstractTableModel
|
|
64
111
|
#~ Qt::MatchRegExp 4 Performs string-based matching using a regular expression as the search term.
|
65
112
|
#~ Qt::MatchWildcard 5 Performs string-based matching using a string with wildcards as the search term.
|
66
113
|
#~ 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.
|
67
|
-
super
|
114
|
+
#~ super
|
115
|
+
[]
|
68
116
|
end
|
69
117
|
|
70
|
-
def build_dots( dots, attrs, prefix="" )
|
71
|
-
attrs.inject( dots ) do |cols, a|
|
72
|
-
if a[1].respond_to? :attributes
|
73
|
-
build_keys(cols, a[1].attributes, prefix + a[0] + ".")
|
74
|
-
else
|
75
|
-
cols << prefix + a[0]
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def model_class
|
81
|
-
@builder.model_class
|
82
|
-
end
|
118
|
+
#~ def build_dots( dots, attrs, prefix="" )
|
119
|
+
#~ attrs.inject( dots ) do |cols, a|
|
120
|
+
#~ if a[1].respond_to? :attributes
|
121
|
+
#~ build_keys(cols, a[1].attributes, prefix + a[0] + ".")
|
122
|
+
#~ else
|
123
|
+
#~ cols << prefix + a[0]
|
124
|
+
#~ end
|
125
|
+
#~ end
|
126
|
+
#~ end
|
83
127
|
|
84
128
|
# cache metadata (ActiveRecord#column_for_attribute) because it's not going
|
85
129
|
# to change over the lifetime of the table
|
@@ -161,7 +205,11 @@ class TableModel < Qt::AbstractTableModel
|
|
161
205
|
if model_index.metadata.type == :boolean
|
162
206
|
retval = item_boolean_flags
|
163
207
|
end
|
164
|
-
|
208
|
+
|
209
|
+
# read-only
|
210
|
+
unless model_index.field.read_only? || model_index.entity.readonly? || read_only?
|
211
|
+
retval |= qt_item_is_editable.to_i
|
212
|
+
end
|
165
213
|
retval
|
166
214
|
end
|
167
215
|
|
@@ -179,7 +227,7 @@ class TableModel < Qt::AbstractTableModel
|
|
179
227
|
when qt_display_role
|
180
228
|
case orientation
|
181
229
|
when Qt::Horizontal
|
182
|
-
|
230
|
+
labels[section]
|
183
231
|
when Qt::Vertical
|
184
232
|
# don't force a fetch from the db
|
185
233
|
if collection.cached_at?( section )
|
@@ -200,8 +248,27 @@ class TableModel < Qt::AbstractTableModel
|
|
200
248
|
nil
|
201
249
|
|
202
250
|
when qt_tooltip_role
|
203
|
-
|
204
|
-
|
251
|
+
case orientation
|
252
|
+
when Qt::Horizontal
|
253
|
+
fields[section].tooltip
|
254
|
+
|
255
|
+
when Qt::Vertical
|
256
|
+
case
|
257
|
+
when !collection[section].errors.empty?
|
258
|
+
'Invalid data'
|
259
|
+
when collection[section].changed?
|
260
|
+
'Unsaved changes'
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
when qt_background_role
|
265
|
+
if orientation == Qt::Vertical
|
266
|
+
case
|
267
|
+
when !collection[section].errors.empty?
|
268
|
+
Qt::Color.new( 'orange' )
|
269
|
+
when collection[section].changed?
|
270
|
+
Qt::Color.new( 'yellow' )
|
271
|
+
end
|
205
272
|
end
|
206
273
|
|
207
274
|
else
|
@@ -240,22 +307,36 @@ class TableModel < Qt::AbstractTableModel
|
|
240
307
|
|
241
308
|
when qt_text_alignment_role
|
242
309
|
index.field.alignment
|
243
|
-
|
310
|
+
|
244
311
|
# these are just here to make debug output quieter
|
245
312
|
when qt_size_hint_role;
|
246
|
-
|
313
|
+
|
314
|
+
# show field with a red background if there's an error
|
315
|
+
when qt_background_role
|
316
|
+
Qt::Color.new( 'red' ) if index.has_errors?
|
317
|
+
|
247
318
|
when qt_font_role;
|
248
319
|
when qt_foreground_role
|
249
|
-
|
320
|
+
if index.field.read_only? || index.entity.readonly? || read_only?
|
321
|
+
Qt::Color.new( 'dimgray' )
|
322
|
+
end
|
323
|
+
|
250
324
|
when qt_decoration_role;
|
251
325
|
|
252
|
-
# provide a tooltip when an empty relational field is encountered
|
253
326
|
when qt_tooltip_role
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
327
|
+
case
|
328
|
+
# show ActiveRecord validation errors
|
329
|
+
when index.has_errors?
|
330
|
+
index.errors.join("\n")
|
331
|
+
|
332
|
+
# provide a tooltip when an empty relational field is encountered
|
333
|
+
when index.metadata.type == :association
|
334
|
+
index.field.delegate.if_empty_message
|
335
|
+
|
336
|
+
# read-only field
|
337
|
+
when index.field.read_only?
|
338
|
+
'Read-only'
|
339
|
+
end
|
259
340
|
else
|
260
341
|
puts "data index: #{index}, role: #{const_as_string(role)}" if $options[:debug]
|
261
342
|
nil
|
@@ -264,7 +345,7 @@ class TableModel < Qt::AbstractTableModel
|
|
264
345
|
# return a variant
|
265
346
|
retval.to_variant
|
266
347
|
rescue Exception => e
|
267
|
-
puts e.backtrace
|
348
|
+
puts e.backtrace
|
268
349
|
puts "#{index.inspect} #{value.inspect} #{index.entity.inspect} #{e.message}"
|
269
350
|
nil.to_variant
|
270
351
|
end
|
@@ -276,7 +357,7 @@ class TableModel < Qt::AbstractTableModel
|
|
276
357
|
case role
|
277
358
|
when qt_edit_role
|
278
359
|
# Don't allow the primary key to be changed
|
279
|
-
return false if index.attribute ==
|
360
|
+
return false if index.attribute == model_class.primary_key.to_sym
|
280
361
|
|
281
362
|
if ( index.column < 0 || index.column >= dots.size )
|
282
363
|
raise "invalid column #{index.column}"
|
@@ -338,7 +419,7 @@ class TableModel < Qt::AbstractTableModel
|
|
338
419
|
|
339
420
|
when qt_checkstate_role
|
340
421
|
if index.metadata.type == :boolean
|
341
|
-
index.entity.toggle
|
422
|
+
index.entity.toggle( index.attribute )
|
342
423
|
true
|
343
424
|
else
|
344
425
|
false
|
@@ -379,6 +460,7 @@ class TableModel < Qt::AbstractTableModel
|
|
379
460
|
end
|
380
461
|
|
381
462
|
# return a set of indexes that match the search criteria
|
463
|
+
# TODO this implementation is very un-ruby.
|
382
464
|
def search( start_index, search_criteria )
|
383
465
|
# get the search value parameter, in SQL format
|
384
466
|
search_value =
|
@@ -388,17 +470,32 @@ class TableModel < Qt::AbstractTableModel
|
|
388
470
|
"%#{search_criteria.search_text}%"
|
389
471
|
end
|
390
472
|
|
391
|
-
# build up the conditions
|
473
|
+
# build up the ordering conditions
|
392
474
|
bits = collection.build_sql_find( start_index.entity, search_criteria.direction )
|
393
|
-
|
475
|
+
|
476
|
+
# do the conditions for the search value
|
477
|
+
conditions =
|
478
|
+
if start_index.field.is_association?
|
479
|
+
# for related tables
|
480
|
+
# TODO this will only work with a path value with no dots
|
481
|
+
"#{start_index.field.path} #{like_operator} :search_value"
|
482
|
+
else
|
483
|
+
# for this table
|
484
|
+
"#{model_class.connection.quote_column_name( start_index.field_name )} #{like_operator} :search_value"
|
485
|
+
end
|
486
|
+
|
487
|
+
# add ordering conditions
|
394
488
|
conditions += ( " and " + bits[:sql] ) unless search_criteria.from_start?
|
489
|
+
|
395
490
|
params = { :search_value => search_value }
|
396
491
|
params.merge!( bits[:params] ) unless search_criteria.from_start?
|
492
|
+
|
397
493
|
# find the first match
|
398
494
|
entity = model_class.find(
|
399
495
|
:first,
|
400
496
|
:conditions => [ conditions, params ],
|
401
|
-
:order => search_criteria.direction == :forwards ? collection.order : collection.reverse_order
|
497
|
+
:order => search_criteria.direction == :forwards ? collection.order : collection.reverse_order,
|
498
|
+
:joins => ( start_index.field.meta.name if start_index.field.is_association? )
|
402
499
|
)
|
403
500
|
|
404
501
|
# return matched indexes
|
@@ -411,7 +508,7 @@ class TableModel < Qt::AbstractTableModel
|
|
411
508
|
end
|
412
509
|
|
413
510
|
def field_for_index( model_index )
|
414
|
-
|
511
|
+
fields[model_index.column]
|
415
512
|
end
|
416
513
|
|
417
514
|
end
|
data/lib/clevic/table_view.rb
CHANGED
@@ -2,25 +2,47 @@ require 'rubygems'
|
|
2
2
|
require 'Qt4'
|
3
3
|
require 'fastercsv'
|
4
4
|
require 'clevic/model_builder.rb'
|
5
|
+
require 'qtext/action_builder.rb'
|
5
6
|
|
6
7
|
module Clevic
|
7
8
|
|
8
9
|
# The view class, implementing neat shortcuts and other pleasantness
|
9
10
|
class TableView < Qt::TableView
|
10
|
-
|
11
|
+
include ActionBuilder
|
12
|
+
|
13
|
+
attr_reader :model_class
|
11
14
|
# whether the model is currently filtered
|
12
15
|
# TODO better in QAbstractSortFilter?
|
13
16
|
attr_accessor :filtered
|
17
|
+
def filtered?; self.filtered; end
|
14
18
|
|
15
|
-
#
|
16
|
-
|
19
|
+
# status_text is emitted when this object was to display something in the status bar
|
20
|
+
# filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
|
21
|
+
signals 'status_text(QString)', 'filter_status(bool)'
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
# model_builder_record is:
|
24
|
+
# - a subclass of Clevic::Record or ActiveRecord::Base
|
25
|
+
# - an instance of ModelBuilder
|
26
|
+
# - an instance of TableModel
|
27
|
+
def initialize( model_builder_record, parent, &block )
|
28
|
+
# need the empty block here, otherwise Qt bindings grab &block
|
29
|
+
super( parent ) {}
|
23
30
|
|
31
|
+
# the model/model_class/builder
|
32
|
+
case
|
33
|
+
when model_builder_record.kind_of?( TableModel )
|
34
|
+
self.model = model_builder_record
|
35
|
+
|
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
|
+
else
|
43
|
+
raise "Don't know what to do with #{model_builder_record}"
|
44
|
+
end
|
45
|
+
|
24
46
|
# see closeEditor
|
25
47
|
@index_override = false
|
26
48
|
|
@@ -34,21 +56,278 @@ class TableView < Qt::TableView
|
|
34
56
|
|
35
57
|
# turn off "Object#type deprecated" messages
|
36
58
|
$VERBOSE = nil
|
59
|
+
|
60
|
+
init_actions
|
61
|
+
self.context_menu_policy = Qt::ActionsContextMenu
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_builder( model_builder, &block )
|
65
|
+
model_builder.instance_eval( &block ) unless block.nil?
|
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 )
|
77
|
+
end
|
78
|
+
|
79
|
+
def connect_model_class_signals( model_class )
|
80
|
+
# this is only here because model_class.data_changed needs the view.
|
81
|
+
# Should probably fix that.
|
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
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# return menu actions for the model, or an empty array if there aren't any
|
90
|
+
def model_actions
|
91
|
+
@model_actions ||= []
|
92
|
+
end
|
93
|
+
|
94
|
+
# hook for the sanity_check_xxx methods
|
95
|
+
# called for the actions set up by ActionBuilder
|
96
|
+
# it just wraps the action block/method in a catch
|
97
|
+
# block for :insane
|
98
|
+
def action_triggered( &block )
|
99
|
+
catch :insane do
|
100
|
+
yield
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def init_actions
|
105
|
+
# add model actions, if they're defined
|
106
|
+
if model_class.respond_to?( :actions )
|
107
|
+
list( :model ) do |ab|
|
108
|
+
model_class.actions( self, ab )
|
109
|
+
end
|
110
|
+
separator
|
111
|
+
end
|
112
|
+
|
113
|
+
# list of actions called edit
|
114
|
+
list( :edit ) do
|
115
|
+
#~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
|
116
|
+
action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
|
117
|
+
action :action_paste, '&Paste', :shortcut => Qt::KeySequence::Paste, :method => :paste
|
118
|
+
separator
|
119
|
+
action :action_ditto, '&Ditto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
|
120
|
+
action :action_ditto_right, 'Ditto R&ight', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
|
121
|
+
action :action_ditto_left, '&Ditto L&eft', :shortcut => 'Ctrl+[', :method => :ditto_left, :tool_tip => 'Copy field one to left from previous record'
|
122
|
+
action :action_insert_date, 'Insert Date', :shortcut => 'Ctrl+;', :method => :insert_current_date
|
123
|
+
action :action_open_editor, '&Open Editor', :shortcut => 'F4', :method => :open_editor
|
124
|
+
separator
|
125
|
+
action :action_row, 'New Ro&w', :shortcut => 'Ctrl+N', :method => :new_row
|
126
|
+
action :action_refresh, '&Refresh', :shortcut => 'Ctrl+R', :method => :refresh
|
127
|
+
action :action_delete_rows, 'Delete Rows', :shortcut => 'Ctrl+Delete', :method => :delete_rows
|
128
|
+
|
129
|
+
if $options[:debug]
|
130
|
+
action :action_dump, 'D&ump', :shortcut => 'Ctrl+Shift+D' do
|
131
|
+
puts model.collection[current_index.row].inspect
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
separator
|
137
|
+
|
138
|
+
# list of actions called search
|
139
|
+
list( :search ) do
|
140
|
+
action :action_find, '&Find', :shortcut => Qt::KeySequence::Find, :method => :find
|
141
|
+
action :action_find_next, 'Find &Next', :shortcut => Qt::KeySequence::FindNext, :method => :find_next
|
142
|
+
action :action_filter, 'Fil&ter', :checkable => true, :shortcut => 'Ctrl+L', :method => :filter_by_current
|
143
|
+
action :action_highlight, '&Highlight', :visible => false, :shortcut => 'Ctrl+H'
|
144
|
+
end
|
37
145
|
end
|
38
146
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
147
|
+
def copy_current_selection
|
148
|
+
text = String.new
|
149
|
+
selection_model.selection.each do |selection_range|
|
150
|
+
(selection_range.top..selection_range.bottom).each do |row|
|
151
|
+
row_ary = Array.new
|
152
|
+
selection_model.selected_indexes.each do |index|
|
153
|
+
row_ary << index.gui_value if index.row == row
|
154
|
+
end
|
155
|
+
text << row_ary.to_csv
|
47
156
|
end
|
48
157
|
end
|
49
|
-
|
158
|
+
Qt::Application::clipboard.text = text
|
159
|
+
end
|
160
|
+
|
161
|
+
def paste
|
162
|
+
sanity_check_read_only
|
163
|
+
|
164
|
+
# remove trailing "\n" if there is one
|
165
|
+
text = Qt::Application::clipboard.text.chomp
|
166
|
+
arr = FasterCSV.parse( text )
|
167
|
+
|
168
|
+
return true if selection_model.selection.size != 1
|
169
|
+
|
170
|
+
selection_range = selection_model.selection[0]
|
171
|
+
selected_index = selection_model.selected_indexes[0]
|
172
|
+
|
173
|
+
if selection_range.single_cell?
|
174
|
+
# only one cell selected, so paste like a spreadsheet
|
175
|
+
if text.empty?
|
176
|
+
# just clear the current selection
|
177
|
+
model.setData( selected_index, nil.to_variant )
|
178
|
+
else
|
179
|
+
paste_to_index( selected_index, arr )
|
180
|
+
end
|
181
|
+
else
|
182
|
+
return true if selection_range.height != arr.size
|
183
|
+
return true if selection_range.width != arr[0].size
|
184
|
+
|
185
|
+
# size is the same, so do the paste
|
186
|
+
paste_to_index( selected_index, arr )
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def sanity_check_ditto
|
191
|
+
if current_index.row == 0
|
192
|
+
emit status_text( 'No previous record to copy.' )
|
193
|
+
throw :insane
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def sanity_check_read_only
|
198
|
+
if current_index.field.read_only?
|
199
|
+
emit status_text( 'Can\'t copy into read-only field.' )
|
200
|
+
elsif current_index.entity.readonly?
|
201
|
+
emit status_text( 'Can\'t copy into read-only record.' )
|
202
|
+
else
|
203
|
+
sanity_check_read_only_table
|
204
|
+
return
|
205
|
+
end
|
206
|
+
throw :insane
|
207
|
+
end
|
208
|
+
|
209
|
+
def sanity_check_read_only_table
|
210
|
+
if model.read_only?
|
211
|
+
emit status_text( 'Can\'t modify a read-only table.' )
|
212
|
+
throw :insane
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def ditto
|
217
|
+
sanity_check_ditto
|
218
|
+
sanity_check_read_only
|
219
|
+
one_up_index = model.create_index( current_index.row - 1, current_index.column )
|
220
|
+
previous_value = one_up_index.attribute_value
|
221
|
+
if current_index.attribute_value != previous_value
|
222
|
+
current_index.attribute_value = previous_value
|
223
|
+
emit model.dataChanged( current_index, current_index )
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def ditto_right
|
228
|
+
sanity_check_ditto
|
229
|
+
sanity_check_read_only
|
230
|
+
unless current_index.column < model.column_count
|
231
|
+
emit status_text( 'No column to the right' )
|
232
|
+
else
|
233
|
+
one_up_right_index = model.create_index( current_index.row - 1, current_index.column + 1 )
|
234
|
+
current_index.attribute_value = one_up_right_index.attribute_value
|
235
|
+
emit model.dataChanged( current_index, current_index )
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def ditto_left
|
240
|
+
sanity_check_ditto
|
241
|
+
sanity_check_read_only
|
242
|
+
unless current_index.column > 0
|
243
|
+
emit status_text( 'No column to the left' )
|
244
|
+
else
|
245
|
+
one_up_left_index = model.create_index( current_index.row - 1, current_index.column - 1 )
|
246
|
+
current_index.attribute_value = one_up_left_index.attribute_value
|
247
|
+
emit model.dataChanged( current_index, current_index )
|
248
|
+
end
|
50
249
|
end
|
250
|
+
|
251
|
+
def insert_current_date
|
252
|
+
sanity_check_read_only
|
253
|
+
current_index.attribute_value = Time.now
|
254
|
+
emit model.dataChanged( current_index, current_index )
|
255
|
+
end
|
256
|
+
|
257
|
+
def open_editor
|
258
|
+
edit( current_index )
|
259
|
+
delegate = item_delegate( current_index )
|
260
|
+
delegate.full_edit
|
261
|
+
end
|
262
|
+
|
263
|
+
def new_row
|
264
|
+
sanity_check_read_only_table
|
265
|
+
model.add_new_item
|
266
|
+
new_row_index = model.index( model.collection.size - 1, 0 )
|
267
|
+
currentChanged( new_row_index, current_index )
|
268
|
+
selection_model.clear
|
269
|
+
self.current_index = new_row_index
|
270
|
+
end
|
271
|
+
|
272
|
+
def deleted_selection
|
273
|
+
sanity_check_read_only
|
51
274
|
|
275
|
+
# translate from ModelIndex objects to row indices
|
276
|
+
rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
|
277
|
+
unless rows.empty?
|
278
|
+
# header rows are selected, so delete them
|
279
|
+
model.remove_rows( rows )
|
280
|
+
else
|
281
|
+
# otherwise various cells are selected, so delete the cells
|
282
|
+
delete_cells
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# display a search dialog, and find the entered text
|
287
|
+
def find
|
288
|
+
@search_dialog ||= SearchDialog.new
|
289
|
+
result = @search_dialog.exec( current_index.gui_value )
|
290
|
+
|
291
|
+
override_cursor( Qt::BusyCursor ) do
|
292
|
+
case result
|
293
|
+
when Qt::Dialog::Accepted
|
294
|
+
search_for = @search_dialog.search_text
|
295
|
+
search( @search_dialog )
|
296
|
+
when Qt::Dialog::Rejected
|
297
|
+
puts "Don't search"
|
298
|
+
else
|
299
|
+
puts "unknown dialog code #{result}"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def find_next
|
305
|
+
if @search_dialog.nil?
|
306
|
+
emit status_text( 'No previous find' )
|
307
|
+
else
|
308
|
+
override_cursor( Qt::BusyCursor ) do
|
309
|
+
save_from_start = @search_dialog.from_start?
|
310
|
+
@search_dialog.from_start = false
|
311
|
+
search( @search_dialog )
|
312
|
+
@search_dialog.from_start = save_from_start
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# force a complete reload of the current tab's data
|
318
|
+
def refresh
|
319
|
+
override_cursor( Qt::BusyCursor ) do
|
320
|
+
model.reload_data
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# toggle the filter, based on current selection.
|
325
|
+
def filter_by_current( bool_filter )
|
326
|
+
# TODO if there's no selection, use the current index instead
|
327
|
+
filter_by_indexes( selection_model.selected_indexes )
|
328
|
+
emit filter_status( bool_filter )
|
329
|
+
end
|
330
|
+
|
52
331
|
# alternative access for auto_size_column
|
53
332
|
def auto_size_attribute( attribute, sample )
|
54
333
|
col = model.attributes.index( attribute )
|
@@ -109,9 +388,20 @@ class TableView < Qt::TableView
|
|
109
388
|
# make sure row size is correct
|
110
389
|
# show error messages for data
|
111
390
|
def setModel( model )
|
391
|
+
# must do this otherwise model gets garbage collected
|
392
|
+
@model = model
|
393
|
+
|
394
|
+
# make sure we get nice spacing
|
112
395
|
vertical_header.default_section_size = vertical_header.minimum_section_size
|
113
396
|
super
|
114
397
|
|
398
|
+
# set delegates
|
399
|
+
self.item_delegate = Clevic::ItemDelegate.new( self )
|
400
|
+
model.fields.each_with_index do |field, index|
|
401
|
+
set_item_delegate_for_column( index, field.delegate )
|
402
|
+
end
|
403
|
+
|
404
|
+
# data errors
|
115
405
|
model.connect( SIGNAL( 'data_error(QModelIndex, QVariant, QString)' ) ) do |index,variant,msg|
|
116
406
|
error_message = Qt::ErrorMessage.new( self )
|
117
407
|
error_message.show_message( "Incorrect value '#{variant.value}' entered for field [#{index.attribute.to_s}].\nMessage was: #{msg}" )
|
@@ -129,7 +419,7 @@ class TableView < Qt::TableView
|
|
129
419
|
# resize all fields based on heuristics rather
|
130
420
|
# than iterating through the entire data model
|
131
421
|
def resize_columns
|
132
|
-
|
422
|
+
model.fields.each_with_index do |field, index|
|
133
423
|
auto_size_column( index, field.sample )
|
134
424
|
end
|
135
425
|
end
|
@@ -158,6 +448,8 @@ class TableView < Qt::TableView
|
|
158
448
|
end
|
159
449
|
|
160
450
|
def delete_multiple_cells?
|
451
|
+
sanity_check_read_only
|
452
|
+
|
161
453
|
# go ahead with delete if there's only 1 cell, or the user says OK
|
162
454
|
delete_ok =
|
163
455
|
if selection_model.selected_indexes.size > 1
|
@@ -195,10 +487,14 @@ class TableView < Qt::TableView
|
|
195
487
|
end
|
196
488
|
end
|
197
489
|
|
490
|
+
def delete_rows
|
491
|
+
if delete_multiple_cells?
|
492
|
+
model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# handle certain key combinations that aren't shortcuts
|
198
497
|
def keyPressEvent( event )
|
199
|
-
# for some reason, trying to call another method inside
|
200
|
-
# the begin .. rescue block throws a superclass method not
|
201
|
-
# found error. Weird.
|
202
498
|
begin
|
203
499
|
# call to model class for shortcuts
|
204
500
|
if model.model_class.respond_to?( :key_press_event )
|
@@ -213,138 +509,30 @@ class TableView < Qt::TableView
|
|
213
509
|
end
|
214
510
|
end
|
215
511
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
# on the right-bottom cell, and tab is pressed
|
224
|
-
# then add a new row
|
225
|
-
when event.tab? && last_cell?
|
226
|
-
model.add_new_item
|
227
|
-
|
228
|
-
# delete the current row
|
229
|
-
when event.ctrl? && event.delete?
|
230
|
-
if delete_multiple_cells?
|
231
|
-
model.remove_rows( selection_model.selected_indexes.map{|index| index.row} )
|
232
|
-
end
|
233
|
-
|
234
|
-
# copy the value from the row one above
|
235
|
-
when event.ctrl? && event.apostrophe?
|
236
|
-
if current_index.row > 0
|
237
|
-
one_up_index = model.create_index( current_index.row - 1, current_index.column )
|
238
|
-
previous_value = one_up_index.attribute_value
|
239
|
-
if current_index.attribute_value != previous_value
|
240
|
-
current_index.attribute_value = previous_value
|
241
|
-
emit model.dataChanged( current_index, current_index )
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
# copy the value from the previous row, one cell right
|
246
|
-
when event.ctrl? && event.bracket_right?
|
247
|
-
if current_index.row > 0 && current_index.column < model.column_count
|
248
|
-
one_up_right_index = model.create_index( current_index.row - 1, current_index.column + 1 )
|
249
|
-
current_index.attribute_value = one_up_right_index.attribute_value
|
250
|
-
emit model.dataChanged( current_index, current_index )
|
251
|
-
end
|
252
|
-
|
253
|
-
# copy the value from the previous row, one cell left
|
254
|
-
when event.ctrl? && event.bracket_left?
|
255
|
-
if current_index.row > 0 && current_index.column > 0
|
256
|
-
one_up_left_index = model.create_index( current_index.row - 1, current_index.column - 1 )
|
257
|
-
current_index.attribute_value = one_up_left_index.attribute_value
|
258
|
-
emit model.dataChanged( current_index, current_index )
|
259
|
-
end
|
260
|
-
|
261
|
-
# insert today's date in the current field
|
262
|
-
when event.ctrl? && event.semicolon?
|
263
|
-
current_index.attribute_value = Time.now
|
264
|
-
emit model.dataChanged( current_index, current_index )
|
265
|
-
|
266
|
-
# dump current record to stdout
|
267
|
-
when event.ctrl? && event.d?
|
268
|
-
puts model.collection[current_index.row].inspect
|
269
|
-
|
270
|
-
# add new record and go to it
|
271
|
-
when event.ctrl? && ( event.n? || event.return? )
|
272
|
-
model.add_new_item
|
273
|
-
new_row_index = model.index( model.collection.size - 1, 0 )
|
274
|
-
currentChanged( new_row_index, current_index )
|
275
|
-
selection_model.clear
|
276
|
-
self.current_index = new_row_index
|
277
|
-
|
278
|
-
# handle deletion of entire rows
|
279
|
-
when event.delete?
|
280
|
-
# translate from ModelIndex objects to row indices
|
281
|
-
rows = vertical_header.selection_model.selected_rows.map{|x| x.row}
|
282
|
-
unless rows.empty?
|
283
|
-
# header rows are selected, so delete them
|
284
|
-
model.remove_rows( rows )
|
285
|
-
# make sure no other handlers get this event
|
286
|
-
return true
|
287
|
-
else
|
288
|
-
# otherwise various cells are selected, so delete the cells
|
289
|
-
delete_cells
|
290
|
-
# nobody else handles this
|
291
|
-
return true
|
292
|
-
end
|
293
|
-
|
294
|
-
# f4 should open editor immediately
|
295
|
-
when event.f4?
|
296
|
-
edit( current_index, Qt::AbstractItemView::AllEditTriggers, event )
|
297
|
-
delegate = item_delegate( current_index )
|
298
|
-
delegate.full_edit
|
299
|
-
|
300
|
-
# copy currently selected data in csv format
|
301
|
-
when event.ctrl? && event.c?
|
302
|
-
text = String.new
|
303
|
-
selection_model.selection.each do |selection_range|
|
304
|
-
(selection_range.top..selection_range.bottom).each do |row|
|
305
|
-
row_ary = Array.new
|
306
|
-
selection_model.selected_indexes.each do |index|
|
307
|
-
row_ary << index.gui_value if index.row == row
|
308
|
-
end
|
309
|
-
text << row_ary.to_csv
|
310
|
-
end
|
311
|
-
end
|
312
|
-
Qt::Application::clipboard.text = text
|
313
|
-
return true
|
314
|
-
|
315
|
-
when event.ctrl? && event.v?
|
316
|
-
# remove trailing "\n" if there is one
|
317
|
-
text = Qt::Application::clipboard.text.chomp
|
318
|
-
arr = FasterCSV.parse( text )
|
319
|
-
|
320
|
-
return true if selection_model.selection.size != 1
|
321
|
-
|
322
|
-
selection_range = selection_model.selection[0]
|
323
|
-
selected_index = selection_model.selected_indexes[0]
|
324
|
-
|
325
|
-
if selection_range.single_cell?
|
326
|
-
# only one cell selected, so paste like a spreadsheet
|
327
|
-
if text.empty?
|
328
|
-
# just clear the current selection
|
329
|
-
model.setData( selected_index, nil.to_variant )
|
330
|
-
else
|
331
|
-
paste_to_index( selected_index, arr )
|
332
|
-
end
|
333
|
-
else
|
334
|
-
return true if selection_range.height != arr.size
|
335
|
-
return true if selection_range.width != arr[0].size
|
512
|
+
catch :insane do
|
513
|
+
case
|
514
|
+
# on the last row, and down is pressed
|
515
|
+
# add a new row
|
516
|
+
when event.down? && last_row?
|
517
|
+
new_row
|
336
518
|
|
337
|
-
|
338
|
-
|
519
|
+
# on the right-bottom cell, and tab is pressed
|
520
|
+
# then add a new row
|
521
|
+
when event.tab? && last_cell?
|
522
|
+
new_row
|
523
|
+
|
524
|
+
# add new record and go to it
|
525
|
+
# TODO this is actually a shortcut
|
526
|
+
when event.ctrl? && event.return?
|
527
|
+
new_row
|
528
|
+
|
529
|
+
else
|
530
|
+
#~ puts event.inspect
|
339
531
|
end
|
340
|
-
return true
|
341
|
-
|
342
|
-
else
|
343
|
-
#~ puts event.inspect
|
344
532
|
end
|
345
533
|
super
|
346
534
|
rescue Exception => e
|
347
|
-
puts e.backtrace
|
535
|
+
puts e.backtrace
|
348
536
|
puts e.message
|
349
537
|
error_message = Qt::ErrorMessage.new( self )
|
350
538
|
error_message.show_message( "Error in #{current_index.attribute.to_s}: \"#{e.message}\"" )
|
@@ -366,7 +554,7 @@ class TableView < Qt::TableView
|
|
366
554
|
saved = model.save( index )
|
367
555
|
if !saved
|
368
556
|
error_message = Qt::ErrorMessage.new( self )
|
369
|
-
msg = model.collection[index.row].errors.join("\n")
|
557
|
+
msg = model.collection[index.row].errors.to_a.join("\n")
|
370
558
|
error_message.show_message( msg )
|
371
559
|
error_message.show
|
372
560
|
end
|
@@ -420,7 +608,7 @@ class TableView < Qt::TableView
|
|
420
608
|
|
421
609
|
def focusOutEvent( event )
|
422
610
|
super
|
423
|
-
save_current_row
|
611
|
+
#~ save_current_row
|
424
612
|
end
|
425
613
|
|
426
614
|
# this is the only method that is called when an itemDelegate is open
|
@@ -455,6 +643,7 @@ class TableView < Qt::TableView
|
|
455
643
|
# otherwise turn filtering off.
|
456
644
|
# Sets self.filter to true if filtering worked, false otherwise.
|
457
645
|
# indexes is a collection of Qt::ModelIndex
|
646
|
+
# TODO combine with filter_by_current
|
458
647
|
def filter_by_indexes( indexes )
|
459
648
|
unless indexes[0].field.filterable?
|
460
649
|
emit status_text( "Can't filter on #{indexes[0].field.label}" )
|
@@ -496,6 +685,11 @@ class TableView < Qt::TableView
|
|
496
685
|
# create a new index and move to it
|
497
686
|
unless found_row.nil?
|
498
687
|
self.current_index = model.create_index( found_row, save_index.column )
|
688
|
+
if self.filtered?
|
689
|
+
emit status_text( "Filtered on #{current_index.field_name} = #{current_index.gui_value}" )
|
690
|
+
else
|
691
|
+
emit status_text( nil )
|
692
|
+
end
|
499
693
|
end
|
500
694
|
end
|
501
695
|
|
@@ -521,6 +715,7 @@ class TableView < Qt::TableView
|
|
521
715
|
puts "itemDelegateForColumn #{column}"
|
522
716
|
super
|
523
717
|
end
|
718
|
+
|
524
719
|
end
|
525
720
|
|
526
721
|
end
|