tkxxs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+