diff options
author | nagai <nagai@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2006-11-06 06:56:37 +0000 |
---|---|---|
committer | nagai <nagai@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2006-11-06 06:56:37 +0000 |
commit | 2ec88c167b2dd0e3c13a77dadb9518f8d06d0c59 (patch) | |
tree | 5a810d5880298b9c85fb4109db9262931a131be4 /ext/tk/sample/tktextio.rb | |
parent | ced53248ff41863ef7adf62db215380d5089bc24 (diff) |
* ext/tk/lib/tk/itemconfig.rb: bug fix on 'itemconfiginfo' method, and
modify to make it easy to override 'itemconfiginfo' method.
* ext/tk/lib/tkextlib/tile/treeview.rb : support Tile 0.7.8.
* ext/tk/lib/tkextlib/version.rb : [new] add Tk::Tkextlib_RELEASE_DATE
to get the information from scripts.
* ext/tk/lib/tk.rb: load 'tkextlib/version.rb', and update RELEASE_DATE.
* ext/tk/lib/tkextlib/SUPPORT_STATUS: update.
* ext/tk/sample/editable_listbox.rb: [new] the listbox with editable
items. It's one of the example about usage of Place geometry manager.
* ext/tk/sample/tktextio.rb: improve the functions of TkTextIO class.
Those are required by 'irbtkw.rbw'.
* ext/tk/sample/irbtkw.rbw: [new] IRB on Ruby/Tk. It doesn't need any
real console. IRB works on a text widget without I/O blocking. That
is, thread switching on IRB will work properly, even if on Windows.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@11283 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'ext/tk/sample/tktextio.rb')
-rw-r--r-- | ext/tk/sample/tktextio.rb | 547 |
1 files changed, 497 insertions, 50 deletions
diff --git a/ext/tk/sample/tktextio.rb b/ext/tk/sample/tktextio.rb index cb59c2d9d6..0b78f45b10 100644 --- a/ext/tk/sample/tktextio.rb +++ b/ext/tk/sample/tktextio.rb @@ -1,7 +1,7 @@ #!/usr/bin/env ruby # -# sample class of handling I/O stream on a TkText widget -# by Hidetoshi NAGAI +# TkTextIO class :: handling I/O stream on a TkText widget +# by Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp) # # NOTE: TkTextIO supports 'character' (not 'byte') access only. # So, for example, TkTextIO#getc returns a character, TkTextIO#pos @@ -14,69 +14,376 @@ # TkTextIO. # require 'tk' +require 'tk/text' +require 'tk/textmark' +require 'thread' class TkTextIO < TkText + # keep safe level + @@create_queues = proc{ [Queue.new, Mutex.new, Queue.new, Mutex.new] } + + OPT_DEFAULTS = { + 'mode' => nil, + 'overwrite' => false, + 'text' => nil, + 'show' => :pos, + 'wrap' => 'char', + 'sync' => true, + 'prompt' => nil, + 'prompt_cmd' => nil, + 'hist_size' => 1000, + } + def create_self(keys) - mode = nil - ovwt = false - text = nil - wrap = 'char' - show = :pos - - if keys.kind_of?(Hash) - mode = keys.delete('mode') - ovwt = keys.delete('overwrite') - text = keys.delete('text') - show = keys.delete('show') if keys.has_key?('show') - wrap = keys.delete('wrap') || 'char' - end + opts = _get_io_params((keys.kind_of?(Hash))? keys: {}) super(keys) - self['wrap'] = wrap - insert('1.0', text) + @count_var = TkVariable.new - @txtpos = TkTextMark.new(self, '1.0') - @txtpos.gravity = :left + @write_buffer = '' + @read_buffer = '' + @buf_size = 0 + @buf_max = 1024 - self.show_mode = show + @write_buf_queue, @write_buf_mutex, + @read_buf_queue, @read_buf_mutex = @@create_queues.call - @sync = true - @overwrite = (ovwt)? true: false + @idle_flush = TkTimer.new(:idle, 1, proc{ @flusher.run rescue nil }) + @timer_flush = TkTimer.new(250, -1, proc{ @flusher.run rescue nil }) + + @flusher = Thread.new{ loop { Thread.stop; flush() } } + + @receiver = Thread.new{ + begin + loop { + str = @write_buf_queue.deq + @write_buf_mutex.synchronize { @write_buffer << str } + @idle_flush.start + } + ensure + @flusher.kill + end + } + + @timer_flush.start + + _setup_io(opts) + end + private :create_self + + def destroy + @flusher.kill rescue nil + + @idle_flush.stop rescue nil + @timer_flush.stop rescue nil + + @receiver.kill rescue nil + + super() + end + + #################################### + + def _get_io_params(keys) + opts = {} + self.class.const_get(:OPT_DEFAULTS).each{|k, v| + if keys.has_key?(k) + opts[k] = keys.delete(k) + else + opts[k] = v + end + } + opts + end + + def _setup_io(opts) + unless defined? @txtpos + @txtpos = TkTextMark.new(self, '1.0') + else + @txtpos.set('1.0') + end + @txtpos.gravity = :left @lineno = 0 @line_offset = 0 - @count_var = TkVariable.new + + @hist_max = opts['hist_size'] + @hist_index = 0 + @history = Array.new(@hist_max) + @history[0] = '' + + self['wrap'] = wrap + + self.show_mode = opts['show'] + + self.value = opts['text'] if opts['text'] + + @overwrite = (opts['overwrite'])? true: false + + @sync = opts['sync'] + + @prompt = opts['prompt'] + @prompt_cmd = opts['prompt_cmd'] @open = {:r => true, :w => true} # default is 'r+' - case mode - when 'r' + @console_mode = false + @end_of_stream = false + @console_buffer = nil + + case opts['mode'] + when nil + # do nothing + + when :console, 'console' + @console_mode = true + # @console_buffer = TkTextIO.new(:mode=>'r') + @console_buffer = self.class.new(:mode=>'r') + self.show_mode = :insert + + when 'r', 'rb' @open[:r] = true; @open[:w] = nil - when 'r+' + when 'r+', 'rb+', 'r+b' @open[:r] = true; @open[:w] = true - when 'w' + when 'w', 'wb' @open[:r] = nil; @open[:w] = true self.value='' - when 'w+' + when 'w+', 'wb+', 'w+b' @open[:r] = true; @open[:w] = true self.value='' - when 'a' + when 'a', 'ab' @open[:r] = nil; @open[:w] = true - @txtpos = TkTextMark.new(self, 'end - 1 char') + @txtpos.set('end - 1 char') @txtpos.gravity = :right - when 'a+' + when 'a+', 'ab+', 'a+b' @open[:r] = true; @open[:w] = true - @txtpos = TkTextMark.new(self, 'end - 1 char') + @txtpos.set('end - 1 char') @txtpos.gravity = :right + + else + fail ArgumentError, "unknown mode `#{opts['mode']}'" + end + + unless defined? @ins_head + @ins_head = TkTextMark.new(self, 'insert') + @ins_head.gravity = :left + end + + unless defined? @ins_tail + @ins_tail = TkTextMark.new(self, 'insert') + @ins_tail.gravity = :right + end + + unless defined? @tmp_mark + @tmp_mark = TkTextMark.new(self, 'insert') + @tmp_mark.gravity = :left + end + + if @console_mode + _set_console_line + _setup_console_bindings + end + end + private :_get_io_params, :_setup_io + + def _set_console_line + @tmp_mark.set(@ins_tail) + + mark_set('insert', 'end') + + prompt = '' + prompt << @prompt_cmd.call if @prompt_cmd + prompt << @prompt if @prompt + insert(@tmp_mark, prompt) + + @ins_head.set(@ins_tail) + @ins_tail.set('insert') + + @txtpos.set(@tmp_mark) + + _see_pos + end + + def _replace_console_line(str) + self.delete(@ins_head, @ins_tail) + self.insert(@ins_head, str) + end + + def _get_console_line + @tmp_mark.set(@ins_tail) + s = self.get(@ins_head, @tmp_mark) + _set_console_line + s + end + private :_set_console_line, :_replace_console_line, :_get_console_line + + def _cb_up + @history[@hist_index].replace(self.get(@ins_head, @ins_tail)) + @hist_index += 1 + @hist_index -= 1 if @hist_index >= @hist_max || !@history[@hist_index] + _replace_console_line(@history[@hist_index]) if @history[@hist_index] + Tk.callback_break + end + def _cb_down + @history[@hist_index].replace(self.get(@ins_head, @ins_tail)) + @hist_index -= 1 + @hist_index = 0 if @hist_index < 0 + _replace_console_line(@history[@hist_index]) if @history[@hist_index] + Tk.callback_break + end + def _cb_left + if @console_mode && compare('insert', '<=', @ins_head) + mark_set('insert', @ins_head) + Tk.callback_break + end + end + def _cb_backspace + if @console_mode && compare('insert', '<=', @ins_head) + Tk.callback_break + end + end + def _cb_ctrl_a + if @console_mode + mark_set('insert', @ins_head) + Tk.callback_break + end + end + private :_cb_up, :_cb_down, :_cb_left, :_cb_backspace, :_cb_ctrl_a + + def _setup_console_bindings + @bindtag = TkBindTag.new + + tags = self.bindtags + tags[tags.index(self)+1, 0] = @bindtag + self.bindtags = tags + + @bindtag.bind('Return'){ + insert('end - 1 char', "\n") + if (str = _get_console_line) + @read_buf_queue.push(str) + + @history[0].replace(str.chomp) + @history.pop + @history.unshift('') + @hist_index = 0 + end + + Tk.update + Tk.callback_break + } + @bindtag.bind('Alt-Return'){ + Tk.callback_continue + } + + @bindtag.bind('FocusIn'){ + if @console_mode + mark_set('insert', @ins_tail) + Tk.callback_break + end + } + + ins_mark = TkTextMark.new(self, 'insert') + + @bindtag.bind('ButtonPress'){ + if @console_mode + ins_mark.set('insert') + end + } + + @bindtag.bind('ButtonRelease-1'){ + if @console_mode && compare('insert', '<=', @ins_head) + mark_set('insert', ins_mark) + Tk.callback_break + end + } + + @bindtag.bind('ButtonRelease-2', '%x %y'){|x, y| + if @console_mode + # paste a text at 'insert' only + x1, y1, x2, y2 = bbox(ins_mark) + unless x == x1 && y == y1 + Tk.event_generate(self, 'ButtonRelease-2', :x=>x1, :y=>y1) + Tk.callback_break + end + end + } + + @bindtag.bind('Up'){ _cb_up } + @bindtag.bind('Control-p'){ _cb_up } + + @bindtag.bind('Down'){ _cb_down } + @bindtag.bind('Control-n'){ _cb_down } + + @bindtag.bind('Left'){ _cb_left } + @bindtag.bind('Control-b'){ _cb_left } + + @bindtag.bind('BackSpace'){ _cb_backspace } + @bindtag.bind('Control-h'){ _cb_backspace } + + @bindtag.bind('Home'){ _cb_ctrl_a } + @bindtag.bind('Control-a'){ _cb_ctrl_a } + end + private :_setup_console_bindings + + def _block_read(size = nil, ret = '', block_mode = true) + return '' if size == 0 + return nil if ! @read_buf_queue && @read_buffer.empty? + ret = '' unless ret.kind_of?(String) + ret.replace('') unless ret.empty? + + if block_mode == nil # partial + if @read_buffer.empty? + ret << @read_buffer.slice!(0..-1) + return ret + end + end + + if size.kind_of?(Numeric) + loop{ + @read_buf_mutex.synchronize { + buf_len = @read_buffer.length + if buf_len >= size + ret << @read_buffer.slice!(0, size) + return ret + else + ret << @read_buffer.slice!(0..-1) + size -= buf_len + return ret unless @read_buf_queue + end + } + @read_buffer << @read_buf_queue.pop + } + else # readline + rs = (size)? size: $/ + rs = rs.to_s if rs.kind_of?(Regexp) + loop{ + @read_buf_mutex.synchronize { + if (str = @read_buffer.slice!(/\A(.*)(#{rs})/m)) + ret << str + return ret + else + ret << @read_buffer.slice!(0..-1) + return ret unless @read_buf_queue + end + } + @read_buffer << @read_buf_queue.pop + } end end + def _block_write + ###### currently, not support + end + private :_block_read, :_block_write + + #################################### + def <<(obj) _write(obj) self @@ -107,14 +414,15 @@ class TkTextIO < TkText nil end - def closed? - close_read? && close_write? - end - def closed_read? - !@open[:r] - end - def closed_write? - !@open[:w] + def closed?(dir=nil) + case dir + when :r, 'r' + !@open[:r] + when :w, 'w' + !@open[:w] + else + !@open[:r] && !@open[:w] + end end def _check_readable @@ -129,7 +437,7 @@ class TkTextIO < TkText def each_line(rs = $/) _check_readable - while(s = gets) + while(s = self.gets(rs)) yield(s) end self @@ -138,7 +446,7 @@ class TkTextIO < TkText def each_char _check_readable - while(c = getc) + while(c = self.getc) yield(c) end self @@ -151,7 +459,7 @@ class TkTextIO < TkText alias eof eof? def fcntl(*args) - fail NotImplementedError, 'fcntl is not implemented on TkTextIO' + fail NotImplementedError, "fcntl is not implemented on #{self.class}" end def fsync @@ -163,11 +471,19 @@ class TkTextIO < TkText end def flush - Tk.update if @open[:w] && @sync + Thread.pass + if @open[:w] || ! @write_buffer.empty? + @write_buf_mutex.synchronize { + _sync_write_buf(@write_buffer) + @write_buffer[0..-1] = '' + } + end self end def getc + return _block_read(1) if @console_mode + _check_readable return nil if eof? c = get(@txtpos) @@ -177,8 +493,10 @@ class TkTextIO < TkText end def gets(rs = $/) + return _block_read(rs) if @console_mode + _check_readable - return nil if eof? + return nil if eof? _readline(rs) end @@ -233,7 +551,6 @@ class TkTextIO < TkText alias tell pos def pos=(idx) - # @txtpos.set((idx.kind_of?(Numeric))? "1.0 + #{idx} char": idx) seek(idx, IO::SEEK_SET) idx end @@ -306,6 +623,8 @@ class TkTextIO < TkText private :_read def read(len=nil, buf=nil) + return _block_read(len, buf) if @console_mode + _check_readable if len return "" if len == 0 @@ -321,6 +640,8 @@ class TkTextIO < TkText end def readchar + return _block_read(1) if @console_mode + _check_readable fail EOFError if eof? c = get(@txtpos) @@ -334,6 +655,7 @@ class TkTextIO < TkText s = get(@txtpos, 'end - 1 char') @txtpos.set('end - 1 char') elsif rs == '' + @count_var.value # make it global idx = tksearch_with_count([:regexp], @count_var, "\n(\n)+", @txtpos, 'end - 1 char') if idx @@ -345,6 +667,7 @@ class TkTextIO < TkText @txtpos.set('end - 1 char') end else + @count_var.value # make it global idx = tksearch_with_count(@count_var, rs, @txtpos, 'end - 1 char') if idx s = get(@txtpos, "#{idx} + #{@count_var.value} char") @@ -363,12 +686,22 @@ class TkTextIO < TkText private :_readline def readline(rs = $/) + return _block_readline(rs) if @console_mode + _check_readable fail EOFError if eof? _readline(rs) end def readlines(rs = $/) + if @console_mode + lines = [] + while (line = _block_readline(rs)) + lines << line + end + return lines + end + _check_readable lines = [] until(eof?) @@ -379,7 +712,11 @@ class TkTextIO < TkText end def readpartial(maxlen, buf=nil) + #return @console_buffer.readpartial(maxlen, buf) if @console_mode + return _block_read(maxlen, buf, nil) if @console_mode + _check_readable + fail EOFError if eof? s = _read(maxlen) buf.replace(s) if buf.kind_of?(String) s @@ -471,6 +808,8 @@ class TkTextIO < TkText end def sysread(len, buf=nil) + return _block_read(len, buf) if @console_mode + _check_readable fail EOFError if eof? s = _read(len) @@ -492,6 +831,13 @@ class TkTextIO < TkText end def ungetc(c) + if @console_mode + @read_buf_mutex.synchronize { + @read_buffer[0,0] = c.chr + } + return nil + end + _check_readable c = c.chr if c.kind_of?(Fixnum) if compare(@txtpos, '>', '1.0') @@ -506,8 +852,10 @@ class TkTextIO < TkText nil end +=begin def _write(obj) - s = _get_eval_string(obj) + #s = _get_eval_string(obj) + s = (obj.kind_of?(String))? obj: obj.to_s n = number(tk_call('string', 'length', s)) delete(@txtpos, @txtpos + "#{n} char") if @overwrite self.insert(@txtpos, s) @@ -518,6 +866,37 @@ class TkTextIO < TkText n end private :_write +=end +#=begin + def _sync_write_buf(s) + if (n = number(tk_call('string', 'length', s))) > 0 + delete(@txtpos, @txtpos + "#{n} char") if @overwrite + self.insert(@txtpos, s) + #Tk.update + + @txtpos.set(@txtpos + "#{n} char") + @txtpos.set('end - 1 char') if compare(@txtpos, '>=', :end) + + @ins_head.set(@txtpos) if compare(@txtpos, '>', @ins_head) + + _see_pos + end + self + end + private :_sync_write_buf + + def _write(obj) + s = (obj.kind_of?(String))? obj: obj.to_s + n = number(tk_call('string', 'length', s)) + @write_buf_queue.enq(s) + if @sync + Thread.pass + Tk.update + end + n + end + private :_write +#=end def write(obj) _check_writable @@ -529,13 +908,19 @@ end # TEST #################### if __FILE__ == $0 + ev_loop = Thread.new{Tk.mainloop} + f = TkFrame.new.pack - tio = TkTextIO.new(f, :show=>:pos, + #tio = TkTextIO.new(f, :show=>:nil, + #tio = TkTextIO.new(f, :show=>:pos, + tio = TkTextIO.new(f, :show=>:insert, :text=>">>> This is an initial text line. <<<\n\n"){ - yscrollbar(TkScrollbar.new(f).pack(:side=>:right, :fill=>:y)) +# yscrollbar(TkScrollbar.new(f).pack(:side=>:right, :fill=>:y)) pack(:side=>:left, :fill=>:both, :expand=>true) } + Tk.update + $stdin = tio $stdout = tio $stderr = tio @@ -599,5 +984,67 @@ if __FILE__ == $0 tio.seek(0, IO::SEEK_END) - Tk.mainloop + STDOUT.print("tio.sync == #{tio.sync}\n") +# tio.sync = false +# STDOUT.print("tio.sync == #{tio.sync}\n") + + (0..10).each{|i| + STDOUT.print("#{i}\n") + s = '' + (0..1000).each{ s << '*' } + print(s) + } + print("\n") + print("\n=========================================================\n\n") + + s = '' + timer = TkTimer.new(:idle, -1, proc{ + #STDOUT.print("idle call\n") + unless s.empty? + print(s) + s = '' + end + }).start + (0..10).each{|i| + STDOUT.print("#{i}\n") + (0..1000).each{ s << '*' } + } +# timer.stop + until s.empty? + sleep 0.1 + end + timer.stop + +=begin + tio.sync = false + print("\n") + #(0..10000).each{ putc('*') } + (0..10).each{|i| + STDOUT.print("#{i}\n") + (0..1000).each{ putc('*') } + } + + (0..10).each{|i| + STDOUT.print("#{i}\n") + s = '' + (0..1000).each{ s << '*' } + print(s) + } +=end + + num = 0 +# io = TkTextIO.new(:mode=>:console, :prompt=>'').pack +#=begin + io = TkTextIO.new(:mode=>:console, + :prompt_cmd=>proc{ + s = "[#{num}]" + num += 1 + s + }, + :prompt=>'-> ').pack +#=end + Thread.new{loop{sleep 2; io.puts 'hoge'}} + Thread.new{loop{p io.gets}} + + ev_loop.join end |