"Fossies" - the Fresh Open Source Software Archive

Member "sofastats-1.5.2/sofa_main/settings_grid.py" (3 Sep 2018, 46255 Bytes) of package /linux/misc/sofastats-1.5.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "settings_grid.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 1.4.6_vs_1.5.0.

    1 import wx  #@UnusedImport
    2 import wx.grid
    3 
    4 from . import my_globals as mg
    5 from . import lib
    6 from . import controls
    7 
    8 COL_STR = 'col_string'
    9 COL_INT = 'col_integer'
   10 COL_FLOAT = 'col_float'
   11 COL_TEXT_BROWSE = 'col_button'
   12 COL_DROPDOWN = 'col_dropdown'
   13 COL_PWD = 'col_pwd'
   14 
   15 EvtCellMove = wx.NewEventType()  ## event you can instantiate to post or append to an event handler
   16 EVT_CELL_MOVE = wx.PyEventBinder(EvtCellMove, 1)  ## event type you can bind a function handler to
   17 
   18 
   19 class CellMoveEvent(wx.PyCommandEvent):
   20     "See 3.6.1 in wxPython in Action"
   21     def __init__(self, _id):
   22         wx.PyCommandEvent.__init__(self, EvtCellMove, _id)
   23 
   24     def add_dets(self, dest_row=None, dest_col=None, direction=None):
   25         self.dest_row = dest_row
   26         self.dest_col = dest_col
   27         self.direction = direction
   28 
   29 
   30 class DlgSettingsEntry(wx.Dialog):
   31     def __init__(self, title, grid_size, col_dets,
   32             init_settings_data, settings_data,
   33             insert_data_func=None, row_validation_func=None):
   34         """
   35         :param list col_dets: list of dicts with keys col_label, coltype, and,
   36          optionally, colwidth, file_phrase, file_wildcard, empty_ok, col_min_val,
   37          col_max_val, col_precision. Also dropdown_vals which is a list of
   38          values for the dropdown.
   39         :param list init_settings_data: list of tuples (tuples must have at
   40          least one item, even if only a "rename me"). Empty list OK.
   41         :param list settings_data: is effectively "returned". Add details to it
   42          in form of a list of tuples. E.g.
   43          [('1', 'Japan'), ('2', 'Italy'), ('3', 'Germany')]
   44         :param fn insert_data_func: what data do you want to see in a new
   45          inserted row (if any). Must take grid_data as argument.
   46         """
   47         wx.Dialog.__init__(self, None, title=title, size=(400, 400),
   48             style=wx.RESIZE_BORDER|wx.CAPTION|wx.SYSTEM_MENU,
   49             pos=(mg.HORIZ_OFFSET+100, 0))
   50         self.panel = wx.Panel(self)
   51         self.szr_main = wx.BoxSizer(wx.VERTICAL)
   52         self.tabentry = SettingsEntry(self, self.panel, grid_size, col_dets,
   53             init_settings_data, settings_data,
   54             insert_data_func, row_validation_func, force_focus=False)
   55         self.szr_main.Add(self.tabentry.grid, 1, wx.GROW|wx.ALL, 5)
   56         ## Close only
   57         self.setup_btns()
   58         ## sizers
   59         self.szr_main.Add(self.szr_btns, 0, wx.ALL, 10)
   60         self.panel.SetSizer(self.szr_main)
   61         self.szr_main.SetSizeHints(self)
   62         self.Layout()
   63         self.tabentry.grid.SetFocus()
   64 
   65     def setup_btns(self, *, read_only=False):
   66         """
   67         Separated for text_browser reuse
   68         """
   69         if not read_only:
   70             btn_cancel = wx.Button(self.panel, wx.ID_CANCEL)
   71             btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel)
   72         if read_only:
   73             btn_ok = wx.Button(self.panel, wx.ID_OK)
   74         else:
   75             btn_ok = wx.Button(self.panel, wx.ID_OK, _('Update')) # must have ID 
   76             ## of wx.ID_OK to trigger validators (no event binding needed) and for std dialog button layout
   77         btn_ok.Bind(wx.EVT_BUTTON, self.on_ok)
   78         btn_ok.SetDefault()
   79         if not read_only:
   80             btn_delete = wx.Button(self.panel, wx.ID_DELETE)
   81             btn_delete.Bind(wx.EVT_BUTTON, self.on_delete)
   82             btn_insert = wx.Button(self.panel, -1, _('Insert Before'))
   83             btn_insert.Bind(wx.EVT_BUTTON, self.on_insert)
   84         ## using the approach which will follow the platform convention for standard buttons
   85         self.szr_btns = wx.StdDialogButtonSizer()
   86         if not read_only:
   87             self.szr_btns.AddButton(btn_cancel)
   88         self.szr_btns.AddButton(btn_ok)
   89         self.szr_btns.Realize()
   90         if not read_only:
   91             self.szr_btns.Insert(0, btn_delete, 0)
   92             self.szr_btns.Insert(0, btn_insert, 0, wx.RIGHT, 10)
   93 
   94     def on_cancel(self, _event):
   95         """
   96         No validation - just get out
   97         """
   98         self.Destroy()
   99         self.SetReturnCode(wx.ID_CANCEL)
  100 
  101     def on_ok(self, _event):
  102         if not self.panel.Validate():  ## runs validators on all assoc controls
  103             return True
  104         self.tabentry.update_settings_data()
  105         self.Destroy()
  106         self.SetReturnCode(wx.ID_OK)
  107 
  108     def on_delete(self, event):
  109         self.tabentry.try_to_delete_row()
  110         self.tabentry.grid.SetFocus()
  111         event.Skip()
  112 
  113     def insert_before(self):
  114         """
  115         Returns row inserted before (or None if no insertion) and row data (or
  116         None if no content added).
  117         """
  118         selected_rows = self.tabentry.grid.GetSelectedRows()
  119         if not selected_rows: 
  120             return False, None, None
  121         pos = selected_rows[0]
  122         bolinserted, row_data = self.tabentry.insert_row_above(pos)
  123         return bolinserted, pos, row_data
  124 
  125     def on_insert(self, event):
  126         """
  127         Insert before.
  128         """
  129         unused, unused, unused = self.insert_before()
  130         self.tabentry.grid.SetFocus()
  131         event.Skip()
  132 
  133 
  134 def cell_invalidation(frame, val, row, col, grid, col_dets):
  135     "Return boolean and string message"
  136     return False, ''
  137 
  138 def cell_response(self, val, row, col, grid, col_dets):
  139     pass
  140 
  141 
  142 class SettingsEntry:
  143 
  144     def __init__(self, frame, panel, grid_size, col_dets,
  145             init_settings_data, settings_data,
  146             insert_data_func=None, cell_invalidation_func=None,
  147             cell_response_func=None,
  148             *, force_focus=False, read_only=False):
  149         """
  150         :param list col_dets: list of dicts. Keys: "col_label", "coltype", and,
  151          optionally, "colwidth", "file_phrase", "file_wildcard", "empty_ok",
  152          "col_min_val", "col_max_val", "col_precision". Also "dropdown_vals"
  153          which is a list of values for the dropdown.
  154         :param list init_settings_data: list of tuples (must have at least one
  155          item, even if only a "rename me").
  156         :param list settings_data: is effectively "returned" - add details to it
  157          in form of a list of tuples.
  158         :param fn insert_data_func: return row_data and receive row_idx,
  159          grid_data
  160         :param fn cell_invalidation_func: return boolean, and string message
  161          and receives row, col, grid, col_dets
  162         :param fn cell_response_func: some code run when leaving a valid cell
  163          e.g. might tell user something about the value they just entered
  164         :param bool force_focus: needed sometimes and better without others
  165         """
  166         self.debug = False
  167         self.new_is_dirty = False
  168         self.frame = frame
  169         self.panel = panel
  170         self.read_only = read_only
  171         self.col_dets = col_dets
  172         self.force_focus = force_focus
  173         self.insert_data_func = insert_data_func
  174         self.cell_invalidation_func = (cell_invalidation_func
  175             if cell_invalidation_func else cell_invalidation)
  176         self.cell_response_func = (cell_response_func
  177             if cell_response_func else cell_response)
  178         ## store any fixed min colwidths
  179         self.colwidths = [None for _x in range(len(self.col_dets))]  ## init
  180         for col_idx, col_det in enumerate(self.col_dets):
  181             if col_det.get('colwidth'):
  182                 self.colwidths[col_idx] = col_det['colwidth']
  183         self.init_settings_data = init_settings_data
  184         self.settings_data = settings_data
  185         self.any_editor_shown = False
  186         self.new_editor_shown = False
  187         ## grid control
  188         self.grid = wx.grid.Grid(self.panel, size=grid_size)        
  189         self.respond_to_select_cell = True      
  190         self.rows_n = len(self.init_settings_data)
  191         if not read_only:
  192             self.rows_n += 1
  193         self.cols_n = len(self.col_dets)
  194         if self.rows_n > 1:
  195             data_cols_n = len(self.init_settings_data[0])
  196             #pprint.pprint(self.init_settings_data) # debug
  197             if data_cols_n != self.cols_n:
  198                 raise Exception(
  199                     'There must be one set of column details per column of data'
  200                     f' (currently {self.cols_n} details '
  201                     f'for {data_cols_n} columns)')
  202         self.grid.CreateGrid(numRows=self.rows_n, numCols=self.cols_n)
  203         self.grid.EnableEditing(not self.read_only)
  204         ## Set any col min widths specifically specified
  205         for col_idx in range(len(self.col_dets)):
  206             colwidth = self.colwidths[col_idx]
  207             if colwidth:
  208                 self.grid.SetColMinimalWidth(col_idx, colwidth)
  209                 self.grid.SetColSize(col_idx, colwidth)
  210                 ## otherwise will only see effect after resizing
  211             else:
  212                 self.grid.AutoSizeColumn(col_idx, setAsMin=False)
  213         self.grid.ForceRefresh()
  214         ## set col rendering and editing (string is default)
  215         for col_idx, col_det in enumerate(self.col_dets):
  216             coltype = col_det['coltype']
  217             if coltype == COL_INT:
  218                 self.grid.SetColFormatNumber(col_idx)
  219             elif coltype == COL_FLOAT:
  220                 width, precision = self.get_width_precision(col_idx)
  221                 self.grid.SetColFormatFloat(col_idx, width, precision)
  222             ## must set editor cell by cell amazingly
  223             for row_idx in range(self.rows_n):
  224                 renderer, editor = self.get_new_renderer_editor(col_idx)
  225                 self.grid.SetCellRenderer(row_idx, col_idx, renderer)
  226                 self.grid.SetCellEditor(row_idx, col_idx, editor)
  227         ## set min row height if text browser used
  228         for col_idx, col_det in enumerate(self.col_dets):
  229             coltype = col_det['coltype']
  230             if coltype == COL_TEXT_BROWSE:
  231                 self.grid.SetDefaultRowSize(30)
  232                 break
  233         ## unlike normal grids, we can assume limited number of rows
  234         self.grid.SetRowLabelSize(40)
  235         ## grid event handling
  236         self.grid.Bind(wx.EVT_KEY_DOWN, self.on_grid_key_down)
  237         self.grid.Bind(EVT_CELL_MOVE, self.on_cell_move)
  238         self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGING, self.on_cell_change)
  239         self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.on_select_cell)
  240 
  241         self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.on_mouse_cell)
  242         self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.on_mouse_cell)
  243 
  244         self.grid.Bind(
  245             controls.EVT_TEXT_BROWSE_KEY_DOWN, self.on_text_browse_key_down)
  246         self.frame.Bind(
  247             wx.grid.EVT_GRID_EDITOR_CREATED, self.on_grid_editor_created)
  248         self.frame.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.on_editor_shown)
  249         self.frame.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.on_editor_hidden)
  250         self.grid.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.on_label_click)
  251         ## misc
  252         for col_idx, col_det in enumerate(self.col_dets):
  253             self.grid.SetColLabelValue(col_idx, col_det['col_label'])
  254         self.rows_to_fill = self.rows_n if self.read_only else self.rows_n - 1
  255         for i in range(self.rows_to_fill):
  256             for j in range(self.cols_n):
  257                 self.grid.SetCellValue(
  258                     row=i, col=j,
  259                     s=str(self.init_settings_data[i][j]))
  260         if not self.read_only:
  261                 self.grid.SetRowLabelValue(self.rows_n - 1, mg.NEW_IS_READY)
  262         self.current_col_idx = 0
  263         if self.read_only:
  264             self.current_row_idx = 0
  265             self.grid.SetGridCursor(0, 0)
  266         else:
  267             self.current_row_idx = self.rows_n - 1
  268             self.grid.SetGridCursor(self.rows_n - 1, 0) # triggers OnSelect
  269             self.grid.MakeCellVisible(self.rows_n - 1, 0)
  270         self.control = None
  271 
  272     def SetFocus(self):
  273         self.grid.SetFocus()
  274 
  275     def get_new_renderer_editor(self, col_idx):
  276         """
  277         For a given column index, return a fresh renderer and editor object.
  278 
  279         Objects must be unique to cell.
  280         Nearly (but not quite) worth making classes ;-)
  281 
  282         Returns renderer, editor.
  283         """
  284         coltype = self.col_dets[col_idx]['coltype']
  285         if coltype == COL_INT:
  286             col_min_val = self.col_dets[col_idx].get('col_min_val', -1)  ## -1 no minimum
  287             col_max_val = self.col_dets[col_idx].get('col_max_val', col_min_val)
  288             renderer = wx.grid.GridCellNumberRenderer()
  289             editor = wx.grid.GridCellNumberEditor(col_min_val, col_max_val)
  290         elif coltype == COL_FLOAT:
  291             width, precision = self.get_width_precision(col_idx)
  292             renderer = wx.grid.GridCellFloatRenderer(width, precision)
  293             editor = wx.grid.GridCellFloatEditor(width, precision)
  294         elif coltype == COL_TEXT_BROWSE:
  295             renderer = wx.grid.GridCellStringRenderer()
  296             file_phrase = self.col_dets[col_idx].get('file_phrase', '')
  297             ## use * - *.* will not pickup files without extensions in Ubuntu
  298             wildcard = self.col_dets[col_idx].get(
  299                 'file_wildcard', _('Any file') + u' (*)|*')
  300             editor = controls.GridCellTextBrowseEditor(
  301                 self.grid, file_phrase, wildcard)
  302         elif coltype == COL_DROPDOWN:
  303             dropdown_vals = self.col_dets[col_idx].get('dropdown_vals')
  304             if dropdown_vals:
  305                 renderer = wx.grid.GridCellStringRenderer()
  306                 editor = wx.grid.GridCellChoiceEditor(dropdown_vals)
  307             else:
  308                 raise Exception('settings_grid.get_new_renderer_editor: '
  309                     'needed to supply dropdown_vals')
  310         elif coltype == COL_PWD:
  311             renderer = controls.GridCellPwdRenderer()
  312             editor = controls.GridCellPwdEditor()
  313         else:
  314             renderer = wx.grid.GridCellStringRenderer()
  315             editor = wx.grid.GridCellTextEditor()
  316         return renderer, editor
  317 
  318     def get_width_precision(self, col_idx):
  319         """
  320         Returns width, precision.
  321         width (int): Minimum number of characters to be shown.
  322         precision (int): Number of digits after the decimal dot.
  323         """
  324         width = self.col_dets[col_idx].get('colwidth', 5)
  325         precision = self.col_dets[col_idx].get('col_precision', 1)
  326         return width, precision
  327 
  328     def on_label_click(self, event):
  329         "Need to give grid the focus so can process keystrokes e.g. delete"
  330         self.grid.SetFocus()
  331         event.Skip()
  332 
  333     ## processing MOVEMENTS AWAY FROM CELLS e.g. saving values //////////////////
  334 
  335     def add_cell_move_evt(self, direction, dest_row=None, dest_col=None):
  336         """
  337         Add special cell move event.
  338 
  339         :param str direction: MOVE_LEFT, MOVE_RIGHT, MOVE_UP, etc
  340         :param row dest_row: row we are going to (None if here by keystroke -
  341          yet to be determined).
  342         :param col dest_col: column we are going to (as above).
  343         """
  344         evt_cell_move = CellMoveEvent(self.grid.GetId())
  345         evt_cell_move.add_dets(dest_row, dest_col, direction)
  346         evt_cell_move.SetEventObject(self.grid)
  347         self.grid.GetEventHandler().AddPendingEvent(evt_cell_move)
  348 
  349     def on_select_cell(self, event):
  350         """
  351         Capture use of move away from a cell. May be result of mouse click
  352         or a keypress.
  353         """
  354         debug = False
  355         if not self.respond_to_select_cell:
  356             self.respond_to_select_cell = True
  357             event.Skip()
  358             return
  359         src_row = self.current_row_idx  ## row being moved from
  360         src_col = self.current_col_idx  ## col being moved from
  361         dest_row = event.GetRow()
  362         dest_col = event.GetCol()
  363         if dest_row == src_row:
  364             if dest_col > src_col:
  365                 direction = mg.MOVE_RIGHT
  366             else:
  367                 direction = mg.MOVE_LEFT
  368         elif dest_col == src_col:
  369             if dest_row > src_row:
  370                 direction = mg.MOVE_DOWN
  371             else:
  372                 direction = mg.MOVE_UP
  373         elif dest_col > src_col and dest_row > src_row:
  374                 direction = mg.MOVE_DOWN_RIGHT
  375         elif dest_col > src_col and dest_row < src_row:
  376                 direction = mg.MOVE_UP_RIGHT
  377         elif dest_col < src_col and dest_row > src_row:
  378                 direction = mg.MOVE_DOWN_LEFT
  379         elif dest_col < src_col and dest_row < src_row:
  380                 direction = mg.MOVE_UP_LEFT
  381         else:
  382             raise Exception(
  383                 'settings_grid.on_select_cell - where is direction?')
  384         if self.debug or debug: 
  385             print(
  386                 f'on_select_cell - selected row: {dest_row}, col: {dest_col}, '
  387                 f'direction: {direction} ******************************') 
  388         self.add_cell_move_evt(direction, dest_row, dest_col)
  389 
  390     def on_mouse_cell(self, event):
  391         self.update_new_is_dirty()
  392         event.Skip()
  393 
  394     def on_grid_editor_created(self, event):
  395         """
  396         Need to bind KeyDown to the control itself e.g. a choice control.
  397 
  398         wx.WANTS_CHARS makes it work.
  399         """
  400         debug = False
  401         self.control = event.GetControl()
  402         if debug: 
  403             print(f'Created editor: {self.control}')
  404             if isinstance(self.control, wx.ComboBox):
  405                 self.update_new_is_dirty()
  406                 print('Selected combobox')
  407         self.control.WindowStyle |= wx.WANTS_CHARS
  408         self.control.Bind(wx.EVT_KEY_DOWN, self.on_grid_key_down)
  409         event.Skip()
  410 
  411     def on_grid_key_down(self, event):
  412         """
  413         Potentially capture use of keypress to move away from a cell.
  414 
  415         The only case where we can't rely on on_select_cell to take care of
  416         add_cell_move_evt for us is if we are moving right or down from the last
  417         col after a keypress.
  418 
  419         Must process here. NB dest row and col yet to be determined.
  420 
  421         If a single row is selected, the key is a delete, and we are not inside
  422         and editor, delete selected row if possible.
  423         """
  424         debug = False
  425         keycode = event.GetKeyCode()
  426         if self.debug or debug: 
  427             print(f'on_grid_key_down - keycode {keycode} pressed')
  428         if (keycode in [wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE]
  429                 and not self.read_only):
  430             ## None if no deletion occurs
  431             if self.try_to_delete_row(assume_row_deletion_attempt=False):
  432                 ## don't skip. Smother event so delete not entered anywhere.
  433                 return
  434             else:
  435                 if not self.any_editor_shown:
  436                     ## set to empty string
  437                     self.grid.SetCellValue(
  438                         self.current_row_idx, self.current_col_idx, '')
  439                 else:
  440                     event.Skip()
  441         elif keycode in [wx.WXK_TAB, wx.WXK_RETURN]:
  442             if keycode == wx.WXK_TAB:
  443                 if event.ShiftDown():
  444                     direction = mg.MOVE_LEFT
  445                 else:
  446                     direction = mg.MOVE_RIGHT
  447             elif keycode == wx.WXK_RETURN:
  448                 direction = mg.MOVE_DOWN
  449             src_row=self.current_row_idx
  450             src_col=self.current_col_idx
  451             if self.debug or debug: print('on_grid_key_down - keypress in row '
  452                 f'{src_row} col {src_col} *****************************')
  453             final_col = (src_col == len(self.col_dets) - 1)
  454             if final_col and direction in [mg.MOVE_RIGHT, mg.MOVE_DOWN]:
  455                 self.add_cell_move_evt(direction)
  456                 """
  457                 Do not Skip and send event on its way.
  458 
  459                 Smother the event here so our code can determine where the
  460                 selection goes next. Matters when a Return which will otherwise
  461                 natively appear in cell below and trigger other responses.
  462                 """
  463             elif keycode == wx.WXK_RETURN:
  464                 ## A return but not at the end - normally would go down but we
  465                 ## want to go right. Whether OK to or not will be decided when
  466                 ## event processed.
  467                 self.add_cell_move_evt(direction=mg.MOVE_RIGHT)
  468                 """
  469                 Do not Skip and send event on its way.
  470 
  471                 Smother the event here so our code can determine where the
  472                 selection goes next. Otherwise Return will cause us to natively
  473                 appear in cell below and trigger other responses.
  474                 """
  475             else:
  476                 event.Skip()
  477                 return
  478         else:
  479             self.update_new_is_dirty()
  480             event.Skip()
  481             return
  482 
  483     def update_new_is_dirty(self):
  484         debug = False
  485         if self.is_new_row(self.current_row_idx):
  486             self.new_is_dirty = True
  487             if debug: print('Updated new_is_dirty to True')
  488             self.grid.SetRowLabelValue(self.current_row_idx, mg.NEW_IS_DIRTY)
  489         else:
  490             self.grid.SetRowLabelValue(self.get_new_row_idx(), mg.NEW_IS_READY)
  491 
  492     def on_text_browse_key_down(self, event):
  493         """
  494         Text browser - hit enter from text box part of composite control
  495         OR clicked on Browse button.
  496 
  497         If the final col, will go to left of new line. Otherwise, will just move
  498         right.
  499 
  500         NB we only get here if editing the text browser. If in the cell
  501         otherwise, enter will move you down, which is consistent with all other
  502         controls.
  503         """
  504         keycode = event.get_key_code()  ## custom event class
  505         if keycode in [
  506                 controls.MY_KEY_TEXT_BROWSE_MOVE_NEXT,
  507                 controls.MY_KEY_TEXT_BROWSE_BROWSE_BTN]:
  508             self.grid.DisableCellEditControl()
  509             self.add_cell_move_evt(mg.MOVE_RIGHT)
  510         elif keycode == wx.WXK_ESCAPE:
  511             self.grid.DisableCellEditControl()
  512             self.grid.SetFocus()
  513 
  514     def on_cell_move(self, event):
  515         """
  516         Response to custom event - used to start process of validating move and
  517         allowing or disallowing.
  518 
  519         Must occur after steps like SetValue (in case of changing data) so that
  520         enough information is available to validate data in cell.
  521 
  522         Only update self.current_row_idx and self.current_col_idx once decisions
  523         have been made.
  524 
  525         Should not get here from a key move left in the first column (not a cell
  526         move).
  527 
  528         NB must get the table to refresh itself and thus call SetValue(). Other-
  529         wise we can't get the value just entered so we can evaluate it for
  530         validation.
  531         """
  532         debug = False
  533         src_ctrl = self.control
  534         src_row=self.current_row_idx # row being moved from
  535         src_col=self.current_col_idx # col being moved from
  536         dest_row = event.dest_row # row being moved towards
  537         dest_col = event.dest_col # col being moved towards
  538         direction = event.direction
  539         if self.debug or debug:
  540             print(f'settings_grid.on_cell_move src_row: {src_row} '
  541                 f'src_col {src_col} dest_row: {dest_row} dest_col: {dest_col} '
  542                 f'direction {direction}')
  543         ## process_cell_move called from text editor as well so keep separate
  544         self.process_cell_move(src_ctrl,
  545             src_row, src_col,
  546             dest_row, dest_col,
  547             direction)
  548         ## Only SetFocus if moving. Otherwise if this is embedded, we can't set
  549         ## the focus anywhere else (because it triggers EVT_CELL_MOVE and then
  550         ## we grab the focus again below!).
  551         moved = ((src_row, src_col) != (dest_row, dest_col))
  552         if self.force_focus and moved:
  553             self.grid.SetFocus()
  554             ## http://www.nabble.com/Setting-focus-to-grid-td17920756.html
  555             for window in self.grid.GetChildren():
  556                 window.SetFocus()
  557         event.Skip()
  558 
  559     def process_cell_move(self, src_ctrl,
  560             src_row, src_col,
  561             dest_row, dest_col,
  562             direction):
  563         """
  564         dest row and col still unknown if from a return or TAB keystroke.
  565         So is the direction (could be down or down_left if end of line).
  566 
  567         Returns stayed_still, saved_new_row (needed for table config).
  568         """
  569         debug = False
  570         saved_new_row = False
  571         stayed_still = True
  572         if self.debug or debug:
  573             print('process_cell_move - '
  574                 f'source row {src_row} source col {src_col} '
  575                 f'dest row {dest_row} dest col {dest_col} '
  576                 f'direction: {direction}')
  577         move_type, dest_row, dest_col = self.get_move_dets(
  578             src_row, src_col, dest_row, dest_col, direction)
  579         if move_type in [mg.MOVING_IN_EXISTING, mg.LEAVING_EXISTING]:
  580             move_to_dest = self.leaving_cell_in_existing_row()
  581         elif move_type == mg.MOVING_IN_NEW:
  582             move_to_dest = self.moving_in_new_row()
  583         elif move_type == mg.LEAVING_NEW:
  584             move_to_dest, saved_new_row = self.leaving_new_row(
  585                 dest_row, dest_col, direction)
  586         else:
  587             raise Exception('process_cell_move - Unknown move_type')
  588         if self.debug or debug:
  589             print(f'move_type: {move_type} move_to_dest: {move_to_dest} '
  590                 f'dest_row: {dest_row} dest_col: {dest_col}')
  591 
  592         if move_to_dest:
  593             stayed_still = False
  594             self.respond_to_select_cell = False  ## to prevent infinite loop!
  595             self.grid.SetGridCursor(dest_row, dest_col)
  596             self.grid.MakeCellVisible(dest_row, dest_col)
  597             self.current_row_idx = dest_row
  598             self.current_col_idx = dest_col
  599         else:
  600             if debug: print(f'Stay here at {src_row} {src_col}')
  601             #self.respond_to_select_cell = False # to prevent infinite loop!
  602             if src_ctrl:
  603                 if debug: 
  604                     print(f'Last control was: {src_ctrl}')
  605                     print(f'Control text: {src_ctrl.GetValue()}')
  606                 self.grid.EnableCellEditControl(enable=True)
  607                 try:
  608                     src_ctrl.SetInsertionPointEnd()
  609                 except Exception:
  610                     pass  ## OK if source control has no ability to set insertion point.
  611         return stayed_still, saved_new_row
  612 
  613     def get_move_dets(self, src_row, src_col, dest_row, dest_col, direction):
  614         """
  615         Gets move details.
  616 
  617         Returns move_type, dest_row, dest_col.
  618         move_type - MOVING_IN_EXISTING, MOVING_IN_NEW, LEAVING_EXISTING, or
  619         LEAVING_NEW.
  620 
  621         dest_row and dest_col are where we the selection should go unless there
  622         is a validation issue.
  623 
  624         dest_row and dest_col may need to be worked out e.g. if cell move caused
  625             by a tab keypress.
  626 
  627         Take into account whether a new row or not.
  628 
  629         If on new row, take into account if final column.
  630 
  631         Main task is to decide whether to allow the move.
  632         If not, set focus on source.
  633 
  634         Decide based on validation of cell and, if a new row, the row as a whole
  635 
  636         Don't allow to leave cell in invalid state.
  637 
  638         Overview of checks made:
  639             If jumping around within new row, cell cannot be invalid.
  640             If not in a new row (i.e. in existing), cell must be ok to save.
  641             If leaving new row, must be ready to save whole row unless a clean
  642                 row and moving up.
  643 
  644         If any rules are broken, put focus on source cell. Otherwise got to
  645         cell at destination row and col.
  646         """
  647         debug = False
  648         ## 1) move type
  649         final_col = (src_col == len(self.col_dets) - 1)
  650         was_new_row = self.is_new_row(self.current_row_idx)
  651         dest_row_is_new = self.dest_row_is_current_new(
  652             src_row, direction, final_col)
  653         if debug or self.debug:
  654             print(f'Current row idx: {self.current_row_idx}, '
  655                 f'src_row: {src_row}, was_new_row: {was_new_row}, '
  656                 f'dest_row_is_new: {dest_row_is_new}')
  657         if was_new_row and dest_row_is_new:
  658             move_type = mg.MOVING_IN_NEW
  659         elif was_new_row and not dest_row_is_new:
  660             move_type = mg.LEAVING_NEW
  661         elif not was_new_row and not dest_row_is_new:
  662             move_type = mg.MOVING_IN_EXISTING
  663         elif not was_new_row and dest_row_is_new:
  664             move_type = mg.LEAVING_EXISTING
  665         else:
  666             raise Exception('settings_grid.get_move_dets(). Unknown move.')
  667         ## 2) dest row and dest col
  668         if dest_row is None and dest_col is None:  ## known if from on_select_cell
  669             if final_col and direction in [mg.MOVE_RIGHT, mg.MOVE_DOWN]:
  670                 dest_row = src_row + 1
  671                 dest_col = 0
  672             else:
  673                 if direction == mg.MOVE_RIGHT:
  674                     dest_row = src_row
  675                     dest_col = src_col + 1
  676                 elif direction == mg.MOVE_LEFT:                    
  677                     dest_row = src_row
  678                     dest_col = src_col - 1 if src_col > 0 else 0
  679                 elif direction == mg.MOVE_DOWN:
  680                     dest_row = src_row + 1
  681                     dest_col = src_col
  682                 else:
  683                     raise Exception(
  684                         'settings_grid.get_move_dets no destination (so from a '
  685                         'TAB or Return) yet not a left, right, or down.')
  686         return move_type, dest_row, dest_col
  687 
  688     def dest_row_is_current_new(self, src_row, direction, final_col):
  689         """
  690         Is the destination row (assuming no validation problems) the current
  691         new row?
  692 
  693         If currently on the new row and leaving it, the destination row, even
  694         if it becomes a new row is not the current new row.
  695         """
  696         ## organised for clarity not minimal lines of code ;-)
  697         if self.is_new_row(src_row):  ## new row
  698             if final_col:
  699                 ## only LEFT stays in _current_ new row
  700                 if direction == mg.MOVE_LEFT:
  701                     dest_row_is_new = True
  702                 else:
  703                     dest_row_is_new = False
  704             else:  ## only left and right stay in _current_ new row
  705                 if direction in [mg.MOVE_LEFT, mg.MOVE_RIGHT]:
  706                     dest_row_is_new = True  ## moving sideways within new
  707                 else:
  708                     dest_row_is_new = False
  709         elif self.is_new_row(src_row + 1):  ## row just above the new row
  710             ## only down (inc down left and right), or right in final col, 
  711             ## take to new
  712             direct_down_move = (direction in
  713                 [mg.MOVE_DOWN, mg.MOVE_DOWN_LEFT, mg.MOVE_DOWN_RIGHT])
  714             down_cos_end_move = (direction == mg.MOVE_RIGHT and final_col)
  715             if direct_down_move or down_cos_end_move:
  716                 dest_row_is_new = True
  717             else:
  718                 dest_row_is_new = False
  719         else:  ## more than one row away from new row
  720             dest_row_is_new = False
  721         return dest_row_is_new
  722 
  723     def is_new_row(self, row):
  724         """
  725         e.g. if 2 rows inc new - is a new row if idx = 1 i.e. can calculate by
  726         subtracting 1.
  727         """
  728         debug = False
  729         if debug: print(f'row: {row}; rows_to_fill: {self.rows_to_fill}')
  730         new_row = (row == self.rows_to_fill)
  731         return new_row
  732 
  733     def get_new_row_idx(self):
  734         """
  735         if 2 rows to fill then the final row (2) will be the new one -
  736         not zero-based indexing.
  737         """
  738         return self.rows_to_fill
  739 
  740     def leaving_cell_in_existing_row(self):
  741         """
  742         Process the attempt to leave an existing cell (whether or not leaving
  743         existing row).
  744 
  745         Will not move if cell data not OK to save.
  746         Return move_to_dest.
  747         """
  748         debug = False
  749         if self.debug or debug: print('Was in existing, ordinary row')
  750         move_to_dest, msg = self.cell_ok_to_save(
  751             self.current_row_idx, self.current_col_idx)
  752         if msg: wx.MessageBox(msg)
  753         return move_to_dest
  754 
  755     def moving_in_new_row(self):
  756         """
  757         Process the attempt to move away from a cell in the new row to another
  758         cell in the same row. Will not move if cell is invalid.
  759 
  760         Return move_to_dest.
  761         """
  762         debug = False
  763         if self.debug or debug: print('Moving within new row')
  764         row = self.current_row_idx
  765         col = self.current_col_idx
  766         invalid, msg = self.cell_invalid(row, col)
  767         if msg: wx.MessageBox(msg)
  768         move_to_dest = not invalid 
  769         return move_to_dest
  770 
  771     def leaving_new_row(self, dest_row, dest_col, direction):
  772         """
  773         Process attempt to leave new row.
  774 
  775         Always OK to leave new row in an upwards direction if it has not been
  776         altered (i.e. not dirty).
  777 
  778         Otherwise, must see if row is OK to Save.  If not, e.g. faulty data,
  779         keep selection where it was.  If OK, add new row.
  780 
  781         NB actual direction could be down_left instead of down if in final col.
  782 
  783         Return move_to_dest, saved_new_row (used by table config when processing
  784         cell move).
  785         """
  786         debug = False
  787         saved_new_row = False
  788         is_dirty = self.is_dirty(self.current_row_idx)
  789         if self.debug or debug: 
  790             print(f'leaving_new_row - dest row {dest_row} dest col {dest_col} '
  791                 f'original direction {direction} dirty {is_dirty}')
  792         moved_up = direction in [mg.MOVE_UP, mg.MOVE_UP_RIGHT, mg.MOVE_UP_LEFT]
  793         if moved_up and not is_dirty:
  794             move_to_dest = True  ## always OK
  795             self.new_is_dirty = False
  796         else:  ## must check OK to move
  797             ok_to_save, msg = self.cell_ok_to_save(
  798                 self.current_row_idx, self.current_col_idx)
  799             if not ok_to_save:
  800                 wx.MessageBox(msg)
  801                 move_to_dest = False
  802             elif not self.row_ok_to_save(
  803                     row=self.current_row_idx,
  804                     col2skip=self.current_col_idx):  ## just passed
  805                 move_to_dest = False
  806             else:
  807                 move_to_dest = True
  808             if move_to_dest:
  809                 self.add_new_row()
  810                 if debug or self.debug: print('Added new row')
  811                 saved_new_row = True
  812                 self.new_is_dirty = False
  813         return move_to_dest, saved_new_row
  814 
  815     # VALIDATION ///////////////////////////////////////////////////////////////
  816 
  817     def is_dirty(self, row):
  818         "Dirty means there are some values which are not empty strings"
  819         for col_idx in range(len(self.col_dets)):
  820             if self.grid.GetCellValue(row, col_idx) != '':
  821                 return True
  822         return False
  823 
  824     def cell_invalid(self, row, col):
  825         """
  826         Return boolean and string message.
  827         NB must flush values in any open editors onto grid.
  828         """
  829         self.grid.DisableCellEditControl()
  830         val = self.get_val(row, col)
  831         return self.cell_invalidation_func(
  832             self.frame, val, row, col, self.grid, self.col_dets)
  833 
  834     def get_val(self, row, col):
  835         """
  836         What was the value of a cell?
  837 
  838         If it has just been edited, GetCellValue(), will not have caught up yet.
  839 
  840         Need to get version stored by editor. So MUST close editors which
  841         presumably flushes the value to where it becomes available to
  842         GetCellValue().
  843         """
  844         self.grid.DisableCellEditControl()
  845         val = self.grid.GetCellValue(row, col)
  846         return val
  847 
  848     def cell_ok_to_save(self, row, col):
  849         """
  850         Cannot be an invalid value (must be valid or empty string).
  851         And if empty string value, must be empty ok.
  852         Returns boolean and string message.
  853         """
  854         debug = False
  855         empty_ok = self.col_dets[col].get('empty_ok', False)
  856         cell_val = self.get_val(row, col)
  857         if self.debug or debug:
  858             print(f'cell_ok_to_save - row: {row}, col: {col}, '
  859                 f'empty_ok: {empty_ok}, cell_val: {cell_val}')
  860         empty_not_ok_prob = (cell_val == '' and not empty_ok)
  861         invalid, msg = self.cell_invalid(row, col)
  862         if not invalid:
  863             self.cell_response_func(
  864                 self.frame, cell_val, row, col, self.grid, self.col_dets)
  865         if not msg and empty_not_ok_prob:
  866             msg = _('Data cell cannot be empty.')
  867         ok_to_save = not invalid and not empty_not_ok_prob
  868         return ok_to_save, msg
  869 
  870     def row_ok_to_save(self, row, col2skip=None):
  871         """
  872         Each cell must be OK to save. NB validation may be stricter than what
  873         the database will accept into its fields e.g. must be one of three
  874         strings ("Numeric", "Text", or "Date").
  875 
  876         col2skip -- so we can skip validating a cell that has just passed e.g.
  877         in leaving_new_row
  878         """
  879         if self.debug: print(f'row_ok_to_save - row {row}')
  880         for col_idx, col_det in enumerate(self.col_dets):
  881             if col_idx == col2skip:
  882                 continue
  883             ok_to_save, msg = self.cell_ok_to_save(row=row, col=col_idx)
  884             if not ok_to_save:
  885                 wx.MessageBox(_('Unable to save new row.  Invalid value '
  886                     "in the \"%(col_label)s\" column. %(msg)s") % \
  887                     {'col_label': col_det['col_label'], 'msg': msg})
  888                 return False
  889         return True
  890     
  891     # MISC /////////////////////////////////////////////////////////////////////
  892 
  893     def get_cols_n(self):
  894         return len(self.col_dets)
  895 
  896     def ok_to_delete_row(self, row):
  897         """
  898         Can delete any row except the new row.
  899         Cannot delete if in middle of editing a cell.
  900         Returns boolean and msg.
  901         """
  902         if self.is_new_row(row):
  903             return False, _('Unable to delete new row')
  904         elif self.new_is_dirty:
  905             return False, _(
  906                 'Cannot delete a row while in the middle of making a new one')
  907         elif self.any_editor_shown:
  908             return False, _(
  909                 'Cannot delete a row while in the middle of editing a cell')  
  910         else:
  911             return True, None
  912 
  913     def try_to_delete_row(self, *, assume_row_deletion_attempt=True):
  914         """
  915         Delete row if a row selected and not the data entry row and put focus on
  916         new line.
  917 
  918         If it is assumed there was a row deletion attempt (e.g. clicked a delete
  919         button), then warn if no selection. If no such assumption, silently cope
  920         with situation where no selection.
  921 
  922         :return: row idx deleted (or None if deletion did not occur).
  923         """
  924         selected_rows = self.grid.GetSelectedRows()
  925         sel_rows_n = len(selected_rows)
  926         if sel_rows_n == 1:
  927             row = selected_rows[0]
  928             ok_to_delete, msg = self.ok_to_delete_row(row)
  929             if ok_to_delete:
  930                 self.delete_row(row)
  931                 return row
  932             else:
  933                 wx.MessageBox(msg)
  934         elif sel_rows_n == 0:
  935             if assume_row_deletion_attempt:
  936                 wx.MessageBox(_(
  937                     'Please select a row first (click to the left of the row)'))
  938             else:
  939                 pass
  940         else:
  941             wx.MessageBox(_('Can only delete one row at a time'))
  942         return None
  943 
  944     def delete_row(self, row):
  945         """
  946         Delete a row.
  947 
  948         row -- row index (not necessarily same as id value).
  949 
  950         If the currently selected cell is in a row below that being deleted,
  951         move up one.
  952 
  953         Move to the correct cell and set self.respond_to_select_cell to False.
  954 
  955         Assumed to be OK because not shifting cell as such. Just following it
  956         :-) or jumping to a cell in an already-validated row.
  957         """
  958         self.grid.DeleteRows(pos=row, numRows=1)
  959         self.reset_row_n(change=-1)
  960         self.grid.SetRowLabelValue(self.rows_n - 1, mg.NEW_IS_READY)
  961         self.grid.HideCellEditControl()
  962         self.grid.ForceRefresh()
  963         self.safe_layout_adjustment()
  964         self.respond_to_select_cell = False
  965         if row < self.current_row_idx:
  966             self.current_row_idx -= 1
  967             self.grid.SetGridCursor(self.current_row_idx, self.current_col_idx)
  968         self.grid.SetFocus()
  969 
  970     def on_editor_shown(self, event):
  971         """Disable resizing until finished"""
  972         self.grid.DisableDragColSize()
  973         self.grid.DisableDragRowSize()
  974         self.any_editor_shown = True
  975         if event.GetRow() == self.rows_n - 1:
  976             self.new_editor_shown = True
  977         event.Skip()
  978 
  979     def on_editor_hidden(self, event):
  980         """Re-enable resizing"""
  981         self.grid.EnableDragColSize()
  982         self.grid.EnableDragRowSize()
  983         self.any_editor_shown = False
  984         self.new_editor_shown = False
  985         event.Skip()
  986 
  987     def on_cell_change(self, event):
  988         debug = False
  989         if debug: print(f'Current row idx is: {self.current_row_idx}')
  990         self.update_new_is_dirty()
  991         if self.debug or debug: print('Cell changed')
  992         self.grid.ForceRefresh()
  993         self.safe_layout_adjustment()
  994         event.Skip()
  995 
  996     def insert_row_above(self, pos):
  997         """
  998         Insert row above selected row.
  999 
 1000         If data supplied (list), insert values into row.
 1001 
 1002         Change row labels appropriately.
 1003 
 1004         If below the rows being inserted, jump down a row and set
 1005         self.respond_to_select_cell to False. Assumed to be OK because not
 1006         shifting cell as such.  Just following it :-)
 1007 
 1008         Returns bolinserted, and row_data (list if content put into inserted
 1009         row, None if not).
 1010         """
 1011         if self.new_is_dirty:
 1012             wx.MessageBox(_(
 1013                 'Cannot insert a row while in the middle of making a new one'))
 1014             return False, None
 1015         grid_data = self.get_grid_data()  ## only needed to prevent field name
 1016         ## collisions
 1017         row_idx = pos
 1018         self.grid.InsertRows(row_idx)
 1019         row_data = None
 1020         if self.insert_data_func:
 1021             row_data = self.insert_data_func(grid_data)
 1022             for col_idx in range(len(self.col_dets)):
 1023                 renderer, editor = self.get_new_renderer_editor(col_idx)
 1024                 self.grid.SetCellRenderer(row_idx, col_idx, renderer)
 1025                 self.grid.SetCellEditor(row_idx, col_idx, editor)
 1026             for i, value in enumerate(row_data):
 1027                 self.grid.SetCellValue(row_idx, i, value)
 1028         self.reset_row_n(change=1)
 1029         ## reset label for all rows after insert
 1030         self.update_next_row_labels(row_idx)
 1031         self.grid.SetRowLabelValue(self.rows_n - 1, mg.NEW_IS_READY)
 1032         if row_idx <= self.current_row_idx:  ## inserting above our selected cell
 1033             self.current_row_idx += 1
 1034             self.grid.SetGridCursor(self.current_row_idx, self.current_col_idx)
 1035         self.grid.SetFocus()
 1036         return True, row_data
 1037 
 1038     def update_next_row_labels(self, pos):
 1039         for i in range(pos, self.rows_n - 1):
 1040             self.grid.SetRowLabelValue(i, str(i + 1))
 1041 
 1042     def reset_row_n(self, change=1):
 1043         "Reset rows_n and rows_to_fill by incrementing or decrementing."
 1044         self.rows_n += change
 1045         self.rows_to_fill += change
 1046 
 1047     def add_new_row(self):
 1048         """
 1049         Add new row.
 1050         """
 1051         self.new_is_dirty = False
 1052         new_row_idx = self.rows_n  ## e.g. 1 row 1 new (2 rows), new is 2
 1053         # change label from * and add a new entry row on end of grid
 1054         self.grid.AppendRows()
 1055         ## set up cell rendering and editing
 1056         for col_idx in range(self.cols_n):
 1057             renderer, editor = self.get_new_renderer_editor(col_idx)
 1058             self.grid.SetCellRenderer(new_row_idx, col_idx, renderer)
 1059             self.grid.SetCellEditor(new_row_idx, col_idx, editor)
 1060         self.reset_row_n(change=1)
 1061         self.grid.SetRowLabelValue(self.rows_n - 2, str(self.rows_n - 1))
 1062         self.grid.SetRowLabelValue(self.rows_n - 1, mg.NEW_IS_READY)
 1063         self.safe_layout_adjustment()
 1064 
 1065     def safe_layout_adjustment(self):
 1066         """
 1067         Uses CallAfter to avoid infinite recursion.
 1068         http://lists.wxwidgets.org/pipermail/wxpython-users/2007-April/063536.html
 1069         """
 1070         wx.CallAfter(self.run_safe_layout_adjustment)
 1071 
 1072     def run_safe_layout_adjustment(self):
 1073         for col_idx in range(len(self.col_dets)):
 1074             current_width = self.grid.GetColSize(col_idx)
 1075             ## identify optimal width according to content
 1076             self.grid.AutoSizeColumn(col_idx, setAsMin=False)
 1077             new_width = self.grid.GetColSize(col_idx)
 1078             if new_width < current_width: ## only the user can shrink a column
 1079                 ## restore to current size
 1080                 self.grid.SetColSize(col_idx, current_width)
 1081         ## otherwise will only see effect after resizing
 1082         self.grid.ForceRefresh()
 1083 
 1084     def row_has_data(self, row):
 1085         """
 1086         Has the row got any data stored yet?
 1087 
 1088         NB data won't be picked up if you are in the middle of entering it.
 1089         """
 1090         has_data = False
 1091         for i in range(self.cols_n):
 1092             cell_val = self.grid.GetCellValue(row=row, col=i)
 1093             if cell_val:
 1094                 has_data = True
 1095                 break
 1096         return has_data
 1097 
 1098     def get_grid_data(self):
 1099         """
 1100         Get data from grid.
 1101 
 1102         If read_only, get all rows.
 1103 
 1104         If not read_only, get all but final row (either empty or not saved).
 1105 
 1106         Only returns values - not types etc.
 1107         """
 1108         grid_data = []
 1109         data_rows_n = self.rows_n if self.read_only else self.rows_n - 1 
 1110         for row_idx in range(data_rows_n):
 1111             row_data = []
 1112             for col_idx in range(len(self.col_dets)):
 1113                 val = self.grid.GetCellValue(row=row_idx, col=col_idx)
 1114                 row_data.append(lib.fix_eols(val))
 1115             grid_data.append(tuple(row_data))
 1116         return grid_data
 1117 
 1118     def update_settings_data(self):
 1119         """
 1120         Update settings_data. Separated for reuse. NB clear it first so can
 1121         refresh repeatedly.
 1122         """
 1123         grid_data = self.get_grid_data()
 1124         ## need to stay pointed to same memory but empty it
 1125         while True:
 1126             try:
 1127                 del self.settings_data[0]
 1128             except IndexError:
 1129                 break
 1130         self.settings_data += grid_data