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.
- data/History.txt +10 -0
- data/Manifest.txt +209 -30
- data/README.txt +16 -20
- data/Rakefile +8 -8
- data/TODO +6 -7
- data/bin/clevic +12 -73
- data/lib/clevic/action_builder.rb +168 -0
- data/lib/clevic/ar_methods.rb +120 -0
- data/lib/clevic/attribute_list.rb +56 -0
- data/lib/clevic/cache_table.rb +60 -37
- data/lib/clevic/default_view.rb +3 -16
- data/lib/clevic/delegate.rb +46 -0
- data/lib/clevic/emitter.rb +38 -0
- data/lib/clevic/extensions.rb +61 -114
- data/lib/clevic/field.rb +159 -228
- data/lib/clevic/field_valuer.rb +165 -0
- data/lib/clevic/filter_command.rb +2 -6
- data/lib/clevic/generic_format.rb +52 -0
- data/lib/clevic/{ui → icons}/icon.png +0 -0
- data/lib/clevic/many_field.rb +7 -0
- data/lib/clevic/model_builder.rb +234 -146
- data/lib/clevic/model_column.rb +61 -13
- data/lib/clevic/order_attribute.rb +10 -0
- data/lib/clevic/qt.rb +35 -0
- data/lib/clevic/qt/action_builder.rb +47 -0
- data/lib/clevic/qt/boolean_delegate.rb +8 -0
- data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
- data/lib/clevic/qt/clipboard.rb +35 -0
- data/lib/clevic/qt/combo_delegate.rb +198 -0
- data/lib/clevic/qt/delegates.rb +1 -0
- data/lib/clevic/qt/distinct_delegate.rb +35 -0
- data/lib/clevic/qt/extensions.rb +52 -0
- data/lib/clevic/qt/field.rb +18 -0
- data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
- data/lib/clevic/qt/relational_delegate.rb +87 -0
- data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
- data/lib/clevic/qt/set_delegate.rb +44 -0
- data/lib/clevic/qt/table_model.rb +331 -0
- data/lib/clevic/qt/table_view.rb +344 -0
- data/lib/clevic/qt/text_area_delegate.rb +8 -0
- data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
- data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
- data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
- data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
- data/lib/clevic/rails_models_loaders.rb +56 -0
- data/lib/clevic/record.rb +2 -17
- data/lib/clevic/sampler.rb +81 -0
- data/lib/clevic/sequel_ar_adapter.rb +215 -0
- data/lib/clevic/sequel_length_validation.rb +23 -0
- data/lib/clevic/sequel_meta.rb +65 -0
- data/lib/clevic/sequel_naked.rb +30 -0
- data/lib/clevic/swing.rb +38 -0
- data/lib/clevic/swing/action.rb +125 -0
- data/lib/clevic/swing/action_builder.rb +47 -0
- data/lib/clevic/swing/boolean_delegate.rb +26 -0
- data/lib/clevic/swing/browser.rb +282 -0
- data/lib/clevic/swing/cell_editor.rb +95 -0
- data/lib/clevic/swing/cell_renderer.rb +44 -0
- data/lib/clevic/swing/clipboard.rb +135 -0
- data/lib/clevic/swing/combo_delegate.rb +336 -0
- data/lib/clevic/swing/confirm_dialog.rb +57 -0
- data/lib/clevic/swing/delegate.rb +40 -0
- data/lib/clevic/swing/distinct_delegate.rb +30 -0
- data/lib/clevic/swing/extensions.rb +274 -0
- data/lib/clevic/swing/field.rb +35 -0
- data/lib/clevic/swing/relational_delegate.rb +48 -0
- data/lib/clevic/swing/row_header.rb +210 -0
- data/lib/clevic/swing/search_dialog.rb +230 -0
- data/lib/clevic/swing/selection_model.rb +90 -0
- data/lib/clevic/swing/set_delegate.rb +41 -0
- data/lib/clevic/swing/swing_table_index.rb +43 -0
- data/lib/clevic/swing/table_model.rb +200 -0
- data/lib/clevic/swing/table_view.rb +385 -0
- data/lib/clevic/swing/table_view_focus.rb +47 -0
- data/lib/clevic/swing/tag_delegate.rb +127 -0
- data/lib/clevic/swing/tag_editor.rb +101 -0
- data/lib/clevic/swing/text_area_delegate.rb +46 -0
- data/lib/clevic/swing/text_delegate.rb +31 -0
- data/lib/clevic/swing/ui/build.xml +74 -0
- data/lib/clevic/swing/ui/dist/README.TXT +33 -0
- data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
- data/lib/clevic/swing/ui/manifest.mf +3 -0
- data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
- data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
- data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
- data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
- data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
- data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
- data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
- data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
- data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
- data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
- data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
- data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
- data/lib/clevic/table_index.rb +100 -0
- data/lib/clevic/table_model.rb +54 -425
- data/lib/clevic/table_searcher.rb +113 -116
- data/lib/clevic/table_view.rb +171 -399
- data/lib/clevic/table_view_paste.rb +199 -0
- data/lib/clevic/version.rb +3 -2
- data/lib/clevic/view.rb +94 -43
- data/models/accounts_models.rb +13 -13
- data/models/minimal_models.rb +5 -9
- data/models/times_models.rb +19 -14
- data/models/times_psql_models.rb +10 -0
- data/models/times_sqlite_models.rb +1 -8
- data/models/values_models.rb +2 -8
- data/tasks/clevic.rake +1 -1
- data/tasks/rdoc.rake +1 -5
- data/tasks/website.rake +1 -1
- data/test/test_cache_table.rb +15 -29
- data/test/test_helper.rb +14 -83
- data/test/test_order_attribute.rb +1 -1
- data/test/test_table_model.rb +0 -21
- data/test/test_table_searcher.rb +67 -61
- metadata +262 -78
- data/lib/clevic.rb +0 -4
- data/lib/clevic/db_options.rb +0 -112
- data/lib/clevic/delegates.rb +0 -386
@@ -0,0 +1,108 @@
|
|
1
|
+
/*
|
2
|
+
* To change this template, choose Tools | Templates
|
3
|
+
* and open the template in the editor.
|
4
|
+
*/
|
5
|
+
|
6
|
+
/*
|
7
|
+
* TagEditor.java
|
8
|
+
*
|
9
|
+
* Created on Oct 8, 2010, 11:28:47 AM
|
10
|
+
*/
|
11
|
+
|
12
|
+
/**
|
13
|
+
*
|
14
|
+
* @author panic
|
15
|
+
*/
|
16
|
+
public class TagEditor extends javax.swing.JPanel {
|
17
|
+
|
18
|
+
/** Creates new form TagEditor */
|
19
|
+
public TagEditor() {
|
20
|
+
initComponents();
|
21
|
+
}
|
22
|
+
|
23
|
+
/** This method is called from within the constructor to
|
24
|
+
* initialize the form.
|
25
|
+
* WARNING: Do NOT modify this code. The content of this method is
|
26
|
+
* always regenerated by the Form Editor.
|
27
|
+
*/
|
28
|
+
@SuppressWarnings("unchecked")
|
29
|
+
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
|
30
|
+
private void initComponents() {
|
31
|
+
|
32
|
+
text_field = new javax.swing.JTextField();
|
33
|
+
item_list = new javax.swing.JList();
|
34
|
+
ok_button = new javax.swing.JButton();
|
35
|
+
cancel_button = new javax.swing.JButton();
|
36
|
+
add_button = new javax.swing.JButton();
|
37
|
+
remove_button = new javax.swing.JButton();
|
38
|
+
|
39
|
+
text_field.setName("text_field"); // NOI18N
|
40
|
+
|
41
|
+
item_list.setName("item_list"); // NOI18N
|
42
|
+
|
43
|
+
ok_button.setText("OK"); // NOI18N
|
44
|
+
ok_button.setToolTipText("Accept selection"); // NOI18N
|
45
|
+
ok_button.setName("ok_button"); // NOI18N
|
46
|
+
|
47
|
+
cancel_button.setText("Cancel"); // NOI18N
|
48
|
+
cancel_button.setName("cancel_button"); // NOI18N
|
49
|
+
|
50
|
+
add_button.setText("+"); // NOI18N
|
51
|
+
add_button.setToolTipText("Add a new item"); // NOI18N
|
52
|
+
add_button.setBorder(null);
|
53
|
+
add_button.setName("add_button"); // NOI18N
|
54
|
+
|
55
|
+
remove_button.setText("-"); // NOI18N
|
56
|
+
remove_button.setToolTipText("Remove selected item"); // NOI18N
|
57
|
+
remove_button.setBorder(null);
|
58
|
+
remove_button.setName("remove_button"); // NOI18N
|
59
|
+
|
60
|
+
org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this);
|
61
|
+
this.setLayout(layout);
|
62
|
+
layout.setHorizontalGroup(
|
63
|
+
layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
|
64
|
+
.add(layout.createSequentialGroup()
|
65
|
+
.addContainerGap()
|
66
|
+
.add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
|
67
|
+
.add(text_field, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 210, Short.MAX_VALUE)
|
68
|
+
.add(layout.createSequentialGroup()
|
69
|
+
.add(ok_button)
|
70
|
+
.addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, 82, Short.MAX_VALUE)
|
71
|
+
.add(cancel_button))
|
72
|
+
.add(layout.createSequentialGroup()
|
73
|
+
.add(add_button, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 27, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
|
74
|
+
.addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
|
75
|
+
.add(remove_button, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 27, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
|
76
|
+
.add(item_list, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 207, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
|
77
|
+
.addContainerGap())
|
78
|
+
);
|
79
|
+
layout.setVerticalGroup(
|
80
|
+
layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
|
81
|
+
.add(layout.createSequentialGroup()
|
82
|
+
.addContainerGap()
|
83
|
+
.add(text_field, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
|
84
|
+
.addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
|
85
|
+
.add(item_list, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 199, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
|
86
|
+
.add(15, 15, 15)
|
87
|
+
.add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
|
88
|
+
.add(add_button, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 20, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
|
89
|
+
.add(remove_button, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 19, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
|
90
|
+
.addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, 30, Short.MAX_VALUE)
|
91
|
+
.add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
|
92
|
+
.add(ok_button)
|
93
|
+
.add(cancel_button))
|
94
|
+
.addContainerGap())
|
95
|
+
);
|
96
|
+
}// </editor-fold>//GEN-END:initComponents
|
97
|
+
|
98
|
+
|
99
|
+
// Variables declaration - do not modify//GEN-BEGIN:variables
|
100
|
+
public javax.swing.JButton add_button;
|
101
|
+
public javax.swing.JButton cancel_button;
|
102
|
+
public javax.swing.JList item_list;
|
103
|
+
public javax.swing.JButton ok_button;
|
104
|
+
public javax.swing.JButton remove_button;
|
105
|
+
public javax.swing.JTextField text_field;
|
106
|
+
// End of variables declaration//GEN-END:variables
|
107
|
+
|
108
|
+
}
|
File without changes
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'clevic/field_valuer.rb'
|
2
|
+
|
3
|
+
module Clevic
|
4
|
+
# to be included in something that responds to model, row, column
|
5
|
+
module TableIndex
|
6
|
+
include FieldValuer
|
7
|
+
|
8
|
+
# return the Clevic::Field for this index
|
9
|
+
def field
|
10
|
+
@field ||= model.field_for_index( self )
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump
|
14
|
+
if valid?
|
15
|
+
<<-EOF
|
16
|
+
field: #{field_name} => #{field_value}
|
17
|
+
attribute: #{attribute.inspect} => #{attribute_value.inspect}
|
18
|
+
meta: #{meta.inspect}
|
19
|
+
EOF
|
20
|
+
else
|
21
|
+
'invalid'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def prev
|
26
|
+
choppy( :row => row - 1 )
|
27
|
+
end
|
28
|
+
|
29
|
+
# return the attribute of the underlying entity corresponding
|
30
|
+
# to the column of this index
|
31
|
+
def attribute
|
32
|
+
model.attributes[column]
|
33
|
+
end
|
34
|
+
|
35
|
+
# returns the list of ModelColumn metadata
|
36
|
+
def meta
|
37
|
+
# use the optimised version
|
38
|
+
# TODO just use the model version instead?
|
39
|
+
field.meta
|
40
|
+
end
|
41
|
+
|
42
|
+
# return the table's field name. For associations, this would
|
43
|
+
# be suffixed with _id
|
44
|
+
def field_name
|
45
|
+
meta.name
|
46
|
+
end
|
47
|
+
|
48
|
+
# return the value of the field, it may be the _id value
|
49
|
+
def field_value
|
50
|
+
entity.send( field_name )
|
51
|
+
end
|
52
|
+
|
53
|
+
# the underlying entity
|
54
|
+
# TODO caching doesn't help with Qt because ModelIndex objects are
|
55
|
+
# extremely short-lived. Not sure about swing.
|
56
|
+
def entity
|
57
|
+
return nil if model.nil?
|
58
|
+
model.collection[row]
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_writer :entity
|
62
|
+
|
63
|
+
# return true if validation failed for this indexes field
|
64
|
+
def has_errors?
|
65
|
+
# virtual fields don't have metadata
|
66
|
+
if meta.nil?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
entity.errors.invalid?( field_name.to_sym )
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# return a collection of errors. Unlike AR, this
|
74
|
+
# will always return an array that will have zero, one
|
75
|
+
# or many elements.
|
76
|
+
def errors
|
77
|
+
[ entity.errors[field_name.to_sym] ].flatten
|
78
|
+
end
|
79
|
+
|
80
|
+
# sort by row, then column
|
81
|
+
def <=>( other )
|
82
|
+
row_comp = self.row <=> other.row
|
83
|
+
if row_comp == 0
|
84
|
+
self.column <=> other.column
|
85
|
+
else
|
86
|
+
row_comp
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"#<TableIndex (#{row},#{column}) '#{raw_value rescue "no raw value: #{$!.message}"}'>"
|
92
|
+
end
|
93
|
+
|
94
|
+
# return a string (row,column)
|
95
|
+
# suitable for displaying to users, ie 1-based not 0-based
|
96
|
+
def rc
|
97
|
+
"(#{row+1},#{column+1})"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/clevic/table_model.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
|
-
require 'Qt4'
|
2
1
|
require 'date'
|
3
2
|
|
4
|
-
require 'qtext/flags.rb'
|
5
|
-
require 'qtext/extensions.rb'
|
6
|
-
|
7
3
|
require 'clevic/extensions.rb'
|
8
4
|
require 'clevic/model_column'
|
9
5
|
|
@@ -12,12 +8,10 @@ module Clevic
|
|
12
8
|
=begin rdoc
|
13
9
|
An instance of Clevic::TableModel is constructed by Clevic::ModelBuilder from the
|
14
10
|
UI definition in a Clevic::View, or from the default Clevic::View created by
|
15
|
-
including the Clevic::Record module in a
|
11
|
+
including the Clevic::Record module in a Sequel::Model subclass.
|
16
12
|
=end
|
17
|
-
class TableModel
|
18
|
-
|
19
|
-
|
20
|
-
# the CacheTable of Clevic::Record or ActiveRecord::Base objects
|
13
|
+
class TableModel
|
14
|
+
# the CacheTable of Clevic::Record or Sequel::Model objects
|
21
15
|
attr_reader :collection
|
22
16
|
alias_method :cache_table, :collection
|
23
17
|
|
@@ -35,25 +29,19 @@ class TableModel < Qt::AbstractTableModel
|
|
35
29
|
attr_accessor :entity_view
|
36
30
|
attr_accessor :builder
|
37
31
|
|
32
|
+
# If somre or all the entities in the collection is related to a single entity
|
33
|
+
# somewhere else, this is it. Not sure if this is the right
|
34
|
+
# way to do it, but try anyway.
|
35
|
+
attr_accessor :one
|
36
|
+
|
38
37
|
def entity_class
|
39
38
|
entity_view.entity_class
|
40
39
|
end
|
41
40
|
|
42
|
-
signals(
|
43
|
-
# index where error occurred, value, message
|
44
|
-
'data_error(QModelIndex,QVariant,QString)'
|
45
|
-
)
|
46
|
-
|
47
|
-
def initialize( parent = nil )
|
48
|
-
super
|
49
|
-
@metadatas = []
|
50
|
-
end
|
51
|
-
|
52
41
|
def fields=( arr )
|
53
42
|
@fields = arr
|
54
43
|
|
55
44
|
# reset these
|
56
|
-
@metadatas = []
|
57
45
|
@labels = nil
|
58
46
|
@attributes = nil
|
59
47
|
end
|
@@ -79,87 +67,53 @@ class TableModel < Qt::AbstractTableModel
|
|
79
67
|
def collection=( arr )
|
80
68
|
@collection = arr
|
81
69
|
# fill in an empty record for data entry
|
82
|
-
if collection.
|
83
|
-
collection << entity_class.new
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def sort( col, order )
|
88
|
-
puts 'sort'
|
89
|
-
puts "col: #{col.inspect}"
|
90
|
-
#~ Qt::AscendingOrder
|
91
|
-
#~ Qt::DescendingOrder
|
92
|
-
puts "order: #{order.inspect}"
|
93
|
-
super
|
94
|
-
end
|
95
|
-
|
96
|
-
# this is called for read-only tables.
|
97
|
-
def match( start_index, role, search_value, hits, match_flags )
|
98
|
-
#~ Qt::MatchExactly 0 Performs QVariant-based matching.
|
99
|
-
#~ Qt::MatchFixedString 8 Performs string-based matching. String-based comparisons are case-insensitive unless the MatchCaseSensitive flag is also specified.
|
100
|
-
#~ Qt::MatchContains 1 The search term is contained in the item.
|
101
|
-
#~ Qt::MatchStartsWith 2 The search term matches the start of the item.
|
102
|
-
#~ Qt::MatchEndsWith 3 The search term matches the end of the item.
|
103
|
-
#~ Qt::MatchCaseSensitive 16 The search is case sensitive.
|
104
|
-
#~ Qt::MatchRegExp 4 Performs string-based matching using a regular expression as the search term.
|
105
|
-
#~ Qt::MatchWildcard 5 Performs string-based matching using a string with wildcards as the search term.
|
106
|
-
#~ Qt::MatchWrap 32 Perform a search that wraps around, so that when the search reaches the last item in the model, it begins again at the first item and continues until all items have been examined.
|
107
|
-
#~ super
|
108
|
-
[]
|
109
|
-
end
|
110
|
-
|
111
|
-
# cache metadata (ActiveRecord#column_for_attribute) because it's not going
|
112
|
-
# to change over the lifetime of the table
|
113
|
-
# if the column is an attribute, create a ModelColumn
|
114
|
-
# TODO use ActiveRecord::Base.reflections instead
|
115
|
-
def metadata( column )
|
116
|
-
if @metadatas[column].nil?
|
117
|
-
meta = entity_class.columns_hash[attributes[column].to_s]
|
118
|
-
if meta.nil?
|
119
|
-
meta = entity_class.columns_hash[ "#{attributes[column]}_id" ]
|
120
|
-
if meta.nil?
|
121
|
-
return nil
|
122
|
-
else
|
123
|
-
@metadatas[column] = ModelColumn.new( attributes[column], :association, meta )
|
124
|
-
end
|
125
|
-
else
|
126
|
-
@metadatas[column] = meta
|
127
|
-
end
|
128
|
-
end
|
129
|
-
@metadatas[column]
|
70
|
+
add_new_item if collection.empty? && auto_new?
|
130
71
|
end
|
131
72
|
|
132
73
|
# add a new item, and set defaults from the Clevic::View
|
74
|
+
# add_new_item_start and add_new_item_end are provided by
|
75
|
+
# the GUI framework-specific class
|
133
76
|
def add_new_item
|
134
|
-
|
135
|
-
|
77
|
+
add_new_item_start
|
78
|
+
|
136
79
|
entity = entity_class.new
|
80
|
+
|
81
|
+
# set default values without triggering changed
|
137
82
|
fields.each do |f|
|
138
|
-
unless f.default.nil?
|
139
|
-
f.set_default_for( entity )
|
140
|
-
end
|
83
|
+
f.set_default_for( entity ) unless f.default.nil?
|
141
84
|
end
|
142
85
|
|
143
86
|
collection << entity
|
144
87
|
|
145
|
-
|
88
|
+
add_new_item_end
|
146
89
|
end
|
147
90
|
|
148
91
|
# rows is a collection of integers specifying row indices to remove
|
149
92
|
def remove_rows( rows )
|
150
|
-
# delete
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
93
|
+
# don't delete rows twice
|
94
|
+
# put the entities to be removed in a separate collection
|
95
|
+
# I can't figure out why the collection fails when they're
|
96
|
+
# removed directly
|
97
|
+
rows_in_order = rows.uniq.sort
|
98
|
+
removals = rows_in_order.map do |index|
|
99
|
+
collection[index]
|
100
|
+
end
|
101
|
+
|
102
|
+
remove_notify( rows_in_order ) do
|
103
|
+
removals.each do |to_remove, index|
|
104
|
+
# destroy the db object, and its associated table row
|
105
|
+
to_remove.destroy unless to_remove.nil? || to_remove.new?
|
106
|
+
end
|
160
107
|
end
|
161
108
|
|
109
|
+
# because it's too bloody complicated to figure out which items
|
110
|
+
# were deleted and then remove them from the collection. And there
|
111
|
+
# most likely isn't a big hit for doing this, unless there's a lot
|
112
|
+
# of cached calcuation in the entities.
|
113
|
+
self.collection = collection.renew
|
114
|
+
|
162
115
|
# create a new row if auto_new is on
|
116
|
+
# should really be in a signal handler
|
163
117
|
add_new_item if collection.empty? && auto_new?
|
164
118
|
end
|
165
119
|
|
@@ -170,7 +124,7 @@ class TableModel < Qt::AbstractTableModel
|
|
170
124
|
if item.changed?
|
171
125
|
if item.valid?
|
172
126
|
retval = item.save
|
173
|
-
|
127
|
+
data_changed( index )
|
174
128
|
retval
|
175
129
|
else
|
176
130
|
false
|
@@ -179,322 +133,30 @@ class TableModel < Qt::AbstractTableModel
|
|
179
133
|
# AR model not changed
|
180
134
|
true
|
181
135
|
end
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
end
|
187
|
-
|
188
|
-
# Not looked up or aliased properly by Qt bindings
|
189
|
-
def row_count
|
190
|
-
collection.size
|
191
|
-
end
|
192
|
-
|
193
|
-
def columnCount( parent = nil )
|
194
|
-
fields.size
|
195
|
-
end
|
196
|
-
|
197
|
-
# Not looked up or aliased properly by Qt bindings
|
198
|
-
def column_count
|
199
|
-
fields.size
|
200
|
-
end
|
201
|
-
|
202
|
-
def flags( model_index )
|
203
|
-
retval = super
|
204
|
-
|
205
|
-
# sometimes this actually happens.
|
206
|
-
# TODO probably a bug in the combo editor exit code
|
207
|
-
return retval if model_index.column >= columnCount
|
208
|
-
|
209
|
-
# TODO don't return IsEditable if the model is read-only
|
210
|
-
if model_index.metadata.type == :boolean
|
211
|
-
retval = item_boolean_flags
|
212
|
-
end
|
213
|
-
|
214
|
-
unless model_index.field.read_only? || model_index.entity.readonly? || read_only?
|
215
|
-
retval |= qt_item_is_editable.to_i
|
216
|
-
end
|
217
|
-
retval
|
136
|
+
rescue
|
137
|
+
puts $!.message
|
138
|
+
puts $!.backtrace
|
139
|
+
emit_data_error( index, nil, $!.message )
|
218
140
|
end
|
219
141
|
|
220
142
|
def reload_data( options = nil )
|
221
143
|
# renew cache. All records will be dropped and reloaded.
|
222
|
-
self.collection = self.
|
144
|
+
self.collection = self.cache_table.renew( options )
|
223
145
|
# tell the UI we had a major data change
|
224
146
|
reset
|
225
147
|
end
|
226
148
|
|
227
|
-
#
|
228
|
-
|
229
|
-
|
230
|
-
case role
|
231
|
-
when qt_display_role
|
232
|
-
case orientation
|
233
|
-
when Qt::Horizontal
|
234
|
-
labels[section]
|
235
|
-
when Qt::Vertical
|
236
|
-
# don't force a fetch from the db
|
237
|
-
if collection.cached_at?( section )
|
238
|
-
collection[section].id
|
239
|
-
else
|
240
|
-
section
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
when qt_text_alignment_role
|
245
|
-
case orientation
|
246
|
-
when Qt::Vertical
|
247
|
-
Qt::AlignRight | Qt::AlignVCenter
|
248
|
-
end
|
249
|
-
|
250
|
-
when Qt::SizeHintRole
|
251
|
-
# anything other than nil here makes the headers disappear.
|
252
|
-
nil
|
253
|
-
|
254
|
-
when qt_tooltip_role
|
255
|
-
case orientation
|
256
|
-
when Qt::Horizontal
|
257
|
-
fields[section].tooltip
|
258
|
-
|
259
|
-
when Qt::Vertical
|
260
|
-
case
|
261
|
-
when !collection[section].errors.empty?
|
262
|
-
'Invalid data'
|
263
|
-
when collection[section].changed?
|
264
|
-
'Unsaved changes'
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
when qt_background_role
|
269
|
-
if orientation == Qt::Vertical
|
270
|
-
item = collection[section]
|
271
|
-
case
|
272
|
-
when !item.errors.empty?
|
273
|
-
Qt::Color.new( 'orange' )
|
274
|
-
when item.changed?
|
275
|
-
Qt::Color.new( 'yellow' )
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
else
|
280
|
-
#~ puts "headerData section: #{section}, role: #{const_as_string(role)}" if $options[:debug]
|
281
|
-
nil
|
282
|
-
end
|
283
|
-
|
284
|
-
return value.to_variant
|
285
|
-
end
|
286
|
-
|
287
|
-
# Provide data to UI.
|
288
|
-
def data( index, role = qt_display_role )
|
289
|
-
#~ puts "data for index: #{index.inspect}, field #{index.field.attribute.inspect} and role: #{const_as_string role}"
|
290
|
-
begin
|
291
|
-
retval =
|
292
|
-
case role
|
293
|
-
when qt_display_role
|
294
|
-
# boolean values generally don't have text next to them in this context
|
295
|
-
# check this explicitly to avoid fetching the entity from
|
296
|
-
# the model's collection (and maybe db) when we
|
297
|
-
# definitely don't need to
|
298
|
-
unless index.metadata.type == :boolean
|
299
|
-
value = index.gui_value
|
300
|
-
index.field.do_format( value ) unless value.nil?
|
301
|
-
end
|
302
|
-
|
303
|
-
when qt_edit_role
|
304
|
-
# see comment for qt_display_role
|
305
|
-
unless index.metadata.type == :boolean
|
306
|
-
value = index.gui_value
|
307
|
-
index.field.do_edit_format( value ) unless value.nil?
|
308
|
-
end
|
309
|
-
|
310
|
-
when qt_checkstate_role
|
311
|
-
if index.metadata.type == :boolean
|
312
|
-
index.gui_value ? qt_checked : qt_unchecked
|
313
|
-
end
|
314
|
-
|
315
|
-
when qt_text_alignment_role
|
316
|
-
case index.field.alignment
|
317
|
-
when :left; qt_alignleft
|
318
|
-
when :right; qt_alignright
|
319
|
-
when :centre; qt_aligncenter
|
320
|
-
when :justified; qt_alignjustified
|
321
|
-
end
|
322
|
-
|
323
|
-
# just here to make debug output quieter
|
324
|
-
when qt_size_hint_role;
|
325
|
-
|
326
|
-
# show field with a red background if there's an error
|
327
|
-
when qt_background_role
|
328
|
-
index.field.background_for( index.entity ) || Qt::Color.new( 'red' ) if index.has_errors?
|
329
|
-
|
330
|
-
when qt_font_role;
|
331
|
-
|
332
|
-
when qt_foreground_role
|
333
|
-
index.field.foreground_for( index.entity ) ||
|
334
|
-
if index.field.read_only? || index.entity.readonly? || read_only?
|
335
|
-
Qt::Color.new( 'dimgray' )
|
336
|
-
end
|
337
|
-
|
338
|
-
when qt_decoration_role;
|
339
|
-
index.field.decoration_for( index.entity )
|
340
|
-
|
341
|
-
when qt_tooltip_role
|
342
|
-
case
|
343
|
-
# show ActiveRecord validation errors
|
344
|
-
when index.has_errors?
|
345
|
-
index.errors.join("\n")
|
346
|
-
|
347
|
-
# provide a tooltip when an empty relational field is encountered
|
348
|
-
# TODO should be part of field definition
|
349
|
-
when index.metadata.type == :association
|
350
|
-
index.field.delegate.if_empty_message
|
351
|
-
|
352
|
-
# read-only field
|
353
|
-
when index.field.read_only?
|
354
|
-
'Read-only'
|
355
|
-
|
356
|
-
else
|
357
|
-
index.field.tooltip_for( index.entity )
|
358
|
-
end
|
359
|
-
else
|
360
|
-
puts "data index: #{index}, role: #{const_as_string(role)}" if $options[:debug]
|
361
|
-
nil
|
362
|
-
end
|
363
|
-
|
364
|
-
# return a variant
|
365
|
-
#~ puts "retval: #{retval.inspect}"
|
366
|
-
retval.to_variant
|
367
|
-
rescue Exception => e
|
368
|
-
puts "#{index.inspect} #{value.inspect} #{index.entity.inspect} for and role: #{const_as_string role}"
|
369
|
-
puts e.message
|
370
|
-
puts e.backtrace
|
371
|
-
nil.to_variant
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
# data sent from UI
|
376
|
-
# return true if conversion from variant was successful,
|
377
|
-
# or false if something went wrong.
|
378
|
-
def setData( index, variant, role = qt_edit_role )
|
379
|
-
if index.valid?
|
380
|
-
case role
|
381
|
-
when qt_edit_role
|
382
|
-
# Don't allow the primary key to be changed
|
383
|
-
return false if index.attribute == entity_class.primary_key.to_sym
|
384
|
-
|
385
|
-
if ( index.column < 0 || index.column >= column_count )
|
386
|
-
raise "invalid column #{index.column}"
|
387
|
-
end
|
388
|
-
|
389
|
-
type = index.metadata.type
|
390
|
-
value = variant.value
|
391
|
-
#~ puts "#{type.inspect} is #{value}"
|
392
|
-
|
393
|
-
# translate the value from the ui to something that
|
394
|
-
# the AR model will understand
|
395
|
-
begin
|
396
|
-
index.attribute_value =
|
397
|
-
case
|
398
|
-
when value.class.name == 'Qt::Date'
|
399
|
-
Date.new( value.year, value.month, value.day )
|
400
|
-
|
401
|
-
when value.class.name == 'Qt::Time'
|
402
|
-
Time.new( value.hour, value.min, value.sec )
|
403
|
-
|
404
|
-
# allow flexibility in entering dates. For example
|
405
|
-
# 16jun, 16-jun, 16 jun, 16 jun 2007 would be accepted here
|
406
|
-
# TODO need to be cleverer about which year to use
|
407
|
-
# for when you're entering 16dec and you're in the next
|
408
|
-
# year
|
409
|
-
when [:date,:datetime].include?( type ) && value =~ %r{^(\d{1,2})[ /-]?(\w{3})$}
|
410
|
-
Date.parse( "#$1 #$2 #{Time.now.year.to_s}" )
|
411
|
-
|
412
|
-
# if a digit only is entered, fetch month and year from
|
413
|
-
# previous row
|
414
|
-
when [:date,:datetime].include?( type ) && value =~ %r{^(\d{1,2})$}
|
415
|
-
previous_entity = collection[index.row - 1]
|
416
|
-
# year,month,day
|
417
|
-
Date.new( previous_entity.date.year, previous_entity.date.month, $1.to_i )
|
418
|
-
|
419
|
-
# this one is mostly to fix date strings that have come
|
420
|
-
# out of the db and been formatted
|
421
|
-
when [:date,:datetime].include?( type ) && value =~ %r{^(\d{2})[ /-](\w{3})[ /-](\d{2})$}
|
422
|
-
Date.parse( "#$1 #$2 20#$3" )
|
423
|
-
|
424
|
-
# allow lots of flexibility in entering times
|
425
|
-
# 01:17, 0117, 117, 1 17, are all accepted
|
426
|
-
when type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
|
427
|
-
Time.parse( "#$1:#$2" )
|
428
|
-
|
429
|
-
# remove thousand separators, allow for space and comma
|
430
|
-
# instead of . as a decimal separator
|
431
|
-
when type == :decimal
|
432
|
-
# do various transforms
|
433
|
-
value =
|
434
|
-
case
|
435
|
-
# accept a space or a comma instead of a . for floats
|
436
|
-
when value =~ /(.*?)(\d)[ ,](\d{2})$/
|
437
|
-
"#$1#$2.#$3"
|
438
|
-
else
|
439
|
-
value
|
440
|
-
end
|
441
|
-
|
442
|
-
# strip remaining commas
|
443
|
-
value.gsub( ',', '' )
|
444
|
-
|
445
|
-
else
|
446
|
-
value
|
447
|
-
end
|
448
|
-
|
449
|
-
data_changed( index )
|
450
|
-
# value conversion was successful
|
451
|
-
true
|
452
|
-
rescue Exception => e
|
453
|
-
puts e.backtrace.join( "\n" )
|
454
|
-
puts e.message
|
455
|
-
emit data_error( index, variant, e.message )
|
456
|
-
# value conversion was not successful
|
457
|
-
false
|
458
|
-
end
|
459
|
-
|
460
|
-
when qt_checkstate_role
|
461
|
-
if index.metadata.type == :boolean
|
462
|
-
index.entity.toggle( index.attribute )
|
463
|
-
true
|
464
|
-
else
|
465
|
-
false
|
466
|
-
end
|
467
|
-
|
468
|
-
# user-defined role
|
469
|
-
# TODO this only works with single-dotted paths
|
470
|
-
when qt_paste_role
|
471
|
-
if index.metadata.type == :association
|
472
|
-
field = index.field
|
473
|
-
association_class = field.class_name.constantize
|
474
|
-
candidates = association_class.find( :all, :conditions => [ "#{field.attribute_path[1]} = ?", variant.value ] )
|
475
|
-
case candidates.size
|
476
|
-
when 0; puts "No match for #{variant.value}"
|
477
|
-
when 1; index.attribute_value = candidates[0]
|
478
|
-
else; puts "Too many for #{variant.value}"
|
479
|
-
end
|
480
|
-
else
|
481
|
-
index.attribute_value = variant.value
|
482
|
-
end
|
483
|
-
true
|
484
|
-
|
485
|
-
else
|
486
|
-
puts "role: #{role.inspect}"
|
487
|
-
true
|
488
|
-
|
489
|
-
end
|
490
|
-
else
|
491
|
-
false
|
492
|
-
end
|
493
|
-
end
|
494
|
-
|
495
|
-
# return a set of indexes that match the search criteria
|
149
|
+
# return a collection of indexes that match the search criteria
|
150
|
+
# at the moment this only returns the first index found
|
151
|
+
# TODO could handle dataset creation better
|
496
152
|
def search( start_index, search_criteria )
|
497
|
-
|
153
|
+
ordered_dataset = entity_class.dataset.order( *cache_table.order_attributes.map{|oa| oa.attribute.to_sym.send( oa.direction ) } )
|
154
|
+
searcher = Clevic::TableSearcher.new(
|
155
|
+
ordered_dataset,
|
156
|
+
search_criteria,
|
157
|
+
start_index.field
|
158
|
+
)
|
159
|
+
entity = searcher.search( start_index.entity )
|
498
160
|
|
499
161
|
# return matched indexes
|
500
162
|
if entity != nil
|
@@ -536,39 +198,6 @@ class TableModel < Qt::AbstractTableModel
|
|
536
198
|
end
|
537
199
|
end
|
538
200
|
|
539
|
-
# A rubyish way of doing dataChanged
|
540
|
-
# - if args has one element, it's either a single ModelIndex
|
541
|
-
# or something that understands top_left and bottom_right. These
|
542
|
-
# will be turned into a ModelIndex by calling create_index
|
543
|
-
# - if args has two element, assume it's a two ModelIndex instances
|
544
|
-
# - otherwise create a new DataChange and pass it to the block.
|
545
|
-
def data_changed( *args, &block )
|
546
|
-
case args.size
|
547
|
-
when 1
|
548
|
-
arg = args.first
|
549
|
-
if ( arg.respond_to?( :top_left ) && arg.respond_to?( :bottom_right ) ) || arg.is_a?( Qt::ItemSelectionRange )
|
550
|
-
# object is a DataChange, or a SelectionRange
|
551
|
-
top_left_index = create_index( arg.top_left.row, arg.top_left.column )
|
552
|
-
bottom_right_index = create_index( arg.bottom_right.row, arg.bottom_right.column )
|
553
|
-
emit dataChanged( top_left_index, bottom_right_index )
|
554
|
-
else
|
555
|
-
# assume it's a ModelIndex
|
556
|
-
emit dataChanged( arg, arg )
|
557
|
-
end
|
558
|
-
|
559
|
-
when 2
|
560
|
-
emit dataChanged( args.first, args.last )
|
561
|
-
|
562
|
-
else
|
563
|
-
unless block.nil?
|
564
|
-
change = DataChange.new
|
565
|
-
block.call( change )
|
566
|
-
# recursive call
|
567
|
-
data_changed( change )
|
568
|
-
end
|
569
|
-
end
|
570
|
-
end
|
571
|
-
|
572
201
|
end
|
573
202
|
|
574
203
|
end #module
|