I have managed to create a workaround but it is not perfect as there will be an empty column at the end of table. With that said, the row swapping works perfectly now and does not even break when you remove or add any row.
It comes from me actually using the fact that adding to a pre-existing row in a position without a control results in a new row in front of it with only that one item while the old row gets pushed down with the rest.
#Author-Willie Anderson
#Description-Adjustable Table Demo
# This adjustable table testbench was adapted from table section of ADSK Input Sampler
import adsk.core, adsk.fusion, traceback
from copy import deepcopy
_app = None
_ui : adsk.core.UserInterface = None
# Global set of event handlers to keep them referenced for the duration of the command
_handlers = []
# Units available for parameter input - see addRowToTable()
_units = ['integer','real','---','mm', 'cm','m','μ','---','"','ft','yd','mil','---','rad','°','grad']
# column header labels (sorry for shouting :-)
_columnLabels = ['Heading 0','Heading 1','Heading 2','Heading 3']
# other global parameters
#_tableStyle = transparentBackgroundTablePresentationStyle # Python cannot find this enum
_tableStyle = 2 # should be transparentBackgroundTablePresentationStyle
_tableSpace = 0 # padding between rows and columns
_min_rows = 1 # minimum rows shown in the data table
_max_rows = 5 # maximum rows shown in the data table
_column_width_basis = 30.0 # used for column widening / narrowing; if too large, wide columns fail
_cmd = 'testTable' # command name
_colSpec = '8:16:4:4' # initial column ratios
_rows_created = 0 # used in dummy addRowToTable() to generate unique IDs
_selected_column = -1
_tables = []
_commandInputs: adsk.core.CommandInputs = None
# build header (column label) table
# we build a separate table (as opposed to just having a special first row)
# so that we can keep this header list visible as the bottom table scrolls
def labelTable(inputs):
table = inputs.addTableCommandInput(_cmd + 'Header', '', 0, _colSpec)
table.tablePresentationStyle = _tableStyle
table.minimumVisibleRows = 1
table.maximumVisibleRows = 1
table.rowSpacing = table.columnSpacing = _tableSpace
cmds = adsk.core.CommandInputs.cast(table.commandInputs)
for column in list(range(len(_columnLabels))):
label = _columnLabels[column]
header = cmds.addBoolValueInput((_cmd + 'Header{}').format(column),label,False)
header.text = label
header.isFullWidth = True # supress name when it would otherwise be shown
table.addCommandInput(header, 0, column)
return table
# adjust table column width
def adjustColumnWidth(tab:adsk.core.TableCommandInput,col,delta):
# parse column ratio spec into list of integers
width = list(map(int,tab.columnRatio.split(':')))
if col != -1:
width[col] += delta
# normalize width[] so that its members sum to _column_width_basis but
# maintain relative size; may cause issues for wide columns, be wary;
# this algorithm is far from perfect, but user can work around it
ratio = _column_width_basis/float(sum(width))
for i in list(range(len(width))):
width[i] = round(ratio * float(width[i]))
# rebuild the column ratio spec
columnSpec = ':'.join(map(str,width))
tab.columnRatio = columnSpec
# convenience function for lambda in inputChangedHandler
def adjustAllColumns(column, delta):
for t in _tables:
adjustColumnWidth(t, column, delta)
# swap table rows - used to move rows up and down
def swapRows(table : adsk.core.TableCommandInput, i, j, selected):
global _ui, _commandInputs
# initialized by run()
tableiRow = [_commandInputs.itemById(table.getInputAtPosition(i,c).id) for c in range(table.numberOfColumns)]
tablejRow = [_commandInputs.itemById(table.getInputAtPosition(j,c).id) for c in range(table.numberOfColumns)]
tableCols = table.numberOfColumns
table.addCommandInput(tableiRow[0],i,table.numberOfColumns)
#These are two dummy inputs for the table to have something to delete
tempinput =_commandInputs.addStringValueInput('tempID', 'tempName', ' ')
tempinput2 =_commandInputs.addStringValueInput('tempID2', 'tempName2', ' ')
#Moves the current rows first input to the end
table.addCommandInput(tempinput, i,table.numberOfColumns)
#Moves the next rows first input to the end
table.addCommandInput(tempinput2, j, table.numberOfColumns)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
table.removeInput(i,0)
# #Creates a new row and starts moving objects into it
for c in range(table.numberOfColumns - 3):
table.addCommandInput(tablejRow[c], i, c)
# #Creates a new row and starts moving objects into it
for c in range(table.numberOfColumns - 3):
table.addCommandInput(tableiRow[c], j, c)
# Deletes the old rows with the dummy inputs
table.deleteRow(i+2)
table.deleteRow(i)
table.selectedRow = selected
# Adds a new row to the table;
# Here's where the various columns are defined
def addRowToTable(tableInput: adsk.core.TableCommandInput):
global _rows_created, _commandInputs
row = _rows_created
col = len(_columnLabels)
tableRows = tableInput.rowCount
# Get the CommandInputs object associated with the parent command.
cmdInputs =_commandInputs
# cmdInputs = adsk.core.CommandInputs.cast(tableInput.commandInputs)
# Create new command inputs and value display
# just do something that fills in boxes
for c in list(range(col)):
ident = _cmd + '_{}_{}'.format(row, c)
if c < col - 1:
inputCmd = cmdInputs.addStringValueInput(ident,ident,ident)
else: # make the last column a drop down unit selector, just for fun
inputCmd = cmdInputs.addDropDownCommandInput(ident,ident,adsk.core.DropDownStyles.TextListDropDownStyle)
for unit in _units:
if unit == _units[0]:
inputCmd.listItems.add(unit, True)
elif unit == '---':
inputCmd.listItems.addSeparator(-1) # addSeparator fails here
else:
inputCmd.listItems.add(unit, False)
inputCmd.isFullWidth = True
tableInput.addCommandInput(inputCmd,tableRows, c)
_rows_created += 1
# Event handler that reacts to any changes the user makes to any of the command inputs.
class MyCommandInputChangedHandler(adsk.core.InputChangedEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
global _selected_column, _commandInputs
try:
eventArgs = adsk.core.InputChangedEventArgs.cast(args)
cmdInput = eventArgs.input
table:adsk.core.TableCommandInput = eventArgs.inputs.itemById(_cmd)
rows = table.rowCount
selected = table.selectedRow
cmdSwitch = {
_cmd+'Add':
lambda:
addRowToTable(table) ,
_cmd+'Delete':
lambda:
table.deleteRow(selected) if selected != -1
else _ui.messageBox('Select a row to delete.'),
_cmd+'RowUp':
lambda:
swapRows(table, selected, selected - 1, selected - 1) if selected > 0
else _ui.messageBox('Select a row that can move up.'),
_cmd+'RowDown':
lambda:
swapRows(table, selected + 1, selected, selected + 1) if selected - 1 < rows - 2
else _ui.messageBox('Select a row that can move down.'),
_cmd+'ColumnWiden':
lambda:
adjustAllColumns(_selected_column, 1) if _selected_column != -1
else _ui.messageBox('Select a column to adjust.'),
_cmd+'ColumnNarrow':
lambda:
adjustAllColumns(_selected_column, -1) if _selected_column != -1
else _ui.messageBox('Select a column to adjust.')
}
# the lambda default arg below allows other
# strings to pass (e.g. column selections below)
cmdSwitch.get(cmdInput.id, lambda: 0)()
# detect column selections
if cmdInput.id.startswith(_cmd+'Header'):
_selected_column = int(cmdInput.id.replace(_cmd+'Header','',1))
# handle other input changes here
for i in range(table.rowCount):
if (table.getInputAtPosition(i, 2) is not None): table.getInputAtPosition(i, 2).value = 'testTable: row# {}'.format(i)
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
# Event handler that reacts to when the command is destroyed. This terminates the script.
class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
# When the command is done, terminate the script
# This will release all globals which will remove all event handlers
adsk.terminate()
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
# Event handler that reacts when the command definitio is executed which
# results in the command being created and this event being fired.
class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
global _commandInputs
# Get the command that was created.
cmd = adsk.core.Command.cast(args.command)
# Connect to the command destroyed event.
onDestroy = MyCommandDestroyHandler()
cmd.destroy.add(onDestroy)
_handlers.append(onDestroy)
# Connect to the input changed event.
onInputChanged = MyCommandInputChangedHandler()
cmd.inputChanged.add(onInputChanged)
_handlers.append(onInputChanged)
# Get the CommandInputs collection associated with the command.
inputs = cmd.commandInputs
_commandInputs = inputs
# Create header table
headers = labelTable(inputs)
_tables.append(headers)
# Create table input
tableInput = inputs.addTableCommandInput(_cmd, _cmd, 0, _colSpec)
# tableInput.tablePresentationStyle = _tableStyle
tableInput.tablePresentationStyle = adsk.core.TablePresentationStyles.itemBorderTablePresentationStyle
tableInput.minimumVisibleRows = 4
tableInput.maximumVisibleRows = 12
tableInput.rowSpacing = tableInput.columnSpacing = _tableSpace
addRowToTable(tableInput)
addRowToTable(tableInput)
addRowToTable(tableInput)
addRowToTable(tableInput)
# Add buttons to the bottom of the table.
for button in ['Add','Delete','RowUp','RowDown','ColumnWiden','ColumnNarrow']:
tableInput.addToolbarCommandInput(inputs.addBoolValueInput(_cmd+button, button, False))
_tables.append(tableInput)
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
def run(context):
try:
global _app, _ui
_app = adsk.core.Application.get()
_ui = _app.userInterface
# Get the existing command definition or create it if it doesn't already exist.
cmdDef = _ui.commandDefinitions.itemById(_cmd+'Cmd')
if not cmdDef:
cmdDef = _ui.commandDefinitions.addButtonDefinition(_cmd+'Cmd', 'Table Input Testbench', 'Testbench for flexible table.')
# Connect to the command created event.
onCommandCreated = MyCommandCreatedHandler()
cmdDef.commandCreated.add(onCommandCreated)
_handlers.append(onCommandCreated)
# Execute the command definition.
cmdDef.execute()
# Prevent this module from being terminated when the script returns, because we are waiting for event handlers to fire.
adsk.autoTerminate(False)
except:
if _ui:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))