ektoplayer 0.1.6 → 0.1.11

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -12
  3. data/lib/ektoplayer/application.rb +6 -5
  4. data/lib/ektoplayer/bindings.rb +61 -55
  5. data/lib/ektoplayer/common.rb +19 -6
  6. data/lib/ektoplayer/compat.rb +3 -3
  7. data/lib/ektoplayer/config.rb +1 -11
  8. data/lib/ektoplayer/controllers/browser.rb +7 -5
  9. data/lib/ektoplayer/controllers/help.rb +1 -1
  10. data/lib/ektoplayer/controllers/info.rb +1 -1
  11. data/lib/ektoplayer/controllers/playlist.rb +24 -12
  12. data/lib/ektoplayer/icurses.rb +21 -0
  13. data/lib/ektoplayer/icurses/curses.rb +53 -0
  14. data/lib/ektoplayer/icurses/ffi_ncurses.rb +69 -0
  15. data/lib/ektoplayer/icurses/ncurses.rb +79 -0
  16. data/lib/ektoplayer/icurses/ncursesw.rb +1 -0
  17. data/lib/ektoplayer/icurses/sugar.rb +65 -0
  18. data/lib/ektoplayer/icurses/test.rb +99 -0
  19. data/lib/ektoplayer/models/player.rb +2 -2
  20. data/lib/ektoplayer/{mp3player.rb → players/mpg_portaudio_player.rb} +3 -3
  21. data/lib/ektoplayer/players/mpg_wrapper_player.rb +107 -0
  22. data/lib/ektoplayer/theme.rb +1 -6
  23. data/lib/ektoplayer/ui.rb +100 -129
  24. data/lib/ektoplayer/ui/colors.rb +14 -14
  25. data/lib/ektoplayer/ui/widgets.rb +4 -4
  26. data/lib/ektoplayer/ui/widgets/labelwidget.rb +1 -1
  27. data/lib/ektoplayer/ui/widgets/listwidget.rb +115 -46
  28. data/lib/ektoplayer/views/help.rb +7 -10
  29. data/lib/ektoplayer/views/info.rb +29 -38
  30. data/lib/ektoplayer/views/mainwindow.rb +2 -5
  31. data/lib/ektoplayer/views/playinginfo.rb +15 -20
  32. data/lib/ektoplayer/views/progressbar.rb +30 -10
  33. data/lib/ektoplayer/views/splash.rb +24 -25
  34. data/lib/ektoplayer/views/tabbar.rb +6 -5
  35. data/lib/ektoplayer/views/trackrenderer.rb +20 -14
  36. metadata +15 -47
  37. data/lib/ektoplayer/views/volumemeter.rb +0 -76
@@ -1,5 +1,5 @@
1
1
  require_relative 'model'
2
- require_relative '../mp3player'
2
+ require_relative '../players/mpg_wrapper_player'
3
3
 
4
4
  module Ektoplayer
5
5
  module Models
@@ -7,7 +7,7 @@ module Ektoplayer
7
7
  def initialize(client)
8
8
  super()
9
9
  @client = client
10
- @player = Mp3Player.new
10
+ @player = MpgWrapperPlayer.new
11
11
  @events.register(:position_change, :track_completed, :pause, :stop, :play)
12
12
  @player.events.on_all(&@events.method(:trigger))
13
13
 
@@ -2,7 +2,7 @@ require 'thread'
2
2
  require 'mpg123'
3
3
  require 'portaudio'
4
4
 
5
- require_relative 'events'
5
+ require_relative '../events'
6
6
 
7
7
  class Mpg123
8
8
  alias :samples_per_frame :spf
@@ -38,7 +38,7 @@ class Mpg123
38
38
  end
39
39
  end
40
40
 
41
- class Mp3Player
41
+ class MpgPortaudioPlayer
42
42
  attr_reader :events
43
43
 
44
44
  def initialize(buffer_size = 2**12)
@@ -148,4 +148,4 @@ class Mp3Player
148
148
  def forward(seconds = 2)
149
149
  seek(position + seconds)
150
150
  end
151
- end
151
+ e
@@ -0,0 +1,107 @@
1
+ require 'thread'
2
+ require 'open3'
3
+ require 'scanf'
4
+
5
+ require_relative '../events'
6
+
7
+ fail 'MpgWrapperPlayer: mpg123 not found' unless system('which mpg123 >/dev/null')
8
+
9
+ class MpgWrapperPlayer
10
+ attr_reader :events, :file
11
+
12
+ STATE_STOPPED, STATE_PAUSED, STATE_PLAYING = 0, 1, 2
13
+
14
+ def initialize
15
+ @events = Events.new(:play, :pause, :stop, :position_change)
16
+ @lock = Mutex.new
17
+ @mpg123_in, @mpg123_out, @mpg123_thread = nil, nil, nil
18
+ @state = 0
19
+ @file = ''
20
+
21
+ @frames_played = @frames_remaining =
22
+ @seconds_played = @seconds_remaining = 0
23
+ end
24
+
25
+ def play(file=nil)
26
+ start_mpg123_thread
27
+ @file = file if file
28
+ write("L #{@file}")
29
+ end
30
+
31
+ def pause; write(?P) if @state == STATE_PLAYING end
32
+ def stop; write(?S) if @state != STATE_STOPPED end
33
+ def toggle; write(?P) end
34
+
35
+ def level; 30 end
36
+ def paused?; @state == STATE_PAUSED end
37
+ def stopped?; @state == STATE_STOPPED end
38
+ def playing?; @state == STATE_PLAYING end
39
+
40
+ def status
41
+ return :playing if playing?
42
+ return :paused if paused?
43
+ return :stopped if stopped?
44
+ end
45
+
46
+ def position; @seconds_played end
47
+ def length; @seconds_played + @seconds_remaining end
48
+ def position_percent; Float(@seconds_played) / length end
49
+
50
+ def seek(seconds) write("J #{seconds}s") end
51
+ def rewind(seconds = 2) write("J -#{seconds}s") end
52
+ def forward(seconds = 2) write("J +#{seconds}s") end
53
+ alias :backward :rewind
54
+
55
+ private def write(string)
56
+ @lock.synchronize do
57
+ @mpg123_in.write(string + ?\n)
58
+ end
59
+ rescue
60
+ Ektoplayer::Application.log(self, $!)
61
+ end
62
+
63
+ private def start_mpg123_thread
64
+ @lock.synchronize do
65
+ unless @mpg123_thread
66
+ Thread.new do
67
+ begin
68
+ @mpg123_in, @mpg123_out, _, @mpg123_thread =
69
+ Open3.popen3('mpg123', '-o', 'jack,pulse,alsa,oss', '--fuzzy', '-R')
70
+
71
+ while (line = @mpg123_out.readline)
72
+ if line[1] == ?F
73
+ @frames_played, @frames_remaining,
74
+ @seconds_played, @seconds_remaining =
75
+ line.scanf('@F %d %d %f %f')
76
+ @events.trigger(:position_change)
77
+ elsif line[1] == ?P
78
+ if (@state = line[3].to_i) == STATE_STOPPED
79
+ if @seconds_remaining < 3
80
+ @events.trigger(:stop, :track_completed)
81
+ else
82
+ @events.trigger(:stop)
83
+ end
84
+ elsif @state == STATE_PAUSED
85
+ @events.trigger(:pause)
86
+ elsif @state == STATE_PLAYING
87
+ @events.trigger(:play)
88
+ end
89
+ end
90
+ end
91
+ rescue
92
+ Ektoplayer::Application.log(self, $!)
93
+ ensure
94
+ # shouldn't reach here
95
+ Ektoplayer::Application.log(self, 'player closed')
96
+ @mpg123_thread.kill
97
+ @mpg123_thread = nil
98
+ @mpg123_in.close
99
+ @mpg123_out.close
100
+ end
101
+ end
102
+
103
+ sleep 0.1 while not @mpg123_thread
104
+ end
105
+ end
106
+ end
107
+ end
@@ -24,14 +24,12 @@ module Ektoplayer
24
24
  :'progressbar.progress' => [:blue ].freeze,
25
25
  :'progressbar.rest' => [:black ].freeze,
26
26
 
27
- :'volumemeter.level' => [:magenta ].freeze,
28
- :'volumemeter.rest' => [:black ].freeze,
29
-
30
27
  :'tabbar.selected' => [:blue ].freeze,
31
28
  :'tabbar.unselected' => [:none ].freeze,
32
29
 
33
30
  :'list.item_even' => [:blue ].freeze,
34
31
  :'list.item_odd' => [:blue ].freeze,
32
+ :'list.item_selection' => [:magenta ].freeze,
35
33
 
36
34
  :'playinginfo.position' => [:magenta ].freeze,
37
35
  :'playinginfo.state' => [:cyan ].freeze,
@@ -54,9 +52,6 @@ module Ektoplayer
54
52
  :'progressbar.progress' => [23 ].freeze,
55
53
  :'progressbar.rest' => [236 ].freeze,
56
54
 
57
- :'volumemeter.level' => [:magenta ].freeze,
58
- :'volumemeter.rest' => [236 ].freeze,
59
-
60
55
  :'tabbar.selected' => [75 ].freeze,
61
56
  :'tabbar.unselected' => [250 ].freeze,
62
57
 
data/lib/ektoplayer/ui.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'curses'
1
+ require_relative 'icurses'
2
2
  require 'readline'
3
3
  require 'io/console'
4
4
 
@@ -6,19 +6,17 @@ require_relative 'ui/colors'
6
6
  require_relative 'events'
7
7
 
8
8
  module UI
9
- CONDITION_SIGNALS = ConditionSignals.new
10
-
11
9
  class WidgetSizeError < Exception; end
12
10
 
13
11
  class Canvas
14
- extend Curses
12
+ extend ICurses
15
13
 
16
14
  def self.size
17
- UI::Size.new(height: Curses.lines, width: Curses.cols)
15
+ UI::Size.new(height: ICurses.lines, width: ICurses.cols)
18
16
  end
19
17
 
20
18
  def self.cursor
21
- UI::Point.new(y: Curses.cury, x: Curses.curx)
19
+ #UI::Point.new(y: ICurses.cury, x: ICurses.curx)
22
20
  end
23
21
 
24
22
  def self.pos
@@ -28,10 +26,10 @@ module UI
28
26
  def self.start
29
27
  @@widget = nil
30
28
 
31
- %w(init_screen crmode noecho start_color use_default_colors).
32
- each {|_|Curses.send(_)}
33
- Curses.mousemask(Curses::ALL_MOUSE_EVENTS)
34
- Curses.stdscr.keypad(true)
29
+ %w(initscr cbreak noecho start_color use_default_colors).
30
+ each {|_|ICurses.send(_)}
31
+ ICurses.mousemask(ICurses::ALL_MOUSE_EVENTS | ICurses::REPORT_MOUSE_POSITION)
32
+ ICurses.stdscr.keypad(true)
35
33
  UI::Colors.start
36
34
 
37
35
  self.enable_resize_detection
@@ -43,7 +41,7 @@ module UI
43
41
 
44
42
  def self.widget; @@widget end
45
43
  def self.widget=(w) @@widget = w end
46
- def self.stop; Curses.close_screen end
44
+ def self.stop; ICurses.endwin end
47
45
  def self.visible?; true end
48
46
  def self.inivsibile?; false end
49
47
 
@@ -52,36 +50,33 @@ module UI
52
50
  widget
53
51
  end
54
52
 
55
- def self.getch(timeout=-1)
56
- Curses.stdscr.timeout=(timeout)
57
- UI::Input::KEYMAP_WORKAROUND[Curses.stdscr.getch]
58
- end
53
+ #def self.getch(timeout=-1)
54
+ # ICurses.stdscr.timeout(timeout)
55
+ # UI::Input::KEYMAP_WORKAROUND[ICurses.stdscr.getch]
56
+ #end
59
57
 
60
- def self.update_screen(force_redraw=false)
58
+ def self.update_screen(force_redraw=false, force_resize=false)
61
59
  @@updating ||= Mutex.new
62
60
  @@want_resize ||= false
63
61
 
64
62
  if @@updating.try_lock
65
63
  begin
66
- if @@want_resize
64
+ if @@want_resize or force_resize
67
65
  @@want_resize = false
68
66
  h, w = IO.console.winsize()
69
- Curses.resizeterm(h, w)
70
- Curses.clear
71
- Curses.refresh
67
+ ICurses.resizeterm(h, w)
72
68
  @@widget.size=(Size.new(height: h, width: w)) if @@widget
73
69
  @@widget.display(true, true, true) if @@widget
74
70
  else
75
71
  @@widget.display(true, force_redraw) if @@widget
76
72
  end
77
73
  rescue UI::WidgetSizeError
78
- Curses.clear
79
- Curses.addstr('terminal too small!')
74
+ ICurses.stdscr.clear
75
+ ICurses.stdscr.addstr('terminal too small!')
80
76
  rescue
81
77
  Ektoplayer::Application.log(self, $!)
82
78
  end
83
79
 
84
- Curses.doupdate
85
80
  @@updating.unlock
86
81
  end
87
82
  end
@@ -96,8 +91,8 @@ module UI
96
91
 
97
92
  class Input
98
93
  KEYMAP_WORKAROUND = {
99
- 13 => Curses::KEY_ENTER,
100
- 127 => Curses::KEY_BACKSPACE
94
+ 13 => ICurses::KEY_ENTER,
95
+ 127 => ICurses::KEY_BACKSPACE
101
96
  }
102
97
  KEYMAP_WORKAROUND.default_proc = proc { |h,k| k }
103
98
  KEYMAP_WORKAROUND.freeze
@@ -111,45 +106,51 @@ module UI
111
106
 
112
107
  loop do
113
108
  unless @@readline_obj.active?
114
- Curses.curs_set(0)
115
- Curses.nonl
109
+ ICurses.curs_set(0)
110
+ ICurses.nonl
116
111
 
117
112
  begin
118
- UI::Canvas.widget.win.keypad=(true)
119
- c = KEYMAP_WORKAROUND[UI::Canvas.widget.win.getch1]
113
+ UI::Canvas.widget.win.keypad(true)
120
114
 
121
- if c == Curses::KEY_MOUSE
122
- if c = Curses.getmouse
123
- UI::Canvas.widget.mouse_click(c)
115
+ if (c = UI::Canvas.widget.win.getch1(600))
116
+ if c == ICurses::KEY_MOUSE
117
+ if c = ICurses.getmouse
118
+ UI::Canvas.widget.mouse_click(c)
119
+ end
120
+ else
121
+ c = KEYMAP_WORKAROUND[c.ord]
122
+ UI::Canvas.widget.key_press(c) if c >= 0
124
123
  end
125
- elsif c # (not nil)
126
- UI::Canvas.widget.key_press(c.is_a?(Integer) ? c : c.to_sym)
127
124
  end
125
+
126
+ ICurses.doupdate
128
127
  end while !@@readline_obj.active?
129
128
  else
130
- Curses.curs_set(1)
131
- Curses.nl
129
+ ICurses.curs_set(1)
130
+ ICurses.nl
132
131
 
133
132
  begin
134
133
  win = UI::Canvas.widget.win
135
- win.keypad=(false)
136
- next unless (c = win.getch1)
134
+ win.keypad(false)
135
+ @@readline_obj.redraw
136
+ next unless (c = (win.getch1(100).ord rescue -1)) > 0
137
137
 
138
138
  if c == 10 or c == 4
139
- @@readline_obj.feed(?\n)
139
+ @@readline_obj.feed(?\n.ord)
140
140
  else
141
- @@readline_obj.feed(c.chr)
141
+ @@readline_obj.feed(c)
142
142
 
143
143
  if c == 27 # pass 3-character escape sequence
144
- if c = win.getch1(1)
145
- @@readline_obj.feed(c.chr)
146
- if c = win.getch1(1)
147
- @@readline_obj.feed(c.chr)
144
+ win.timeout(5)
145
+ if (c = (win.getch.ord rescue -1)) > 0
146
+ @@readline_obj.feed(c)
147
+ if (c = (win.getch.ord rescue -1)) > 0
148
+ @@readline_obj.feed(c)
148
149
  end
149
150
  end
150
151
  end
151
152
  end
152
- rescue
153
+ #rescue
153
154
  # getch() returned something weird that could not be chr()d
154
155
  end while @@readline_obj.active?
155
156
  end
@@ -157,15 +158,14 @@ module UI
157
158
  end
158
159
 
159
160
  def self.readline(*args, **opts, &block)
160
- (@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts) do
161
- Canvas.class_variable_get('@@updating').synchronize { yield }
161
+ (@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts) do |result|
162
+ Canvas.class_variable_get('@@updating').synchronize { yield result }
162
163
  end
163
164
  end
164
165
  end
165
166
 
166
167
  class ReadlineWindow
167
168
  def initialize
168
- @mutex, @cond = Mutex.new, ConditionVariable.new
169
169
  Readline.input, @readline_in_write = IO.pipe
170
170
  Readline.output = File.open(File::NULL, ?w)
171
171
  @thread = nil
@@ -173,35 +173,38 @@ module UI
173
173
 
174
174
  def active?; @thread; end
175
175
 
176
+ def redraw
177
+ return unless @window
178
+ @window.erase
179
+ buffer = @prompt + Readline.line_buffer.to_s
180
+ @window.addstr(buffer[(buffer.size - @size.width).clamp(0, buffer.size)..-1])
181
+ @window.move(0, Readline.point + @prompt.size)
182
+ @window.refresh
183
+ end
184
+
176
185
  def readline(pos, size, prompt: '', add_hist: false, &block)
177
186
  @thread ||= Thread.new do
178
- window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
179
-
180
- rlt = Thread.new { Readline.readline(prompt, add_hist) }
181
- Readline.set_screen_size(size.height, size.width)
182
- Readline.delete_text
183
- @readline_in_write.read_nonblock(100) rescue nil
184
-
185
- while rlt.alive?
186
- window.erase
187
- buffer = prompt + Readline.line_buffer.to_s
188
- window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
189
- window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
190
- window.refresh
191
- CONDITION_SIGNALS.wait(:readline, 0.2)
192
- end
187
+ @size, @prompt = size, prompt
188
+ @window = ICurses.newwin(size.height, size.width, pos.y, pos.x)
193
189
 
194
- window.clear
195
- block.(Readline.line_buffer)
196
- UI::Canvas.update_screen(true)
197
- @thread = nil
190
+ begin
191
+ Readline.set_screen_size(size.height, size.width)
192
+ Readline.delete_text
193
+ @readline_in_write.read_nonblock(100) rescue nil
194
+ block.(Readline.readline(prompt, add_hist))
195
+ ensure
196
+ @window.clear
197
+ @window = @thread = nil
198
+ UI::Canvas.update_screen(true)
199
+ end
198
200
  end
199
201
  end
200
202
 
201
203
  def feed(c)
202
- @readline_in_write.write(c)
203
- CONDITION_SIGNALS.signal(:readline)
204
- @thread = nil if c == ?\n
204
+ @readline_in_write.putc(c)
205
+ Thread.pass
206
+ @thread = nil if c == ?\n.ord
207
+ redraw
205
208
  end
206
209
  end
207
210
 
@@ -251,49 +254,9 @@ module UI
251
254
  def to_s; "[(Size) height=#{height}, width=#{width}]" end
252
255
  end
253
256
 
254
- # We want to change the mouse coordinates as we pass the mouse event
255
- # through the widgets. The attributes of Curses::MouseEvent are
256
- # readonly, therefore we need to carry out our own MouseEvent class.
257
- class FakeMouseEvent
258
- attr_accessor :x, :y, :z, :bstate
259
-
260
- def initialize(mouse_event=nil)
261
- if mouse_event
262
- from_mouse_event!(mouse_event)
263
- else
264
- @x, @y, @z, @bstate = 0, 0, 0, Curses::BUTTON1_CLICKED
265
- end
266
- end
267
-
268
- def from_mouse_event!(m)
269
- @x, @y, @z, @bstate = m.x, m.y, m.z, m.bstate
270
- end
271
-
272
- def update!(x: nil, y: nil, z: nil, bstate: nil)
273
- @x, @y, @z = (x or @x), (y or @y), (z or @z)
274
- @bstate = (bstate or @bstate)
275
- end
276
-
277
- def pos
278
- Point.new(x: @x, y: @y)
279
- end
280
-
281
- def to_fake
282
- FakeMouseEvent.new(self)
283
- end
284
-
285
- def to_s
286
- name = Curses.constants.
287
- select { |c| c =~ /^BUTTON_/ }.
288
- select { |c| Curses.const_get(c) & @bstate > 0 }[0]
289
- name ||= @button
290
- "[(FakeMouseEvent) button=#{name}, x=#{x}, y=#{y}, z=#{z}]"
291
- end
292
- end
293
-
294
257
  class MouseEvents < Events
295
258
  def on(mouse_event, &block)
296
- return on_all(&block) if mouse_event == Curses::ALL_MOUSE_EVENTS
259
+ return on_all(&block) if mouse_event == ICurses::ALL_MOUSE_EVENTS
297
260
  super(mouse_event, &block)
298
261
  end
299
262
 
@@ -334,8 +297,8 @@ module UI
334
297
  end
335
298
  end
336
299
 
337
- module Curses
338
- class Window
300
+ module ICurses
301
+ class IWindow
339
302
  alias :height :maxy
340
303
  alias :width :maxx
341
304
  alias :clear_line :clrtoeol
@@ -345,11 +308,11 @@ module Curses
345
308
  def size; UI::Size.new(height: maxy, width: maxx) end
346
309
 
347
310
  def cursor=(new)
348
- setpos(new.y, new.x) # or fail "Could not set cursor: #{new} #{size}"
311
+ move(new.y, new.x) # or fail "Could not set cursor: #{new} #{size}"
349
312
  end
350
313
 
351
314
  def pos=(new)
352
- move(new.y, new.x)
315
+ mvwin(new.y, new.x)
353
316
  end
354
317
 
355
318
  def size=(new)
@@ -361,18 +324,18 @@ module Curses
361
324
  end
362
325
 
363
326
  def getch1(timeout=-1)
364
- self.timeout=(timeout)
327
+ self.timeout(timeout)
365
328
  getch
366
329
  end
367
330
 
368
- def on_line(n) setpos(n, curx) ;self;end
369
- def on_column(n) setpos(cury, n) ;self;end
370
- def next_line; setpos(cury + 1, 0) ;self;end
371
- def mv_left(n) setpos(cury, curx - 1) ;self;end
372
- def line_start(l=0) setpos(l, 0) ;self;end
373
- def from_left(size) setpos(cury, size) ;self;end
374
- def from_right(size) setpos(cury, (maxx - size)) ;self;end
375
- def center(size) setpos(cury, (maxx / 2) - (size / 2)) ;self;end
331
+ def on_line(n) move(n, curx) ;self;end
332
+ def on_column(n) move(cury, n) ;self;end
333
+ def next_line; move(cury + 1, 0) ;self;end
334
+ def mv_left(n) move(cury, curx - 1) ;self;end
335
+ def line_start(l=0) move(l, 0) ;self;end
336
+ def from_left(size) move(cury, size) ;self;end
337
+ def from_right(size) move(cury, (maxx - size)) ;self;end
338
+ def center(size) move(cury, (maxx / 2) - (size / 2)) ;self;end
376
339
 
377
340
  def center_string(string)
378
341
  center(string.size)
@@ -380,24 +343,32 @@ module Curses
380
343
  self end
381
344
 
382
345
  def insert_top
383
- setpos(0, 0)
346
+ move(0, 0)
384
347
  insertln
385
348
  self end
386
349
 
387
350
  def append_bottom
388
- setpos(0, 0)
351
+ move(0, 0)
389
352
  deleteln
390
- setpos(maxy - 1, 0)
353
+ move(maxy - 1, 0)
391
354
  self end
392
355
  end
393
356
 
394
- class MouseEvent
357
+ class IMouseEvent
395
358
  def pos
396
359
  UI::Point.new(x: x, y: y)
397
360
  end
398
361
 
399
362
  def to_fake
400
- UI::FakeMouseEvent.new(self)
363
+ IMouseEvent.new(self)
364
+ end
365
+
366
+ def to_s
367
+ name = ICurses.constants.
368
+ select { |c| c =~ /^BUTTON_/ }.
369
+ select { |c| ICurses.const_get(c) & @bstate > 0 }[0]
370
+ name ||= @button
371
+ "[(IMouseEvent) button=#{name}, x=#{x}, y=#{y}, z=#{z}]"
401
372
  end
402
373
  end
403
374
  end