001package jmri.jmrix.can.cbus.swing.modules.merg; 002 003import java.awt.*; 004import java.awt.event.ActionEvent; 005 006import javax.swing.*; 007import javax.swing.border.*; 008import javax.swing.event.*; 009 010import jmri.jmrix.can.cbus.node.CbusNode; 011import jmri.jmrix.can.cbus.node.CbusNodeNVTableDataModel; 012import jmri.jmrix.can.cbus.swing.modules.*; 013 014import org.slf4j.Logger; 015import org.slf4j.LoggerFactory; 016 017/** 018 * Node Variable edit frame for a MERG CANACC8 CBUS module 019 * 020 * @author Andrew Crosland Copyright (C) 2021 021 */ 022public class Canacc8EditNVPane extends AbstractEditNVPane { 023 024 // Number of outputs 025 public static final int OUTPUTS = 8; 026 027 // Output type 028 public static final int TYPE_CONTINUOUS = 0; 029 public static final int TYPE_SINGLE = 1; 030 public static final int TYPE_REPEAT = 2; 031 032 // Startup action 033 public static final int ACTION_OFF = 3; 034 public static final int ACTION_SAVED = 1; 035 public static final int ACTION_NONE = 0; 036 037 // Conversion between NV and display values 038 public static final int PULSE_WIDTH_STEP_SIZE = 20; 039 public static final int PULSE_WIDTH_NUM_STEPS = 127; 040 public static final double FEEDBACK_DELAY_STEP_SIZE = 0.5; 041 042 OutPane [] out = new OutPane[OUTPUTS+1]; 043 044 private final UpdateNV pulseUpdateFn = new UpdatePulse(); 045 private final UpdateNV startupUpdateFn = new UpdateStartup(); 046 private final UpdateNV feedbackUpdateFn = new UpdateFeedback(); 047 048 private TitledSpinner feedbackSpinner; 049 050 protected Canacc8EditNVPane(CbusNodeNVTableDataModel dataModel, CbusNode node) { 051 super(dataModel, node); 052 } 053 054 /** {@inheritDoc} */ 055 @Override 056 public AbstractEditNVPane getContent() { 057 058 JPanel gridPane = new JPanel(new GridBagLayout()); 059 GridBagConstraints c = new GridBagConstraints(); 060 c.fill = GridBagConstraints.HORIZONTAL; 061 c.weightx = 1; 062 c.weighty = 1; 063 c.gridy = 0; 064 065 // Four columns for the outputs 066 for (int y = 0; y < OUTPUTS/4; y++) { 067 c.gridx = 0; 068 for (int x = 0; x < 4; x++) { 069 int index = y*4 + x + 1; // NVs indexed from 1 070 out[index] = new OutPane(index); 071 gridPane.add(out[index], c); 072 c.gridx++; 073 } 074 c.gridy++; 075 } 076 077 c.gridx = 0; 078 c.gridy = 3; 079 feedbackSpinner = new TitledSpinner(Bundle.getMessage("FeedbackDelayUnits"), Canacc8PaneProvider.FEEDBACK_DELAY, feedbackUpdateFn); 080 feedbackSpinner.setToolTip(Bundle.getMessage("FeedbackDelayTt")); 081 feedbackSpinner.init(getSelectValue8(Canacc8PaneProvider.FEEDBACK_DELAY)*FEEDBACK_DELAY_STEP_SIZE, 0, 082 FEEDBACK_DELAY_STEP_SIZE*255, FEEDBACK_DELAY_STEP_SIZE); 083 084 gridPane.add(feedbackSpinner, c); 085 086 JScrollPane scroll = new JScrollPane(gridPane); 087 add(scroll); 088 089 return this; 090 } 091 092 /** {@inheritDoc} */ 093 @Override 094 public void tableChanged(TableModelEvent e) { 095 if (e.getType() == TableModelEvent.UPDATE) { 096 int row = e.getFirstRow(); 097 int nv = row + 1; 098 int value = getSelectValue8(nv); 099 if ((nv > 0) && (nv <= 8)) { 100 //log.debug("Update NV {} to {}", nv, value); 101 int oldSpinnerValue = out[nv].pulseSpinner.getIntegerValue()/PULSE_WIDTH_STEP_SIZE; 102 out[nv].setButtons(value, oldSpinnerValue); 103 out[nv].pulseSpinner.setValue((value & 0x7f)*PULSE_WIDTH_STEP_SIZE); 104 log.debug("NV {} Now {}", nv, (out[nv].pulseSpinner.getIntegerValue())); 105 } else if (nv == 9) { 106 //log.debug("Update feedback delay to {}", value); 107 feedbackSpinner.setValue(value*FEEDBACK_DELAY_STEP_SIZE); 108 } else if ((nv == 10) || (nv == 11)) { 109 //log.debug("Update startup action", value); 110 for (int i = 1; i <= 8; i++) { 111 out[i].action.setButtons(); 112 } 113 } else { 114 // Not used, or row was -1 115// log.debug("Update unknown NV {}", nv); 116 } 117 } 118 } 119 120 /** 121 * Update the NVs controlling the pulse width and type 122 */ 123 protected class UpdatePulse implements UpdateNV { 124 125 /** {@inheritDoc} */ 126 @Override 127 public void setNewVal(int index) { 128 int pulseWidth = out[index].pulseSpinner.getIntegerValue(); 129 pulseWidth /= PULSE_WIDTH_STEP_SIZE; 130 if (out[index].cont.isSelected()) { 131 pulseWidth = 0; 132 } 133 if (out[index].repeat.isSelected()) { 134 pulseWidth |= 0x80; 135 } 136 // Preserve continuous (bit 7) from old value unless we selected single button 137 if ((getSelectValue8(index) >= 0x80) && !(out[index].buttonFlag && out[index].single.isSelected())) { 138 pulseWidth |= 0x80; 139 } 140 // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc 141 _dataModel.setValueAt(pulseWidth, index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 142 } 143 } 144 145 /** 146 * Update the NVs controlling the startup action 147 */ 148 protected class UpdateStartup implements UpdateNV { 149 150 @Override 151 public void setNewVal(int index) { 152 int newNV10 = getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (~(1<<(index-1))); 153 int newNV11 = getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (~(1<<(index-1))); 154 155 // Startup action is in NV10 and NV11, 1 bit per output 156 if (out[index].action.off.isSelected()) { 157 // 11 158 newNV10 |= (1<<(index-1)); 159 newNV11 |= (1<<(index-1)); 160 } else if (out[index].action.saved.isSelected()) { 161 // 01 162 newNV11 |= (1<<(index-1)); 163 } 164 165 // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc 166 _dataModel.setValueAt(newNV10, Canacc8PaneProvider.STARTUP_POSITION-1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 167 _dataModel.setValueAt(newNV11, Canacc8PaneProvider.STARTUP_MOVE-1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 168 } 169 } 170 171 /** 172 * Update the NV controlling the feedback delay 173 */ 174 protected class UpdateFeedback implements UpdateNV { 175 176 /** {@inheritDoc} */ 177 @Override 178 public void setNewVal(int index) { 179 double delay = feedbackSpinner.getDoubleValue(); 180 int newInt = (int)(delay/FEEDBACK_DELAY_STEP_SIZE); 181 // Note that changing the data model will result in tableChanged() being called, which can manipulate the buttons, etc 182 _dataModel.setValueAt(newInt, index - 1, CbusNodeNVTableDataModel.NV_SELECT_COLUMN); 183 } 184 } 185 186 /** 187 * Construct pane to allow configuration of the module outputs 188 */ 189 private class OutPane extends JPanel { 190 191 int _index; 192 193 protected JRadioButton cont; 194 protected JRadioButton single; 195 protected JRadioButton repeat; 196 protected TitledSpinner pulseSpinner; 197 protected StartupActionPane action; 198 protected boolean buttonFlag = false; 199 200 public OutPane(int index) { 201 super(); 202 _index = index; 203 JPanel gridPane = new JPanel(new GridBagLayout()); 204 GridBagConstraints c = new GridBagConstraints(); 205 c.fill = GridBagConstraints.HORIZONTAL; 206 c.weightx = 1; 207 c.weighty = 1; 208 c.gridx = 0; 209 c.gridy = 0; 210 211 Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); 212 TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("OutputX", _index)); 213 setBorder(title); 214 215 cont = new JRadioButton(Bundle.getMessage("Continuous")); 216 cont.setToolTipText(Bundle.getMessage("ContinuousTt")); 217 single = new JRadioButton(Bundle.getMessage("Single")); 218 single.setToolTipText(Bundle.getMessage("SingleTt")); 219 repeat = new JRadioButton(Bundle.getMessage("Repeat")); 220 repeat.setToolTipText(Bundle.getMessage("RepeatTt")); 221 222 cont.addActionListener((ActionEvent e) -> { 223 typeActionListener(); 224 }); 225 single.addActionListener((ActionEvent e) -> { 226 typeActionListener(); 227 }); 228 repeat.addActionListener((ActionEvent e) -> { 229 typeActionListener(); 230 }); 231 232 ButtonGroup buttons = new ButtonGroup(); 233 buttons.add(cont); 234 buttons.add(single); 235 buttons.add(repeat); 236 237 pulseSpinner = new TitledSpinner(Bundle.getMessage("PulseWidth"), _index, pulseUpdateFn); 238 pulseSpinner.setToolTip(Bundle.getMessage("PulseWidthTt")); 239 pulseSpinner.init(((getSelectValue8(_index) & 0x7f)*PULSE_WIDTH_STEP_SIZE), 0, 240 PULSE_WIDTH_NUM_STEPS*PULSE_WIDTH_STEP_SIZE, PULSE_WIDTH_STEP_SIZE); 241 242 setButtonsInit(getSelectValue8(index)); 243 244 gridPane.add(cont, c); 245 c.gridy++; 246 gridPane.add(single, c); 247 c.gridy++; 248 gridPane.add(repeat, c); 249 c.gridy++; 250 gridPane.add(pulseSpinner, c); 251 252 c.gridx = 1; 253 c.gridy = 0; 254 c.gridheight = 4; 255 action = new StartupActionPane(_index); 256 gridPane.add(action, c); 257 258 add(gridPane); 259 } 260 261 /** 262 * Set Initial pulse type button states to reflect pulse width from initial NV value 263 * 264 * @param pulseWidth 265 */ 266 protected void setButtonsInit(int pulseWidth) { 267 if ((pulseWidth == 0) || (pulseWidth == 128)) { 268 cont.setSelected(true); 269 pulseSpinner.setEnabled(false); 270 } else if (pulseWidth > 128) { 271 repeat.setSelected(true); 272 } else { 273 single.setSelected(true); 274 } 275 } 276 277 /** 278 * Set pulse type button states to reflect new setting from change in 279 * table model (which may result from changes in this gui). 280 * 281 * Changes to table data model from this gui fire a data changed event 282 * back to us so we have a conflict between who is changing the raw 283 * value or who is changing button states, hence the slightly complex 284 * logic. 285 * 286 * @param pulseWidth from the table change event 287 * @param oldPulseWidth from the spinner in this edit gui 288 */ 289 protected void setButtons(int pulseWidth, int oldPulseWidth) { 290 if (buttonFlag == true) { 291 // User clicked a button 292 if (cont.isSelected()) { 293 pulseSpinner.setEnabled(false); 294 } else { 295 pulseSpinner.setEnabled(true); 296 } 297 buttonFlag = false; 298 } else { 299 // Change came from spinner or generic NV pane 300 if (!pulseSpinner.isEnabled()) { 301 // Spinner disabled, change from generic NV pane 302 if ((pulseWidth != 0) && (pulseWidth != 128)) { 303 pulseSpinner.setEnabled(true); 304 if (pulseWidth >= 128) { 305 repeat.setSelected(true); 306 } else { 307 single.setSelected(true); 308 } 309 } else { 310 cont.setSelected(true); 311 } 312 } else { 313 // Spinner enabled so was not continuous 314 if (pulseWidth != oldPulseWidth) { 315 // Change of value in generic NV pane 316 if ((pulseWidth & 0x7F) == 0) { 317 // Continuous 318 cont.setSelected(true); 319 pulseSpinner.setEnabled(false); 320 } else { 321 if (pulseWidth >= 128) { 322 repeat.setSelected(true); 323 } else { 324 single.setSelected(true); 325 } 326 } 327 } else if ((pulseWidth & 0x7F) == 0) { 328 // Change of spinner in this edit pane 329 cont.setSelected(true); 330 pulseSpinner.setEnabled(false); 331 } 332 } 333 } 334 } 335 336 /** 337 * Call the callback to update from radio button selection state. 338 */ 339 protected void typeActionListener() { 340 buttonFlag = true; 341 pulseUpdateFn.setNewVal(_index); 342 } 343 } 344 345 /** 346 * Construct pane to allow configuration of the output startup action 347 */ 348 private class StartupActionPane extends JPanel { 349 350 int _index; 351 352 JRadioButton off; 353 JRadioButton none; 354 JRadioButton saved; 355 356 public StartupActionPane(int index) { 357 super(); 358 _index = index; 359 JPanel gridPane = new JPanel(new GridBagLayout()); 360 GridBagConstraints c = new GridBagConstraints(); 361 c.fill = GridBagConstraints.HORIZONTAL; 362 c.weightx = 1; 363 c.weighty = 1; 364 c.gridx = 0; 365 c.gridy = 0; 366 367 Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); 368 TitledBorder title = BorderFactory.createTitledBorder(border, Bundle.getMessage("StartupAction")); 369 setBorder(title); 370 371 off = new JRadioButton(Bundle.getMessage("Off")); 372 off.setToolTipText(Bundle.getMessage("OffTt")); 373 none = new JRadioButton(Bundle.getMessage("None")); 374 none.setToolTipText(Bundle.getMessage("NoneTt")); 375 saved = new JRadioButton(Bundle.getMessage("SavedAction")); 376 saved.setToolTipText(Bundle.getMessage("SavedActionTt")); 377 378 off.addActionListener((ActionEvent e) -> { 379 startupActionListener(); 380 }); 381 none.addActionListener((ActionEvent e) -> { 382 startupActionListener(); 383 }); 384 saved.addActionListener((ActionEvent e) -> { 385 startupActionListener(); 386 }); 387 388 ButtonGroup buttons = new ButtonGroup(); 389 buttons.add(off); 390 buttons.add(none); 391 buttons.add(saved); 392 setButtons(); 393 // Startup action is in NV10 and NV11, 1 bit per output 394 if ((getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (1<<(_index-1)))>0) { 395 // 1x 396 off.setSelected(true); 397 } else if ((getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) { 398 // 01 399 saved.setSelected(true); 400 } else { 401 // 00 402 none.setSelected(true); 403 } 404 405 gridPane.add(off, c); 406 c.gridy++; 407 gridPane.add(none, c); 408 c.gridy++; 409 gridPane.add(saved, c); 410 411 add(gridPane); 412 } 413 414 /** 415 * Set startup action button states 416 */ 417 public void setButtons() { 418 // Startup action is in NV10 and NV11, 1 bit per output 419 if ((getSelectValue8(Canacc8PaneProvider.STARTUP_POSITION) & (1<<(_index-1)))>0) { 420 // 1x 421 off.setSelected(true); 422 } else if ((getSelectValue8(Canacc8PaneProvider.STARTUP_MOVE) & (1<<(_index-1)))>0) { 423 // 01 424 saved.setSelected(true); 425 } else { 426 // 00 427 none.setSelected(true); 428 } 429 } 430 431 /** 432 * Call the callback to update from radio button selection state. 433 */ 434 protected void startupActionListener() { 435 startupUpdateFn.setNewVal(_index); 436 } 437 } 438 439 private final static Logger log = LoggerFactory.getLogger(Canacc8EditNVPane.class); 440 441}