clevic 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # whether the model is currently filtered
15
+ # the current filter command
14
16
  # TODO better in QAbstractSortFilter?
15
17
  attr_accessor :filtered
16
- def filtered?; self.filtered; end
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
- # model_builder_record is:
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
- @index_override = false
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
- model.fields.each_with_index {|x,i| return i if x.attribute == field }
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
- row_ary << index.gui_value if index.row == row
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[0]
168
- selected_index = selection_model.selected_indexes[0]
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
- return true if selection_range.height != arr.size
180
- return true if selection_range.width != arr[0].size
181
-
182
- # size is the same, so do the paste
183
- paste_to_index( selected_index, arr )
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
- emit model.dataChanged( current_index, current_index )
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
- emit model.dataChanged( current_index, current_index )
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
- emit model.dataChanged( current_index, current_index )
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
- emit model.dataChanged( current_index, current_index )
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
- model.reload_data
358
+ restore_entity do
359
+ model.reload_data
360
+ end
333
361
  end
334
362
  end
335
363
 
336
- # toggle the filter, based on current selection.
337
- def filter_by_current( bool_filter )
338
- # TODO if there's no selection, use the current index instead
339
- filter_by_indexes( selection_model.selected_indexes )
340
- emit filter_status( bool_filter )
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
- bottom_right_index = top_left_index.choppy {|i| i.row += csv_arr.size - 1; i.column += csv_arr[0].size - 1 }
472
- emit model.dataChanged( top_left_index, bottom_right_index )
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 emit dataChanged
547
+ # deletes were done, so call data_changed
512
548
  if cells_deleted
513
549
  # save affected rows
514
- selection_model.selected_rows.each { |index| index.entity.save }
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
- emit dataChanged( selection_range.top_left, selection_range.bottom_right )
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
- @index_override = false
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
- # this is to allow entity model UI handlers to tell the view
614
- # where to move the current editing index to. If it's left blank
615
- # default is based on the editing hint.
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
- set_current_index( model_index )
619
- @index_override = true
655
+ @next_index = model_index
620
656
  end
621
657
 
622
- # call set_current_index with model_index unless override is true.
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
- if !@index_override
625
- # move to next cell
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
- # If self.filter is false, use the data in the indexes to filter the data set;
682
- # otherwise turn filtering off.
683
- # Sets self.filter to true if filtering worked, false otherwise.
684
- # indexes is a collection of Qt::ModelIndex
685
- # TODO combine with filter_by_current
686
- def filter_by_indexes( indexes )
687
- unless indexes[0].field.filterable?
688
- emit status_text( "Can't filter on #{indexes[0].field.label}" )
689
- return
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
- save_entity = current_index.entity
693
- save_entity.save if save_entity.changed?
694
- save_index = current_index
736
+ retval = yield
695
737
 
696
- unless self.filtered
697
- # filter by current selection
698
- # TODO handle a multiple-selection
699
- if indexes.empty?
700
- self.filtered = false
701
- elsif indexes.size > 1
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
- if indexes[0].entity.new_record?
767
+ when indexes.first.entity.new_record?
707
768
  emit status_text( "Can't filter on a new row" )
708
- self.filtered = false
709
- return
769
+
710
770
  else
711
- model.reload_data( :conditions => { indexes[0].field_name => indexes[0].field_value } )
712
- self.filtered = true
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
- else
715
- # unfilter
716
- model.reload_data( :conditions => {} )
717
- self.filtered = false
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( save_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
- self.current_index = model.create_index( found_row, save_index.column )
728
- if self.filtered?
729
- emit status_text( "Filtered on #{current_index.field.label} = #{current_index.gui_value}" )
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[0].row}" )
815
+ emit status_text( "Found #{search_criteria.search_text} at row #{indexes.first.row}" )
747
816
  selection_model.clear
748
- self.current_index = indexes[0]
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
- def itemDelegateForColumn( column )
755
- puts "itemDelegateForColumn #{column}"
756
- super
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