From e7670411b02b4b96c7f946a6bc73b3b7a8bf105e Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Mon, 9 Dec 2024 15:01:16 +0900 Subject: [PATCH] WIP: Adding Sophia developer interface --- src/gmc_con.erl | 7 +- src/gmc_grids.erl | 8 +- src/gmc_gui.erl | 26 +- src/gmc_v_devman.erl | 551 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 575 insertions(+), 17 deletions(-) create mode 100644 src/gmc_v_devman.erl diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 79e82f9..1c8eae7 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -49,7 +49,8 @@ -type state() :: #s{}. -type ui_name() :: gmc_v_netman - | gmc_v_wallman. + | gmc_v_wallman + | gmc_v_devman. @@ -451,7 +452,9 @@ task_data(gmc_v_netman, #s{wallet = #wallet{nets = Nets}}) -> task_data(gmc_v_netman, #s{wallet = none}) -> []; task_data(gmc_v_wallman, #s{wallets = Wallets}) -> - Wallets. + Wallets; +task_data(gmc_v_devman, #s{}) -> + []. diff --git a/src/gmc_grids.erl b/src/gmc_grids.erl index ba6e1bf..db63290 100644 --- a/src/gmc_grids.erl +++ b/src/gmc_grids.erl @@ -6,7 +6,7 @@ %%% Version 1 of the protocol consists of two verbs with two contexts each, collapsed to %%% four symbols for brevity. %%% -%%% The GRIDS schema begins with "grids://" or "grids://" +%%% The GRIDS schema begins with "grids://" or "grid://" %%% Which way this is interpreted can vary depending on the verb. %%% %%% The typical "host" component is either an actual hostname or address and an optional @@ -78,12 +78,6 @@ parse(URL) -> U = #{path := "/1/d/" ++ L, scheme := "grid"} -> NewURL = uri_string:recompose(U#{scheme := "http", path := L}), {ok, {{sign, http}, NewURL}}; - U = #{path := "/1/v/" ++ L, scheme := "grids"} -> - NewURL = uri_string:recompose(U#{scheme := "https", path := L}), - {ok ,{{mess, https}, NewURL}}; - U = #{path := "/1/v/" ++ L, scheme := "grid"} -> - NewURL = uri_string:recompose(U#{scheme := "http", path := L}), - {ok, {{mess, http}, NewURL}}; {error, Reason, Info} -> ok = tell("URL parsing failed with ~w: ~p", [Reason, Info]), {error, bad_url}; diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 3544f03..66f1c05 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -92,12 +92,14 @@ init(Prefs) -> MainSz = wxBoxSizer:new(?wxVERTICAL), Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), - WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]), - WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB}, + WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]), + WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB}, ChainB = wxButton:new(Frame, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]), - ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB}, - NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]), - NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB}, + ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB}, + NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]), + NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB}, + DevB = wxButton:new(Frame, ?wxID_ANY, [{label, "𝑓 () →"}]), + DevW = #w{name = dev, id = wxButton:getId(DevB), wx = DevB}, ID_L = wxStaticText:new(Frame, ?wxID_ANY, J("Account ID: ")), ID_T = wxStaticText:new(Frame, ?wxID_ANY, ""), @@ -140,7 +142,7 @@ init(Prefs) -> #w{name = Name, id = wxButton:getId(B), wx = B} end, - Buttons = [WallW, ChainW, NodeW | lists:map(MakeButton, ButtonTemplates)], + Buttons = [WallW, ChainW, NodeW, DevW | lists:map(MakeButton, ButtonTemplates)], ChainSz = wxBoxSizer:new(?wxHORIZONTAL), AccountSz = wxBoxSizer:new(?wxHORIZONTAL), @@ -148,9 +150,10 @@ init(Prefs) -> ActionsSz = wxBoxSizer:new(?wxHORIZONTAL), HistorySz = wxBoxSizer:new(?wxVERTICAL), - _ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)), + _ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, ChainB, zxw:flags(wide)), - _ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)), + _ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)), + _ = wxSizer:add(ChainSz, DevB, zxw:flags(base)), #w{wx = CopyBn} = lists:keyfind(copy, #w.name, Buttons), #w{wx = WWW_Bn} = lists:keyfind(www, #w.name, Buttons), @@ -277,6 +280,7 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked}, case lists:keyfind(ID, #w.id, Buttons) of #w{name = wallet} -> wallman(State); #w{name = chain} -> netman(State); + #w{name = dev} -> devman(State); #w{name = node} -> set_node(State); #w{name = make_key} -> make_key(State); #w{name = recover} -> recover_key(State); @@ -345,6 +349,12 @@ netman(State) -> ok = gmc_con:show_ui(gmc_v_netman), State. + +devman(State) -> + ok = gmc_con:show_ui(gmc_v_devman), + State. + + set_node(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")), Sizer = wxBoxSizer:new(?wxVERTICAL), diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl new file mode 100644 index 0000000..c552dd8 --- /dev/null +++ b/src/gmc_v_devman.erl @@ -0,0 +1,551 @@ +-module(gmc_v_devman). +-vsn("0.1.4"). +-author("Craig Everett "). +-copyright("QPQ AG "). +-license("GPL-3.0-or-later"). + +-behavior(wx_object). +%-behavior(gmc_v). +-include_lib("wx/include/wx.hrl"). +-export([to_front/1]). +-export([set_manifest/1]). +-export([start_link/1]). +-export([init/1, terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). +-include("$zx_include/zx_logger.hrl"). +-include("gmc.hrl"). + + +-record(w, + {name = none :: atom() | {FunName :: binary(), call | dryr}, + id = 0 :: integer(), + wx = none :: none | wx:wx_object()}). + +-record(f, + {name = <<"">> :: binary(), + call = #w{} :: #w{}, + dryrun = #w{} :: none | #w{}, + args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). + +-record(p, + {path = "" :: file:filename(), + win = none :: none | wx:wx_object(), + code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), + cons = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), + side_sz = none :: none | wx:wx_object(), + instances = none :: none | wx:wx_object(), + funs = {#w{}, []} :: {#w{}, [#f{}]}, + builds = #{} :: #{InstanceID :: binary() := Build :: map()}}). + +-record(s, + {wx = none :: none | wx:wx_object(), + frame = none :: none | wx:wx_object(), + lang = en :: en | jp, + j = none :: none | fun(), + prefs = #{} :: map(), + buttons = #{} :: #{WX_ID :: integer() := #w{}}, + book = {none, []} :: {Notebook :: none | wx:wx_object(), Pages :: [#p{}]}}). + +-type argt() :: int | string | address | list(argt()). + +%%% Interface + +-spec to_front(Win) -> ok + when Win :: wx:wx_object(). + +to_front(Win) -> + wx_object:cast(Win, to_front). + + +-spec set_manifest(Entries) -> ok + when Entries :: [ael:conf_meta()]. + +set_manifest(Entries) -> + case is_pid(whereis(?MODULE)) of + true -> wx_object:cast(?MODULE, {set_manifest, Entries}); + false -> ok + end. + + + + +%%% Startup Functions + +start_link(Args) -> + wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). + + +init({Prefs, Manifest}) -> + Lang = maps:get(lang, Prefs, en_us), + Trans = gmc_jt:read_translations(?MODULE), + J = gmc_jt:j(Lang, Trans), + Wx = wx:new(), + Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), + + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + + ButtonTemplates = + [{new, J("New")}, + {open, J("Open")}, + {save, J("Save")}, + {saven, J("Save (new name)")}, + {compile, J("Compile")}, + {close, J("Close")}], + + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + + Buttons = lists:map(MakeButton, ButtonTemplates), + + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + + Notebook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Notebook, zxw:flags(wide)), + _ = wxFrame:setSizer(Frame, MainSz), + _ = wxSizer:layout(MainSz), + + ok = gmc_v:safe_size(Frame, Prefs), + + ok = wxFrame:connect(Frame, close_window), + ok = wxFrame:connect(Frame, command_button_clicked), + ok = wxFrame:center(Frame), + true = wxFrame:show(Frame), + + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + State = + #s{wx = Wx, frame = Frame, + j = J, prefs = Prefs, + buttons = ButtonMap, book = {Notebook, []}}, + NewState = add_pages(State, Manifest), + {Frame, NewState}. + + +add_pages(State, Files) -> + lists:foldl(fun add_page/2, State, Files). + +add_page(State = #s{book = {Notebook, Pages}}, File) -> + case keyfind_index(File, #p.path, Pages) of + error -> + add_page2(State, File); + {ok, Index} -> + _ = wxNotebook:setSelection(Notebook, Index - 1), + State + end. + +add_page2(State = #s{j = J}, File) -> + case file:read_file(File) of + {ok, Bin} -> + case unicode:characters_to_list(Bin) of + Code when is_list(Code) -> + add_page(State, File, Code); + Error -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), + ok = gmc_gui:trouble(Message), + State + end; + {error, Reason} -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), + ok = gmc_gui:trouble(Message), + State + end. + +add_page(State = #s{j = J, book = {Notebook, Pages}}, File, Code) -> +% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will +% have to contend with system theme issues (light/dark themese, namely) +% Leaving this little thing here to remind myself how any of that works later. +% The call below returns a wx_color4() type (not that we need alpha...). +% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), +% tell("Color: ~p", [Color]), + Window = wxWindow:new(Notebook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxHORIZONTAL), + + ProgSz = wxBoxSizer:new(?wxVERTICAL), + CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Code")}]), + ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), + CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, + ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + TextAt = wxTextAttr:new(), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Code), + + _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), + _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 5}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + + SideSz = wxBoxSizer:new(?wxVERTICAL), + + InstanceSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Instances")}]), + Instances = wxChoice:new(Window, ?wxID_ANY, [{choices, []}]), + _ = wxSizer:add(InstanceSz, Instances, zxw:flags(wide)), + + ScrollWin = wxScrolledWindow:new(Window), + FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), + ok = wxWindow:setSizer(ScrollWin, FunSz), + _ = wxSizer:add(SideSz, InstanceSz, zxw:flags(base)), + _ = wxSizer:add(SideSz, ScrollWin, zxw:flags(wide)), + + _ = wxSizer:add(PageSz, ProgSz, [{proportion, 2}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, SideSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + FileName = filename:basename(File), + true = wxNotebook:addPage(Notebook, Window, FileName, [{bSelect, true}]), + Page = #p{path = File, win = Window, + code = CodeTx, cons = ConsTx, + side_sz = SideSz, instances = Instances, + funs = {ScrollWin, []}}, + NewPages = Pages ++ [Page], + State#s{book = {Notebook, NewPages}}. + + + +%%% OTP callbacks + +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), + {noreply, State}. + + +handle_cast(to_front, State = #s{frame = Frame}) -> + ok = wxFrame:raise(Frame), + {noreply, State}; +handle_cast(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, + id = ID}, + State = #s{buttons = Buttons}) -> + NewState = + case maps:get(ID, Buttons, undefined) of + #w{name = new} -> new_file(State); + #w{name = open} -> open_file(State); + #w{name = close} -> close_file(State); + #w{name = compile} -> compile(State); + #w{name = Name} -> clicked(State, Name); + undefined -> + tell("Received message: ~w", [E]), + State + end, + {noreply, NewState}; +handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> + Geometry = + case wxTopLevelWindow:isMaximized(Frame) of + true -> + max; + false -> + {X, Y} = wxWindow:getPosition(Frame), + {W, H} = wxWindow:getSize(Frame), + {X, Y, W, H} + end, + NewPrefs = maps:put(geometry, Geometry, Prefs), + ok = gmc_con:save(?MODULE, NewPrefs), + ok = wxWindow:destroy(Frame), + {noreply, State}; +handle_event(Event, State) -> + ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), + {noreply, State}. + + +code_change(_, State, _) -> + {ok, State}. + + +terminate(Reason, State) -> + ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), + wx:destroy(). + + + +%%% Doers + +clicked(State = #s{book = {Notebook, Pages}}, Name) -> + case wxNotebook:getSelection(Notebook) of + ?wxNOT_FOUND -> + State; + Index -> + Page = lists:nth(Index + 1, Pages), + clicked(State, Page, Name) + end. + +clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}) -> + BuildLabel = wxChoice:getStringSelection(Is), + Build = maps:get(BuildLabel, Builds), + #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), + InitArgs = lists:map(fun get_arg/1, Args), + ok = tell("BuildLabel: ~p~nArgs: ~p~nInitArgs: ~p", [BuildLabel, Args, InitArgs]), +% contract_create_built( + State; +clicked(State, Page, Name) -> + ok = tell("Button: ~p~nPage: ~p", [Name, Page]), + State. + +get_arg({_, InputField, _}) -> + wxTextCtrl:getValue(InputField). + + +new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, "my_contract.aes"}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_page(NextState, Path, "") + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +compile(State = #s{book = {Notebook, Pages}}) -> + case wxNotebook:getSelection(Notebook) of + ?wxNOT_FOUND -> + State; + Index -> + Page = #p{code = CodeTx} = lists:nth(Index + 1, Pages), + Source = wxTextCtrl:getValue(CodeTx), + compile(State, Page, Source) + end. + +compile(State = #s{book = {Notebook, Pages}, j = J, buttons = Buttons}, + Page = #p{path = Path, win = Win, cons = ConsTx, + side_sz = SideSz, instances = Instances, funs = Funs, builds = Builds}, + Source) -> + case aeso_compiler:from_string(Source, [{aci, json}]) of + {ok, Build = #{aci := ACI}} -> + BuildName = build_name(Path), + I = wxChoice:append(Instances, BuildName), + ok = wxChoice:select(Instances, I), + NewBuilds = maps:put(BuildName, Build, Builds), + FunDefs = find_main(ACI), + UpdateInterfaces = fun() -> fun_interfaces(Win, Buttons, Funs, FunDefs, J) end, + {NewButtons, NewFuns} = wx:batch(UpdateInterfaces), + ScrollWin = element(1, NewFuns), + _ = wxSizer:add(SideSz, ScrollWin, zxw:flags(wide)), + ok = wxSizer:layout(SideSz), + NewPage = Page#p{funs = NewFuns, builds = NewBuilds}, + NewPages = lists:keystore(Path, #p.path, Pages, NewPage), + State#s{book = {Notebook, NewPages}, buttons = NewButtons}; + Other -> + ok = wxTextCtrl:setValue(ConsTx, io_lib:format("~tp", [Other])), + State + end. + +build_name(Path) -> + File = filename:basename(Path, ".aes"), + TS = integer_to_list(os:system_time(millisecond)), + unicode:characters_to_list([File, "-", TS]). + +find_main(ACI) -> + find_main(ACI, none, []). + +find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> + find_main(T, M, [I | Is]); +find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> + find_main(T, M, Is); +find_main([#{namespace := _} | T], M, Is) -> + find_main(T, M, Is); +find_main([C | T], M, Is) -> + ok = tell("Surprising ACI element: ~p", [C]), + find_main(T, M, Is); +find_main([], M, Is) -> + {M, Is}. + +fun_interfaces(Window, + Buttons, + {OldScrollWin, OldIfaces}, + {#{name := Name, functions := Funs}, ConIfaces}, + J) -> + ok = wxScrolledWindow:destroy(OldScrollWin), + OldButtonIDs = button_key_list(OldIfaces), + NextButtons = maps:without(OldButtonIDs, Buttons), + ScrollWin = wxScrolledWindow:new(Window), + FSOpts = [{label, J("Function Interfaces")}], + FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, FSOpts), + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), + ConName = wxStaticText:new(ScrollWin, ?wxID_ANY, Name), + _ = wxSizer:add(FunSizer, ConName), + MakeIface = + fun(#{name := N, arguments := As}) -> + FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), + FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + {CallButton, DryRunButton} = + case N =:= <<"init">> of + false -> + CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), + DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), + true = wxButton:disable(CallBn), + true = wxButton:disable(DryRBn), + _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, + #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; + true -> + Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), + _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, + none} + end, + _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), + _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), + _ = wxSizer:add(FunSizer, FN, zxw:flags(base)), + #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} + end, + Ifaces = lists:map(MakeIface, Funs), + NewButtons = lists:foldl(fun map_iface_buttons/2, NextButtons, Ifaces), + ok = wxSizer:layout(FunSizer), + {NewButtons, {ScrollWin, Ifaces}}. + +button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> + [C, D | button_key_list(T)]; +button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> + [C | button_key_list(T)]; +button_key_list([]) -> + []. + +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> + maps:put(DID, D, maps:put(CID, C, A)); +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> + maps:put(CID, C, A). + + +open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Load Contract Source")}, + {defaultDir, DefaultDir}, + {wildCard, "*.aes"}, + {style, ?wxFD_OPEN}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_page(NextState, Path) + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +close_file(State = #s{book = {Notebook, Pages}}) -> + case wxNotebook:getSelection(Notebook) of + ?wxNOT_FOUND -> + State; + Index -> + NewPages = drop_nth(Index + 1, Pages), + true = wxNotebook:deletePage(Notebook, Index), + State#s{book = {Notebook, NewPages}} + end. + + +drop_nth(1, [_ | T]) -> T; +drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. + + +keyfind_index(K, E, L) -> + keyfind_index(K, E, 1, L). + +keyfind_index(K, E, I, [H | T]) -> + case element(E, H) =:= K of + false -> keyfind_index(K, E, I + 1, T); + true -> {ok, I} + end; +keyfind_index(_, _, _, []) -> + error.