-module(gs_dialog). % behaviour
-define(VERSION, '2001.1101').
-vsn(?VERSION).
-author('cpressey@catseye.mb.ca').
-copyright('Copyright (c)2001 Cat`s Eye Technologies. All rights reserved.').

%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%%   Redistributions of source code must retain the above copyright
%%%   notice, this list of conditions and the following disclaimer.
%%%
%%%   Redistributions in binary form must reproduce the above copyright
%%%   notice, this list of conditions and the following disclaimer in
%%%   the documentation and/or other materials provided with the
%%%   distribution.
%%%
%%%   Neither the name of Cat's Eye Technologies nor the names of its
%%%   contributors may be used to endorse or promote products derived
%%%   from this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
%%% CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
%%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
%%% DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
%%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
%%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
%%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
%%% OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
%%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
%%% OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
%%% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
%%% POSSIBILITY OF SUCH DAMAGE. 

-export([behaviour_info/1,
         show/4,
	 test/0]).

%%% BEGIN gs_dialog.erl %%%

%%% This module specifies a common behaviour for "modal" dialog boxes
%%% using GS.

%%--------------------------------------------------------------------
%% behaviour_info(callbacks) -> ListOfFunctionArityTuples
%% Used by R8 to check the implementation modules for consistency
%% with the behaviour specification, what callbacks this module needs.

behaviour_info(callbacks) ->
  [

    %%----------------------------------------------------------------
    %% Module:buttons() -> ListOfAtoms
    %% Should return the labels used on the main (non-extra) buttons
    %% of the dialog box.
    %% e.g. ['OK', 'Cancel'].

    {buttons, 0},

    %%----------------------------------------------------------------
    %% Module:icon() -> FileNameString | {Text, FgColour, BgColour}
    %% Should return the the icon displayed in the dialog box.
    %% This should either be the fully qualified filename of a 32x32 GIF
    %% file (e.g. in the application's priv dir,) or a 3-tuple
    %% describing a simple "circle" icon to be rendered by GS itself.
    %% The latter option was added because some versions of Erlang for
    %% Windows use a Tk emulation package which is not always on the
    %% ball when it comes to correct image transparency and colour.
    %% e.g. filename:join(code:priv_dir(?MODULE), "notify.gif")

    {icon, 0},

    %%----------------------------------------------------------------
    %% Module:controls(Parent, ArgList) -> {GSControl | nil, NewArgList}
    %% Used by the implementation to provide extra controls in the dialog
    %% box, if any.  If not, nil should be returned instead of the control.
    %% If many controls are added, it is recommended they are placed in a
    %% frame, with the frame returned as the control.
    %% The control need not have positioning information, as it will be
    %% assigned a pack_xy option when it is placed into the Parent frame.
    %% The list of arguments may be modified by this callback.
    %% e.g. {nil, Args}

    {controls, 2},

    %%----------------------------------------------------------------
    %% Module:on_key(ExtraControl, KeyAtom, ArgList) ->
    %%   {button, ButtonNameAtom} | nil
    %% Called when a key is pressed in the dialog box.  The return value
    %% specified whether it is linked to pressing a button, or whether it
    %% it is ignored and passed on to a further handler (if any.)
    
    {on_key, 3},

    %%----------------------------------------------------------------
    %% Module:on_button(ExtraControl, ButtonNameAtom, ArgList) -> Result
    %% Called when one of the main (non-extra) buttons are pressed in
    %% the dialog box.  Since this closes the dialog box, the implementation
    %% module is expected to provide a final result term with this function.

    {on_button, 3}, 

    %%----------------------------------------------------------------
    %% Module:on_event(ExtraControl, Event, ArgList) -> Result
    %% Allows the implementation module to handle other GS events,
    %% e.g. those generated by the extra controls specified.
 
    {on_event, 3}
  ].

%%% Public Interface

%%--------------------------------------------------------------------
%% show(ModuleNameAtom, TitleString, MessageString, ArgList) -> Result
%% Display a generic modal dialog box, customized by the
%% callback functions in Module.  This should be called by
%% the 'show' function in the Module in question.
%% The argument list is passed back to the callback functions in the
%% module, for retaining information pertinent to the callback module;
%% the behaviour itself does not inspect or care about this list.

show(Module, Title, Message, Args) ->
  Screen = gs:start(),
  Buttons = Module:buttons(),
  NumButtons = length(Buttons),
  application:load(?MODULE),
  {ok, Font} = application:get_env(?MODULE, font),
  {ok, {ScreenWidth, ScreenHeight}} =
    application:get_env(?MODULE, screen_size),
  {ok, {DialogWidth, DialogHeight}} =
    application:get_env(?MODULE, dialog_size),
  Window = gs:create(window, Screen,
    [{width, DialogWidth}, {height, DialogHeight},
     {x, (ScreenWidth - DialogWidth) div 2},
     {y, (ScreenHeight - DialogHeight) div 2},
     {title, Title},
     {configure, true}, {keypress, true}]),
  Frame = gs:create(frame, Window,
    [{bw, 0},
     {packer_x, lists:duplicate(NumButtons, {stretch, 1})},
     {packer_y, [{stretch, 1},{stretch, 2},{stretch, 1}]}]),
  case Module:icon() of
    nil ->
      Label = gs:create(label, Frame,
        [{label, {text, Message}}, {font, Font}, {justify, center},
         {pack_xy, {{1, NumButtons}, 1}}]);
    {Text, Fg, Bg} ->
      InnerFrame = gs:create(frame, Frame,
        [{pack_xy, {{1, NumButtons}, 1}}, {bw, 0},
	 {packer_x, [{stretch, 1}, {fixed, 32}, {stretch, 8}]},
	 {packer_y, [{stretch, 1}, {fixed, 32}, {stretch, 1}]}]),
      IconCanvas = gs:create(canvas, InnerFrame,
        [{pack_xy, {2, 2}}]),
      IconCircle = gs:create(oval, IconCanvas,
        [{coords, [{0, 0}, {31, 31}]}, {fg, black}, {fill, Bg}]),
      IconFont = {screen, bold, 24},
      {ITW,ITH} = gs:read(IconCanvas, {font_wh, {IconFont, Text}}),
      ITX = 16 - ITW div 2,
      ITY = 16 - ITH div 2,
      IconText = gs:create(text, IconCanvas,
        [{coords, [{ITX, ITY}]}, {fg, Fg}, {text, Text}, {font, IconFont}]),
      Label = gs:create(label, InnerFrame,
        [{label, {text, Message}}, {font, Font}, {justify, center},
         {pack_xy, {3, {1,3}}}]);
    FileName when list(FileName) ->
      InnerFrame = gs:create(frame, Frame,
        [{pack_xy, {{1, NumButtons}, 1}}, {bw, 0},
	 {packer_x, [{stretch, 1}, {fixed, 32}, {stretch, 8}]},
	 {packer_y, [{stretch, 1}, {fixed, 32}, {stretch, 1}]}]),
      IconCanvas = gs:create(canvas, InnerFrame,
        [{pack_xy, {2, 2}}]),
      Icon = gs:create(image, IconCanvas, [{coords, [{0, 0}]},
                                           {load_gif, FileName}]),
      Label = gs:create(label, InnerFrame,
        [{label, {text, Message}}, {font, Font}, {justify, center},
         {pack_xy, {3, {1,3}}}])
  end,
  {Extra, NewArgs} = Module:controls(Frame, Args),
  case Extra of
    nil -> gs:config(Frame, {packer_y, [{stretch, 2},{fixed, 0},{stretch, 1}]});
    _   -> gs:config(Extra, {pack_xy, {{1, NumButtons}, 2}})
  end,
  lists:foldl(fun(X, A) ->
       I = gs:create(frame, Frame, [{packer_x, [{stretch, 1}, {fixed, 80}, {stretch, 1}]}, 
                                    {packer_y, [{stretch, 1}, {fixed, 24}, {stretch, 1}]}, 
                                    {pack_xy, {A, 3}}]),
       gs:create(button, I, [{label, {text, atom_to_list(X)}}, {font, Font},
                             {data, {button, X}},
                             {pack_xy, {2, 2}}]),
       A + 1
       end, 1, Buttons),
  gs:config(Frame, [{width, DialogWidth}, {height, DialogHeight}]),
  {MessageWidth, MessageHeight} = gs:read(Frame, {font_wh, {Font, Message}}),
  case MessageWidth of
    N1 when N1 > trunc(DialogWidth * 0.8) ->
        NewDialogWidth = trunc(MessageWidth * 1.2),
        gs:config(Window,
          [{width, NewDialogWidth},
	   {x, (ScreenWidth - NewDialogWidth) div 2}]);
    _ -> ok
  end,
  case MessageHeight of
    N2 when N2 > trunc(DialogHeight * 0.666) ->
        NewDialogHeight = trunc(MessageHeight * 1.666),
        gs:config(Window,
          [{height, NewDialogHeight},
	   {y, (ScreenHeight - NewDialogHeight) div 2}]);
    _ -> ok
  end,
  gs:config(Window, {map, true}),
  dialog_loop(Module, Window, Frame, Extra, NewArgs).

%%--------------------------------------------------------------------
%% dialog_loop(Module, Window, Frame, Extra, Args) -> Result
%% Called by show/4, handles generic events in a dialog box.

dialog_loop(Module, Window, Frame, Extra, Args) ->
  receive
    {gs, Window, destroy, Data, EventArgs} ->
       Module:on_button(Extra, 'Cancel', Args);
    {gs, Window, configure, Data, [W,H | Rest]} ->
       gs:config(Frame, [{width, W}, {height, H}]),
       dialog_loop(Module, Window, Frame, Extra, Args);
    {gs, Window, keypress, Data, [KeyCode | Rest]} ->
       case Module:on_key(Extra, KeyCode, Args) of
         {button, ButtonType} ->
           Return = Module:on_button(Extra, ButtonType, Args),
           gs:destroy(Window),
           Return;
	 _ -> dialog_loop(Module, Window, Frame, Extra, Args)
       end;
    {gs, Button, click, {button, ButtonType}, EventArgs} ->
       Return = Module:on_button(Extra, ButtonType, Args),
       gs:destroy(Window),
       Return;
    Other -> % io:fwrite("~w~n", [Other]),
      case Module:on_event(Extra, Other, Args) of
         {button, ButtonType} ->
           Return = Module:on_button(Extra, ButtonType, Args),
           gs:destroy(Window),
           Return;
	 _ -> dialog_loop(Module, Window, Frame, Extra, Args)
      end
  end.

%%--------------------------------------------------------------------
%% test() -> ResultTuple.
%% Tests some of the common dialog boxes implemented with this behaviour.

test() ->
  A = gs_dialog_notify:show("Notification", "This is a notification dialog."),
  B = gs_dialog_confirm:show("Confirmation",
      "Are you sure you want to\ntake some sort of drastic action?"),
  C = gs_dialog_question:show("Question", "Save your barcodes first?"),
  D = gs_dialog_entry:show("Text Entry",
      "Enter the address of this order:", "555 Twenty-third St."),
  E = gs_dialog_list:show("Select One", "Select a game to play.",
      ["Chess", "Checkers", "Othello", "Go", "Backgammon", "Kali", "Sink"]),
  F = gs_dialog_color:show("Choose Colour", "Pick your favourite colour.",
      {255, 0, 128}),
  G = gs_dialog_notify:show("Lengthy Notification",
      "This is an extremely long message with no line breaks.  "
      "The dialog box should expand to display the entire message."),
  H = gs_dialog_notify:show("Lengthy Notification",
      "This is an extremely\nlong message with\nmany lines.\n\n"
      "The dialog box\nshould\nexpand\nto\ndisplay\nthe\nentire\nmessage."),
  {A,B,C,D,E,F,G,H}.

%%% END of gs_dialog.erl %%%
