-module(gd_v_wallman). -vsn("0.5.2"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). -export([show/2]). -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("gd.hrl"). -record(w, {name = none :: atom(), id = 0 :: integer(), wx = none :: none | wx:wx_object()}). -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: map(), wallets = [] :: [#wr{}], picker = none :: none | wx:wx_object(), buttons = [] :: [#w{}]}). %%% Interface -spec to_front(Win) -> ok when Win :: wx:wx_object(). to_front(Win) -> wx_object:cast(Win, to_front). -spec show(Win, Manifest) -> ok when Win :: wx:xw_object(), Manifest :: [#wr{}]. show(Win, Manifest) -> wx_object:cast(Win, {show, Manifest}). %%% Startup start_link(Args) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). init({Prefs, Manifest}) -> Lang = maps:get(lang, Prefs, en_us), Trans = gd_jt:read_translations(?MODULE), J = gd_jt:j(Lang, Trans), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, J("Wallets")), MainSz = wxBoxSizer:new(?wxVERTICAL), Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), Names = [Name || #wr{name = Name} <- Manifest], ok = wxListBox:set(Picker, Names), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{open, J("Open")}, {new, J("New")}, {move, J("Move")}, {import, J("Import")}, {drop, J("Drop")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), _ = wxSizer:add(ButtSz, B, zxw:flags(wide)), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), _ = wxSizer:add(MainSz, Picker, zxw:flags(wide)), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), ok = wxFrame:setSizer(Frame, MainSz), ok = wxSizer:layout(MainSz), ok = gd_v:safe_size(Frame, Prefs), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, close_window), true = wxFrame:show(Frame), ok = wxListBox:connect(Picker, command_listbox_doubleclicked), State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, wallets = Manifest, picker = Picker, buttons = Buttons}, {Frame, State}. %%% wx_object 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({show, Manifest}, State) -> NewState = do_show(Manifest, State), {noreply, NewState}; 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(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{buttons = Buttons}) -> NewState = case lists:keyfind(ID, #w.id, Buttons) of #w{name = open} -> do_open(State); #w{name = new} -> do_new(State); #w{name = import} -> do_import(State); #w{name = drop} -> do_drop(State); #w{name = Name} -> handle_button(Name, State); false -> State end, {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked, commandInt = Selected}}, State) -> NewState = do_open2(Selected + 1, State), {noreply, NewState}; handle_event(#wx{event = #wxClose{}}, State) -> ok = do_close(State), {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(wx_deleted, _) -> wx:destroy(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% doers do_show(Manifest, State = #s{picker = Picker}) -> Names = [Name || #wr{name = Name} <- Manifest], ok = wxListBox:set(Picker, Names), State#s{wallets = Manifest}. do_close(#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 = gd_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame). handle_button(Name, State) -> ok = tell("Button Click: ~p", [Name]), State. do_open(State = #s{wallets = []}) -> State; do_open(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> do_open2(Selected + 1, State) end. do_open2(Selected, State = #s{wallets = Wallets}) -> case lists:nth(Selected, Wallets) of #wr{pass = true, path = Path} -> do_open3(Path, State); #wr{pass = false, path = Path} -> ok = gd_con:open_wallet(Path, none), ok = do_close(State), State end. do_open3(Path, State = #s{frame = Frame, j = J}) -> Label = J("Password"), Dialog = wxDialog:new(Frame, ?wxID_ANY, Label), Sizer = wxBoxSizer:new(?wxVERTICAL), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), _ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 130}), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(PassTx), case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(PassTx) of "" -> ok = wxDialog:destroy(Dialog), State; Phrase -> ok = wxDialog:destroy(Dialog), ok = gd_con:open_wallet(Path, Phrase), ok = do_close(State), State end; ?wxID_CANCEL -> ok = wxDialog:destroy(Dialog), State end. do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")), Options = [{message, J("Save Location")}, {defaultDir, DefaultDir}, {defaultFile, "default.gaju"}, {wildCard, "*.gaju"}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], Dialog = wxFileDialog:new(Frame, Options), case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), File = wxFileDialog:getFilename(Dialog), Path = filename:join(Dir, File), ok = wxFileDialog:destroy(Dialog), case do_new2(Path, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), do_close(State#s{prefs = NewPrefs}); abort -> State end; ?wxID_CANCEL -> ok = wxFileDialog:destroy(Dialog), State end. do_new2(Path, J, Frame) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), Label1 = J("Passphrase (optional)"), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label1}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), _ = wxSizer:add(PassSz, PassTx, zxw:flags(wide)), Label2= J("Passphrase Confirmation"), PassConSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label2}]), PassConTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), _ = wxSizer:add(PassConSz, PassConTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassConSz, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), Result = case wxDialog:showModal(Dialog) of ?wxID_OK -> Name = case wxTextCtrl:getValue(NameTx) of "" -> Path; N -> N end, Pass = case {wxTextCtrl:getValue(PassTx), wxTextCtrl:getValue(PassConTx)} of {"", ""} -> none; {P, P} -> P; {_, _} -> bad end, do_new3(Name, Path, Pass); ?wxID_CANCEL -> abort end, ok = wxDialog:destroy(Dialog), Result. do_new3(_, _, bad) -> abort; do_new3(Name, Path, Pass) -> gd_con:new_wallet(Name, Path, Pass). do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")), Options = [{message, J("Select Wallet File")}, {defaultDir, DefaultDir}, {wildCard, "*.gaju"}, {style, ?wxFD_OPEN}], Dialog = wxFileDialog:new(Frame, Options), case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), File = wxFileDialog:getFilename(Dialog), ok = wxFileDialog:destroy(Dialog), case do_import2(Dir, File, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), do_close(State#s{prefs = NewPrefs}); abort -> State end; ?wxID_CANCEL -> ok = wxFileDialog:destroy(Dialog), State end. do_import2(_, "", _, _) -> abort; do_import2(Dir, File, J, Frame) -> Path = filename:join(Dir, File), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Import Wallet")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Passphrase")}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(PassSz, PassTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {500, 200}), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), Result = case wxDialog:showModal(Dialog) of ?wxID_OK -> Name = case wxTextCtrl:getValue(NameTx) of "" -> Path; N -> N end, Pass = case wxTextCtrl:getValue(PassTx) of "" -> none; P -> P end, gd_con:import_wallet(Name, Path, Pass); ?wxID_CANCEL -> abort end, ok = wxDialog:destroy(Dialog), Result. do_drop(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> do_drop(Selected + 1, State) end. do_drop(Selected, State = #s{j = J, frame = Frame, wallets = Wallets}) -> #wr{name = Name, path = Path} = lists:nth(Selected, Wallets), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Drop Wallet")), Sizer = wxBoxSizer:new(?wxVERTICAL), MessageM = J("REALLY delete wallet?"), Message = ["\r\n", MessageM, "\r\n\r\n\"", Name, "\""], MessageT = wxStaticText:new(Dialog, ?wxID_ANY, Message, [{style, ?wxALIGN_CENTRE_HORIZONTAL}]), DeleteM = J("Delete file data?"), DeleteCheck = wxCheckBox:new(Dialog, ?wxID_ANY, DeleteM), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, MessageT, zxw:flags(base)), _ = wxSizer:add(Sizer, DeleteCheck, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {500, 200}), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> Delete = wxCheckBox:getValue(DeleteCheck), gd_con:drop_wallet(Path, Delete); ?wxID_CANCEL -> ok end, State.