1# xm-enved.rb -- Translation of xm-enved.scm and enved.scm
2
3# Translator/Author: Michael Scholz <mi-scholz@users.sourceforge.net>
4# Created: 03/03/18 00:18:35
5# Changed: 20/09/19 00:41:47
6
7# Tested with Snd 20.x
8#             Ruby 2.6
9#             Motif 2.3.3 X11R6
10#
11# module Snd_enved
12#  channel_enved(snd, chn)
13#  set_channel_enved(new_env, snd, chn)
14#  channel_envelope(snd, chn)
15#  set_channel_envelope(new_env, snd, chn)
16#  mouse_press_envelope(snd, chn, button, state, x, y)
17#  mouse_drag_envelope(snd, chn, button, state, x, y)
18#  mouse_release_envelope(snd, chn, button, state, x, y, axis)
19#  create_initial_envelopes(snd)
20#  enveloping_key_press(snd, chn, key, state)
21#  start_enveloping
22#  stop_enveloping
23#  play_with_envs(snd = false)
24#
25#  envelope?(obj)
26#  make_enved(enved)
27#  enved?(obj)
28#  make_graph_enved(enved, snd, chn)
29#  graph_enved?(obj)
30#  make_xenved(name, parent, *rest)
31#  xenved?(obj)
32#  xenved_test(name)
33#
34# class Enved
35#  initialize(enved)
36#  inspect
37#  to_s
38#  envelope
39#  envelope=(new_env)
40#  reset
41#  interp(x, base)
42#  stretch(old_att, new_att, old_dec, new_dec)
43#  stretch!(old_att, new_att, old_dec, new_dec)
44#  scale(scale, offset)
45#  scale!(scale, offset)
46#  normalize(new_max)
47#  normalize!(new_max)
48#  reverse
49#  reverse!
50#  point(idx, *args)
51#  min
52#  max
53#  length
54#  first
55#  last
56#  first_x
57#  last_x
58#  first_y
59#  last_y
60#  in_range?(x)
61#  each
62#  each_with_index
63#  map
64#  map!
65#
66# class Graph_enved < Enved
67#  initialize(enved, snd, chn)
68#  click_time
69#  click_time=(val)
70#  before_enved_hook
71#  after_enved_hook
72#  inspect
73#  to_s
74#  reset
75#  redraw
76#  mouse_press_cb(x, y)
77#  mouse_drag_cb(x, y)
78#  mouse_release_cb
79#
80# class Xenved < Graph_enved
81#  initialize(name, parent, enved, bounds, args, axis_label)
82#  inspect
83#  to_s
84#  clear
85#  envelope=(new_env)
86#  axis_bounds
87#  axis_bounds=(bounds)
88#  point(idx, *args)
89#  create
90#  close
91
92require "env"
93require "hooks"
94require "extensions"
95
96if provided?(:snd_motif)
97  require "snd-xm"
98  include Snd_XM
99else
100  $with_motif = false
101end
102
103#
104# defined in snd-xm.rb:
105#
106# $with_motif
107#
108
109module Snd_enved
110  # returns Graph_enved object or nil
111  def channel_enved(snd = false, chn = false)
112    channel_property(:enved_envelope, snd, chn)
113  end
114
115  # sets Graph_enved object
116  def set_channel_enved(new_ge, snd = false, chn = false)
117    set_channel_property(:enved_envelope, new_ge, snd, chn)
118  end
119
120  add_help(:channel_envelope,
121           "channel_envelope(snd=false, chn=false)  \
122Returns the current enved envelope associated with SND's channel CHN.")
123  def channel_envelope(snd = false, chn = false)
124    if graph_enved?(ge = channel_enved(snd, chn))
125      ge.envelope
126    elsif envelope?(en = channel_property(:channel_envelope, snd, chn))
127      set_channel_enved(make_graph_enved(en, snd, chn), snd, chn)
128      en
129    else
130      nil
131    end
132  end
133
134  def set_channel_envelope(new_env, snd = false, chn = false)
135    set_channel_property(:channel_envelope, new_env, snd, chn)
136    if graph_enved?(ge = channel_enved(snd, chn))
137      ge.envelope = new_env
138    else
139      ge = make_graph_enved(new_env, snd, chn)
140      set_channel_enved(ge, snd, chn)
141    end
142    ge
143  end
144
145  #   left button: set/delete point
146  # middle button: reset to original env
147  def mouse_press_envelope(snd, chn, button, state, x, y)
148    case button
149    when 1
150      graph_enved?(ge = channel_enved(snd, chn)) and ge.mouse_press_cb(x, y)
151    when 2
152      graph_enved?(ge = channel_enved(snd, chn)) and ge.reset
153    end
154  end
155
156  def mouse_drag_envelope(snd, chn, button, state, x, y)
157    graph_enved?(ge = channel_enved(snd, chn)) and ge.mouse_drag_cb(x, y)
158  end
159
160  def mouse_release_envelope(snd, chn, button, state, x, y, axis)
161    if axis == Lisp_graph
162      graph_enved?(ge = channel_enved(snd, chn)) and ge.mouse_release_cb
163      true
164    else
165      false
166    end
167  end
168
169  def create_initial_envelopes(snd)
170    channels(snd).times do |chn|
171      set_dot_size(8, snd, chn)
172      set_channel_envelope([0.0, 1.0, 1.0, 1.0], snd, chn)
173    end
174  end
175
176  def enveloping_key_press(snd, chn, key, state)
177    # C-g returns to original env
178    # C-. applies current env to amplitude
179    if key == ?. and state == 4
180      env_channel((channel_envelope(snd, chn) or [0, 1, 1, 1]),
181                  0, framples(snd, chn), snd, chn)
182      true
183    else
184      if key == ?g and state == 4
185        graph_enved?(ge = channel_enved(snd, chn)) and ge.reset
186        true
187      else
188        false
189      end
190    end
191  end
192
193  Hook_name = "graph-enved"
194
195  add_help(:start_enveloping,
196           "start_enveloping()  \
197Starts the enved processes, displaying an envelope editor in each channel.")
198  def start_enveloping
199    unless $after_open_hook.member?(Hook_name)
200      $after_open_hook.add_hook!(Hook_name) do |snd|
201        create_initial_envelopes(snd)
202      end
203      $mouse_press_hook.add_hook!(Hook_name) do |snd, chn, button, state, x, y|
204        mouse_press_envelope(snd, chn, button, state, x, y)
205      end
206      $mouse_drag_hook.add_hook!(Hook_name) do |snd, chn, button, state, x, y|
207        mouse_drag_envelope(snd, chn, button, state, x, y)
208      end
209      $mouse_click_hook.add_hook!(Hook_name) do |snd, chn, but, st, x, y, axis|
210        mouse_release_envelope(snd, chn, but, st, x, y, axis)
211      end
212      $key_press_hook.add_hook!(Hook_name) do |snd, chn, key, state|
213        enveloping_key_press(snd, chn, key, state)
214      end
215      true
216    else
217      false
218    end
219  end
220
221  add_help(:stop_enveloping,
222           "stop_enveloping()  \
223Turns off the enved channel-specific envelope editors.")
224  def stop_enveloping
225    $after_open_hook.remove_hook!(Hook_name)
226    $mouse_press_hook.remove_hook!(Hook_name)
227    $mouse_drag_hook.remove_hook!(Hook_name)
228    $mouse_click_hook.remove_hook!(Hook_name)
229    $key_press_hook.remove_hook!(Hook_name)
230    Snd.catch do
231      set_lisp_graph?(false, Snd.snd, Snd.chn)
232    end
233    nil
234  end
235
236  # some examples
237
238  add_help(:play_with_envs,
239           "play_with_envs(snd=false)  \
240Sets channel amps during playback from the associated enved envelopes.")
241  def play_with_envs(snd = false)
242    channel_envelope(snd, 0) or create_initial_envelopes(snd)
243    channels(snd).times do |chn|
244      player = make_player(snd, chn)
245      e = make_env(:envelope, channel_envelope(snd, chn),
246                   :length, (framples(snd, chn).to_f / dac_size).floor)
247      add_player(player, 0, -1, -1,
248                 lambda do |reason|
249                   $play_hook.reset_hook!
250                 end)
251      $play_hook.add_hook!(get_func_name) do |fr|
252        set_amp_control(env(e), player)
253      end
254    end
255    start_playing(channels(snd), srate(snd))
256  end
257
258  def envelope?(obj)
259    array?(obj) and obj.length >= 4 and obj.length.even?
260  end
261
262  def make_enved(enved = [0, 0, 1, 1])
263    assert_type(envelope?(enved), enved, 0,
264                "an envelope, at least 2 points [x0, y0, x1, y1, ...]")
265    Enved.new(enved)
266  end
267
268  def enved?(obj)
269    obj.instance_of?(Enved)
270  end
271
272  def make_graph_enved(enved = [0, 0, 1, 1], snd = Snd.snd, chn = Snd.chn)
273    assert_type(envelope?(enved), enved, 0,
274                "an envelope, at least 2 points [x0, y0, x1, y1, ...]")
275    (ge = Graph_enved.new(enved, snd, chn)).redraw
276    ge
277  end
278
279  def graph_enved?(obj)
280    obj.instance_of?(Graph_enved)
281  end
282
283  if $with_motif
284    def make_xenved(name, parent, *rest)
285      envelope, bounds, args, label = optkey(rest,
286                                             [:envelope, [0, 0, 1, 1]],
287                                             [:axis_bounds, [0, 1, 0, 1]],
288                                             [:args, []],
289                                             :axis_label)
290      unless string?(name) and (not name.empty?)
291        name = "xenved"
292      end
293      assert_type(widget?(parent), parent, 1, "a widget")
294      assert_type((array?(bounds) and bounds.length == 4), bounds, 3,
295                  "an array of 4 elements [x0, x1, y0, y1]")
296      unless array?(label) and label.length == 4
297        label = bounds
298      end
299      Xenved.new(name, parent, envelope, bounds, args, label)
300    end
301
302    def xenved?(obj)
303      obj.instance_of?(Xenved)
304    end
305
306    Test_widget_type = RxmFormWidgetClass
307    Test_widget_args = [RXmNheight, 200]
308    Test_xenved_args = [RXmNleftAttachment,   RXmATTACH_WIDGET,
309                        RXmNtopAttachment,    RXmATTACH_WIDGET,
310                        RXmNbottomAttachment, RXmATTACH_WIDGET,
311                        RXmNrightAttachment,  RXmATTACH_WIDGET]
312
313    def xenved_test(name = "xenved")
314      make_xenved(name,
315                  add_main_pane(name, Test_widget_type, Test_widget_args),
316                  :envelope,    [0, 0, 1, 1],
317                  :axis_bounds, [0, 1, 0, 1],
318                  :args,        Test_xenved_args)
319    end
320  end
321end
322
323include Snd_enved
324
325class Enved
326  include Enumerable
327  include Info
328
329  def initialize(enved = [0, 0, 1, 1])
330    (@envelope = enved).map! do |x|
331      x.to_f
332    end
333    @init = @envelope.dup
334    set_enved_help
335  end
336  attr_reader :envelope
337
338  def inspect
339    format("%s.new(%s)", self.class, @envelope)
340  end
341
342  def to_s
343    format("#<%s: envelope: %s>", self.class, @envelope.to_string)
344  end
345
346  def envelope=(enved)
347    assert_type(envelope?(enved), enved, 0,
348                "an envelope, at least 2 points [x0, y0, x1, y1, ...]")
349    @envelope = enved.map do |x|
350      x.to_f
351    end
352  end
353
354  def reset
355    @envelope = @init.dup
356  end
357
358  def interp(x, base = 0)
359    envelope_interp(x, @envelope, base)
360  end
361
362  def stretch(old_att = nil, new_att = nil, old_dec = nil, new_dec = nil)
363    stretch_envelope(@envelope, old_att, new_att, old_dec, new_dec)
364  end
365
366  def stretch!(old_att = nil, new_att = nil, old_dec = nil, new_dec = nil)
367    self.envelope = self.stretch(old_att, new_att, old_dec, new_dec)
368  end
369
370  def scale(scale = 1.0, offset = 0.0)
371    scale_envelope(@envelope, scale, offset)
372  end
373
374  def scale!(scale = 1.0, offset = 0.0)
375    self.envelope = self.scale(scale, offset)
376  end
377
378  def normalize(new_max = 1.0)
379    self.scale(new_max / self.max)
380  end
381
382  def normalize!(new_max = 1.0)
383    self.envelope = self.normalize(new_max)
384  end
385
386  def reverse
387    reverse_envelope(@envelope)
388  end
389
390  def reverse!
391    self.envelope = self.reverse
392  end
393
394  def point(idx, *args)
395    x, y = optkey(args, :x, :y)
396    if x
397      @envelope[idx * 2] = x
398    end
399    if y
400      @envelope[idx * 2 + 1] = y
401    end
402    @envelope[idx * 2, 2]
403  end
404
405  def min
406    min_envelope(@envelope)
407  end
408
409  def max
410    max_envelope(@envelope)
411  end
412
413  def length
414    @envelope.length / 2
415  end
416
417  def first
418    if @envelope.length > 1
419      @envelope[0, 2]
420    else
421      [0.0, 0.0]
422    end
423  end
424
425  def last
426    if @envelope.length > 3
427      @envelope[-2, 2]
428    else
429      [1.0, 0.0]
430    end
431  end
432
433  def first_x
434    @envelope[0]
435  end
436
437  def first_y
438    @envelope[1]
439  end
440
441  def last_x
442    @envelope[-2]
443  end
444
445  def last_y
446    @envelope[-1]
447  end
448
449  def in_range?(x)
450    x > @envelope[0] and x < @envelope[-2]
451  end
452
453  def each
454    0.step(@envelope.length - 1, 2) do |i|
455      yield(@envelope[i, 2])
456    end
457    @envelope
458  end
459
460  def each_with_index
461    0.step(@envelope.length - 1, 2) do |i|
462      yield(@envelope[i, 2] + [i])
463    end
464    @envelope
465  end
466
467  def map
468    res = make_array(@envelope.length)
469    0.step(@envelope.length - 1, 2) do |i|
470      res[i, 2] = yield(@envelope[i, 2])
471    end
472    res
473  end
474
475  def map!
476    0.step(@envelope.length - 1, 2) do |i|
477      @envelope[i, 2] = yield(@envelope[i, 2])
478    end
479    @envelope
480  end
481
482  private
483  def set_enved_help
484    self.description = "\
485# make_enved(env)
486#
487# class Enved
488#   initialize(env)
489#
490# getter and setter:
491#   envelope=(new_env)
492#   envelope
493#
494# methods:
495#   interp(x, base)
496#   stretch(oatt, natt, odec, ndec) stretch!(oatt, natt, odec, ndec)
497#   scale(scale, offset)            scale!(scale, offset)
498#   normalize(new_max)              normalize!(new_max)
499#   reverse                         reverse!
500#   max                             min
501#   first   (first [x, y])          last   (last [x, y])
502#   first_x                         last_x
503#   first_y                         last_y
504#   map do |x, y| ... end           map! do |x, y| ... end
505#   each do |x, y| ... end          each_with_index do |x, y, i| ... end
506#   length
507#   point(idx, *args)               # point(idx) ==> [x, y]
508#                                   # point(idx, :x, x_val, :y, y_val)
509#                                   # sets x, y or both and returns new [x, y]
510#   in_range?(x)   (x > first_x and x < last_x)
511#   help           (alias info and description)
512"
513    add_help(:Enved, self.description)
514  end
515end
516
517class Graph_enved < Enved
518  def initialize(enved, snd = false, chn = false)
519    super(enved)
520    @graph_name = short_file_name(snd)
521    @snd = snd
522    @chn = chn
523    @before_enved_hook = Hook.new("@before_enved_hook", 4, "\
524lambda do |pos, x, y, reason| ... end: called before changing a \
525breakpoint in @envelope.  This hook runs the global $enved_hook as \
526first hook, subsequent procedures can directly manipulate @envelope.
527
528This instance hook is like the global $enved_hook; POS is @envelope's \
529x-position, X and Y are the new points, and REASON is one of \
530Enved_add_point, Enved_delete_point, Enved_move_point.  If the last \
531hook procedure in the hook list returns `false', the class changes the \
532breakpoint, otherwise the hook procedures are responsible for \
533manipulating @envelope itself.
534
535From dlocsig.rb:
536
537@velocity = make_xenved(\"velocity (v)\", frame,
538                        :envelope, [0.0, 0.0, 1.0, 0.0],
539                        :axis_bounds, [0.0, 1.0, 0.0, 1.0],
540                        :axis_label, [-20.0, 20.0, 0.0, 2.0])
541@velocity.before_enved_hook.reset_hook!   # to prevent running $enved_hook
542@velocity.before_enved_hook.add_hook!(\"dlocsig-hook\") do |pos, x, y, reason|
543  if reason == Enved_move_point
544    if @velocity.in_range?(x)
545      old_x = @velocity.point(pos).first
546      @velocity.stretch!(old_x, x)
547      @velocity.point(pos, :y, y)
548    else
549      false
550    end
551  else
552    false
553  end
554end
555
556In contrast the same procedure on the global $enved_hook:
557
558$enved_hook.add_hook!(\"snd-init-hook\") do |env, pt, x, y, reason|
559  if reason == Enved_move_point
560    if x > 0.0 and x < env[-2]
561      old_x = env[2 * pt]
562      new_env = stretch_envelope(env, old_x, x)
563      new_env[pt * 2 + 1] = y
564      new_env
565    else
566      # env               # first and last points are fixed
567      false               # first and last points can be moved
568    end
569  else
570    false
571  end
572end")
573    hn = "initialize-xm-enved-hook"
574    @before_enved_hook.add_hook!(hn) do |pos, x, y, reason|
575      if $enved_hook.empty?
576        false
577      else
578        e = nil
579        $enved_hook.run_hook do |prc|
580          case e = prc.call(@envelope.dup, pos, x, y, reason)
581          when Array
582            self.envelope = e
583          when Enved, Graph_enved, Xenved
584            self.envelope = e.envelope
585          end
586        end
587        e.class != FalseClass
588      end
589    end
590    @after_enved_hook = Hook.new("@after_enved_hook", 2, "\
591lambda do |pos, reason| ... end: called after redrawing new or changed \
592breakpoints.  POS is @envelope's x-position, and REASON is one of \
593Enved_add_point, Enved_delete_point, Enved_move_point.")
594    init
595    set_enved_help
596  end
597  alias help description
598  attr_accessor :click_time
599  attr_reader :before_enved_hook
600  attr_reader :after_enved_hook
601
602  def inspect
603    format("%s.new(%s, %s, %s)", self.class, @envelope, @snd, @chn)
604  end
605
606  def to_s
607    format("#<%s: %s[%s:%s]: %s>",
608           self.class, @graph_name, @snd, @chn, @envelope.to_string)
609  end
610
611  def init
612    @mouse_up   = 0.0
613    @mouse_down = 0.0
614    @click_time = 0.5
615    @mouse_pos  = 0
616    @mouse_new  = false
617  end
618
619  def envelope=(new_env)
620    super
621    self.redraw
622    @envelope
623  end
624
625  def reset
626    super
627    self.redraw
628    init
629    @envelope
630  end
631
632  def redraw
633    graph(@envelope, @graph_name, 0.0, 1.0, 0.0, 1.0, @snd, @chn)
634    update_lisp_graph(@snd, @chn)
635  end
636
637  Mouse_radius = 0.03
638
639  # To prevent unexpected point movements if position is near first or
640  # last point.
641  Secure_distance = 0.001
642
643  def mouse_press_cb(x, y)
644    x = [0.0, [x, 1.0].min].max
645    y = [0.0, [y, 1.0].min].max
646    pos = false
647    self.each_with_index do |x1, y1, i|
648      if (x1 - x).abs < Mouse_radius and (y1 - y).abs < Mouse_radius
649        pos = i
650        break
651      end
652    end
653    @mouse_new = (not pos)
654    @mouse_down = Time.now.to_f
655    if pos
656      @mouse_pos = pos
657    else
658      x = [Secure_distance, [x, 1.0 - Secure_distance].min].max
659      if run_before_enved_hook(x, y, Enved_add_point)
660        add_envelope_point(x, y)
661      end
662      self.redraw
663      @after_enved_hook.call(@mouse_pos / 2, Enved_add_point)
664    end
665  end
666
667  def mouse_drag_cb(x, y)
668    lx = if @mouse_pos.zero?
669           @envelope[0]
670         elsif @mouse_pos >= (@envelope.length - 2)
671           @envelope[-2]
672         else
673           [@envelope[@mouse_pos - 2] + Secure_distance,
674            [x, @envelope[@mouse_pos + 2] - Secure_distance].min].max
675         end
676    #ly = [0.0, [y, 1.0].min].max
677    ly = y
678    if run_before_enved_hook(lx, ly, Enved_move_point)
679      @envelope[@mouse_pos, 2] = [lx, ly]
680    end
681    self.redraw
682    @after_enved_hook.call(@mouse_pos / 2, Enved_move_point)
683  end
684
685  def mouse_release_cb
686    @mouse_up = Time.now.to_f
687    if (not @mouse_new) and (@mouse_up - @mouse_down) <= @click_time and
688        @mouse_pos.nonzero? and @mouse_pos < (@envelope.length - 2)
689      if run_before_enved_hook(@envelope[@mouse_pos], @envelope[@mouse_pos + 1],
690                               Enved_delete_point)
691        @envelope.slice!(@mouse_pos, 2)
692      end
693      self.redraw
694      @after_enved_hook.call(@mouse_pos / 2, Enved_delete_point)
695    end
696    @mouse_new = false
697  end
698
699  protected
700  # If the last hook procedure returns false, change the envelope,
701  # otherwise the hook procedure is responsible.
702  def run_before_enved_hook(x, y, reason)
703    if @before_enved_hook.empty?
704      true
705    else
706      e = nil
707      @before_enved_hook.run_hook do |prc|
708        e = prc.call(@mouse_pos / 2, x, y, reason)
709      end
710      e.class == FalseClass
711    end
712  end
713
714  def add_envelope_point(x, y)
715    idx = @mouse_pos
716    test_env = @envelope.to_pairs
717    if cur_pair = test_env.assoc(x)
718      idx = test_env.index(cur_pair) * 2
719      @envelope[idx + 1] = y
720    else
721      cur_pair = test_env.detect do |pair|
722        x < pair[0]
723      end
724      if cur_pair
725        idx = test_env.index(cur_pair) * 2
726        @envelope.insert(idx, x, y)
727      end
728    end
729    @mouse_pos = idx
730  end
731
732  private
733  def set_enved_help
734    super
735    self.description += "\
736#
737# make_graph_enved(enved, snd, chn)
738#
739# class Graph_enved < Enved (see enved.scm)
740#   initialize(enved, snd, chn)
741#   before_enved_hook              lambda do |pos, x, y, reason| ... end
742#   after_enved_hook               lambda do |pos, reason| ... end
743#
744# getter and setter:
745#   click_time=(val)
746#   click_time
747#
748# interactive methods:
749#   init
750#   reset
751#   redraw
752#   mouse_press_cb(x, y)
753#   mouse_drag_cb(x, y)
754#   mouse_release_cb
755#   help     (alias info and description)
756
757# more examples in xm-enved.rb, module Snd_enved
758
759ge = make_graph_enved([0, 0, 1, 1], 0, 0)
760ge.envelope                         # ==> [0.0, 0.0, 1.0, 1.0]
761ge.click_time                       # ==> 0.2
762ge.envelope = [0, 1, 1, 1]
763ge.help                             # this help
764"
765    add_help(:Graph_enved, self.description)
766  end
767end
768
769if $with_motif
770  class Xenved < Graph_enved
771    def initialize(name, parent, enved, bounds, args, axis_label)
772      super(enved)
773      @name = name
774      @parent = parent
775      @x0, @x1, @y0, @y1 = bounds.map do |x|
776        x.to_f
777      end
778      @args = args
779      unless @args.member?(RXmNforeground)
780        @args += [RXmNforeground, data_color]
781      end
782      unless @args.member?(RXmNbackground)
783        @args += [RXmNbackground, graph_color]
784      end
785      @lx0, @lx1, @ly0, @ly1 = if envelope?(axis_label)
786                                 axis_label.map do |x|
787                                   x.to_f
788                                 end
789                               else
790                                 [0.0, 1.0, -1.0, 1.0]
791                               end
792      @gc = snd_gcs[0]
793      @drawer = @dpy = @window = nil
794      @px0 = @px1 = @py0 = @py1 = nil
795      @dragging = false
796      set_enved_help
797      create
798    end
799    alias help description
800
801    def inspect
802      format("%s.new(%p, %s, %s, %s, %s, %s)",
803             self.class,
804             @name,
805             @parent,
806             @envelope,
807             [@x0, @x1, @y0, @y1],
808             @args,
809             [@lx0, @lx1, @ly0, @ly1])
810    end
811
812    def to_s
813      format("#<%s: name: %p, envelope: %s>",
814             self.class, @name, @envelope.to_string)
815    end
816
817    def axis_bounds
818      [@x0, @x1, @y0, @y1]
819    end
820
821    def axis_bounds=(bounds)
822      assert_type((array?(bounds) and bounds.length == 4), bounds, 0,
823                  "an array of 4 elements [x0, x1, y0, y1]")
824      @x0, @x1, @y0, @y1 = bounds.map do |x|
825        x.to_f
826      end
827      self.envelope = @init
828    end
829
830    def point(idx, *args)
831      if args.length > 0
832        super
833        redraw
834      end
835      @envelope[idx * 2, 2]
836    end
837
838    def create
839      if widget?(@drawer)
840        show_widget(@drawer)
841      else
842        create_enved
843      end
844    end
845    alias open create
846
847    def close
848      hide_widget(@drawer)
849    end
850
851    protected
852    def redraw
853      if is_managed?(@drawer) and @px0 and @py0 > @py1
854        RXClearWindow(@dpy, @window)
855        # Motif's DRAW-AXES takes 6 optional arguments.
856        # '( x0 y0 x1 y1 ) = draw-axes(wid gc label
857        #                              x0=0.0 x1=1.0 y0=-1.0 y1=1.0
858        #                              style=x-axis-in-seconds
859        #                              axes=show-all-axes)
860        # arity #( 3 6 #f )
861        draw_axes(@drawer, @gc, @name, @lx0, @lx1, @ly0, @ly1)
862        lx = ly = nil
863        @envelope.each_pair do |x, y|
864          cx = grfx(x)
865          cy = grfy(y)
866          RXFillArc(@dpy, @window, @gc,
867                    cx - Mouse_r, cy - Mouse_r, Mouse_d, Mouse_d, 0, 360 * 64)
868          if lx
869            RXDrawLine(@dpy, @window, @gc, lx, ly, cx, cy)
870          end
871          lx, ly = cx, cy
872        end
873      end
874    end
875
876    private
877    def set_enved_help
878      super
879      self.description += "\
880#
881# make_xenved(name, parent, *rest)
882#   name     String
883#   parent   Widget
884#   *rest
885#     :envelope,    [0, 0, 1, 1]   x0, y0, x1, y1, ...
886#     :axis_bounds, [0, 1, 0, 1]   x0, x1, y0, y1
887#     :args,        []             Motif properties
888#     :axis_label,  nil            if axes labels should have
889#                                  other values than axis_bounds,
890#                                  (see dlocsig.rb)
891#
892# class Xenved < Graph_enved (see xm-enved.scm)
893#   initialize(name, parent, env, axis_bounds, args, axis_label)
894#
895# getter and setter:
896#   axis_bounds=(new_bounds)
897#   axis_bounds
898#
899# interactive methods:
900#   create   (alias open)
901#   close
902#   reset
903#   help     (alias info and description)
904
905# more examples in effects.rb
906
907xe = xenved_test
908xe.envelope                         # ==> [0.0, 0.0, 1.0, 1.0]
909xe.click_time                       # ==> 0.5
910xe.envelope = [0, 1, 1, 1]
911# some clicks later
912xe.envelope                         # ==> [0.0, 0.0,
913                                    #      0.190735694822888, 0.562264150943396,
914                                    #      0.632152588555858, 0.932075471698113,
915                                    #      0.848773841961853, 0.316981132075472,
916                                    #      1.0, 1.0]
917xe.help                             # this help
918"
919      add_help(:Xenved, self.description)
920    end
921
922    Mouse_d = 10
923    Mouse_r = 5
924
925    def create_enved
926      @drawer = RXtCreateManagedWidget(@name, RxmDrawingAreaWidgetClass,
927                                       @parent, @args)
928      @dpy = RXtDisplay(@drawer)
929      @window = RXtWindow(@drawer)
930      RXtAddCallback(@drawer, RXmNresizeCallback,
931                     lambda do |w, c, i|
932                       draw_axes_cb
933                     end)
934      RXtAddCallback(@drawer, RXmNexposeCallback,
935                     lambda do |w, c, i|
936                       draw_axes_cb
937                     end)
938      RXtAddEventHandler(@drawer, RButtonPressMask, false,
939                         lambda do |w, c, e, f|
940                           mouse_press_cb(ungrfx(Rx(e)), ungrfy(Ry(e)))
941                         end)
942      RXtAddEventHandler(@drawer, RButtonReleaseMask, false,
943                         lambda do |w, c, e, f|
944                           mouse_release_cb
945                         end)
946      RXtAddEventHandler(@drawer, RButtonMotionMask, false,
947                         lambda do |w, c, e, f|
948                           mouse_drag_cb(ungrfx(Rx(e)), ungrfy(Ry(e)))
949                         end)
950      RXtAddEventHandler(@drawer, REnterWindowMask, false,
951                         lambda do |w, cursor, e, f|
952                           RXDefineCursor(@dpy, @window, cursor)
953                         end, RXCreateFontCursor(@dpy, RXC_crosshair))
954      RXtAddEventHandler(@drawer, RLeaveWindowMask, false,
955                         lambda do |w, c, e, f|
956                           RXUndefineCursor(@dpy, @window)
957                         end)
958    end
959
960    # Motif's DRAW_AXES takes 3 required and 6 optional arguments.
961    # draw_axes(wid, gc, label,
962    #           x0=0.0, x1=1.0, y0=-1.0, y1=1.0,
963    #           style=X_axis_in_seconds,
964    #           axes=Show_all_axes)
965    def draw_axes_cb
966      @px0, @py0, @px1, @py1 = draw_axes(@drawer, @gc, @name,
967                                         @lx0, @lx1, @ly0, @ly1,
968                                         X_axis_in_seconds,
969                                         Show_all_axes)
970      redraw
971    end
972
973    def ungrfx(x)
974      if @px0 == @px1
975        @x0
976      else
977        [@x1,
978         [@x0, @x0 + ((@x1 - @x0) * ((x - @px0) / (@px1.to_f - @px0)))].max].min
979      end
980    end
981
982    def ungrfy(y)
983      if @py0 == @py1
984        @y1
985      else
986        [@y1,
987         [@y0, @y0 + ((@y1 - @y0) * ((@py0 - y) / (@py0.to_f - @py1)))].max].min
988      end
989    end
990
991    def grfx(x)
992      if @px0 == @px1
993        @px0
994      else
995        [@px1,
996         [@px0,
997          (@px0 +
998           ((@px1 - @px0) * ((x - @x0) / (@x1.to_f - @x0)))).round].max].min
999      end
1000    end
1001
1002    def grfy(y)
1003      if @py0 == @py1
1004        @py0
1005      else
1006        [@py0,
1007         [@py1,
1008          (@py1 +
1009           ((@py0 - @py1) * ((y - @y1) / (@y0.to_f - @y1)))).round].max].min
1010      end
1011    end
1012  end
1013end
1014
1015# xm-enved.rb ends here
1016