clevic 0.12.0 → 0.13.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +209 -30
  3. data/README.txt +16 -20
  4. data/Rakefile +8 -8
  5. data/TODO +6 -7
  6. data/bin/clevic +12 -73
  7. data/lib/clevic/action_builder.rb +168 -0
  8. data/lib/clevic/ar_methods.rb +120 -0
  9. data/lib/clevic/attribute_list.rb +56 -0
  10. data/lib/clevic/cache_table.rb +60 -37
  11. data/lib/clevic/default_view.rb +3 -16
  12. data/lib/clevic/delegate.rb +46 -0
  13. data/lib/clevic/emitter.rb +38 -0
  14. data/lib/clevic/extensions.rb +61 -114
  15. data/lib/clevic/field.rb +159 -228
  16. data/lib/clevic/field_valuer.rb +165 -0
  17. data/lib/clevic/filter_command.rb +2 -6
  18. data/lib/clevic/generic_format.rb +52 -0
  19. data/lib/clevic/{ui → icons}/icon.png +0 -0
  20. data/lib/clevic/many_field.rb +7 -0
  21. data/lib/clevic/model_builder.rb +234 -146
  22. data/lib/clevic/model_column.rb +61 -13
  23. data/lib/clevic/order_attribute.rb +10 -0
  24. data/lib/clevic/qt.rb +35 -0
  25. data/lib/clevic/qt/action_builder.rb +47 -0
  26. data/lib/clevic/qt/boolean_delegate.rb +8 -0
  27. data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
  28. data/lib/clevic/qt/clipboard.rb +35 -0
  29. data/lib/clevic/qt/combo_delegate.rb +198 -0
  30. data/lib/clevic/qt/delegates.rb +1 -0
  31. data/lib/clevic/qt/distinct_delegate.rb +35 -0
  32. data/lib/clevic/qt/extensions.rb +52 -0
  33. data/lib/clevic/qt/field.rb +18 -0
  34. data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
  35. data/lib/clevic/qt/relational_delegate.rb +87 -0
  36. data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
  37. data/lib/clevic/qt/set_delegate.rb +44 -0
  38. data/lib/clevic/qt/table_model.rb +331 -0
  39. data/lib/clevic/qt/table_view.rb +344 -0
  40. data/lib/clevic/qt/text_area_delegate.rb +8 -0
  41. data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
  42. data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
  43. data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
  44. data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
  45. data/lib/clevic/rails_models_loaders.rb +56 -0
  46. data/lib/clevic/record.rb +2 -17
  47. data/lib/clevic/sampler.rb +81 -0
  48. data/lib/clevic/sequel_ar_adapter.rb +215 -0
  49. data/lib/clevic/sequel_length_validation.rb +23 -0
  50. data/lib/clevic/sequel_meta.rb +65 -0
  51. data/lib/clevic/sequel_naked.rb +30 -0
  52. data/lib/clevic/swing.rb +38 -0
  53. data/lib/clevic/swing/action.rb +125 -0
  54. data/lib/clevic/swing/action_builder.rb +47 -0
  55. data/lib/clevic/swing/boolean_delegate.rb +26 -0
  56. data/lib/clevic/swing/browser.rb +282 -0
  57. data/lib/clevic/swing/cell_editor.rb +95 -0
  58. data/lib/clevic/swing/cell_renderer.rb +44 -0
  59. data/lib/clevic/swing/clipboard.rb +135 -0
  60. data/lib/clevic/swing/combo_delegate.rb +336 -0
  61. data/lib/clevic/swing/confirm_dialog.rb +57 -0
  62. data/lib/clevic/swing/delegate.rb +40 -0
  63. data/lib/clevic/swing/distinct_delegate.rb +30 -0
  64. data/lib/clevic/swing/extensions.rb +274 -0
  65. data/lib/clevic/swing/field.rb +35 -0
  66. data/lib/clevic/swing/relational_delegate.rb +48 -0
  67. data/lib/clevic/swing/row_header.rb +210 -0
  68. data/lib/clevic/swing/search_dialog.rb +230 -0
  69. data/lib/clevic/swing/selection_model.rb +90 -0
  70. data/lib/clevic/swing/set_delegate.rb +41 -0
  71. data/lib/clevic/swing/swing_table_index.rb +43 -0
  72. data/lib/clevic/swing/table_model.rb +200 -0
  73. data/lib/clevic/swing/table_view.rb +385 -0
  74. data/lib/clevic/swing/table_view_focus.rb +47 -0
  75. data/lib/clevic/swing/tag_delegate.rb +127 -0
  76. data/lib/clevic/swing/tag_editor.rb +101 -0
  77. data/lib/clevic/swing/text_area_delegate.rb +46 -0
  78. data/lib/clevic/swing/text_delegate.rb +31 -0
  79. data/lib/clevic/swing/ui/build.xml +74 -0
  80. data/lib/clevic/swing/ui/dist/README.TXT +33 -0
  81. data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
  82. data/lib/clevic/swing/ui/manifest.mf +3 -0
  83. data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
  84. data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
  85. data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
  86. data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
  87. data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
  88. data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
  89. data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
  90. data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
  91. data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
  92. data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
  93. data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
  94. data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
  95. data/lib/clevic/table_index.rb +100 -0
  96. data/lib/clevic/table_model.rb +54 -425
  97. data/lib/clevic/table_searcher.rb +113 -116
  98. data/lib/clevic/table_view.rb +171 -399
  99. data/lib/clevic/table_view_paste.rb +199 -0
  100. data/lib/clevic/version.rb +3 -2
  101. data/lib/clevic/view.rb +94 -43
  102. data/models/accounts_models.rb +13 -13
  103. data/models/minimal_models.rb +5 -9
  104. data/models/times_models.rb +19 -14
  105. data/models/times_psql_models.rb +10 -0
  106. data/models/times_sqlite_models.rb +1 -8
  107. data/models/values_models.rb +2 -8
  108. data/tasks/clevic.rake +1 -1
  109. data/tasks/rdoc.rake +1 -5
  110. data/tasks/website.rake +1 -1
  111. data/test/test_cache_table.rb +15 -29
  112. data/test/test_helper.rb +14 -83
  113. data/test/test_order_attribute.rb +1 -1
  114. data/test/test_table_model.rb +0 -21
  115. data/test/test_table_searcher.rb +67 -61
  116. metadata +262 -78
  117. data/lib/clevic.rb +0 -4
  118. data/lib/clevic/db_options.rb +0 -112
  119. data/lib/clevic/delegates.rb +0 -386
@@ -0,0 +1,199 @@
1
+ # This has all the code for handling pasting
2
+ require 'hpricot'
3
+
4
+ module Clevic
5
+
6
+ class PasteError < RuntimeError
7
+ end
8
+
9
+ class TableView
10
+ # get something from the clipboard and put it at the current selection
11
+ # intended to be called by action / keyboard / menu handlers
12
+ def paste
13
+ busy_cursor do
14
+ sanity_check_read_only
15
+
16
+ # Try text/html then text/plain as tsv or csv
17
+ # LATER maybe use the java-native-application at some point for
18
+ # cut'n'paste internally?
19
+ case
20
+ when clipboard.html?
21
+ paste_html
22
+ when clipboard.text?
23
+ paste_text
24
+ else
25
+ raise PasteError, "clipboard has neither text nor html, so can't paste"
26
+ end
27
+ end
28
+ rescue PasteError => e
29
+ show_error e.message
30
+ end
31
+
32
+ # Paste suitable html to the selection
33
+ # Check for presence of tr tags, and make sure there are no colspan or rowspan attributes
34
+ # on td tags.
35
+ def paste_html
36
+ emit_status_text "Fetching data."
37
+ html = clipboard.html
38
+
39
+ # This should really be factored out somewhere and tested thoroughly
40
+ emit_status_text "Analysing data."
41
+ doc =
42
+ if html.is_a? Hpricot::Doc
43
+ html
44
+ else
45
+ Hpricot.parse( html )
46
+ end
47
+
48
+ # call the plain text paste if we don't have tabular data
49
+ if doc.search( "//tr" ).size == 0
50
+ paste_text
51
+ else
52
+ # throw exception if there are [col|row]span > 1
53
+ spans = doc.search( "//td[@rowspan > 1 || @colspan > 1]" )
54
+ if spans.size > 0
55
+ # make an itemised list of
56
+ cell_list = spans.map{|x| "- #{x.inner_text}"}.join("\n")
57
+ raise PasteError, <<-EOF
58
+ Pasting will not work because source contains spanning cells.
59
+ If the source is a spreadsheet, you probably have merged cells
60
+ somewhere. Split them, and try copy and paste again.
61
+ Cells contain
62
+ #{cell_list}
63
+ EOF
64
+ end
65
+
66
+ # run through the tabular data and convert to simple array
67
+ emit_status_text "Pasting data."
68
+ ary = ( doc / :tr ).map do |row|
69
+ ( row / :td ).map do |cell|
70
+ # trim leading and trailing \r\n\t
71
+
72
+ # check for br
73
+ unless cell.search( '//br' ).empty?
74
+ # treat br as separate lines
75
+ cell.search('//text()').map( &:to_s ).join("\n")
76
+ else
77
+ # otherwise just join text elements
78
+ cell.search( '//text()' ).join('')
79
+ end.gsub( /^[\r\n\t]*/, '').gsub( /[\r\n\t]*$/, '')
80
+ end
81
+ end
82
+
83
+ paste_array ary
84
+ end
85
+ end
86
+
87
+ # LATER probably need a PasteParser or something, to figure
88
+ # out if a file is tsv or csv
89
+ # Try tsv first, because number formats often have embedded ','.
90
+ # if tsv doesn't work, try with csv and test for rectangularness
91
+ # otherwise assume it's one string.
92
+ # TODO could also heuristically check paste selection area
93
+ def paste_text
94
+ text = clipboard.text
95
+
96
+ case text
97
+ when /\t/
98
+ paste_array( FasterCSV.parse( text, :col_sep => "\t" ) )
99
+ # assume multi-line text, or text with commas, is csv
100
+ when /[,\n]/
101
+ paste_array( FasterCSV.parse( text, :col_sep => ',' ) )
102
+ else
103
+ paste_value_to_selection( text )
104
+ end
105
+ end
106
+
107
+ # Paste array to either a single selection or a matching multiple selection
108
+ # TODO Check for rectangularness, ie csv_arr.map{|row| row.size}.uniq.size == 1
109
+ def paste_array( arr )
110
+ if selection_model.single_cell?
111
+ # only one cell selected, so paste like a spreadsheet
112
+ selected_index = selection_model.selected_indexes.first
113
+ if arr.size == 0 or ( arr.size == 1 and arr.first.size == 0 )
114
+ # empty array, so just clear the current selection
115
+ selected_index.attribute_value = nil
116
+ else
117
+ paste_to_index( selected_index, arr )
118
+ end
119
+ else
120
+ if arr.size == 1 && arr.first.size == 1
121
+ # single value to multiple selection
122
+ paste_value_to_selection arr.first.first
123
+ else
124
+ if selection_model.ranges.size != 1
125
+ raise PasteError, "Can't paste tabular data to multiple selection."
126
+ end
127
+
128
+ if selection_model.ranges.first.height != arr.size
129
+ raise PasteError, "Height of paste area (#{selection_model.ranges.first.height}) doesn't match height of data (#{arr.size})."
130
+ end
131
+
132
+ if selection_model.ranges.first.width != arr.first.size
133
+ raise PasteError, "Width of paste area (#{selection_model.ranges.first.width}) doesn't match width of data (#{arr.first.size})."
134
+ end
135
+
136
+ # size is the same, so do the paste
137
+ paste_to_index( selected_index, arr )
138
+ end
139
+ end
140
+ end
141
+
142
+ # set all indexes in the selection to the value
143
+ def paste_value_to_selection( value )
144
+ selection_model.selected_indexes.each do |index|
145
+ index.text_value = value
146
+ # save records to db via view, so we get error messages
147
+ save_row( index )
148
+ end
149
+
150
+ # notify of changed data
151
+ model.data_changed do |change|
152
+ sorted = selection_model.selected_indexes.sort
153
+ change.top_left = sorted.first
154
+ change.bottom_right = sorted.last
155
+ end
156
+ end
157
+
158
+ # Paste an array to the index, replacing whatever is at that index
159
+ # and whatever is at other indices matching the size of the pasted
160
+ # csv array. Create new rows if there aren't enough.
161
+ def paste_to_index( top_left_index, csv_arr )
162
+ csv_arr_size = csv_arr.size
163
+ csv_arr.each_with_index do |row,row_index|
164
+ # append row if we need one
165
+ model.add_new_item if top_left_index.row + row_index >= model.row_count
166
+
167
+ row.each_with_index do |field, field_index|
168
+ unless top_left_index.column + field_index >= model.column_count
169
+ # do paste
170
+ cell_index = top_left_index.choppy {|i| i.row += row_index; i.column += field_index }
171
+ emit_status_text( "pasted #{row_index+1} of #{csv_arr_size}")
172
+ begin
173
+ cell_index.text_value = field
174
+ rescue
175
+ puts $!.message
176
+ puts $!.backtrace
177
+ show_error( $!.message )
178
+ end
179
+ else
180
+ emit_status_text( "#{pluralize( top_left_index.column + field_index, 'column' )} for pasting data is too large. Truncating." )
181
+ end
182
+ end
183
+ # save records to db via view, so we get error messages
184
+ save_row( top_left_index.choppy {|i| i.row += row_index; i.column = 0 } )
185
+ end
186
+
187
+ # make the gui refresh
188
+ model.data_changed do |change|
189
+ change.top_left = top_left_index
190
+ change.bottom_right = top_left_index.choppy do |i|
191
+ i.row += csv_arr.size - 1
192
+ i.column += csv_arr.first.size - 1
193
+ end
194
+ end
195
+ end
196
+
197
+ end
198
+
199
+ end
@@ -1,9 +1,10 @@
1
1
  module Clevic #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 12
4
+ MINOR = 13
5
5
  TINY = 0
6
+ PRE = 'b1'
6
7
 
7
- STRING = [MAJOR, MINOR, TINY].join('.')
8
+ STRING = [MAJOR, MINOR, TINY, PRE].join('.')
8
9
  end
9
10
  end
data/lib/clevic/view.rb CHANGED
@@ -5,79 +5,130 @@ module Clevic
5
5
  # This contains the definition of a particular view of an entity.
6
6
  # See Clevic::ModelBuilder.
7
7
  class View
8
- @order = []
9
- def self.order
10
- @order
11
- end
12
8
 
13
- # Handle situations where the array passed to
14
- # Clevic::View.order = has ActiveRecord::Base
15
- # objects in it. In other words, if there is one, pass back it's
16
- # default view class rather than the ActiveRecord::Base subclass.
17
- def self.order=( array )
18
- @order = array.map do |x|
19
- if x.ancestors.include?( ActiveRecord::Base )
20
- x.default_view_class
9
+ class << self
10
+ def define_ui_block( &block )
11
+ @define_ui_block ||= block
12
+ end
13
+
14
+ def order
15
+ @order ||= []
16
+ end
17
+
18
+ # sometimes order has duplicates. So this is all unique
19
+ # defined views in order of definition, or as specified.
20
+ def views
21
+ order.uniq
22
+ end
23
+
24
+ def []( view_name )
25
+ order.find do |view|
26
+ view.name =~ /#{view_name.to_s}/i
27
+ end
28
+ end
29
+
30
+ # Handle situations where the array passed to
31
+ # Clevic::View.order has entity_class
32
+ # objects in it. In other words, if there is one, pass back it's
33
+ # default view class rather than the entity_class
34
+ def order=( array )
35
+ @order = array.map do |x|
36
+ if x.ancestors.include?( Clevic.base_entity_class )
37
+ x.default_view_class
38
+ else
39
+ x
40
+ end
41
+ end
42
+ end
43
+
44
+ def entity_class( *args )
45
+ if args.size == 0
46
+ @entity_class || raise( "entity_class not specified for #{name}" )
21
47
  else
22
- x
48
+ self.entity_class = args.first
23
49
  end
24
50
  end
25
- end
26
-
27
- def self.entity_class( *args )
28
- if args.size == 0
29
- @entity_class || raise( "entity_class not specified for #{name}" )
30
- else
31
- @entity_class = args.first
51
+
52
+ def entity_class=( some_class )
53
+ @entity_class = some_class
54
+ end
55
+
56
+ def widget_name( *args )
57
+ if args.size == 0
58
+ # the class name by default
59
+ @widget_name || name
60
+ else
61
+ @widget_name = args.first
62
+ end
32
63
  end
33
64
  end
34
65
 
35
- def self.entity_class=( some_class )
36
- @entity_class = some_class
66
+ # args can be anything that has a writer method. Often this
67
+ # will be entity_class
68
+ # block contains the ModelBuilder DSL
69
+ def initialize( args = {}, &block )
70
+ @define_ui_block = block
71
+ unless args.nil?
72
+ args.each do |key,value|
73
+ self.send( "#{key}=", value )
74
+ end
75
+ end
37
76
  end
38
77
 
39
- def self.widget_name( *args )
40
- if args.size == 0
41
- # the class name by default
42
- @widget_name || name
43
- else
44
- @widget_name = args.first
45
- end
78
+ # use block from constructor, or class ui block from eg Clevic::Record
79
+ def define_ui_block
80
+ @define_ui_block || self.class.define_ui_block
46
81
  end
47
82
 
48
83
  # For descendants to override easily
49
84
  def entity_class
50
- self.class.entity_class
85
+ @entity_class || self.class.entity_class
51
86
  end
87
+ attr_writer :entity_class
52
88
 
53
89
  # The title to display, eg in a tab
54
90
  def title
55
- self.class.name
91
+ @title || self.class.name
56
92
  end
93
+ attr_writer :title
57
94
 
58
- # used for the Qt object_name when ModelBuilder is constructing a widget
59
- # here so it can be overridden by descendants
95
+ def fields
96
+ @fields ||= define_ui.fields
97
+ end
98
+
99
+ # used by the framework-specific code to name widgets
60
100
  def widget_name
61
- self.class.widget_name
101
+ @widget_name || self.class.widget_name
62
102
  end
63
103
 
64
- def model_builder( &block )
65
- @model_builder ||= ModelBuilder.new( self )
66
- @model_builder.exec_ui_block( &block )
104
+ def model_builder( value = nil, &block )
105
+ if value.nil?
106
+ @model_builder ||= ModelBuilder.new( self )
107
+ @model_builder.exec_ui_block( &block )
108
+ else
109
+ @model_builder
110
+ end
67
111
  end
112
+ attr_writer :model_builder
68
113
 
69
114
  # return a default UI constructed from model metadata
70
115
  def define_ui
71
- model_builder do
72
- default_ui
116
+ if define_ui_block.nil?
117
+ # use the define_ui from Clevic::View to build a default UI
118
+ model_builder do
119
+ default_ui
120
+ end
121
+ else
122
+ # use the provided block
123
+ model_builder( &define_ui_block )
73
124
  end
74
125
  end
75
126
 
76
- # define view/model specific actions
127
+ # callback for view/model specific actions
77
128
  def define_actions( table_view, action_builder )
78
129
  end
79
130
 
80
- # notify
131
+ # callback for notify
81
132
  def notify_field( table_view, model_index )
82
133
  ndc = model_index.field.notify_data_changed
83
134
  case ndc
@@ -91,7 +142,7 @@ module Clevic
91
142
 
92
143
  # Define data changed events. Default is to call notify_data_changed
93
144
  # for each field in the rectangular area defined by top_left and bottom_right
94
- # (which are Qt::ModelIndex instances)
145
+ # (which are include Clevic::TableIndex)
95
146
  def notify_data_changed( table_view, top_left, bottom_right )
96
147
  if top_left == bottom_right
97
148
  # shortcut to just the one, seeing as it's probably the most common
@@ -107,7 +158,7 @@ module Clevic
107
158
  end
108
159
  end
109
160
 
110
- # be notified of key presses
161
+ # callback for key presses
111
162
  def notify_key_press( table_view, key_press_event, current_model_index )
112
163
  end
113
164
 
@@ -1,18 +1,17 @@
1
1
  require 'clevic.rb'
2
2
 
3
- # db connection
4
- Clevic::DbOptions.connect( $options ) do
5
- # use a different db for testing, so real data doesn't get broken.
6
- if options[:database].nil? || options[:database].empty?
7
- database( debug? ? :accounts_test : :accounts )
8
- else
9
- database options[:database]
10
- end
11
- adapter :postgresql
12
- username options[:username].blank? ? 'accounts' : options[:username]
3
+ host = ENV['PGHOST'] || 'localhost'
4
+ $options ||= {}
5
+
6
+ if respond_to?( :'jruby?' ) && jruby?
7
+ constring = "jdbc:postgresql://#{host}/accounts_test?user=#{$options[:username] || 'accounts'}&password=general"
8
+ puts "constring: #{constring.inspect}"
9
+ Sequel.connect( constring )
10
+ else
11
+ Sequel.connect( "postgres://#{host}/accounts_test?user=#{$options[:username] || 'accounts'}&password=general" )
13
12
  end
14
13
 
15
- class Entry < ActiveRecord::Base
14
+ class Entry < Sequel::Model
16
15
  belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
17
16
  belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
18
17
 
@@ -33,6 +32,7 @@ class Entry < ActiveRecord::Base
33
32
  end
34
33
  end
35
34
  end
35
+ distinct :supplier
36
36
  relational :debit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
37
37
  relational :credit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
38
38
  plain :amount, :sample => 999999.99
@@ -49,7 +49,7 @@ class Entry < ActiveRecord::Base
49
49
  def self.update_from_description( current_index )
50
50
  return if current_index.attribute_value.nil?
51
51
  # most recent entry, ordered in reverse
52
- similar = self.find(
52
+ similar = self.adaptor.find(
53
53
  :first,
54
54
  :conditions => ["#{current_index.attribute} = ?", current_index.attribute_value],
55
55
  :order => 'date desc'
@@ -69,7 +69,7 @@ class Entry < ActiveRecord::Base
69
69
  end
70
70
  end
71
71
 
72
- class Account < ActiveRecord::Base
72
+ class Account < Sequel::Model
73
73
  has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
74
74
  has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
75
75