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.
@@ -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