clevic 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ require 'clevic/item_delegate.rb'
2
+
3
+ module Clevic
4
+
5
+ class TextDelegate < ItemDelegate
6
+
7
+ # Doesn't do anything useful yet, but I'm leaving
8
+ # it here so I don't have to change other code.
9
+ class TextEditor < Qt::PlainTextEdit
10
+ end
11
+
12
+ # this is overridden in Qt::ItemDelegate, but that
13
+ # always catches the return key. Which we want for text editing.
14
+ # Instead, we use Ctrl-Enter to save the edited text.
15
+ # return true if event is handled, false otherwise
16
+ def eventFilter( object, event )
17
+ if object.class == TextEditor && event.class == Qt::KeyEvent
18
+ retval =
19
+ case
20
+ when event.ctrl? && ( event.enter? || event.return? )
21
+ # close and save
22
+ # copied from QItemDelegate.
23
+ emit commitData( object )
24
+ emit closeEditor( object )
25
+ true
26
+
27
+ # send an enter or return to the text editor
28
+ when event.enter? || event.return?
29
+ object.event( event )
30
+ true
31
+ end
32
+ end
33
+ retval || super
34
+ end
35
+
36
+ # maybe open in a separate window?
37
+ def full_edit
38
+ puts "#{self.class.name} full_edit"
39
+ end
40
+
41
+ # Override the Qt method
42
+ def createEditor( parent_widget, style_option_view_item, model_index )
43
+ if false && model_index.gui_value.count("\n") == 0
44
+ # futzing about here, really
45
+ @editor = Qt::LineEdit.new( parent_widget )
46
+ else
47
+ @editor = TextEditor.new( parent_widget )
48
+ @editor.install_event_filter( self )
49
+ end
50
+ @editor
51
+ end
52
+
53
+ # Override the Qt::ItemDelegate method.
54
+ def updateEditorGeometry( editor, style_option_view_item, model_index )
55
+ rect = Qt::Rect.new( style_option_view_item.rect.top_left, style_option_view_item.rect.size )
56
+
57
+ # ask the editor for how much space it wants, and set the editor
58
+ # to that size when it displays in the table
59
+ rect.set_width( [editor.size_hint.width,rect.width].max )
60
+ rect.set_height( editor.size_hint.height )
61
+
62
+ unless editor.parent.rect.contains( rect )
63
+ # 46 because TableView returns an incorrect bottom.
64
+ # And I can't find out how to get the correct value.
65
+ rect.move_bottom( parent.contents_rect.bottom - 46 )
66
+ end
67
+ editor.set_geometry( rect )
68
+ end
69
+
70
+ # Override the Qt method to send data to the editor from the model.
71
+ def setEditorData( editor, model_index )
72
+ editor.plain_text = model_index.gui_value
73
+ end
74
+
75
+ # Send the data from the editor to the model. The data will
76
+ # be translated by translate_from_editor_text,
77
+ def setModelData( editor, abstract_item_model, model_index )
78
+ model_index.attribute_value = editor.to_plain_text
79
+ abstract_item_model.data_changed( model_index )
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -1,8 +1,8 @@
1
1
  module Clevic #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 11
5
- TINY = 1
4
+ MINOR = 12
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/clevic/view.rb CHANGED
@@ -77,8 +77,34 @@ module Clevic
77
77
  def define_actions( table_view, action_builder )
78
78
  end
79
79
 
80
- # define data changed events
81
- def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
80
+ # notify
81
+ def notify_field( table_view, model_index )
82
+ ndc = model_index.field.notify_data_changed
83
+ case ndc
84
+ when Proc
85
+ ndc.call( self, table_view, model_index )
86
+
87
+ when Symbol
88
+ send( ndc, table_view, model_index )
89
+ end
90
+ end
91
+
92
+ # Define data changed events. Default is to call notify_data_changed
93
+ # for each field in the rectangular area defined by top_left and bottom_right
94
+ # (which are Qt::ModelIndex instances)
95
+ def notify_data_changed( table_view, top_left, bottom_right )
96
+ if top_left == bottom_right
97
+ # shortcut to just the one, seeing as it's probably the most common
98
+ notify_field( table_view, top_left )
99
+ else
100
+ # do the entire rectagular area
101
+ (top_left.row..bottom_right.row).each do |row_index|
102
+ (top_left.column..bottom_right.column).each do |column_index|
103
+ model_index = table_view.model.create_index( row_index, column_index )
104
+ notify_field( table_view, model_index )
105
+ end
106
+ end
107
+ end
82
108
  end
83
109
 
84
110
  # be notified of key presses
@@ -9,7 +9,7 @@ Clevic::DbOptions.connect( $options ) do
9
9
  database options[:database]
10
10
  end
11
11
  adapter :postgresql
12
- username 'accounts'
12
+ username options[:username].blank? ? 'accounts' : options[:username]
13
13
  end
14
14
 
15
15
  class Entry < ActiveRecord::Base
@@ -20,7 +20,19 @@ class Entry < ActiveRecord::Base
20
20
 
21
21
  define_ui do
22
22
  plain :date, :sample => '88-WWW-99'
23
- distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26
23
+ distinct :description do |f|
24
+ f.conditions "now() - date <= '1 year'"
25
+ f.sample( 'm' * 26 )
26
+ f.notify_data_changed = lambda do |entity_view, table_view, model_index|
27
+ if model_index.entity.credit.nil? && model_index.entity.debit.nil?
28
+ entity_view.update_from_description( model_index )
29
+
30
+ # move edit cursor to amount field
31
+ table_view.selection_model.clear
32
+ table_view.override_next_index( model_index.choppy( :column => :amount ) )
33
+ end
34
+ end
35
+ end
24
36
  relational :debit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
25
37
  relational :credit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
26
38
  plain :amount, :sample => 999999.99
@@ -32,44 +44,26 @@ class Entry < ActiveRecord::Base
32
44
  records :order => 'date, id'
33
45
  end
34
46
 
35
- # called when data is changed in the UI
36
- def self.data_changed( top_left, bottom_right, view )
37
- if top_left == bottom_right
38
- update_credit_debit( top_left, view )
39
- else
40
- puts "top_left: #{top_left.inspect}"
41
- puts "bottom_right: #{bottom_right.inspect}"
42
- puts "can't do data_changed for a range"
43
- end
44
- end
45
-
46
- # check that the current field is :descriptions, then
47
- # copy the values for the credit and debit fields
48
- # from the previous similar entry
49
- def self.update_credit_debit( current_index, view )
50
- return unless current_index.valid?
51
- current_field = current_index.attribute
52
- if current_field == :description
53
- # most recent entry, ordered in reverse
54
- similar = self.find(
55
- :first,
56
- :conditions => ["#{current_field} = ?", current_index.attribute_value],
57
- :order => 'date desc'
58
- )
59
- if similar != nil
60
- # set the values
61
- current_index.entity.debit = similar.debit
62
- current_index.entity.credit = similar.credit
63
- current_index.entity.category = similar.category
64
-
65
- # emit signal to update view from top_left to bottom_right
66
- top_left_index = current_index.choppy( :column => 0 )
67
- bottom_right_index = current_index.choppy( :column => view.model.column_count - 1 )
68
- view.dataChanged( top_left_index, bottom_right_index )
69
-
70
- # move edit cursor to amount field
71
- view.selection_model.clear
72
- view.override_next_index( current_index.choppy( :column => view.field_column( :amount ) ) )
47
+ # Copy the values for the credit and debit fields
48
+ # from the previous similar entry with a similar description
49
+ def self.update_from_description( current_index )
50
+ return if current_index.attribute_value.nil?
51
+ # most recent entry, ordered in reverse
52
+ similar = self.find(
53
+ :first,
54
+ :conditions => ["#{current_index.attribute} = ?", current_index.attribute_value],
55
+ :order => 'date desc'
56
+ )
57
+ if similar != nil
58
+ # set the values
59
+ current_index.entity.debit = similar.debit
60
+ current_index.entity.credit = similar.credit
61
+ current_index.entity.category = similar.category
62
+
63
+ # emit signal to that whole row has changed
64
+ current_index.model.data_changed do |change|
65
+ change.top_left = current_index.choppy( :column => 0 )
66
+ change.bottom_right = current_index.choppy( :column => current_index.model.column_count - 1 )
73
67
  end
74
68
  end
75
69
  end
@@ -19,11 +19,35 @@ class Entry < ActiveRecord::Base
19
19
 
20
20
  define_ui do
21
21
  plain :date, :sample => '28-Dec-08'
22
- relational :project, :display => 'project', :conditions => 'active = true', :order => 'lower(project)'
22
+
23
+ # The project field
24
+ relational :project do |field|
25
+ field.display = 'project'
26
+ field.conditions = 'active = true'
27
+ field.order = 'lower(project)'
28
+
29
+ # handle data changed events. In this case,
30
+ # auto-fill-in the invoice field.
31
+ field.notify_data_changed do |entity_view, table_view, model_index|
32
+ if model_index.entity.invoice.nil?
33
+ entity_view.invoice_from_project( table_view, model_index ) do
34
+ # move here next if the invoice was changed
35
+ table_view.override_next_index model_index.choppy( :column => :start )
36
+ end
37
+ end
38
+ end
39
+ end
40
+
23
41
  relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
42
+
43
+ # call time_color method for foreground color value
24
44
  plain :start, :foreground => :time_color, :tooltip => :time_tooltip
45
+
46
+ # another way to call time_color method for foreground color value
25
47
  plain :end, :foreground => lambda{|x| x.time_color}, :tooltip => :time_tooltip
26
- plain :description, :sample => 'This is a long string designed to hold lots of data and description'
48
+
49
+ # multiline text
50
+ text :description, :sample => 'This is a long string designed to hold lots of data and description'
27
51
 
28
52
  relational :activity do
29
53
  display 'activity'
@@ -34,18 +58,25 @@ class Entry < ActiveRecord::Base
34
58
 
35
59
  distinct :module, :tooltip => 'Module or sub-project'
36
60
  plain :charge, :tooltip => 'Is this time billable?'
37
- distinct :person, :tooltip => 'The person who did the work'
61
+ distinct :person, :default => 'John', :tooltip => 'The person who did the work'
38
62
 
39
63
  records :order => 'date, start, id'
40
64
  end
41
65
 
42
- def self.actions( view, action_builder )
66
+ def self.define_actions( view, action_builder )
43
67
  action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
44
68
  smart_copy( view )
45
69
  end
46
70
 
47
71
  action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
48
- invoice_from_project( view.current_index, view )
72
+ invoice_from_project( view, view.current_index ) do
73
+ # execute the block if the invoice is changed
74
+
75
+ # save this before selection model is cleared
76
+ current_index = view.current_index
77
+ view.selection_model.clear
78
+ view.current_index = current_index.choppy( :column => :start )
79
+ end
49
80
  end
50
81
  end
51
82
 
@@ -54,63 +85,68 @@ class Entry < ActiveRecord::Base
54
85
  view.sanity_check_read_only
55
86
  view.sanity_check_ditto
56
87
 
57
- # need a reference to current_index here, because selection_model.clear will invalidate
58
- # view.current_index. And anyway, its shorter and easier to read.
88
+ # need a reference to current_index here, because selection_model.clear will
89
+ # invalidate view.current_index. And anyway, its shorter and easier to read.
59
90
  current_index = view.current_index
60
- if current_index.row > 1
91
+ if current_index.row >= 1
61
92
  # fetch previous item
62
93
  previous_item = view.model.collection[current_index.row - 1]
63
94
 
64
95
  # copy the relevant fields
65
- current_index.entity.start = previous_item.end
66
- [:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
96
+ current_index.entity.date = previous_item.date if current_index.entity.date.blank?
97
+ # depends on previous line
98
+ current_index.entity.start = previous_item.end if current_index.entity.date == previous_item.date
99
+
100
+ # copy rest of fields
101
+ [:project, :invoice, :activity, :module, :charge, :person].each do |attr|
67
102
  current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
68
103
  end
69
104
 
70
105
  # tell view to update
71
- top_left_index = current_index.choppy( :column => 0 )
72
- bottom_right_index = current_index.choppy( :column => view.model.fields.size - 1 )
73
- view.dataChanged( top_left_index, bottom_right_index )
106
+ view.model.data_changed do |change|
107
+ change.top_left = current_index.choppy( :column => 0 )
108
+ change.bottom_right = current_index.choppy( :column => view.model.fields.size - 1 )
109
+ end
74
110
 
75
- # move to end time field
76
- view.selection_model.clear
111
+ # move to the first empty time field
77
112
  next_field =
78
113
  if current_index.entity.start.blank?
79
114
  :start
80
115
  else
81
116
  :end
82
117
  end
83
- next_index = current_index.choppy( :column => view.field_column( next_field ) )
84
- view.override_next_index( next_index )
118
+
119
+ # next cursor location
120
+ view.selection_model.clear
121
+ view.current_index = current_index.choppy( :column => next_field )
85
122
  end
86
123
  end
87
124
 
88
- # called when data is changed in this model's table view
89
- def self.data_changed( top_left, bottom_right, view )
90
- invoice_from_project( top_left, view ) if ( top_left == bottom_right )
91
- end
92
-
93
- # auto-complete invoice number field from project
94
- def self.invoice_from_project( current_index, view )
95
- current_field = current_index.attribute
96
- if [:project,:invoice].include?( current_field ) && current_index.entity.project != nil
125
+ # Auto-complete invoice number field from project.
126
+ # &block will be executed if an invoice was assigned
127
+ # If block takes one parameter, pass the new invoice.
128
+ def self.invoice_from_project( table_view, current_index, &block )
129
+ if current_index.entity.project != nil
97
130
  # most recent entry, ordered in reverse
98
131
  invoice = current_index.entity.project.latest_invoice
99
-
100
132
  unless invoice.nil?
101
133
  # make a reference to the invoice
102
134
  current_index.entity.invoice = invoice
103
135
 
104
136
  # update view from top_left to bottom_right
105
- changed_index = current_index.choppy( :column => view.field_column( :invoice ) )
106
- view.dataChanged( changed_index, changed_index )
137
+ table_view.model.data_changed( current_index.choppy( :column => :invoice ) )
107
138
 
108
- # move edit cursor to start time field
109
- view.selection_model.clear
110
- view.override_next_index( current_index.choppy( :column => view.field_column( :start ) ) )
139
+ unless block.nil?
140
+ if block.arity == 1
141
+ block.call( invoice )
142
+ else
143
+ block.call
144
+ end
145
+ end
111
146
  end
112
147
  end
113
148
  end
149
+
114
150
  end
115
151
 
116
152
  class Invoice < ActiveRecord::Base
@@ -123,7 +159,7 @@ class Invoice < ActiveRecord::Base
123
159
  plain :invoice_number
124
160
  restricted :status, :set => ['not sent', 'sent', 'paid', 'debt', 'writeoff', 'internal']
125
161
  restricted :billing, :set => %w{Hours Quote Internal}
126
- plain :quote_date
162
+ plain :quote_date, :format => '%d-%b-%y', :edit_format => '%d-%b-%Y', :tooltip => 'the date and time when the quote was supplied', :default => lambda{|x| DateTime.now}
127
163
  plain :quote_amount
128
164
  plain :description
129
165
 
data/tasks/clevic.rake ADDED
@@ -0,0 +1,111 @@
1
+ task :package => :ui
2
+
3
+ desc "Update ChangeLog from the SVN log"
4
+ task :changelog do |t|
5
+ ARGV.shift
6
+ exec "svn2cl --break-before-msg -o ChangeLog #{ARGV.join(' ')}"
7
+ end
8
+
9
+ # generate a _ui.rb filename from a .ui filename
10
+ def ui_rb_file( ui_file )
11
+ ui_file.gsub( /\.ui$/, '_ui.rb' )
12
+ end
13
+
14
+ # list of .ui files
15
+ UI_FILES = FileList.new( 'lib/clevic/ui/*.ui' )
16
+ CLEAN.include( 'ChangeLog', 'coverage', 'profiling' )
17
+ CLOBBER.include( 'ChangeLog', 'pkg', 'lib/clevic/ui/*_ui.rb' )
18
+
19
+ UI_FILES.each do |ui_file|
20
+ # make tasks to generate _ui.rb files
21
+ file ui_rb_file( ui_file ) => [ ui_file ] do |t|
22
+ sh "rbuic4 #{t.prerequisites} -o #{t.name}"
23
+ end
24
+
25
+ # make tasks to start designer when the ui file is named
26
+ desc "Start Qt designer with #{ui_file}"
27
+ namespace :ui do |n|
28
+ task Pathname.new(ui_file).basename.to_s.ext do |t|
29
+ sh "designer #{ui_file}"
30
+ end
31
+ end
32
+ end
33
+
34
+ desc 'Generate all _ui.rb files'
35
+ task :ui => UI_FILES.map{|x| ui_rb_file( x ) }
36
+
37
+ namespace :ui do
38
+ desc 'Start Qt designer with the argument, or all .ui files.'
39
+ task :design do |t|
40
+ ARGV.shift()
41
+ if ARGV.size == 0
42
+ # start designer with all ui files
43
+ sh "designer #{UI_FILES.join(' ')}"
44
+ else
45
+ # start designer with all files that match an argument
46
+ sh "designer #{ ARGV.map{|x| UI_FILES.grep( /\/#{x}/ ) }.join(' ') }"
47
+ end
48
+ true
49
+ end
50
+ end
51
+
52
+ desc "Runs Clevic in normal mode, with live database."
53
+ task :run => :ui do |t|
54
+ ARGV.shift()
55
+ exec "ruby -Ilib bin/clevic #{ARGV.join(' ')}"
56
+ end
57
+
58
+ desc "Runs Clevic in debug mode, with test databases"
59
+ task :debug => :ui do |t|
60
+ ARGV.shift()
61
+ exec "ruby -w -rdebug -Ilib bin/clevic -D #{ARGV.join(' ')}"
62
+ end
63
+
64
+ desc "irb in this project's context"
65
+ task :irb do |t|
66
+ ARGV.shift()
67
+ ENV['RUBYLIB'] ||= ''
68
+ ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
69
+ exec "irb -Ilib -rclevic"
70
+ end
71
+
72
+ # generate tasks for all model definition files
73
+ MODELS_LIST = FileList.new( '**/*models.rb' )
74
+
75
+ def short_model( model_file )
76
+ Pathname.new( model_file ).basename.to_s.gsub( /_models.rb/, '' )
77
+ end
78
+
79
+ MODELS_LIST.each do |model_file|
80
+ # generate irb contexts
81
+ desc "irb with #{model_file}"
82
+ namespace :irb do
83
+ task short_model( model_file ) do |t|
84
+ ARGV.shift()
85
+ ARGV.shift() if ARGV[0] == '--'
86
+ ENV['RUBYLIB'] ||= '.'
87
+ ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
88
+ exec "irb -Ilib -rclevic -r#{model_file} -rclevic/db_options.rb"
89
+ end
90
+ end
91
+
92
+ # generate runs
93
+ namespace :run do
94
+ desc "run clevic with #{model_file}"
95
+ task short_model( model_file ) => :ui do |t|
96
+ ARGV.shift()
97
+ ARGV.shift() if ARGV[0] == '--'
98
+ cmd = "ruby -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
99
+ puts "cmd: #{cmd.inspect}"
100
+ exec cmd
101
+ end
102
+ end
103
+
104
+ namespace :warn do
105
+ desc "run clevic with #{model_file} and warnings on"
106
+ task short_model( model_file ) => :ui do |t|
107
+ ARGV.shift()
108
+ exec "ruby -w -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
109
+ end
110
+ end
111
+ end