-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, wx = Button} -> clicked(State, Name, Button); 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, Button) -> case wxNotebook:getSelection(Notebook) of ?wxNOT_FOUND -> ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> Page = lists:nth(Index + 1, Pages), clicked(State, Page, Name, Button) end. clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}, _Button) -> Label = wxChoice:getStringSelection(Is), Build = maps:get(Label, Builds), #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), InitArgs = lists:map(fun get_arg/1, Args), ok = tell("Label: ~p~nArgs: ~p~nInitArgs: ~p", [Label, Args, InitArgs]), case gmc_con:deploy(Build, InitArgs) of {contract_id, ConID} -> ok = tell("Got ConID: ~p", [ConID]), State; {tx_hash, TX_Hash} -> ok = tell("Got TX_Hash: ~p", [TX_Hash]), State; {error, Reason} -> ok = tell("Deploy failed with: ~p", [Reason]), State end; clicked(State, Page, Name, Button) -> ok = tell("Button: ~p ~p~nPage: ~p", [Button, Name, Page]), State. get_arg({_, TextCtrl, _}) -> wxTextCtrl:getValue(TextCtrl). 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.