22import dataclasses
33import logging
44import math
5+ import re
56import typing as t
67
78import wx
1516logger .propagate = False
1617
1718
18-
19-
2019# #####################################################################################################################
2120# ############################################## HexEditorBinaryData ##################################################
2221# #####################################################################################################################
@@ -600,6 +599,20 @@ def Clone(self):
600599 return HexCellEditor (self .parentgrid )
601600
602601
602+ @dataclasses .dataclass
603+ class ContextMenuItem :
604+ wx_id : int
605+ name : str
606+ callback : t .Callable [[wx .CommandEvent ], t .Any ]
607+
608+ # None = option
609+ # True = toggle selected
610+ # False = toggle unselected
611+ toggle_state : t .Optional [bool ]
612+
613+ enabled : bool
614+
615+
603616# #####################################################################################################################
604617# ############################################## Grid.Grid ############################################################
605618# #####################################################################################################################
@@ -620,7 +633,9 @@ def __init__(
620633 self ._table = table
621634 self ._binary_data = binary_data
622635 self .read_only = read_only
623- self .on_selection_changed : "CallbackList[[int, t.Optional[int]]]" = CallbackList ()
636+ self .on_selection_changed : "CallbackList[[int, t.Optional[int]]]" = (
637+ CallbackList ()
638+ )
624639
625640 # The second parameter means that the grid is to take
626641 # ownership of the table and will destroy it when done.
@@ -865,7 +880,7 @@ def _remove_selection(self) -> bool:
865880
866881 self .ClearSelection ()
867882 self ._selection = (None , None )
868-
883+
869884 idx = self ._table .get_byte_idx (self .GetGridCursorRow (), self .GetGridCursorCol ())
870885 if idx > len (self ._editor .binary ):
871886 self .SetGridCursor (0 , 0 )
@@ -953,17 +968,9 @@ def _paste(self, overwrite: bool = False, insert: bool = False) -> bool:
953968 clipboard = wx .TextDataObject ()
954969 wx .TheClipboard .GetData (clipboard )
955970 wx .TheClipboard .Close ()
956- byts_str : str = clipboard .GetText ()
957-
958- # convert string to bytes
959- try :
960- byts_str = byts_str .replace (" " , "" )
961- byts = bytes .fromhex (byts_str )
962- except Exception as e :
963- wx .MessageBox (
964- f"Can't convert data from clipboard to bytes.\n \n { str (e )} \n \n Clipboard Data:\n { byts_str } " ,
965- "Warning" ,
966- )
971+ clipboard_txt : str = clipboard .GetText ()
972+ byts = self .string_to_byts (clipboard_txt )
973+ if not byts :
967974 return False
968975
969976 # copy new data to the binary data
@@ -975,6 +982,51 @@ def _paste(self, overwrite: bool = False, insert: bool = False) -> bool:
975982 self .select_range (sel [0 ], sel [0 ] + len (byts ) - 1 )
976983 return True
977984
985+ def string_to_byts (self , byts_str : str ):
986+ """
987+ Normalize pasted string converting it to bytes (can be overridden).
988+ Return a "bytes" variable, or None in case or error.
989+
990+ Possible Strings:
991+ - 0102030405060708090a0b0c0d0e0f
992+ - 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
993+ - 01.02.03.04.05.06.07.08.09.0a.0b.0c.0d.0e.0f
994+ - 01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f
995+ - 1,2,3,4,5,6,7,8,9,a,b,c,d,e,f
996+ - 1 2 3 4 5 6 7 8 9 a b c d e f
997+ - "71 20 98 00 c0 7a 3e 6a 8d 7c c4 0d 04 10 02 f2 00"
998+ - ("71 20 98 00 c0 7a 3e 6a 8d 7c c4 0d 04 10 02 f2 00")
999+ - bytes.fromhex("71 20 98 00 c0 7a 3e 6a 8d 7c c4 0d 04 10 02 f2 00")
1000+ - '71209800c07a3e6a8d7cc40d041002f200'
1001+ - 0x12, 0x23, 0x45,
1002+ - b'\x00 \x00 \x00 \xa4 \xc1 8\xe1 \x81 _\x00 \xcc #b\x0c \r \x15 '
1003+ """
1004+ byts_str = re .sub (r".*[\"'](.*)[\"'].*" , r"\1" , byts_str )
1005+ try :
1006+ byts = bytes .fromhex (byts_str )
1007+ return byts
1008+ except Exception :
1009+ pass
1010+ try :
1011+ byts_str_conv = re .findall (r"[0-9A-Fa-fXx]+" , byts_str )
1012+ byts = bytes (map (lambda x : int (x , 16 ), byts_str_conv ))
1013+ return byts
1014+ except Exception :
1015+ pass
1016+ try :
1017+ byts = bytes (
1018+ byts_str .encode ("utf8" , "backslashreplace" ).decode ("unicode_escape" ),
1019+ encoding = "raw_unicode_escape" ,
1020+ )
1021+ return byts
1022+ except Exception as e :
1023+ wx .MessageBox (
1024+ f"Can't convert data from clipboard to bytes.\n \n { str (e )} "
1025+ f"\n \n Processed clipboard data:\n { byts_str } " ,
1026+ "Warning" ,
1027+ )
1028+ return None
1029+
9781030 def _undo (self ):
9791031 self ._binary_data .command_processor .Undo ()
9801032
@@ -1068,53 +1120,73 @@ def _on_cell_right_click(self, event: Grid.GridEvent):
10681120 if select_cell :
10691121 self .SetGridCursor (event .GetRow (), event .GetCol ())
10701122
1071- menus = [
1072- (
1123+ popup_menu = wx .Menu ()
1124+ for menu in self .build_context_menu ():
1125+ if menu is None :
1126+ popup_menu .AppendSeparator ()
1127+ continue
1128+ if menu .toggle_state != None : # checkbox boolean state
1129+ item : wx .MenuItem = popup_menu .AppendCheckItem (menu .wx_id , menu .name )
1130+ item .Check (menu .toggle_state )
1131+ else :
1132+ item : wx .MenuItem = popup_menu .Append (menu .wx_id , menu .name )
1133+ self .Bind (wx .EVT_MENU , menu .callback , id = item .Id )
1134+ item .Enable (menu .enabled )
1135+
1136+ self .PopupMenu (popup_menu , event .GetPosition ())
1137+ popup_menu .Destroy ()
1138+
1139+ def build_context_menu (
1140+ self ,
1141+ ) -> t .List [t .Optional [ContextMenuItem ]]:
1142+ """Build the context menu. Can be overridden."""
1143+
1144+ return [
1145+ ContextMenuItem (
10731146 wx .ID_CUT ,
10741147 "Cut\t Ctrl+X" ,
10751148 lambda event : self ._cut_selection (),
1149+ None ,
10761150 not self .read_only ,
10771151 ),
1078- (wx .ID_COPY , "Copy\t Ctrl+C" , lambda event : self ._copy_selection (), True ),
1079- (
1152+ ContextMenuItem (
1153+ wx .ID_COPY ,
1154+ "Copy\t Ctrl+C" ,
1155+ lambda event : self ._copy_selection (),
1156+ None ,
1157+ True ,
1158+ ),
1159+ ContextMenuItem (
10801160 wx .ID_PASTE ,
10811161 "Paste (overwrite)\t Ctrl+V" ,
10821162 lambda event : self ._paste (overwrite = True ),
1163+ None ,
10831164 not self .read_only ,
10841165 ),
1085- (
1166+ ContextMenuItem (
10861167 wx .ID_PASTE ,
10871168 "Paste (insert)\t Ctrl+Shift+V" ,
10881169 lambda event : self ._paste (insert = True ),
1170+ None ,
10891171 not self .read_only ,
10901172 ),
10911173 None ,
1092- (
1174+ ContextMenuItem (
10931175 wx .ID_UNDO ,
10941176 "Undo\t Ctrl+Z" ,
10951177 lambda event : self ._undo (),
1178+ None ,
10961179 self ._binary_data .command_processor .CanUndo (),
10971180 ),
1098- (
1181+ ContextMenuItem (
10991182 wx .ID_REDO ,
11001183 "Redo\t Ctrl+Y" ,
11011184 lambda event : self ._redo (),
1185+ None ,
11021186 self ._binary_data .command_processor .CanRedo (),
11031187 ),
11041188 ]
11051189
1106- popup_menu = wx .Menu ()
1107- for menu in menus :
1108- if menu is None :
1109- popup_menu .AppendSeparator ()
1110- continue
1111- item : wx .MenuItem = popup_menu .Append (menu [0 ], menu [1 ])
1112- self .Bind (wx .EVT_MENU , menu [2 ], id = item .Id )
1113- item .Enable (menu [3 ])
1114-
1115- self .PopupMenu (popup_menu , event .GetPosition ())
1116- popup_menu .Destroy ()
1117-
11181190
11191191# #####################################################################################################################
11201192# ############################################## WxHexEditor ##########################################################
0 commit comments