clevic 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +21 -0
- data/Manifest.txt +4 -2
- data/Rakefile +32 -139
- data/TODO +12 -15
- data/bin/clevic +5 -0
- data/lib/clevic/browser.rb +1 -1
- data/lib/clevic/cache_table.rb +15 -34
- data/lib/clevic/default_view.rb +6 -0
- data/lib/clevic/delegates.rb +25 -19
- data/lib/clevic/extensions.rb +1 -1
- data/lib/clevic/field.rb +55 -5
- data/lib/clevic/filter_command.rb +51 -0
- data/lib/clevic/model_builder.rb +40 -23
- data/lib/clevic/table_model.rb +113 -8
- data/lib/clevic/table_view.rb +207 -92
- data/lib/clevic/text_delegate.rb +84 -0
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +28 -2
- data/models/accounts_models.rb +34 -40
- data/models/times_models.rb +69 -33
- data/tasks/clevic.rake +111 -0
- data/tasks/rdoc.rake +16 -0
- data/test/test_cache_table.rb +0 -23
- data/test/test_table_model.rb +61 -0
- data/test/test_table_searcher.rb +1 -1
- metadata +45 -10
- data/config/hoe.rb +0 -82
- data/config/requirements.rb +0 -15
data/lib/clevic/table_view.rb
CHANGED
@@ -1,27 +1,28 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'Qt4'
|
3
3
|
require 'fastercsv'
|
4
|
-
require 'clevic/model_builder.rb'
|
5
4
|
require 'qtext/action_builder.rb'
|
6
5
|
|
6
|
+
require 'clevic/model_builder.rb'
|
7
|
+
require 'clevic/filter_command.rb'
|
8
|
+
|
7
9
|
module Clevic
|
8
10
|
|
9
11
|
# The view class
|
10
12
|
class TableView < Qt::TableView
|
11
13
|
include ActionBuilder
|
12
14
|
|
13
|
-
#
|
15
|
+
# the current filter command
|
14
16
|
# TODO better in QAbstractSortFilter?
|
15
17
|
attr_accessor :filtered
|
16
|
-
def filtered?;
|
18
|
+
def filtered?; !@filtered.nil?; end
|
17
19
|
|
18
20
|
# status_text is emitted when this object was to display something in the status bar
|
19
21
|
# error_test is emitted when an error of some kind must be displayed to the user.
|
20
22
|
# filter_status is emitted when the filtering changes. Param is true for filtered, false for not filtered.
|
21
23
|
signals 'status_text(QString)', 'filter_status(bool)'
|
22
24
|
|
23
|
-
#
|
24
|
-
# - an ActiveRecord::Base subclass that includes Clevic::Record
|
25
|
+
# arg is:
|
25
26
|
# - an instance of Clevic::View
|
26
27
|
# - an instance of TableModel
|
27
28
|
def initialize( arg, parent = nil, &block )
|
@@ -51,7 +52,7 @@ class TableView < Qt::TableView
|
|
51
52
|
end
|
52
53
|
|
53
54
|
# see closeEditor
|
54
|
-
@
|
55
|
+
@next_index = nil
|
55
56
|
|
56
57
|
# set some Qt things
|
57
58
|
self.horizontal_header.movable = false
|
@@ -59,7 +60,6 @@ class TableView < Qt::TableView
|
|
59
60
|
# but need to change the shortcut ideas of next and previous rows
|
60
61
|
self.vertical_header.movable = false
|
61
62
|
self.sorting_enabled = false
|
62
|
-
@filtered = false
|
63
63
|
|
64
64
|
self.context_menu_policy = Qt::ActionsContextMenu
|
65
65
|
end
|
@@ -74,9 +74,9 @@ class TableView < Qt::TableView
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
-
# find the row index for the given field
|
77
|
+
# find the row index for the given field id (symbol)
|
78
78
|
def field_column( field )
|
79
|
-
|
79
|
+
raise "use model.field_column( field )"
|
80
80
|
end
|
81
81
|
|
82
82
|
# return menu actions for the model, or an empty array if there aren't any
|
@@ -104,12 +104,13 @@ class TableView < Qt::TableView
|
|
104
104
|
# add model actions, if they're defined
|
105
105
|
list( :model ) do |ab|
|
106
106
|
entity_view.define_actions( self, ab )
|
107
|
+
separator
|
107
108
|
end
|
108
|
-
separator
|
109
109
|
|
110
110
|
# list of actions in the edit menu
|
111
111
|
list( :edit ) do
|
112
112
|
#~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
|
113
|
+
action :action_copy, '&Save', :shortcut => Qt::KeySequence::Save, :method => :save_current_row
|
113
114
|
action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
|
114
115
|
action :action_paste, '&Paste', :shortcut => Qt::KeySequence::Paste, :method => :paste
|
115
116
|
separator
|
@@ -147,7 +148,13 @@ class TableView < Qt::TableView
|
|
147
148
|
(selection_range.top..selection_range.bottom).each do |row|
|
148
149
|
row_ary = Array.new
|
149
150
|
selection_model.selected_indexes.each do |index|
|
150
|
-
|
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
|
151
158
|
end
|
152
159
|
text << row_ary.to_csv
|
153
160
|
end
|
@@ -162,12 +169,13 @@ class TableView < Qt::TableView
|
|
162
169
|
text = Qt::Application::clipboard.text.chomp
|
163
170
|
arr = FasterCSV.parse( text )
|
164
171
|
|
172
|
+
selection_model.selected_indexes.
|
165
173
|
return true if selection_model.selection.size != 1
|
166
174
|
|
167
|
-
selection_range = selection_model.selection
|
168
|
-
selected_index = selection_model.selected_indexes
|
175
|
+
selection_range = selection_model.selection.first
|
176
|
+
selected_index = selection_model.selected_indexes.first
|
169
177
|
|
170
|
-
if selection_range.single_cell?
|
178
|
+
if selection_model.selection.size == 1 && selection_range.single_cell?
|
171
179
|
# only one cell selected, so paste like a spreadsheet
|
172
180
|
if text.empty?
|
173
181
|
# just clear the current selection
|
@@ -176,11 +184,29 @@ class TableView < Qt::TableView
|
|
176
184
|
paste_to_index( selected_index, arr )
|
177
185
|
end
|
178
186
|
else
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
184
210
|
end
|
185
211
|
end
|
186
212
|
|
@@ -217,7 +243,7 @@ class TableView < Qt::TableView
|
|
217
243
|
previous_value = one_up_index.attribute_value
|
218
244
|
if current_index.attribute_value != previous_value
|
219
245
|
current_index.attribute_value = previous_value
|
220
|
-
|
246
|
+
model.data_changed( current_index )
|
221
247
|
end
|
222
248
|
end
|
223
249
|
|
@@ -239,7 +265,7 @@ class TableView < Qt::TableView
|
|
239
265
|
one_up_right = current_index.choppy {|i| i.row -= 1; i.column += 1 }
|
240
266
|
sanity_check_types( one_up_right, current_index )
|
241
267
|
current_index.attribute_value = one_up_right.attribute_value
|
242
|
-
|
268
|
+
model.data_changed( current_index )
|
243
269
|
end
|
244
270
|
end
|
245
271
|
|
@@ -252,14 +278,14 @@ class TableView < Qt::TableView
|
|
252
278
|
one_up_left = current_index.choppy { |i| i.row -= 1; i.column -= 1 }
|
253
279
|
sanity_check_types( one_up_left, current_index )
|
254
280
|
current_index.attribute_value = one_up_left.attribute_value
|
255
|
-
|
281
|
+
model.data_changed( current_index )
|
256
282
|
end
|
257
283
|
end
|
258
284
|
|
259
285
|
def insert_current_date
|
260
286
|
sanity_check_read_only
|
261
287
|
current_index.attribute_value = Time.now
|
262
|
-
|
288
|
+
model.data_changed( current_index )
|
263
289
|
end
|
264
290
|
|
265
291
|
def open_editor
|
@@ -329,15 +355,20 @@ class TableView < Qt::TableView
|
|
329
355
|
# force a complete reload of the current tab's data
|
330
356
|
def refresh
|
331
357
|
override_cursor( Qt::BusyCursor ) do
|
332
|
-
|
358
|
+
restore_entity do
|
359
|
+
model.reload_data
|
360
|
+
end
|
333
361
|
end
|
334
362
|
end
|
335
363
|
|
336
|
-
#
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
364
|
+
# return an array of the current selection, or the
|
365
|
+
# current index in an array if the selection is empty
|
366
|
+
def selection_or_current
|
367
|
+
indexes_or_current( selection_model.selected_indexes )
|
368
|
+
end
|
369
|
+
|
370
|
+
def selected_rows_or_current
|
371
|
+
indexes_or_current( selection_model.row_indexes )
|
341
372
|
end
|
342
373
|
|
343
374
|
# alternative access for auto_size_column
|
@@ -375,6 +406,7 @@ class TableView < Qt::TableView
|
|
375
406
|
style.sizeFromContents( Qt::Style::CT_HeaderSection, opt, size );
|
376
407
|
end
|
377
408
|
|
409
|
+
# TODO is this even used?
|
378
410
|
def relational_delegate( attribute, options )
|
379
411
|
col = model.attributes.index( attribute )
|
380
412
|
delegate = RelationalDelegate.new( self, model.columns[col], options )
|
@@ -408,7 +440,6 @@ class TableView < Qt::TableView
|
|
408
440
|
super
|
409
441
|
|
410
442
|
# set delegates
|
411
|
-
#~ self.item_delegate = Clevic::ItemDelegate.new( self, field )
|
412
443
|
model.fields.each_with_index do |field, index|
|
413
444
|
set_item_delegate_for_column( index, field.delegate )
|
414
445
|
end
|
@@ -468,8 +499,13 @@ class TableView < Qt::TableView
|
|
468
499
|
end
|
469
500
|
|
470
501
|
# make the gui refresh
|
471
|
-
|
472
|
-
|
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
|
473
509
|
emit model.headerDataChanged( Qt::Vertical, top_left_index.row, top_left_index.row + csv_arr.size )
|
474
510
|
end
|
475
511
|
|
@@ -508,14 +544,16 @@ class TableView < Qt::TableView
|
|
508
544
|
cells_deleted = true
|
509
545
|
end
|
510
546
|
|
511
|
-
# deletes were done, so
|
547
|
+
# deletes were done, so call data_changed
|
512
548
|
if cells_deleted
|
513
549
|
# save affected rows
|
514
|
-
selection_model.
|
550
|
+
selection_model.row_indexes.each do |index|
|
551
|
+
index.entity.save
|
552
|
+
end
|
515
553
|
|
516
554
|
# emit data changed for all ranges
|
517
555
|
selection_model.selection.each do |selection_range|
|
518
|
-
|
556
|
+
model.data_changed( selection_range )
|
519
557
|
end
|
520
558
|
end
|
521
559
|
end
|
@@ -603,37 +641,33 @@ class TableView < Qt::TableView
|
|
603
641
|
|
604
642
|
# save record whenever its row is exited
|
605
643
|
def currentChanged( current_index, previous_index )
|
606
|
-
@
|
644
|
+
@next_index = nil
|
607
645
|
if current_index.row != previous_index.row
|
608
646
|
save_row( previous_index )
|
609
647
|
end
|
610
648
|
super
|
611
649
|
end
|
612
650
|
|
613
|
-
#
|
614
|
-
#
|
615
|
-
#
|
616
|
-
# see closeEditor
|
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).
|
617
654
|
def override_next_index( model_index )
|
618
|
-
|
619
|
-
@index_override = true
|
655
|
+
@next_index = model_index
|
620
656
|
end
|
621
657
|
|
622
|
-
#
|
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.
|
623
660
|
def set_current_unless_override( model_index )
|
624
|
-
|
625
|
-
|
626
|
-
# Qt seems to take care of tab wraparound
|
627
|
-
set_current_index( model_index )
|
628
|
-
end
|
629
|
-
@index_override = false
|
661
|
+
set_current_index( @next_index || model_index )
|
662
|
+
@next_index = nil
|
630
663
|
end
|
631
664
|
|
632
665
|
# work around situation where an ItemDelegate is open
|
633
666
|
# when the surrouding tab is changed, but the right events
|
634
667
|
# don't arrive.
|
635
668
|
def hideEvent( event )
|
636
|
-
super
|
669
|
+
# can't call super here, for some reason. Qt binding says method not found.
|
670
|
+
# super
|
637
671
|
@hiding = true
|
638
672
|
end
|
639
673
|
|
@@ -677,59 +711,94 @@ class TableView < Qt::TableView
|
|
677
711
|
super
|
678
712
|
end
|
679
713
|
end
|
680
|
-
|
681
|
-
#
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
def
|
687
|
-
|
688
|
-
|
689
|
-
|
714
|
+
|
715
|
+
# toggle the filter, based on current selection.
|
716
|
+
def filter_by_current( bool_filter )
|
717
|
+
filter_by_indexes( selection_or_current )
|
718
|
+
end
|
719
|
+
|
720
|
+
def filter_by_options( args )
|
721
|
+
filtered.undo if filtered?
|
722
|
+
self.filtered = FilterCommand.new( self, [], args )
|
723
|
+
emit filter_status( filtered.doit )
|
724
|
+
end
|
725
|
+
|
726
|
+
# Save the current entity, do something, then restore
|
727
|
+
# the cursor position to the entity if possible.
|
728
|
+
# Return the result of the block.
|
729
|
+
def restore_entity( &block )
|
730
|
+
save_entity = current_index.entity
|
731
|
+
unless save_entity.nil?
|
732
|
+
save_entity.save if save_entity.changed?
|
733
|
+
save_index = current_index
|
690
734
|
end
|
691
735
|
|
692
|
-
|
693
|
-
save_entity.save if save_entity.changed?
|
694
|
-
save_index = current_index
|
736
|
+
retval = yield
|
695
737
|
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
738
|
+
# find the entity if possible
|
739
|
+
select_entity( save_entity, save_index.column ) unless save_entity.nil?
|
740
|
+
|
741
|
+
retval
|
742
|
+
end
|
743
|
+
|
744
|
+
# Filter by the value in the current index.
|
745
|
+
# indexes is a collection of Qt::ModelIndex
|
746
|
+
def filter_by_indexes( indexes )
|
747
|
+
case
|
748
|
+
when filtered?
|
749
|
+
# unfilter
|
750
|
+
restore_entity do
|
751
|
+
filtered.undo
|
752
|
+
self.filtered = nil
|
753
|
+
# update status bar
|
754
|
+
emit status_text( nil )
|
755
|
+
emit filter_status( false )
|
756
|
+
end
|
757
|
+
|
758
|
+
when indexes.empty?
|
759
|
+
emit status_text( "No field selected for filter" )
|
760
|
+
|
761
|
+
when !indexes.first.field.filterable?
|
762
|
+
emit status_text( "Can't filter on #{indexes.first.field.label}" )
|
763
|
+
|
764
|
+
when indexes.size > 1
|
702
765
|
emit status_text( "Can't do multiple selection filters yet" )
|
703
|
-
self.filtered = false
|
704
|
-
end
|
705
766
|
|
706
|
-
|
767
|
+
when indexes.first.entity.new_record?
|
707
768
|
emit status_text( "Can't filter on a new row" )
|
708
|
-
|
709
|
-
return
|
769
|
+
|
710
770
|
else
|
711
|
-
|
712
|
-
|
771
|
+
self.filtered = FilterCommand.new( self, indexes, :conditions => { indexes.first.field_name => indexes.first.field_value } )
|
772
|
+
# try to end up on the same entity, even after the filter
|
773
|
+
restore_entity do
|
774
|
+
emit filter_status( filtered.doit )
|
775
|
+
end
|
776
|
+
# update status bar
|
777
|
+
emit status_text( filtered.status_message )
|
713
778
|
end
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
779
|
+
filtered?
|
780
|
+
end
|
781
|
+
|
782
|
+
# Move to the row for the given entity and the given column.
|
783
|
+
# If column is a symbol,
|
784
|
+
# field_column will be called to find the integer index.
|
785
|
+
def select_entity( entity, column = nil )
|
786
|
+
# sanity check that the entity can actually be found
|
787
|
+
Kernel.raise "entity is nil" if entity.nil?
|
788
|
+
unless entity.class == model.entity_class
|
789
|
+
Kernel.raise "entity #{entity.class.name} does not match class #{model.entity_class.name}"
|
718
790
|
end
|
719
791
|
|
720
792
|
# find the row for the saved entity
|
721
793
|
found_row = override_cursor( Qt::BusyCursor ) do
|
722
|
-
model.collection.index_for_entity(
|
794
|
+
model.collection.index_for_entity( entity )
|
723
795
|
end
|
724
796
|
|
725
797
|
# create a new index and move to it
|
726
798
|
unless found_row.nil?
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
else
|
731
|
-
emit status_text( nil )
|
732
|
-
end
|
799
|
+
column = model.field_column( column ) if column.is_a? Symbol
|
800
|
+
selection_model.clear
|
801
|
+
self.current_index = model.create_index( found_row, column || 0 )
|
733
802
|
end
|
734
803
|
end
|
735
804
|
|
@@ -743,17 +812,63 @@ class TableView < Qt::TableView
|
|
743
812
|
def search( search_criteria )
|
744
813
|
indexes = model.search( current_index, search_criteria )
|
745
814
|
if indexes.size > 0
|
746
|
-
emit status_text( "Found #{search_criteria.search_text} at row #{indexes
|
815
|
+
emit status_text( "Found #{search_criteria.search_text} at row #{indexes.first.row}" )
|
747
816
|
selection_model.clear
|
748
|
-
self.current_index = indexes
|
817
|
+
self.current_index = indexes.first
|
749
818
|
else
|
750
819
|
emit status_text( "No match found for #{search_criteria.search_text}" )
|
751
820
|
end
|
752
821
|
end
|
753
822
|
|
754
|
-
|
755
|
-
|
756
|
-
|
823
|
+
# find the TableView instance for the given entity_view
|
824
|
+
# or entity_model. Return nil if no match found.
|
825
|
+
# TODO doesn't really belong here because TableView will not always
|
826
|
+
# be in a TabWidget context.
|
827
|
+
def find_table_view( entity_model_or_view )
|
828
|
+
parent.children.find do |x|
|
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
|
833
|
+
end
|
834
|
+
|
835
|
+
# execute the block with the TableView instance
|
836
|
+
# currently handling the entity_model_or_view.
|
837
|
+
# Don't execute the block if nothing is found.
|
838
|
+
# TODO doesn't really belong here because TableView will not always
|
839
|
+
# be in a TabWidget context.
|
840
|
+
def with_table_view( entity_model_or_view, &block )
|
841
|
+
tv = find_table_view( entity_model_or_view )
|
842
|
+
yield( tv ) unless tv.nil?
|
843
|
+
end
|
844
|
+
|
845
|
+
# make this window visible if it's in a TabWidget
|
846
|
+
# TODO doesn't really belong here because TableView will not always
|
847
|
+
# be in a TabWidget context.
|
848
|
+
def raise
|
849
|
+
# the tab's parent is a StackedWiget, and its parent is TabWidget
|
850
|
+
tab_widget = parent.parent
|
851
|
+
tab_widget.current_widget = self if tab_widget.class == Qt::TabWidget
|
852
|
+
end
|
853
|
+
|
854
|
+
protected
|
855
|
+
|
856
|
+
# return either the set of indexes with all invalid indexes
|
857
|
+
# remove, or the current selection.
|
858
|
+
def indexes_or_current( indexes )
|
859
|
+
retval =
|
860
|
+
if indexes.empty?
|
861
|
+
[ current_index ]
|
862
|
+
else
|
863
|
+
indexes
|
864
|
+
end
|
865
|
+
|
866
|
+
# strip out bad indexes, so other things don't have to check
|
867
|
+
# can't use select because copying indexes causes an abort
|
868
|
+
#~ retval.select{|x| x != nil && x.valid?}
|
869
|
+
retval.reject!{|x| x.nil? || !x.valid?}
|
870
|
+
# retval needed here because reject! returns nil if nothing was rejected
|
871
|
+
retval
|
757
872
|
end
|
758
873
|
|
759
874
|
end
|