tkxxs 0.1.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.
@@ -0,0 +1,259 @@
1
+ # encoding: windows-1252
2
+ # Copyright (c) 2010-2014 Axel Friedrich
3
+ $stdout.sync = true
4
+ $stderr.sync = true
5
+ $FILE_SEP = File::ALT_SEPARATOR || File::SEPARATOR
6
+ $__DIR__ = File.dirname( File.expand_path( __FILE__ ) ).gsub('\\', '/')
7
+ $__DIR0__ = File.dirname( File.expand_path( $0 ) ).gsub('\\', '/')
8
+ $: << File.dirname( File.expand_path( __FILE__ ) )
9
+ $:.uniq!
10
+
11
+ require 'fileutils'
12
+ require 'yaml'
13
+ require 'Platform'
14
+
15
+ class Object
16
+ ##################################################################
17
+ # ,dup,nil,Fixnum,can't dup Fixnum
18
+ #
19
+ # Potential solution for x = x.dup if x; x = x.is_a?(Fixnum) ? x : x.dup
20
+ #
21
+ # From: comp.lang.ruby, Feb 17 2007, "Oppinions on RCR for dup on immutable classes"
22
+ def dup?
23
+ dup # Better?: true
24
+ rescue TypeError
25
+ false
26
+ end
27
+ end # class Object
28
+
29
+ ##########################################################################
30
+ ##########################################################################
31
+ # Keywords: ,configuration ,ini ,preferences ,setup
32
+ # Helps storing and managing configuration data. At present, uses YAML-files to
33
+ # store the data.
34
+ #
35
+ # Important methods:
36
+ #
37
+ # Conf.new
38
+ # Conf#filename Returns the full path of file, where Conf is saved
39
+ # Conf#section= section section must be String, Integer, Float or nil, _not_ a Symbol
40
+ # Conf#or_default(key, defaultValue)
41
+ # Conf#[key]= value key must be a Symbol or String
42
+ # Conf#[key] Returns the stored value
43
+ # Conf#save Writes Conf to file
44
+ # Conf#instant_save = true/false: switches on save to file for every change of Conf
45
+ #
46
+ # Having keys and section of the same type and value is no problem.
47
+ #
48
+ # Example usage:
49
+ #
50
+ # require 'axel/conf'
51
+ # CONF = Conf.new # Filename for storing CONF can be given as attribute;
52
+ #
53
+ # class MyClass
54
+ #
55
+ # def initialize( )
56
+ # CONF.section = "my_section_1" # Optional, default is provided
57
+ # # Defines a section (hash key) where to read and store data
58
+ # p x = CONF.or_default( :x, 123 )
59
+ # # If run the first time, usually Conf["my_section"][:x] is not set to any value.
60
+ # # If Conf["my_section"][:x] is not set to any value, sets x to 123,
61
+ # # stores 123 in Conf["my_section"][:x] and saves it to the conf-File.
62
+ # # If run the next time, Conf["my_section"][:x] is already set to a value
63
+ # # and x is set to the value of Conf["my_section"][:x]
64
+ #
65
+ # p @y = CONF.or_default( :@y, "abc" )
66
+ #
67
+ # puts "Config-File is: " + CONF.filename
68
+ # myMethod(x)
69
+ # end # initialize
70
+ #
71
+ # def myMethod( x )
72
+ # x = x*x
73
+ # @y = "new_y"
74
+ #
75
+ # CONF[:x] = x # sets Conf["my_section"][:x] to the new value of x
76
+ # CONF[:@y] = @y
77
+ # puts "CONF[:x] is " + CONF[:x].inspect
78
+ # puts "CONF[:@y] is " + CONF[:@y].inspect
79
+ #
80
+ # CONF.save # saves the content of Conf.
81
+ # end # myMethod
82
+ #
83
+ # end # MyClass
84
+ #
85
+ # klass = MyClass.new
86
+ #
87
+ class Conf < Hash
88
+
89
+ alias of []
90
+ alias orig_to_hash to_hash
91
+
92
+ attr_accessor :instant_save
93
+ attr_reader :section
94
+ ##########################################################################
95
+ # Attributes: configFileDNE: path including filename and fileextension
96
+ def initialize( configFileDNE=Conf.filename_proposal )
97
+ #p :xxx
98
+ raise unless configFileDNE
99
+ @configFileDNE = configFileDNE.dup.strip.gsub('\\', '/')
100
+ @instant_save = false
101
+ @section = nil
102
+
103
+ if File.file?( @configFileDNE )
104
+ dataFromFile = read
105
+ self.replace( dataFromFile ) if dataFromFile
106
+ end
107
+ #exit
108
+ self.section = nil unless self.has_key?(nil) # section "nil" always needed
109
+
110
+ super()
111
+ save() # writeable?
112
+ end # initialize
113
+
114
+ def delete_conf_file( )
115
+ File.delete( @configFileDNE )
116
+ end # delete_conf_file
117
+
118
+ def to_hash( )
119
+ h = Hash.new
120
+ self.each {|key, val|
121
+ h[key] = val.dup ? val.dup : val
122
+ }
123
+ h
124
+ end # to_hash
125
+
126
+ ##################################################################
127
+ # Returns the filename of the config file
128
+ def filename( )
129
+ @configFileDNE
130
+ end # filename
131
+
132
+ ##################################################################
133
+ # Returns the value corresponding to key, while Config is pointing to the
134
+ # section set by Conf::section= . Attributes: key: Symbol or String
135
+ def []( key )
136
+ raise "Key must be a Symbol or String" unless key.is_a?( Symbol ) || key.is_a?( String )
137
+
138
+ res = self.of(@section)[key]
139
+ res = res.dup? ? res.dup : res
140
+ res
141
+ end # []
142
+
143
+ ##################################################################
144
+ # Sets the value for a corresponding key. If key does not exist, it will be created.
145
+ # Attributes: key: Symbol or String; value
146
+ def []=( key, value )
147
+ raise "key must be a Symbol or String" unless key.is_a?( Symbol ) || key.is_a?( String )
148
+
149
+ self.of(@section).store( key, value)
150
+ self.save if @instant_save
151
+ value
152
+ end # []=
153
+
154
+ ##################################################################
155
+ # If key is not an existing key, key will be created with value =
156
+ # defaultValue and saves it to the config-File. Returns defaultValue.
157
+ #
158
+ # If key is an exsiting key, the correspondig value will be returned.
159
+ def or_default( key, defaultValue )
160
+ raise "key must be a Symbol or String" unless key.is_a?( Symbol ) || key.is_a?( String )
161
+
162
+ if self.of(@section).has_key?( key ) # ToDo: Nicer: create Conf::has_key?
163
+ res = self[key]
164
+ else
165
+ self[key]= defaultValue
166
+ res = defaultValue
167
+ self.save ## if @instant_save # 2009-06-11
168
+ end
169
+
170
+ res
171
+ end # default[]=
172
+
173
+ ##################################################################
174
+ # Sets Config to a section. Different sections may have the same keys.
175
+ # section=nil is a valid section (it's somehow the 'root'-section)
176
+ # Attributes: section = any String, Integer or Float or nil; default is nil
177
+ def section=( section=nil )
178
+ unless ( section.is_a?(String) || section.is_a?(Integer) || section.is_a?(Float) || !section )
179
+ raise "Section must be String, Integer, Float or nil but #{ section.inspect } is a #{ section.class }!"
180
+ end
181
+ unless self.has_key?(section)
182
+ self.store(section, {})
183
+ self.save if @instant_save
184
+ end
185
+
186
+ @section = section.dup? ? section.dup : nil
187
+ end # section=
188
+
189
+ def read()
190
+ FileUtils.mkpath( File.dirname( @configFileDNE ) )
191
+ # Ruby 1.8.7 cannot read psych-formated YAML-Files. As of 2013-03-15, Ruby
192
+ # 1.8.7 and 1.9 can read syck-formated YAML Files. For compatibility, I
193
+ # choose syck-format.
194
+ YAML::ENGINE.yamler = "syck" if YAML.const_defined?( :ENGINE )
195
+ File.open( @configFileDNE ) { |f| YAML.load(f) }
196
+ end
197
+
198
+ ## def save()
199
+ ## FileUtils.mkpath( File.dirname( @configFileDNE ) )
200
+ ## File.open( @configFileDNE, "w" ) { |f| YAML.dump(self, f) }
201
+ ## end
202
+
203
+ def save()
204
+ FileUtils.mkpath( File.dirname( @configFileDNE ) )
205
+ YAML::ENGINE.yamler = "syck" if YAML.const_defined?( :ENGINE )
206
+ File.open( @configFileDNE, "w" ) { |f| YAML.dump(self, f) }
207
+ # I write a 2nd File in psych-format in case syck is not longer available
208
+ # sometimes.
209
+ if YAML.const_defined?( :ENGINE )
210
+ YAML::ENGINE.yamler = "psych"
211
+ File.open( @configFileDNE + '_psy', "w" ) { |f| YAML.dump(self, f) }
212
+ end
213
+ end
214
+
215
+
216
+ ##################################################################
217
+ # Deletes the config-File and clears all data of Conf.
218
+ def delete( )
219
+ File.delete(@configFileDNE)
220
+ self.clear
221
+ self
222
+ end # delete
223
+
224
+ ##################################################################
225
+ # From: Joel VanderWerf: "preferences-0.3.0".
226
+ # Utility method for guessing a suitable directory to store preferences.
227
+ # On win32, tries +APPDATA+, +USERPROFILE+, and +HOME+. On other platforms,
228
+ # tries +HOME+ and ~. Raises EnvError if preferences dir cannot be chosen.
229
+ # Not called by any of the Preferences library code, but available to the
230
+ # client code for constructing an argument to Preferences.new.
231
+ #
232
+ # Some modifications by Axel
233
+ def Conf.dir
234
+ case Platform::OS.to_s.downcase
235
+ when 'win32'
236
+ dir =
237
+ ENV['APPDATA'] || # C:\Documents and Settings\name\Application Data
238
+ ENV['USERPROFILE'] || # C:\Documents and Settings\name
239
+ ENV['HOME']
240
+ else
241
+ dir =
242
+ ENV['HOME'] ||
243
+ File.expand_path('~')
244
+ end
245
+
246
+ unless dir
247
+ raise EnvError, "Can't determine a configuration directory."
248
+ end
249
+ dir = dir.gsub('\\', '/')
250
+ dir
251
+ end # Conf.dir
252
+
253
+ def Conf.filename_proposal( extension='.cfg' )
254
+ fileN = File.basename( $0, File.extname($0) )
255
+ configFileDNE = File.join( Conf.dir, fileN, fileN + extension.to_s)
256
+ configFileDNE
257
+ end # Conf.filename_proposal
258
+
259
+ end # class Conf
@@ -0,0 +1,1571 @@
1
+ # encoding: Windows-1252 :encoding=Windows-1252:
2
+ # Copyright (c) Axel Friedrich 2010-2014
3
+ STDOUT.sync = true
4
+ STDERR.sync = true
5
+
6
+ ##########################################################################
7
+ ##########################################################################
8
+ class Hash
9
+
10
+ ##################################################################
11
+ # Somewhat deeper dup
12
+ def dup2!( )
13
+ self.each_pair {|k,v|
14
+ self[k] = v.dup if v.class == String
15
+ }
16
+ self
17
+ end # dup2!
18
+
19
+ end # class Hash
20
+
21
+ ##########################################################################
22
+ ##########################################################################
23
+ # Creates dialogs for TKXXS
24
+ module TKXXS_CLASSES
25
+
26
+ ##################################################################
27
+ # Purpose:
28
+ # Interpret last argument always as +Hash#, independent from the
29
+ # number of arguments:
30
+ # * Searches for the first argument which is a Hash, from right to
31
+ # left, starting with the last argument. (TODO: Why not simply
32
+ # detect if the last argument is a Hash?)
33
+ # * If Hash exists:
34
+ # ** Move the hash to the last element of args.
35
+ # ** Set the element, where the Hash comes from to nil, if it was
36
+ # not the last element.
37
+ # ** All other elements remain unchanged.
38
+ # * If no Hash exist:
39
+ # ** Sets the last element of args to {}
40
+ # ** All other elements remain unchanged.
41
+ #
42
+ # *Returns:* The rearranged args.
43
+ #
44
+ # Example:
45
+ # def m1( c=nil,b=nil,a=nil,h=nil )
46
+ # c, b, a, hash = args_1(c,b,a,h)
47
+ # p [c, b, a, hash]
48
+ #
49
+ # hash = {:a=>'aa',
50
+ # :b=>'bb',
51
+ # :c=>'cc'
52
+ # }.merge(hash)
53
+ #
54
+ # c = hash[:c] unless c
55
+ # b = hash[:b] unless b
56
+ # a = hash[:a] unless a
57
+ # nil
58
+ # end # m1
59
+ #
60
+ # m1(:c,:b,:a, {:h=>'hh'}) # => [:c, :b, :a, {:h=>"hh"}]
61
+ # m1(:c, {:h=>'hh'}) # => [:c, nil, nil, {:h=>"hh"}]
62
+ # m1(:c) # => [:c, nil, nil, {}]
63
+ # m1() # => [nil, nil, nil, {}]
64
+ #
65
+ def TKXXS_CLASSES.args_1( *args )
66
+ args = args.reverse
67
+ args.each_with_index {|arg, i|
68
+ if arg.class == Hash
69
+ args[i] = nil
70
+ args[0] = arg
71
+ break
72
+ end
73
+ }
74
+ args[0] =
75
+ unless args[0]
76
+ {}
77
+ else
78
+ ## Marshal.load(Marshal.dump( args[0] ) ) # Doesn't work because of Proc
79
+ args[0].dup2! # args[0] must always be a Hash
80
+ end
81
+ args = args.reverse
82
+ args
83
+ end # TKXXS_CLASSES.args_1
84
+
85
+ ##################################################################
86
+ # Set the value of new variables from the values of the hash.
87
+ #
88
+ # New variables are _always_: +question+ and +help+.
89
+ #
90
+ # 'hash' gets modified.
91
+ #
92
+ # On the left side of the equal sign: _always_ 'help, hash,
93
+ # question', thereafter the additional variables.
94
+ #
95
+ # Arguments of args_2: 'help,hash,question' and then the (optional)
96
+ # hash-keys, which shall be assigned to the additional variables.
97
+ # :help and :question can be put at arbitrary position, or omitted.
98
+ # hash _must_ have: :configSection, :question.
99
+ #
100
+ # If the variable +help+ and/or +question+ are already set (not
101
+ # equal +nil+), then they leave unchanged.
102
+ #
103
+ # EXAMPLE:
104
+ # help = 'h'
105
+ # hash = {
106
+ # :help=>'heeelp',
107
+ # :question=>'MyQuestion',
108
+ # :title=>'MyTitle',
109
+ # :SomethingElse=>'bla'
110
+ # }
111
+ # help, hash, question, title =
112
+ # TKXXS_CLASSES.args_2( help, hash, question, :title)
113
+ # # => ["h", {:SomethingElse=>"bla"}, "MyQuestion", "MyTitle"]
114
+ #
115
+ # * 'h' in +help+ _not_ overwritten, because +help+ was defined before;
116
+ # * Every key in +hash+ deleted, which corresponds to the args of +args_2+;
117
+ # * +question+ now set, because it was _not_ defined before;
118
+ # * +title+ set to <tt>hash[:title]</tt>
119
+ def TKXXS_CLASSES.args_2( help,hash,question,*keys )
120
+ hash.delete(:configSection)
121
+ q = hash.delete(:question)
122
+ question = q unless question
123
+ h = hash.delete(:help)
124
+ help = h unless help
125
+
126
+ values = [help,hash,question]
127
+ keys.each {|key|
128
+ val = hash.delete(key)
129
+ values << val
130
+ }
131
+ values[1] = hash
132
+ values
133
+ end # TKXXS_CLASSES.args_2
134
+
135
+ ##########################################################################
136
+ ##########################################################################
137
+ class TextW < TkText
138
+ include Tk::Tile
139
+ def initialize( parent, hash={} )
140
+ #---- Frame
141
+ @frame = Frame.new(parent) {padding "3 3 3 3"}.
142
+ pack(:fill=>:both, :expand=>true)
143
+
144
+ hash = Marshal.load(Marshal.dump( hash ) ) # Should work because never uses Proc
145
+
146
+ hash = {
147
+ :font=>'"Courier New" 10',
148
+ :wrap=>'word',
149
+ :bd=>'2'
150
+ }.merge(hash)
151
+
152
+ #---- self = TkText
153
+ super(@frame, hash)
154
+
155
+ #---- Scrollbars
156
+ sy = Scrollbar.new(@frame)
157
+ sy.pack('side'=>'right', 'fill'=>'y')
158
+ self.yscrollbar(sy)
159
+
160
+ sx = Scrollbar.new(@frame)
161
+ sx.pack('side'=>'bottom', 'fill'=>'x')
162
+ self.xscrollbar(sx)
163
+
164
+ self.pack('expand'=>'yes', 'fill'=>'both')
165
+ end # initialize
166
+
167
+ end # class TextW
168
+
169
+ ##########################################################################
170
+ ##########################################################################
171
+ class AskSingleLineD < Tk::Tile::Entry
172
+ include Tk::Tile
173
+ CONF = nil unless defined?(CONF)
174
+ attr_accessor :dialog # in: "self.dialog
175
+
176
+ ##########################################################################
177
+ # See: TKXXS.ask_single_line
178
+ def initialize( question=nil, help=nil, hash=nil )
179
+ question, help, hash = TKXXS_CLASSES.args_1(question, help, hash)
180
+
181
+ # Must always include: :question, :help, :configSection
182
+ hash = { # Default values
183
+ :question => "?",
184
+ :help => nil,
185
+ :configSection => nil,
186
+ :defaultEntry => '',
187
+ }.merge(hash)
188
+
189
+ # Necessary, because hash[:configSection] is deleted later on
190
+ # with +args_2+.
191
+ CONF.section = hash[:configSection] if CONF # ?
192
+
193
+ help, hash, question, defaultEntry =
194
+ TKXXS_CLASSES.args_2(help,hash,question,:defaultEntry)
195
+
196
+ teardownDone = false
197
+ @ans = nil
198
+ @goOn = goOn = TkVariable.new
199
+ # Because you cannot use instance-vars in procs. TODO: smarter way?
200
+
201
+ Tk.update # Show any calling widget first to make it lower than @dialog
202
+ #---- Toplevel: @dialog
203
+ @dialog=Toplevel.new() {title "Please enter your answer..."}
204
+ @dialog.geometry = CONF[:dialogGeom] if CONF
205
+ @dialog.raise
206
+ @dialog.bind('Destroy') {
207
+ unless teardownDone
208
+ if CONF
209
+ CONF[:dialogGeom] = @dialog.geometry
210
+ CONF.section = nil
211
+ end
212
+ goOn.value = '1' # !
213
+ teardownDone = true
214
+ end
215
+ }
216
+
217
+ #---- Frame
218
+ @frame = Frame.new(@dialog) {padding "3 3 3 3"}.
219
+ pack(:fill=>:both,:expand=>true)
220
+
221
+ #---- Label
222
+ @lbl = Label.new(@frame, :text=>question){
223
+ font $font if $font
224
+ wraplength '5i'
225
+ justify 'left'
226
+ }
227
+ @lbl.grid(:column=>0,:columnspan=>3,:sticky=>'ws')
228
+
229
+ #---- self = TkEntry
230
+ super(@frame, hash)
231
+ self.grid(:column=>0,:columnspan=>3,:sticky=>'ew')
232
+ self.insert(0, defaultEntry)
233
+ bind('Key-Return') {
234
+ ans_( self.get )
235
+ self.dialog.destroy
236
+ goOn.value = '1'
237
+ }
238
+
239
+ #---- Button-Cancel
240
+ @btn = Button.new(@frame, :text=>"Cancel").
241
+ grid(:column=>0,:row=>2)
242
+ @btn.command {
243
+ self.dialog.destroy
244
+ }
245
+
246
+ #---- Button-OK
247
+ @btn2 = Button.new(@frame, :text=>"OK").
248
+ grid(:column=>2,:row=>2)
249
+ @btn2.command {
250
+ ans_( self.get )
251
+ self.dialog.destroy
252
+ goOn.value = '1'
253
+ }
254
+
255
+ #---- Balloonhelp
256
+ if help
257
+ BalloonHelp.new(self, :text => help )
258
+ end
259
+
260
+ @frame.grid_columnconfigure([0,2],:weight=>0)
261
+ @frame.grid_columnconfigure(1,:weight=>1)
262
+ @frame.grid_rowconfigure(0,:weight=>1)
263
+ @frame.grid_rowconfigure([1,2],:weight=>0)
264
+
265
+ focus
266
+ update
267
+ end # initialize
268
+
269
+ ##########################################################################
270
+ # *Returns*: (String or nil) The answer of the dialog. 'Cancel' returns nil.
271
+ def answer( )
272
+ @dialog.raise
273
+ @goOn.wait
274
+ @ans
275
+ end # ans
276
+
277
+ private
278
+
279
+ def private____________________( )
280
+ end # private____________________
281
+
282
+ def ans_( ans ) # TODO: Easier way?
283
+ @ans = ans.dup if ans
284
+ end # ans=
285
+
286
+ end # class AskSingleLineD
287
+
288
+
289
+ ##########################################################################
290
+ ##########################################################################
291
+ class AskMultiLineD < TkText
292
+ # class AskMultiLineD < TextW
293
+ include Tk::Tile
294
+ CONF = nil unless defined?(CONF)
295
+ attr_accessor :dialog # in: "self.dialog
296
+
297
+ ##################################################################
298
+ # TODO: implement in TKXXS.
299
+ def initialize( question=nil, help=nil, hash=nil )
300
+ question, help, hash = TKXXS_CLASSES.args_1(question, help, hash)
301
+
302
+ # Must always include: :question, :help, :configSection
303
+ hash = {
304
+ :question => "?",
305
+ :help => nil,
306
+ :configSection => nil,
307
+ :title => "Please enter your answer..."
308
+ }.merge(hash)
309
+
310
+ # Necessary, because hash[:configSection] is deleted later on
311
+ # in args_2.
312
+ CONF.section = hash[:configSection] if CONF # ?
313
+
314
+ # help, hash, question, title =
315
+ # TKXXS_CLASSES.args_2( help, hash, question, :title)
316
+ help, hash, question, title =
317
+ TKXXS_CLASSES.args_2( help, hash, question, :title)
318
+
319
+ teardownDone = false
320
+ @ans = nil
321
+ @goOn = goOn = TkVariable.new
322
+ # Because you cannot use instance-vars in procs. TODO: smarter way?
323
+
324
+ Tk.update # Show any calling widget first to make it lower than @dialog
325
+ #---- Toplevel: @dialog
326
+ # @dialog=Toplevel.new() {title('title')}
327
+ @dialog=Toplevel.new() {title(title)}
328
+ @dialog.geometry = CONF[:dialogGeom] if CONF
329
+ @dialog.raise
330
+ @dialog.bind('Destroy') {
331
+ unless teardownDone
332
+ if CONF
333
+ CONF[:dialogGeom] = @dialog.geometry
334
+ CONF.section = nil
335
+ end
336
+ goOn.value = '1' # !
337
+ teardownDone = true
338
+ end
339
+ }
340
+
341
+ #---- Frame
342
+ @frame = Frame.new(@dialog) {padding "3 3 3 3"}.
343
+ pack(:fill=>:both,:expand=>true)
344
+
345
+ #---- Label
346
+ @lbl = Label.new(@frame, :text=>question){
347
+ font $font if $font
348
+ wraplength '5i'
349
+ justify 'left'
350
+ }
351
+ @lbl.grid(:sticky=>'news')
352
+
353
+ #---- self = TextW
354
+ super(@frame, hash)
355
+ self.grid(:column=>0,:row=>1,:sticky=>'news')
356
+ @tagSel = TkTextTagSel.new(self)
357
+ bind('Control-Key-a') {
358
+ # From:
359
+ # "Using Control-a to select all text in a text widget : TCL",
360
+ # http://objectmix.com/tcl/35276-using-control-select-all-text-text-widget.html
361
+ @tagSel.add('0.0', :end)
362
+ Kernel.raise TkCallbackBreak
363
+ }
364
+ ##/ bind('Key-Return') {
365
+ ##/ ans_( self.get )
366
+ ##/ self.dialog.destroy
367
+ ##/ goOn.value = '1'
368
+ ##/ }
369
+
370
+ #---- Scrollbars
371
+ sy = Scrollbar.new(@frame)
372
+ sy.grid(:column=>1,:row=>1,:sticky=>'ns')
373
+ self.yscrollbar(sy)
374
+
375
+ sx = Scrollbar.new(@frame)
376
+ sx.grid(:column=>0,:row=>2,:sticky=>'ew')
377
+ self.xscrollbar(sx)
378
+
379
+ #---- Button-Cancel
380
+ @btn = Button.new(@frame, :text=>"Cancel").
381
+ grid(:row=>3,:column=>0,:sticky=>'w')
382
+ @btn.command {
383
+ self.dialog.destroy
384
+ }
385
+
386
+ #---- Button-OK
387
+ @btn2 = Button.new(@frame, :text=>"OK").
388
+ grid(:row=>3,:column=>0,:sticky=>'e')
389
+ ## grid(:row=>3,:column=>0,:sticky=>'e',:columnspan=>2)
390
+ @btn2.command {
391
+ got = self.get('0.0', 'end -1c')
392
+ ans_( got )
393
+ self.dialog.destroy
394
+ goOn.value = '1'
395
+ }
396
+
397
+ @frame.grid_columnconfigure(0, :weight=>1)
398
+ @frame.grid_columnconfigure(1, :weight=>0)
399
+ @frame.grid_rowconfigure([0,2,3], :weight=>0)
400
+ @frame.grid_rowconfigure(1, :weight=>1)
401
+ @lbl.bind('Configure'){
402
+ @lbl.wraplength = TkWinfo.width(@frame) - 40
403
+ }
404
+
405
+ #---- Balloonhelp
406
+ BalloonHelp.new(self,:text => help) if help
407
+
408
+ self.focus
409
+ ##Tk.update
410
+ end # initialize
411
+
412
+ def answer( )
413
+ @dialog.raise
414
+ @goOn.wait
415
+ @ans
416
+ end # ans
417
+
418
+ private
419
+
420
+ def private____________________( )
421
+ end # private____________________
422
+
423
+ def ans_( ans ) # Is there an easier way?
424
+ @ans = ans.dup if ans
425
+ end # ans=
426
+
427
+ end # class AskMultiLineD
428
+
429
+
430
+ ##########################################################################
431
+ ##########################################################################
432
+ class SingleChoiceD < TkListbox
433
+ include Tk::Tile
434
+ CONF = nil unless defined?(CONF)
435
+
436
+ attr_accessor :dialog # in: "self.dialog
437
+
438
+
439
+ ##########################################################################
440
+ # See: TKXXS::single_choice
441
+ def initialize( aryWithChoices, help=nil, hash=nil )
442
+ aryWithChoices, help, hash = TKXXS_CLASSES.args_1(aryWithChoices, help, hash)
443
+
444
+ @allChoices, @allClients, @allHelp = all_choices_clients_help(aryWithChoices)
445
+
446
+ # Must always include: :question, :help, :configSection
447
+ hash = {
448
+ :question => "Choose one by single-click",
449
+ :help => nil,
450
+ :configSection => nil,
451
+ :title => 'Choose one',
452
+ :bd=>'2', # TODO: noch ben�tigt?
453
+ :searchFieldHelp => "Enter RegExp for filter list here\n(not case sensitive)",
454
+ :returnChoiceAndClient=>false
455
+ }.merge(hash)
456
+
457
+ # Necessary, because hash[:configSection] is deleted later on
458
+ # in args_2.
459
+ CONF.section = hash[:configSection] if CONF # ?
460
+
461
+ # help, hash, question, title =
462
+ # TKXXS_CLASSES.args_2( help, hash, question, :title)
463
+ help,hash,question,title,searchFieldHelp,@returnChoiceAndClient=
464
+ TKXXS_CLASSES.args_2(help,hash,question,:title,:searchFieldHelp,:returnChoiceAndClient)
465
+ @allHelp ||= help
466
+
467
+ teardownDone = false
468
+ @ans = nil
469
+ @goOn = goOn = TkVariable.new
470
+ # Because you cannot use instance-vars in procs. TODO: smarter way?
471
+
472
+ @choices, @clients, @help = @allChoices, @allClients, @allHelp
473
+
474
+ Tk.update # Show any calling widget first to make it lower than @dialog
475
+ #---- Toplevel: @dialog
476
+ @dialog=Toplevel.new() {title(title)}
477
+ @dialog.geometry = CONF[:dialogGeom] if CONF
478
+ @dialog.bind('Destroy') {
479
+ unless teardownDone
480
+ goOn.value = '1' # !
481
+ $tkxxs_.delete(object_id)
482
+ if CONF
483
+ CONF[:dialogGeom] = @dialog.geometry
484
+ CONF.section = nil
485
+ ##/ CONF.save
486
+ end
487
+ teardownDone = true
488
+ end # unless
489
+ }
490
+
491
+ #---- Top Frame
492
+ @frame = Frame.new(@dialog) {padding "3 3 3 3"}.
493
+ pack(:fill=>:both,:expand=>true)
494
+
495
+ #---- Label
496
+ @lbl = Label.new(@frame, :text=>question).
497
+ grid(:sticky=>'ew')
498
+
499
+ #---- Search-Entry
500
+ filterRegex = nil
501
+ @entry = Entry.new(@frame) {|ent|
502
+ grid(:sticky=>'ew')
503
+ BalloonHelp.new(ent,:text => searchFieldHelp)
504
+ }
505
+
506
+ @entry.bind('Key-Return') { list_entry_chosen( goOn ) }
507
+ @entry.bind('Key-Down') { self.focus }
508
+ @entry.bind('Any-KeyRelease') {
509
+ entryVal = @entry.get
510
+ begin
511
+ filterRegex = Regexp.new(entryVal, Regexp::IGNORECASE)
512
+ populate_list( filterRegex )
513
+ rescue RegexpError
514
+ @lbl.text = "RegexpError"
515
+ else
516
+ @lbl.text = question
517
+ end
518
+
519
+ }
520
+ @entry.focus
521
+
522
+ # self = TkListbox
523
+ # TODO: Kein Weg, das interne Padding zu vergr��ern??
524
+ super(@frame, hash)
525
+ self.grid(:column=>0,:sticky=>'news')
526
+ self.bind('ButtonRelease-1') {
527
+ list_entry_chosen( goOn )
528
+ }
529
+ self.bind('Key-Return') {
530
+ list_entry_chosen( goOn )
531
+ }
532
+
533
+ #---- Scrollbar
534
+ scrollb = Scrollbar.new(@frame).grid(:column=>1,:row=>2,:sticky=>'ns')
535
+ self.yscrollbar(scrollb)
536
+ ##self.width = 0 # Width as needed
537
+ ##self.height = 30 # TODO: Make configurable
538
+
539
+ #---- Button-Cancel
540
+ @btn = Button.new(@frame, :text=>"Cancel").
541
+ grid(:row=>3,:column=>0,:sticky=>'w')
542
+ @btn.command {
543
+ self.destroy
544
+ }
545
+
546
+ @frame.grid_columnconfigure(0, :weight=>1)
547
+ @frame.grid_columnconfigure(1, :weight=>0)
548
+ @frame.grid_rowconfigure([0,1,3], :weight=>0)
549
+ @frame.grid_rowconfigure(2, :weight=>1)
550
+
551
+ #---- Balloonhelp
552
+ if @help
553
+ $tkxxs_[object_id] = {:listBalloonHelp=>@help.dup}
554
+ list_balloonhelp
555
+ end
556
+
557
+ populate_list(nil)
558
+ ##/ insert(0, *allChoices)
559
+ ##/ self.selection_set 0
560
+ update
561
+ end # initialize
562
+
563
+ def all_choices_clients_help( aryWithChoices, filterRegex=nil )
564
+ return [ [], [], nil] if !aryWithChoices || aryWithChoices.empty?
565
+
566
+ case aryWithChoices[0].class.to_s
567
+ when 'String'
568
+ choices = aryWithChoices
569
+ clients = choices.dup
570
+ ## choicesAndClients = choices.zip(clients)
571
+ help = nil
572
+ when 'Array'
573
+ choices, clients, tmpHelp = aryWithChoices.transpose
574
+ ## choicesAndClients = aryWithChoices
575
+ help = tmpHelp if tmpHelp
576
+ else
577
+ raise "ERROR: aryWithChoices[0].class must be 'String' or 'Array',\n" +
578
+ "but is #{ aryWithChoices[0].inspect }.class = #{ aryWithChoices[0].class } "
579
+ end
580
+
581
+ [choices, clients, help]
582
+ end # all_choices_clients_help
583
+
584
+ def populate_list( filterRegex=nil )
585
+ if filterRegex
586
+ @choices, @clients, help = filtered_choices_clients_help( filterRegex )
587
+ else
588
+ @choices, @clients, help = @allChoices, @allClients, @allHelp
589
+ end
590
+
591
+ self.delete(0, :end)
592
+ if @choices.empty?
593
+ self.insert(0, [] )
594
+ else
595
+ self.insert(0, *@choices)
596
+ self.selection_set(0)
597
+ self.activate(0)
598
+ ## puts sprintf("--DEBUG: $tkxxs_: %1s ", $tkxxs_.inspect) #loe
599
+ ## puts sprintf("--DEBUG: object_id: %1s ", object_id.inspect) #loe
600
+ ## puts sprintf("--DEBUG: help: %1s ", help.inspect) #loe
601
+ $tkxxs_[object_id][:listBalloonHelp].replace(help) if help
602
+ end
603
+ end # populate_list
604
+
605
+ def filtered_choices_clients_help( filterRegex )
606
+ choices = []
607
+ clients = []
608
+ help = []
609
+ @allChoices.each_with_index {|ch, idx|
610
+ if ch[filterRegex]
611
+ choices << ch
612
+ clients << @allClients[idx]
613
+ help << @allHelp[idx] if @allHelp # ToDo: Gef�hrlich, falls nicht Array
614
+ end
615
+ }
616
+ help = nil if help.empty? # +2010-02-15 F�r pathchooser
617
+
618
+ [choices, clients, help]
619
+ end # filtered_choices_clients_help
620
+
621
+ def list_entry_chosen( goOn )
622
+ idx = self.curselection[0]
623
+ ans_( idx )
624
+ goOn.value = '1'
625
+ end # list_entry_chosen
626
+
627
+ def list_balloonhelp( )
628
+ bbb = BalloonHelp.new(self,
629
+ :command=>proc{|x,y,bhelp,parent|
630
+ idx = parent.nearest(y)
631
+ bhelp.text($tkxxs_[object_id][:listBalloonHelp][idx])
632
+ }
633
+ )
634
+ nil
635
+ end # list_balloonhelp
636
+
637
+ ##################################################################
638
+ # *Returns:*
639
+ # The right side of +aryWithChoices+ if :returnChoiceAndClient == +false+ (default),
640
+ # both sides of +aryWithChoices+ if :returnChoiceAndClient == +true+,
641
+ # +nil+, if 'Cancel' was clicked.
642
+ def answer( )
643
+ @goOn.wait
644
+ self.dialog.destroy
645
+ idx = @ans
646
+ if @returnChoiceAndClient
647
+ @ans = [ @choices[idx], @clients[idx] ]
648
+ res = @ans
649
+ else
650
+ res = @ans ? @clients[idx] : nil
651
+ end
652
+ res
653
+ end # ans
654
+
655
+ ##/ def answer( opts={:both=>false} )
656
+ ##/ @goOn.wait
657
+ ##/ self.dialog.destroy
658
+ ##/ idx = @ans
659
+ ##/ if opts[:both]
660
+ ##/ @ans = [ @choices[idx], @clients[idx] ]
661
+ ##/ res = @ans
662
+ ##/ else
663
+ ##/ res = @ans ? @clients[idx] : nil
664
+ ##/ end
665
+ ##/ res
666
+ ##/ end # ans
667
+
668
+ private
669
+
670
+ def private____________________( )
671
+ end # private____________________
672
+
673
+ def ans_( ans ) # Is there an easier way?
674
+ @ans = ans if ans
675
+ end # ans=
676
+
677
+ end # class SingleChoiceD
678
+
679
+ ##########################################################################
680
+ ##########################################################################
681
+ # MultiChoiceD has no search box like SingleChoiceD, up to now.
682
+ class MultiChoiceD < TkListbox
683
+ include Tk::Tile
684
+ CONF = nil unless defined?(CONF)
685
+ attr_accessor :dialog # in: "self.dialog
686
+
687
+ ##################################################################
688
+ # See: TKXXS::multi_choice
689
+ def initialize( aryWithChoices, help=nil, hash=nil )
690
+ aryWithChoices, help, hash = TKXXS_CLASSES.args_1(aryWithChoices, help, hash)
691
+ hash = {
692
+ :selectmode => :multiple,
693
+ :question => "Choose multiple:",
694
+ :help => nil,
695
+ :bd=>'2',
696
+ :configSection => nil,
697
+ :title=>'Please choose multiple...',
698
+ :returnChoiceAndClient=>false
699
+ }.merge(hash)
700
+
701
+ # Necessary, because hash[:configSection] is deleted later on
702
+ # in args_2.
703
+ CONF.section = hash[:configSection] if CONF # ?
704
+
705
+ help,hash,question,title,searchFieldHelp,@returnChoiceAndClient=
706
+ TKXXS_CLASSES.args_2(help,hash,question,:title,:searchFieldHelp,:returnChoiceAndClient)
707
+
708
+ ##/ question = hash[:question] unless question
709
+ ##/ help = hash[:help] unless help
710
+ ##/ CONF.section = hash[:configSection] if CONF
711
+ ##/
712
+ ##/ hash.delete :question
713
+ ##/ hash.delete :help
714
+ ##/ hash.delete :configSection
715
+
716
+ teardownDone = false
717
+ @ans = nil
718
+ @goOn = goOn = TkVariable.new
719
+ # Because you cannot use instance-vars in procs. TODO: smarter way?
720
+ case aryWithChoices[0].class.to_s
721
+ when 'String'
722
+ choices = aryWithChoices
723
+ clients = choices.dup
724
+ choicesAndClients = choices.zip(clients)
725
+ when 'Array'
726
+ choices, clients, tmpHelp = aryWithChoices.transpose
727
+ choicesAndClients = aryWithChoices
728
+ help = tmpHelp if tmpHelp
729
+ else
730
+ raise "ERROR: aryWithChoices[0].class must be 'String' or 'Array',\nbut is #{ aryWithChoices[0].inspect }.class = #{ aryWithChoices[0].class } "
731
+ end
732
+
733
+ Tk.update # Show any calling widget first to make it lower than @dialog
734
+ #---- Toplevel: @dialog
735
+ @dialog=Toplevel.new() {title(title)}
736
+ @dialog.geometry = CONF[:dialogGeom] if CONF
737
+ @dialog.bind('Destroy') {
738
+ unless teardownDone
739
+ goOn.value = '1' # !
740
+ if CONF
741
+ CONF[:dialogGeom] = @dialog.geometry
742
+ CONF.section = nil
743
+ end
744
+ teardownDone = true
745
+ end # unless
746
+ }
747
+
748
+ #---- Frame
749
+ @frame = Frame.new(@dialog) {padding "3 3 3 3"}.
750
+ pack(:fill=>:both,:expand=>true)
751
+
752
+ #---- Label
753
+ @lbl = Label.new(@frame){
754
+ text question
755
+ font $font if $font
756
+ wraplength '400'
757
+ justify 'left'
758
+ }
759
+ @lbl.grid(:sticky=>'news')
760
+
761
+ #---- self = TkListbox
762
+ super(@frame, hash)
763
+ # TODO: Is there no way to increase the internal padding??
764
+ #self.pack(:side=>:left,:expand=>1,:fill=>:both)
765
+ self.grid(:sticky=>'news')
766
+
767
+ #---- Scrollbar
768
+ scrollb = Scrollbar.new(@frame).
769
+ grid(:row=>1,:column=>1,:sticky=>'news')
770
+ self.yscrollbar(scrollb)
771
+
772
+ #---- Button-Cancel
773
+ @btn = Button.new(@frame, :text=>"Cancel").
774
+ grid(:row=>2,:column=>0,:sticky=>'w')
775
+ @btn.command {
776
+ self.destroy
777
+ }
778
+
779
+ #---- Button-OK
780
+ @btn = Button.new(@frame, :text=>"OK").
781
+ grid(:row=>2,:column=>0,:sticky=>'e')
782
+ @btn.command {
783
+ ans = []
784
+ # TODO: Change to self.curselection like in SingleChoiceD
785
+ begin
786
+ choicesAndClients.each_with_index {|cc, idx|
787
+ ans << cc if self.selection_includes(idx)
788
+ }
789
+ rescue
790
+ ans = []
791
+ end
792
+ ans_( ans )
793
+ goOn.value = '1'
794
+ }
795
+
796
+ @frame.grid_columnconfigure(0, :weight=>1)
797
+ @frame.grid_columnconfigure(1, :weight=>0)
798
+ @frame.grid_rowconfigure([0,2], :weight=>0)
799
+ @frame.grid_rowconfigure(1, :weight=>1)
800
+ insert(0, *choices)
801
+ @lbl.bind('Configure'){
802
+ @lbl.wraplength = TkWinfo.width(@frame) - 40
803
+ }
804
+
805
+ #---- Balloonhelp
806
+ if help
807
+ BalloonHelp.new(self,
808
+ :command=>proc{|x,y,bhelp,parent|
809
+ idx = parent.nearest(y)
810
+ bhelp.text( help[idx] )
811
+ }
812
+ )
813
+ end
814
+
815
+ update
816
+ end # initialize
817
+
818
+ ##/ def answer( )
819
+ ##/ @goOn.wait
820
+ ##/ self.dialog.destroy # includes grab("release")
821
+ ##/ @ans = [] unless @ans
822
+ ##/ @ans
823
+ ##/ end # ans
824
+ ##################################################################
825
+ # *Returns:*
826
+ # * An Array of the chosen right sides of +aryWithChoices+ if :returnChoiceAndClient == +false+ (default),
827
+ # * An Array of the chosen right and left sides of +aryWithChoices+ if :returnChoiceAndClient == +true+,
828
+ # * +nil+, if 'Cancel' was clicked.
829
+ #
830
+ def answer( )
831
+ @goOn.wait
832
+ self.dialog.destroy
833
+ @ans = [] unless @ans
834
+
835
+ if @returnChoiceAndClient
836
+ return @ans.transpose[0].zip(@ans.transpose[1]) # wg. help
837
+ else
838
+ return @ans.transpose[1]
839
+ end
840
+ end # ans
841
+
842
+ private
843
+
844
+ def private____________________( )
845
+ end # private____________________
846
+
847
+ def ans_( ans ) # Is there an easier way?
848
+ @ans = ans.dup if ans
849
+ ans
850
+ end # ans=
851
+
852
+ end # class MultiChoiceD
853
+
854
+ class FileAndDirChooser
855
+ include Tk::Tile
856
+ CONF = nil unless defined?(CONF)
857
+
858
+ attr_accessor :dialog # in: "self.dialog
859
+
860
+ ##########################################################################
861
+ # This class provides the common functions for all dir- and file-choosers.
862
+ #
863
+ # To get this dialog explain, run the example and point the mouse
864
+ # at each button.
865
+ #
866
+ # *Params*:
867
+ # * +initialdir+ - (String, optional) Initial dir; default = +nil+
868
+ # -> Working dir at the time of calling this method.
869
+ # * +help+ - (String, optional) ; Text used in the BalloonHelp;
870
+ # default = +nil+ -> No help.
871
+ # * +hash+ - (Hash, optional)
872
+ # * <tt>:initialdir</tt> - Like above.
873
+ # * <tt>:help</tt> - Like above.
874
+ # * <tt>:mode</tt> - One of :choosedir, :openfile, :openfiles, :savefile.
875
+ # * <tt>:question</tt> - (String) Your question; +nil+ -> no question.
876
+ # * <tt>:title</tt> - (String) Title of the dialog window.
877
+ # * <tt>:defaultEntry</tt> - (String) Path, shown in the entry field.
878
+ # * <tt>:validate</tt> - +true+ or +false+; if true, a valid path
879
+ # must be chosen, canceling the dialog is not possible.
880
+ # * <tt>:configSection</tt> - (any String, Integer or Float or nil) Not
881
+ # important. Sets the section in the config-file, where for example the
882
+ # window size and position is stored.
883
+ def initialize( initialdir=nil,help=nil,hash=nil )
884
+ initialdir, help, hash =
885
+ TKXXS_CLASSES.args_1( initialdir,help,hash )
886
+
887
+ # Must always include: :question, :help, :configSection
888
+ hash = {
889
+ :mode=>:openfiles,
890
+ :initialdir => Dir.pwd,
891
+ :question => "Please choose the desired files:",
892
+ :help => nil,
893
+ :configSection => nil,
894
+ :title => 'Choose Files',
895
+ :defaultEntry => '',
896
+ :validate=>nil,
897
+ }.merge(hash)
898
+
899
+ # Necessary, because hash[:configSection] is deleted later on
900
+ # in args_2.
901
+ CONF.section = hash[:configSection] if CONF # ?
902
+
903
+ help,hash,question,initialdirH,mode,defaultEntry,@validate =
904
+ TKXXS_CLASSES.args_2(help,hash,question,:initialdir,:mode,:defaultEntry,:validate)
905
+ hash[:initialdir] = initialdir || initialdirH # Tja, etwas umstaendlich
906
+
907
+ @teardownDone = false
908
+ @mode = mode
909
+ @paths = nil
910
+ @ans = nil
911
+ @goOn = goOn = TkVariable.new
912
+ # Because you cannot use instance-vars in procs. TODO: smarter way?
913
+
914
+ Tk.update # Show any calling widget first to make it lower than @dialog
915
+
916
+ #---- Toplevel: @dialog
917
+ @dialog=Tk::Toplevel.new() {title(hash[:title])}
918
+ @dialog.geometry = CONF[:pathChooserGeom] if CONF
919
+
920
+ # This is neccessary for handling the close-button ('X') of the window
921
+ @dialog.protocol(:WM_DELETE_WINDOW) {validate_and_leave}
922
+
923
+ ##/ @dialog.bind('Destroy') {
924
+ ##/ unless teardownDone
925
+ ##/ goOn.value = '1' # !
926
+ ##/ $tkxxs_.delete(object_id)
927
+ ##/ if CONF
928
+ ##/ CONF[:pathChooserGeom] = @dialog.geometry
929
+ ##/ CONF.section = nil
930
+ ##/ ##/ CONF.save
931
+ ##/ end
932
+ ##/ teardownDone = true
933
+ ##/ end # unless
934
+ ##/ }
935
+
936
+ #---- Top Frame
937
+ @frame = Frame.new(@dialog) {padding "3 3 3 3"}.
938
+ pack(:fill=>:both,:expand=>true)
939
+
940
+ #---- Label (question)
941
+ @qLbl = question_lbl(question,help).grid(:columnspan=>6,:sticky=>'nsew')
942
+
943
+ #---- Labels ("Recent", "Favorites", ...)
944
+ @recLbl = recent_lbl.grid(:column=>0,:columnspan=>2,:row=>1)
945
+ @favLbl = favorite_lbl.grid(:column=>2,:columnspan=>2,:row=>1)
946
+ @pasteLbl = paste_lbl.grid(:column=>4,:row=>1)
947
+
948
+ #---- Buttons ("Dirs", "Files", "Browse", ...
949
+ @recDirsBtn = recent_dirs_btn(hash).grid(:row=>2,:column=>0)
950
+ @recFilesBtn = recent_files_btn.grid(:row=>2,:column=>1)
951
+ @favDirsBtn = favorite_dirs_btn(hash).grid(:row=>2,:column=>2)
952
+ @favFilesBtn = favorite_files_btn.grid(:row=>2,:column=>3)
953
+ @pasteBtn = paste_btn.grid(:row=>2,:column=>4)
954
+ @browseBtn = browse_btn(hash).grid(:row=>2,:column=>5)
955
+
956
+ #---- Entry (Path)
957
+ @entry = entry(defaultEntry).grid(:row=>3,:column=>0,:columnspan=>6,:sticky=>'ew')
958
+
959
+ #---- Buttons ("Cancel", "OK")
960
+ @cancelBtn = cancel_btn.grid(:row=>4,:column=>0)
961
+ @okBtn = ok_btn.grid(:row=>4,:column=>5)
962
+
963
+ #---- CheckButton ("Add to Favorites")
964
+ @favChkLbl = favorite_chk_label.grid(:row=>4,:column=>1,:columnspan=>2,:sticky=>'e')
965
+ @favDirsChkVal = nil
966
+ @favDirsChk = add2favorites_dirs_chk.grid(:row=>4,:column=>3,:columnspan=>1)
967
+ @favFilesChkVal = nil
968
+ unless @mode == :choosedir
969
+ @favFilesChk = add2favorites_files_chk.
970
+ grid(:row=>4,:column=>4,:columnspan=>1,:sticky=>'w')
971
+ end
972
+
973
+ #---- Text (showing other path formats)
974
+ @txt = text2.grid(:row=>5,:column=>0,:columnspan=>6,:sticky=>'sew')
975
+
976
+ #---- grid_configure
977
+ @frame.grid_columnconfigure([0,1,2,3,4,5], :weight=>1)
978
+ @frame.grid_rowconfigure([0,1,2,3,4], :weight=>0)
979
+ @frame.grid_rowconfigure([5], :weight=>1)
980
+
981
+ @entry.focus
982
+ Tk.update
983
+ ##/ @dialog.raise
984
+ end # initialize
985
+
986
+ ##################################################################
987
+ # For FileAndDirChooser, @validate is simply true or false. If
988
+ # user presses "Cancel" or the close button ('X') in the upper
989
+ # right, this is detected and user is informed by a message box.
990
+ # Otherwise, all chosen paths already have been checked "inline",
991
+ # hence no further validation is neccessary.
992
+ def validate_and_leave( )
993
+ # path(s) is/are not valid.
994
+ # TODO: Missleading method name; better(?): handle_invalid_path
995
+ if @validate
996
+ # Retry or completly exit the application
997
+ ans = Tk.messageBox(:icon=>:warning, :type=>:retrycancel,
998
+ :title=>'Message',
999
+ :message=>"INVALID ANSWER\n\nCANCEL will completely EXIT the application!"
1000
+ ## :detail=>"blablabla\n"*50
1001
+ )
1002
+ if ans == 'cancel'
1003
+ Tk.exit
1004
+ exit # Todo: n�tig?
1005
+ end
1006
+ else
1007
+ set_paths(nil) # uses the invalid path
1008
+ end
1009
+ end # validate_and_leave
1010
+
1011
+ def set_paths( paths )
1012
+ @paths = paths
1013
+ favorites_and_recent(paths) if paths && !paths.empty?
1014
+ @goOn.value = '1'
1015
+ nil
1016
+ end # set_paths
1017
+
1018
+ ##################################################################
1019
+ # When dialog is finished, always +set_paths+ must be called. For
1020
+ # 'Cancel', use set_paths(nil)
1021
+ #
1022
+ # *Returns*: (String or Array or nil) Path(s); +nil+, if 'Cancel'
1023
+ # was clicked.
1024
+ def answer( )
1025
+ @dialog.raise # Raise dialog above all other windows
1026
+ @goOn.wait # Force hold on of the dialog
1027
+ # Tear down
1028
+ unless @teardownDone # TODO: not neccessary anymore?
1029
+ $tkxxs_.delete(object_id)
1030
+ if CONF # Save settings (e.g., window size)
1031
+ CONF[:pathChooserGeom] = @dialog.geometry
1032
+ CONF.section = nil
1033
+ ##/ CONF.save
1034
+ end
1035
+ @teardownDone = true
1036
+ ##/ @goOn.value = '1'
1037
+ @dialog.destroy # Remove dialog window
1038
+ end # unless
1039
+ Tk.update
1040
+
1041
+ @paths # return the "answer"
1042
+ end # ans
1043
+
1044
+ ##/ ##################################################################
1045
+ ##/ # Saves CONF!
1046
+ ##/ def dialog_destroy( )
1047
+ ##/ unless @teardownDone
1048
+ ##/ $tkxxs_.delete(object_id)
1049
+ ##/ if CONF
1050
+ ##/ CONF[:pathChooserGeom] = @dialog.geometry
1051
+ ##/ CONF.section = nil
1052
+ ##/ ##/ CONF.save
1053
+ ##/ end
1054
+ ##/ @teardownDone = true
1055
+ ##/ ##/ @goOn.value = '1'
1056
+ ##/ @dialog.destroy
1057
+ ##/ end # unless
1058
+ ##/ nil
1059
+ ##/ end # dialog_destroy
1060
+
1061
+ def question_lbl( question, help)
1062
+ lbl = Label.new(@frame, :text=>question){|w|
1063
+ BalloonHelp.new(w,:text=>help)
1064
+ }
1065
+ end # question_lbl
1066
+
1067
+ def recent_lbl( )
1068
+ lbl = Label.new(@frame, :text=>"Recent")
1069
+ end # recent_lbl
1070
+
1071
+ def favorite_lbl( )
1072
+ lbl = Label.new(@frame, :text=>"Favorites")
1073
+ end # favorite_lbl
1074
+
1075
+ def paste_lbl( )
1076
+ Label.new(@frame, :text=>"Clipboard")
1077
+ end # paste_lbl
1078
+
1079
+ def recent_dirs_btn( hash )
1080
+ btn = Button.new(@frame, :text=>"Dirs"){|w|
1081
+ BalloonHelp.new(w,:text=>"Recent directories")
1082
+ }
1083
+ btn.command {
1084
+ if CONF
1085
+ dirs = CONF[:recentDirs] || []
1086
+ ## dir = SingleChoiceD.new(dirs.sort).answer
1087
+ dir = SingleChoiceD.new(dirs).answer # unsorted
1088
+ if dir
1089
+ hash2 = hash.merge({:initialdir=>dir}).dup
1090
+ ##/ filesStr = Tk.getOpenFile(hash2)
1091
+ ##/ set_paths( tks_result_to_ary( filesStr ) )
1092
+ case @mode
1093
+ ##/ when :choosedir; set_paths(dir)
1094
+ when :choosedir; validate_and_set_path( dir )
1095
+ when :openfiles; choose(hash2)
1096
+ when :openfile; choose(hash2)
1097
+ when :savefile; choose(hash2)
1098
+ else; raise
1099
+ end # case
1100
+ end
1101
+ end # if CONF
1102
+ }
1103
+ btn
1104
+ end # recent_dirs_btn
1105
+
1106
+ def recent_files_btn( )
1107
+ btn = Button.new(@frame, :text=>"Files"){|w|
1108
+ BalloonHelp.new(w,:text=>"Recent files")
1109
+ }
1110
+ btn.command {
1111
+ if CONF
1112
+ files = CONF[:recentFiles] || []
1113
+ ## filePath = SingleChoiceD.new(files.sort).answer
1114
+ filePath = SingleChoiceD.new(files).answer # unsorted
1115
+ if filePath
1116
+ case @mode
1117
+ when :choosedir
1118
+ validate_and_set_path(File.dirname(filePath))
1119
+ when :openfiles
1120
+ validate_and_set_path( filePath )
1121
+ when :openfile, :savefile
1122
+ validate_and_set_path( filePath )
1123
+ else; raise
1124
+ end # case
1125
+ end
1126
+ end # if CONF
1127
+ }
1128
+ btn
1129
+ end # recent_files_btn
1130
+
1131
+ def favorite_dirs_btn( hash )
1132
+ btn = Button.new(@frame, :text=>"Dirs"){|w|
1133
+ BalloonHelp.new(w,:text=>"Favorite directories")
1134
+ }
1135
+ btn.command {
1136
+ if CONF
1137
+ favDirs = CONF[:favoriteDirs] || []
1138
+ dir = SingleChoiceD.new(favDirs.sort).answer
1139
+ if dir
1140
+ hash2 = hash.merge({:initialdir=>dir}).dup
1141
+ ##/ filesStr = Tk.getOpenFile(hash2)
1142
+ ##/ set_paths( tks_result_to_ary( filesStr ) )
1143
+ case @mode
1144
+ when :choosedir; validate_and_set_path( dir )
1145
+ when :openfiles; choose(hash2)
1146
+ when :openfile; choose(hash2)
1147
+ when :savefile; choose(hash2)
1148
+ else; raise
1149
+ end # case
1150
+ end
1151
+ end # if CONF
1152
+ }
1153
+ btn
1154
+ end # favorite_dirs_btn
1155
+
1156
+ def favorite_files_btn( )
1157
+ btn = Button.new(@frame, :text=>"Files"){|w|
1158
+ BalloonHelp.new(w,:text=>"Favorite files")
1159
+ }
1160
+ btn.command {
1161
+ if CONF
1162
+ favFiles = CONF[:favoriteFiles] || []
1163
+ filePath = SingleChoiceD.new(favFiles.sort).answer
1164
+ if filePath
1165
+ case @mode
1166
+ when :choosedir
1167
+ validate_and_set_path(File.dirname(filePath))
1168
+ when :openfiles
1169
+ validate_and_set_path( [filePath] )
1170
+ when :openfile, :savefile
1171
+ validate_and_set_path( filePath )
1172
+ else; raise
1173
+ end # case
1174
+ end
1175
+ end # if CONF
1176
+ }
1177
+ btn
1178
+ end # favorite_files_btn
1179
+
1180
+ def paste_btn( )
1181
+ btn = Button.new(@frame, :text=>"Paste"){|w|
1182
+ BalloonHelp.new(w,:text=>"Paste clipboard to entry field")
1183
+ }
1184
+ btn.command {
1185
+ @entry.delete(0, :end)
1186
+ begin
1187
+ clip = TkClipboard.get
1188
+ rescue => err # Clipboard no String
1189
+ clip = "Nothing usefull in clipboard"
1190
+ end
1191
+ @entry.insert(0, clip)
1192
+ }
1193
+ btn
1194
+ end # paste_btn
1195
+
1196
+ def browse_btn( hash )
1197
+ btn = Button.new(@frame, :text=>"Browse"){|w|
1198
+ BalloonHelp.new(w,:text=>"Search for directory or file manually")
1199
+ }
1200
+ entry = @entry
1201
+ btn.command {
1202
+ entryStr = @entry.get.strip.gsub('\\', '/').chomp('/')
1203
+ unless entryStr.empty?
1204
+ if File.directory?(entryStr)
1205
+ hash[:initialdir] = entryStr
1206
+ elsif File.file?(entryStr)
1207
+ hash[:initialdir] = File.dirname(entryStr)
1208
+ end
1209
+ end
1210
+ choose(hash)
1211
+ }
1212
+ end # browse_btn
1213
+
1214
+ def entry( defaultEntry )
1215
+ entry = Entry.new(@frame) {|ent|
1216
+ BalloonHelp.new(ent,:text=>"Type or paste a path here.")
1217
+ }
1218
+ entry.insert(0, defaultEntry)
1219
+ entry.bind('Key-Return') { use_entry(@entry.get) }
1220
+ end # entry
1221
+
1222
+ def cancel_btn( )
1223
+ btn = Button.new(@frame, :text=>"Cancel"){|w|
1224
+ BalloonHelp.new(w,:text=>"Do nothing")
1225
+ }
1226
+ btn.command {
1227
+ validate_and_leave
1228
+ ###set_paths(nil)
1229
+ ##/ dialog_destroy
1230
+ }
1231
+ btn
1232
+ end # cancel_btn
1233
+
1234
+ def ok_btn( )
1235
+ btn = Button.new(@frame, :text=>"Use entry"){|w|
1236
+ BalloonHelp.new(w,:text=>"Use path from entry box")
1237
+ }
1238
+ btn.command { use_entry(@entry.get) }
1239
+ end # ok_btn
1240
+
1241
+ def use_entry( str )
1242
+ str = str.strip.gsub('\\', '/').chomp('/')
1243
+ validate_and_set_path( str )
1244
+ end # use_entry
1245
+
1246
+ def validate_and_set_path( str )
1247
+ case @mode
1248
+ when :choosedir
1249
+ if File.directory?(str)
1250
+ set_paths(str)
1251
+ elsif File.file?(str)
1252
+ set_paths( File.dirname(str) )
1253
+ else
1254
+ @txt.replace('0.0', :end, %Q<"#{ str }" is no > +
1255
+ "valid directory, please choose a valid one!")
1256
+ @entry.focus
1257
+ end
1258
+ when :openfiles, :openfile
1259
+ if File.file?(str)
1260
+ @mode == :openfile ? set_paths(str) : set_paths([str])
1261
+ else
1262
+ @txt.replace('0.0', :end, %Q<"#{ str }" is no > +
1263
+ "valid file, please choose a valid one!")
1264
+ @entry.focus
1265
+ end
1266
+ when :savefile
1267
+ if @validate
1268
+ dir = File.dirname(str)
1269
+ if File.directory?(dir)
1270
+ set_paths(str)
1271
+ else
1272
+ @txt.replace('0.0', :end, %Q<"#{ dir }" is no > +
1273
+ "valid directory, please choose a valid one!")
1274
+ @entry.focus
1275
+ end
1276
+ else
1277
+ # if file does not exist: create file and dir
1278
+ set_paths(str)
1279
+ end
1280
+ else; raise
1281
+ end # case
1282
+ nil
1283
+ end # validate_and_set_path
1284
+
1285
+ def favorite_chk_label( )
1286
+ lbl = Label.new(@frame, :text=>"Add to favorites")
1287
+ end # favorite_chk_label
1288
+
1289
+ def add2favorites_dirs_chk( )
1290
+ favChkVar = TkVariable.new(0)
1291
+ favChk = CheckButton.new(@frame) {|w|
1292
+ text 'dirs'
1293
+ variable favChkVar
1294
+ ##/ onvalue 'metric'; offvalue 'imperial'
1295
+ BalloonHelp.new(w,:text=>"If checked, the path(s) of files or directories you are going to choose will be added to the favorite directories.")
1296
+ }
1297
+ favChk.command { self.fav_dirs_chk_changed(favChkVar.value) }
1298
+ favChk
1299
+ end # add2favorites_dirs_chk
1300
+
1301
+ def add2favorites_files_chk( )
1302
+ favChkVar = TkVariable.new(0)
1303
+ favChk = CheckButton.new(@frame) {|w|
1304
+ text 'files'
1305
+ variable favChkVar
1306
+ ##/ onvalue 'metric'; offvalue 'imperial'
1307
+ BalloonHelp.new(w,:text=>"If checked, the files(s) you are going to choose will be added to the favorite files.")
1308
+ }
1309
+ favChk.command { self.fav_files_chk_changed(favChkVar.value) }
1310
+ favChk
1311
+ end # add2favorites_files_chk
1312
+
1313
+ def fav_dirs_chk_changed( val )
1314
+ @favDirsChkVal = val == '1' ? true : false
1315
+ @favDirsChkVal
1316
+ end # fav_dirs_chk_changed
1317
+
1318
+ def fav_files_chk_changed( val )
1319
+ @favFilesChkVal = val == '1' ? true : false
1320
+ @favDirsChk.invoke if @favFilesChkVal && !@favDirsChkVal
1321
+ @favFilesChkVal
1322
+ end # fav_files_chk_changed
1323
+
1324
+ def text2( )
1325
+ txt = TkText.new(@frame)
1326
+ tagSel = TkTextTagSel.new(txt)
1327
+ txt.bind('Control-Key-a') {
1328
+ # From: "Using Control-a to select all text in a text widget : TCL", http://objectmix.com/tcl/35276-using-control-select-all-text-text-widget.html
1329
+ tagSel.add('0.0', :end)
1330
+ Kernel.raise TkCallbackBreak
1331
+ }
1332
+ txt
1333
+ end # text2
1334
+
1335
+ def favorites_and_recent( paths )
1336
+ fav_dirs(paths) if @favDirsChkVal # For any mode
1337
+ fav_files(paths) if @favFilesChkVal # For any mode but :choosedir; no @favFilesChk for chooseDirectory
1338
+ recent_dirs(paths)
1339
+ recent_files(paths) unless @mode == :choosedir
1340
+ CONF.save
1341
+ nil
1342
+ end # favorites_and_recents
1343
+
1344
+ def fav_dirs( paths )
1345
+ return nil unless paths
1346
+ paths = paths.dup
1347
+ favDirs = CONF[:favoriteDirs] || []
1348
+ paths = [paths] if paths.class == String
1349
+ paths.map! {|p|
1350
+ File.file?(p) ? File.dirname(p) : p
1351
+ }
1352
+ favDirs.concat(paths)
1353
+ favDirs.uniq!
1354
+ CONF[:favoriteDirs] = favDirs
1355
+ favDirs
1356
+ end # fav_dirs
1357
+
1358
+ def fav_files( paths )
1359
+ return nil unless paths
1360
+ paths = paths.dup
1361
+ favFiles = CONF[:favoriteFiles] || []
1362
+ paths = [paths] if paths.class == String
1363
+ favFiles.concat(paths)
1364
+ favFiles.uniq!
1365
+ CONF[:favoriteFiles] = favFiles
1366
+ favFiles
1367
+ end # fav_files
1368
+
1369
+ def recent_dirs( paths )
1370
+ return nil unless paths
1371
+ paths = paths.dup
1372
+ dirs = CONF[:recentDirs] || []
1373
+ paths = [paths] if paths.class == String
1374
+ paths.map! {|p|
1375
+ p = File.file?(p) ? File.dirname(p) : p
1376
+ p = File.directory?(p) ? p : nil
1377
+ p
1378
+ }
1379
+ paths.compact!
1380
+ dirs = paths + dirs
1381
+ dirs.uniq!
1382
+ CONF[:recentDirs] = dirs[0, CONF[:recentDirsSize] ]
1383
+ dirs
1384
+ end # recent_dirs
1385
+
1386
+ def recent_files( paths)
1387
+ return nil unless paths
1388
+ paths = paths.dup
1389
+ files = CONF[:recentFiles] || []
1390
+ paths = [paths] if paths.class == String
1391
+ files = paths + files
1392
+ files.uniq!
1393
+ CONF[:recentFiles] = files[0, CONF[:recentFilesSize] ]
1394
+ files
1395
+ end # recent_files
1396
+
1397
+ def tks_result_to_ary( filesListStr )
1398
+ paths = []
1399
+ if filesListStr[0,1]=='{'
1400
+ filesListStr.scan( / \{ ( [^\}]+ ) \} /x) {
1401
+ paths << $1
1402
+ }
1403
+ else
1404
+ paths = filesListStr.split(' ')
1405
+ end
1406
+
1407
+ paths
1408
+ end # tks_result_to_ary
1409
+ end # class FileAndDirChooser
1410
+
1411
+ ##########################################################################
1412
+ ##########################################################################
1413
+ class ChooseDirD < FileAndDirChooser
1414
+
1415
+ ##################################################################
1416
+ # See: TKXXS.choose_dir
1417
+ def initialize( initialdir=nil,help=nil,hash=nil )
1418
+ initialdir, help, hash =
1419
+ TKXXS_CLASSES.args_1( initialdir,help,hash )
1420
+ hash = {
1421
+ :mode => :choosedir,
1422
+ :question=>"Please choose a directory",
1423
+ :title=>"Choose Directory",
1424
+ }.merge(hash)
1425
+ super(initialdir, help, hash)
1426
+ end
1427
+
1428
+ def choose( hash )
1429
+ dirStr = Tk.chooseDirectory( hash )
1430
+ path = dirStr.empty? ? nil : dirStr
1431
+
1432
+ if path
1433
+ set_paths( path )
1434
+ else
1435
+ validate_and_leave
1436
+ end
1437
+ nil
1438
+ end # choose
1439
+
1440
+ alias :path :answer
1441
+ end # class ChooseDirD
1442
+
1443
+ ##########################################################################
1444
+ ##########################################################################
1445
+ class OpenFilesD < FileAndDirChooser
1446
+ ##################################################################
1447
+ # See: TKXXS.open_files
1448
+ def initialize( initialdir=nil,help=nil,hash=nil )
1449
+ initialdir, help, hash =
1450
+ TKXXS_CLASSES.args_1( initialdir,help,hash )
1451
+ hash = {
1452
+ :mode => :openfiles,
1453
+ :question=>"Please choose the desired files",
1454
+ :title=>"Open Files",
1455
+ :filetypes => [['All files','*']],
1456
+ :multiple=>true
1457
+ }.merge(hash)
1458
+ super(initialdir, help, hash)
1459
+ end
1460
+
1461
+ def choose( hash )
1462
+ filesStr = Tk.getOpenFile(hash)
1463
+ if filesStr
1464
+ set_paths( tks_result_to_ary( filesStr ) )
1465
+ else
1466
+ validate_and_leave
1467
+ end
1468
+ nil
1469
+ end # choose
1470
+
1471
+ alias :paths :answer
1472
+ end # class OpenFilesD
1473
+
1474
+ ##########################################################################
1475
+ ##########################################################################
1476
+ class OpenFileD < FileAndDirChooser
1477
+
1478
+ ##################################################################
1479
+ # See: TKXXS.open_file
1480
+ def initialize( initialdir=nil,help=nil,hash=nil )
1481
+ initialdir, help, hash =
1482
+ TKXXS_CLASSES.args_1( initialdir,help,hash )
1483
+ hash = {
1484
+ :mode => :openfile,
1485
+ :question=>"Please choose the desired file",
1486
+ :title=>"Open File",
1487
+ :filetypes => [['All files','*']],
1488
+ :multiple=>false
1489
+ }.merge(hash)
1490
+ super(initialdir, help, hash)
1491
+ end
1492
+
1493
+ def choose( hash )
1494
+ fileStr = Tk.getOpenFile(hash)
1495
+ if fileStr
1496
+ set_paths( fileStr )
1497
+ else
1498
+ validate_and_leave
1499
+ end
1500
+ nil
1501
+ end # choose
1502
+
1503
+ alias :path :answer
1504
+ end # class OpenFilesD
1505
+
1506
+ ##########################################################################
1507
+ ##########################################################################
1508
+ class SaveFileD < FileAndDirChooser
1509
+
1510
+ ##################################################################
1511
+ # See: TKXXS.save_file
1512
+ def initialize( initialdir=nil,help=nil,hash=nil )
1513
+ initialdir, help, hash =
1514
+ TKXXS_CLASSES.args_1( initialdir,help,hash )
1515
+ hash = {
1516
+ :mode => :savefile,
1517
+ :question=>"Please choose, where to save the file:",
1518
+ :title=>"Save File As...",
1519
+ :filetypes => [['All files','*']],
1520
+ :initialfile => 'Untitled', # Default filename, extension will be added automatically by filetypes-setting
1521
+ :defaultextension => nil,
1522
+ }.merge(hash)
1523
+ super(initialdir, help, hash)
1524
+ end
1525
+
1526
+ def choose( hash )
1527
+ fileStr = Tk.getSaveFile(hash)
1528
+ path = fileStr.empty? ? nil : fileStr
1529
+ if path
1530
+ set_paths( path )
1531
+ else
1532
+ validate_and_leave
1533
+ end
1534
+ nil
1535
+ end # choose
1536
+
1537
+ alias :path :answer
1538
+ end # class SaveFileD
1539
+
1540
+ ##/ ##########################################################################
1541
+ ##/ ##########################################################################
1542
+ ##/ class OpenFileD < OpenFilesD
1543
+ ##/
1544
+ ##/ def initialize( initialdir=nil,help=nil,hash=nil )
1545
+ ##/ initialdir, help, hash =
1546
+ ##/ TKXXS_CLASSES.args_1( initialdir,help,hash )
1547
+ ##/
1548
+ ##/ # Must always include: :question, :help, :configSection
1549
+ ##/ hash = {
1550
+ ##/ :initialdir => Dir.pwd,
1551
+ ##/ :question => "Please choose the desired file:",
1552
+ ##/ :help => nil,
1553
+ ##/ :configSection => nil,
1554
+ ##/ :filetypes => [['All files','*']],
1555
+ ##/ :multiple => 0,
1556
+ ##/ }.merge(hash)
1557
+ ##/
1558
+ ##/ super(initialdir,help,hash)
1559
+ ##/ end # initialize
1560
+ ##/
1561
+ ##/ def path( )
1562
+ ##/ CONF.section = nil if CONF
1563
+ ##/ @paths[0]
1564
+ ##/ end # paths
1565
+ ##/ alias :answer :path
1566
+ ##/ end # class OpenFileD
1567
+
1568
+
1569
+ end # class
1570
+
1571
+