clevic 0.11.1 → 0.12.0
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 +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
|