#
# $Id: dialog.icn,v 1.7 2004/11/05 19:24:55 rparlett Exp $
#
# This file is in the public domain.
#
# Author: Robert Parlett (parlett@dial.pipex.com)
#

package gui
link graphics

$include "guih.icn"

#
#
# This is the parent class of a dialog window.
#
class Dialog : Component(
   win,                     # The dialog's window.
   is_open,                 # Flag indicates whether window is open              
   owning_dialog,
   child_dialogs,
   focus,                   # Component with current focus            
   unique_flag,             # Flag indicates whether in unique processing mode             
   re_process_flag,         # Flag indicates whether to distribute last
                            # Icon event during unique mode                      
   buffer_win,              # Buffer window for double buffering                 
   min_width,               # Minimum size of window.                
   min_height,              #                  
   click_count,             # Variables controlling multiple clicks
   double_click_delay,
   repeat_delay,            # Repeat event delays
   repeat_rate,
   prev_x,
   prev_y,
   prev_time,
   prev_event,
   is_blocked_flag,
   resize_attrib,
   drag_gesture_x,
   drag_gesture_y,
   curr_drag,
   tried_drag,
   pointer_stack,
   all_valid
   )

   method invoke_validate()
      if \self.all_valid then
         return
      if \self.unique_flag then
         self.unique_flag.validate()
      else
         self.validate()
      self.all_valid := 1
   end

   method needs_validate()
      return /self.all_valid
   end

   method is_shaded()
      return \self.is_shaded_flag
   end

   method is_unshaded()
      return /self.is_shaded_flag
   end

   method is_hidden()
      return /self.is_open
   end

   method is_unhidden()
      return \self.is_open
   end

   method block()
      self.is_blocked_flag := 1
      self.resize_attrib := WAttrib(self.win, "resize")
      WAttrib(self.win, "resize=off")
   end

   method unblock()
      self.is_blocked_flag := &null
      WAttrib(self.win, "resize=" || self.resize_attrib)
   end

   #
   # Returns the number of mouse clicks that have occurred
   # consecutively, with each click in the sequence being less
   # than {double_click_delay} milliseconds apart.  That variable is by default 500
   # milliseconds, but it may be configured with {set_double_click_delay().}
   #
   method get_click_count()
      return self.click_count
   end

   method compute_absolutes()
      self.x := 0
      self.y := 0
      self.w := WAttrib(self.win, "width")
      self.h := WAttrib(self.win, "height")
   end

   #
   # Change pointer, saving current pointer on a stack for restoration.
   #
   method change_pointer(s)
      push(pointer_stack, WAttrib(self.win, "pointer"))
      WAttrib(self.win, "pointer=" || s)
   end

   #
   # Restore pointer, from the stack of pointers.
   #
   method restore_pointer()
      WAttrib(self.win, "pointer=" || pop(pointer_stack))
   end

   #
   # This is a variation on the conventional modal and modeless
   # methods.  The dialog is opened, input to other windows is not blocked, but
   # the call does not return until the window is closed.
   # @param d   The parent dialog, if specified, is blocked until
   # @ the window is closed.
   #
   method show_child(d)
      self.show()
      dispatcher.add(self)
      if \d then {
         insert(d.child_dialogs, self)
         self.owning_dialog := d
         d.block()
         dispatcher.message_loop(self)
         d.unblock()
      } else
         dispatcher.message_loop(self)
   end

   #
   # Displays the dialog as a modeless dialog.  This 
   # means that window events are processed by this dialog
   # and other open dialogs concurrently.  The call to
   # {show_modeless()} opens the dialog and returns immediately.
   #
   # @param d   This optional parameter specifies the parent dialog.
   # @ When a parent dialog is closed, its child dialogs are automatically closed.
   #
   method show_modeless(d)
      self.show()
      dispatcher.add(self)
      if \d then {
         insert(d.child_dialogs, self)
         self.owning_dialog := d
         self.is_blocked_flag := d.is_blocked_flag
      }
   end

   #
   # Displays the dialog as a modal dialog.  In other
   # words, window events to any other open dialogs are blocked
   # until the dialog is closed.  This method doesn't return
   # until the dialog is closed.
   # @param d   The parent dialog.  It will not normally be
   # @ needed.
   #
   method show_modal(d)
      self.show()
      if \d then {
         insert(d.child_dialogs, self)
         self.owning_dialog := d
      }
      l := dispatcher.list_unblocked()
      every (!l).block()
      dispatcher.add(self)
      dispatcher.message_loop(self)
      every (!l).unblock()
   end

   #
   # Returns the Icon window associated with the dialog.
   #
   method get_win()
      return self.win
   end

   method resize_win(w, h)
      WAttrib(self.win, "size=" || w || "," || h)
      Enqueue(self.win, &resize)      
   end

   method init()
      self.parent_dialog := self
      self.cwin := Clone(self.win)
      self.cbwin := Clone(self.buffer_win)
      every (!self.children).init()
   end

   method open_win()
      self.win := (WOpen ! (["inputmask=mc"] ||| self.attribs)) | fatal("couldn't open window")
      self.buffer_win := (WOpen ! (["canvas=hidden"] ||| self.attribs)) | fatal("couldn't open window")
      return
   end

   method close_win()
      WClose(self.buffer_win)
      return WClose(self.win)
   end

   #
   # Sets the minimum dimensions for a window.  The user will not
   # be able to resize the window below this size.
   #
   method set_min_size(w, h)
      self.min_width := w
      self.min_height := h
      return
   end

   method get_buffer_win()
      return self.buffer_win
   end

   method set_unique(c)
      /self.unique_flag := c | stop("internal error")
      return
   end

   method clear_unique(x)
      self.re_process_flag := x
      self.unique_flag := &null
      self.all_valid := &null
      return
   end

   #
   # Sets keyboard focus to the given component.  This method
   # should only be invoked after the dialog has been displayed.
   # To give a component the initial keyboard focus,
   # invoke this method from within {init_dialog()}
   #
   method set_focus(c, e)
      if \self.focus === c then
         return

      (\self.focus).lost_focus(e)
      self.focus := c
      self.focus.got_focus(e)

      return
   end

   #
   # Clear the keyboard focus.
   #
   method clear_focus(e)
      (\self.focus).lost_focus(e)
      self.focus := &null
      return
   end

   #
   # Display all components
   #
   # @p
   method display(buffer_flag)
      if \buffer_flag then {
         EraseArea(buffer_win, 0, 0, get_w_reference(), get_h_reference())
         self.Component.display(1)
         CopyArea(buffer_win, win, 0, 0, get_w_reference(), get_h_reference(), 0, 0)
      } else {
         EraseArea(win, 0, 0, get_w_reference(), get_h_reference())
         self.Component.display()
      }
   end

   #
   # This empty method is invoked just after the dialog is displayed for the first time.
   #
   method init_dialog()
   end

   #
   # This empty method may be overridden to add components to the
   # dialog.  Alternatively, components may be added in the
   # dialog's {initially} method.
   #
   method component_setup()
   end

   #
   # This empty method may be overridden.  It is invoked just
   # before the dialog window is closed.
   #
   method end_dialog()
   end

   method show()
      self.component_setup()
      self.open_win()
      self.init()
      self.resize()
      self.firstly()
      self.is_open := 1
      self.validate()
      self.init_dialog()
   end

   method bevel_dispose()
      BevelDisposeAll()
   end

   method dispose()
      self.end_dialog()
      every (!child_dialogs).dispose()
      self.finally()
      self.bevel_dispose()
      self.close_win()
      self.is_open := &null
      dispatcher.del(self)
      delete((\owning_dialog).child_dialogs, self)
   end

   method consume_same(e)
      while *Pending(self.win) > 0 & Pending(self.win)[1] === e do {
         e := ::Event(self.win)
      }
   end

   method process_event(e)
      if e === -11 then {
         fire(CLOSE_BUTTON_EVENT, e)
      }

      if e === (&lpress | &rpress | &mpress) then {
         check_click_count(e)
      }

      if e === &resize then {
         handle_resize(e)
      }

      if \self.unique_flag then {
         process_unique(e)

         if /self.is_open then
            return
      }

      if /self.unique_flag & /self.re_process_flag then {
         if /self.curr_drag then 
            check_dnd(e)

         if \self.curr_drag then
            process_dnd(e)
         else
            process_normal(e)

         if /self.is_open then
            return
      }

      self.re_process_flag := &null
   end

   #
   # Normal event processing - not dnd or unique
   # @p
   method process_normal(e)
      local c

      if e === (&lpress | &rpress | &mpress) then {
         if c := self.find_focus() then
            self.set_focus(c, e)
      }

      if &meta & type(e) == "string" then {
         if c := self.find_accel(e) then
            c.handle_accel(e)
      }

      do_handle_event(e)

      if (e === ("\t" | Key_Right)) & ( /self.focus | not(self.focus.keeps(e))) then {
         if c := find_next_focus() then
            self.set_focus(c, e)
      } else if e === (Shift_Tab | Key_Left) & ( /self.focus | not(self.focus.keeps(e))) then {
         if c := find_previous_focus() then
            self.set_focus(c, e)
      }

   end

   #
   # Drag & drop mode processing
   # @p
   method process_dnd(e)
      if e === (&ldrag | &rdrag | &mdrag) then {
         if self.invoke_drag_event(self.curr_drag) then
            WAttrib(self.win, "pointer=hand2")
         else
            WAttrib(self.win, "pointer=exchange")
      } else if e === (&lrelease | &rrelease | &mrelease) then {
         if c := self.invoke_can_drop(self.curr_drag) then
            self.curr_drag.get_source().invoke_end_drag(self.curr_drag, c)
         self.curr_drag := &null
         self.invoke_drag_reset()
         restore_pointer()
      }
   end

   #
   # Process a unique-mode event
   # @p
   method process_unique(e)
      self.unique_flag.do_handle_event(e)
   end

   #
   # Check whether we should start a dnd (by setting curr_drag)
   # @p
   method check_dnd(e)
      if e === (&ldrag | &rdrag | &mdrag) then {
         # Note the position of the start of a drag
         /self.drag_gesture_x := &x
         /self.drag_gesture_y := &y
         if /self.tried_drag &
            (abs(&x - self.drag_gesture_x) > 3 |
             abs(&y - self.drag_gesture_y) > 3 ) then {
                # Try to begin a drag.
                self.curr_drag := self.invoke_can_drag(e) 
                self.tried_drag := 1
                if \self.curr_drag then {
                   change_pointer("exchange")
                }
             }
      } else
         self.tried_drag := self.drag_gesture_x := self.drag_gesture_y := &null
   end

   #
   # Process a resize 
   # @p
   method handle_resize(e)
      consume_same(e)
      nw := WAttrib(self.win, "width")
      nh := WAttrib(self.win, "height")
      #
      # Don't allow size to fall below minimum.
      #
      if nw <:= \self.min_width then
         WAttrib(self.win, "width=" || nw)

      if nh <:= \self.min_height then
         WAttrib(self.win, "height=" || nh)

      #
      # Resize buffer canvas
      #
      WAttrib(self.buffer_win, "width=" || nw)
      WAttrib(self.buffer_win, "height=" || nh)
      EraseArea(self.win, 0, 0, nw, nh)
      self.resize()
   end

   #
   # Maybe increment the click count
   # @p
   method check_click_count(e)
      local t

      t := dispatcher.curr_time_of_day()
      if e = \prev_event & prev_x = &x & prev_y = &y & (t - prev_time < double_click_delay) then
         click_count +:= 1
      else
         click_count := 1

      prev_event := e
      prev_time := t
      prev_x := &x
      prev_y := &y
   end

   method get_focus_list()
      local l, c
      l := []
      every c := self.generate_components() do {
         if c.accepts_focus() & c.is_unhidden() & c.is_unshaded() then {
            put(l, c)
         }
      }
      return l
   end

   method find_next_focus()
      local l
      l := get_focus_list()
      every i := 1 to *l - 1 do {
         if l[i] === self.focus then
            return l[i + 1]
      }
      return l[1]
   end

   method find_previous_focus()
      local l
      l := get_focus_list()
      every i := 2 to *l do {
         if l[i] === self.focus then
            return l[i - 1]
      }
      return l[-1]
   end

   #
   # Set the delay in milliseconds between double clicks.  The
   # default is 500 milliseconds
   #
   method set_double_click_delay(i)
      return self.double_click_delay := i
   end

   #
   # Set the delay in milliseconds between an initial repeating event
   # and the start of repeat events.   The
   # default is 500 milliseconds
   #
   method set_repeat_delay(i)
      return self.repeat_delay := i
   end

   #
   # Set the delay in milliseconds between repeating events.
   # The default is 100 milliseconds
   #
   method set_repeat_rate(i)
      return self.repeat_rate := i
   end

   method set_one(attr, val)
      case attr of {
         "double_click_delay" : set_double_click_delay(int_val(attr, val))
         "repeat_delay" : set_repeat_delay(int_val(attr, val))
         "repeat_rate" : set_repeat_rate(int_val(attr, val))
         "min_size" : set_min_size!int_vals(attr, val)
         #
         # For a dialog, interpret pos and size as Icon attributes, not set_pos 
         # and set_size method invocations.
         #
         "pos" | "size" : set_attribs(as_attrib(attr, val))
         default: self.Component.set_one(attr, val)
      }
   end

   initially(a[])
      self.Component.initially()
      self.child_dialogs := set([])
      self.pointer_stack := []
      self.double_click_delay := 500
      self.repeat_delay := 500
      self.repeat_rate := 100
      set_fields(a)
end