From: Daniel Kirschten Date: Mon, 30 Sep 2019 16:34:57 +0000 (+0200) Subject: Made SimulationView(Editor) a View again X-Git-Url: https://mograsim.net/gitweb/?a=commitdiff_plain;h=9ab92f6f3ac3dacda4b9dcf2d80b08c263905682;p=Mograsim.git Made SimulationView(Editor) a View again --- diff --git a/plugins/net.mograsim.plugin.core/META-INF/MANIFEST.MF b/plugins/net.mograsim.plugin.core/META-INF/MANIFEST.MF index d12f07c6..a5add49c 100644 --- a/plugins/net.mograsim.plugin.core/META-INF/MANIFEST.MF +++ b/plugins/net.mograsim.plugin.core/META-INF/MANIFEST.MF @@ -15,6 +15,7 @@ Export-Package: net.mograsim.plugin;uses:="org.eclipse.ui.themes,org.eclipse.swt net.mograsim.plugin.tables.memory, net.mograsim.plugin.tables.mi, net.mograsim.plugin.util, + net.mograsim.plugin.views, net.mograsim.plugin.wizards.newWizards Require-Bundle: org.eclipse.core.runtime, org.eclipse.ui, diff --git a/plugins/net.mograsim.plugin.core/plugin.xml b/plugins/net.mograsim.plugin.core/plugin.xml index 7d7e5e14..bb298a2b 100644 --- a/plugins/net.mograsim.plugin.core/plugin.xml +++ b/plugins/net.mograsim.plugin.core/plugin.xml @@ -95,13 +95,6 @@ id="net.mograsim.plugin.tables.mi.InstructionView"> - - - + + recreateContextDependentControls(); - memCellListener = a -> instPreview.refresh(); - clockObserver = o -> - { - if (((CoreClock) o).isOn()) - { - exec.pauseLiveExecution(); - if (!pauseButton.isDisposed()) - Display.getDefault().asyncExec(() -> - { - if (!pauseButton.isDisposed()) - pauseButton.setSelection(false); - setPauseText(pauseButton, false); - }); - } - }; - } - - @Override - public void createPartControl(Composite parent) - { - this.parent = parent; - // initialize UI - parent.setLayout(new GridLayout()); - - noMachineLabel = new Label(parent, SWT.NONE); - noMachineLabel.setText("No machine present...");// TODO internationalize? - addSimulationControlWidgets(parent); - canvasParent = new Composite(parent, SWT.NONE); - canvasParent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); - canvasParent.setLayout(new FillLayout()); - addInstructionPreviewControlWidgets(parent); - recreateContextDependentControls(); - } - - private void recreateContextDependentControls() - { - if (parent == null) - // createPartControls has not been called yet - return; - - double offX; - double offY; - double zoom; - stopExecAndDeregisterContextDependentListeners(); - if (canvas != null) - { - offX = canvas.getOffX(); - offY = canvas.getOffY(); - zoom = canvas.getZoom(); - canvas.dispose(); - } else - { - offX = 0; - offY = 0; - zoom = -1; - } - - Optional machineOptional; - if (context != null && (machineOptional = context.getActiveMachine()).isPresent()) - { - noMachineLabel.setVisible(false); - resetButton.setEnabled(true); - sbseButton.setEnabled(true); - pauseButton.setEnabled(true); - simSpeedScale.setEnabled(true); - simSpeedInput.setEnabled(true); - - machine = machineOptional.get(); - canvas = new LogicUICanvas(canvasParent, SWT.NONE, machine.getModel()); - canvas.addListener(SWT.MouseDown, e -> canvas.setFocus()); - ZoomableCanvasUserInput userInput = new ZoomableCanvasUserInput(canvas); - userInput.buttonDrag = Preferences.current().getInt("net.mograsim.logic.model.button.drag"); - userInput.buttonZoom = Preferences.current().getInt("net.mograsim.logic.model.button.zoom"); - userInput.enableUserInput(); - if (zoom > 0) - { - canvas.moveTo(offX, offY, zoom); - canvas.commitTransform(); - } - - AssignableMicroInstructionMemory mIMemory = machine.getMicroInstructionMemory(); - instPreview.bindMicroInstructionMemory(mIMemory); - mIMemory.registerCellModifiedListener(memCellListener); - - canvasParent.layout(); - - // update preview - ((ActiveInstructionPreviewContentProvider) instPreview.getTableViewer().getContentProvider()).setMachine(machine); - - // initialize executer - exec = new LogicExecuter(machine.getTimeline()); - updateSpeedFactorFromInput(simSpeedInput.getValue()); - updatePausedState(); - exec.startLiveExecution(); - } else - { - noMachineLabel.setVisible(true); - resetButton.setEnabled(false); - sbseButton.setEnabled(false); - pauseButton.setEnabled(false); - simSpeedScale.setEnabled(false); - simSpeedInput.setEnabled(false); - } - } - - private void stopExecAndDeregisterContextDependentListeners() - { - if (exec != null) - exec.stopLiveExecution(); - if (machine != null) - { - machine.getMicroInstructionMemory().deregisterCellModifiedListener(memCellListener); - machine.getClock().deregisterObserver(clockObserver); - } - } - - private void addSimulationControlWidgets(Composite parent) - { - Composite c = new Composite(parent, SWT.NONE); - c.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); - c.setLayout(new GridLayout(7, false)); - - resetButton = new Button(c, SWT.PUSH); - resetButton.setText("Reset machine"); - resetButton.addListener(SWT.Selection, e -> context.getActiveMachine().get().reset()); - - // TODO do we want this button in the final product? - Button reloadMachineButton = new Button(c, SWT.PUSH); - reloadMachineButton.setText("Reload machine"); - reloadMachineButton.addListener(SWT.Selection, e -> - { - IndirectModelComponentCreator.clearComponentCache(); - context.setActiveMachine(context.getMachineDefinition().get().createNew()); - }); - - sbseButton = new Button(c, SWT.CHECK); - pauseButton = new Button(c, SWT.TOGGLE); - - sbseButton.setText("Step by step execution"); - sbseButton.addListener(SWT.Selection, e -> - { - CoreClock cl = machine.getClock(); - if (sbseButton.getSelection()) - cl.registerObserver(clockObserver); - else - cl.deregisterObserver(clockObserver); - }); - sbseButton.setSelection(false); - - pauseButton.setSelection(true); - setPauseText(pauseButton, false); - - pauseButton.addListener(SWT.Selection, e -> updatePausedState()); - pauseButton.addMouseTrackListener(new MouseTrackListener() - { - @Override - public void mouseHover(MouseEvent e) - { - // nothing - } - - @Override - public void mouseExit(MouseEvent e) - { - setPauseText(pauseButton, false); - } - - @Override - public void mouseEnter(MouseEvent e) - { - setPauseText(pauseButton, true); - } - }); - - new Label(c, SWT.NONE).setText("Simulation Speed: "); - - simSpeedScale = new Scale(c, SWT.NONE); - simSpeedScale.setMinimum(0); - simSpeedScale.setMaximum(SIM_SPEED_SCALE_STEPS); - simSpeedScale.setIncrement(1); - simSpeedScale.setSelection(0); - simSpeedScale.addListener(SWT.Selection, e -> updateSpeedFactorFromScale()); - - simSpeedInput = new DoubleInput(c, SWT.NONE); - simSpeedInput.setPrecision(Preferences.current().getInt("net.mograsim.plugin.core.simspeedprecision")); - simSpeedInput.addChangeListener(this::updateSpeedFactorFromInput); - - updateSpeedFactorFromScale(); - - c.layout(); - } - - private void updatePausedState() - { - setPauseText(pauseButton, false); - if (exec != null) - if (pauseButton.getSelection()) - exec.unpauseLiveExecution(); - else - exec.pauseLiveExecution(); - } - - private void updateSpeedFactorFromScale() - { - double factor = Math.pow(SIM_SPEED_SCALE_STEP_FACTOR, simSpeedScale.getSelection() - SIM_SPEED_SCALE_STEPS); - simSpeedInput.setValue(factor); - if (exec != null) - exec.setSpeedFactor(factor); - } - - private void updateSpeedFactorFromInput(double factor) - { - double factorCheckedFor0; - if (factor != 0) - factorCheckedFor0 = factor; - else - { - factorCheckedFor0 = Math.pow(10, -simSpeedInput.getPrecision()); - simSpeedInput.setValue(factorCheckedFor0); - } - int closestScalePos = (int) Math.round(Math.log(factorCheckedFor0) / SIM_SPEED_SCALE_STEP_FACTOR_LOG + SIM_SPEED_SCALE_STEPS); - simSpeedScale.setSelection(Math.min(Math.max(closestScalePos, 0), SIM_SPEED_SCALE_STEPS)); - if (exec != null) - exec.setSpeedFactor(factorCheckedFor0); - } - - private void addInstructionPreviewControlWidgets(Composite parent) - { - instPreview = new InstructionTable(parent, new DisplaySettings(), getSite().getWorkbenchWindow().getWorkbench().getThemeManager()); - instPreview.getTableViewer().getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); - ActiveInstructionPreviewContentProvider cProv; - instPreview.setContentProvider(cProv = new ActiveInstructionPreviewContentProvider(instPreview.getTableViewer())); - cProv.setMachine(machine); - } - - private static void setPauseText(Button pauseButton, boolean hovered) - { - if (hovered) - if (pauseButton.getSelection()) - pauseButton.setText("Pause?"); - else - pauseButton.setText("Resume?"); - else if (pauseButton.getSelection()) - pauseButton.setText("Running"); - else - pauseButton.setText("Paused"); - } - - @Override - public void init(IEditorSite site, IEditorInput input) throws PartInitException - { - if (input instanceof IFileEditorInput) - { - IFileEditorInput fileInput = (IFileEditorInput) input; - context = ProjectMachineContext.getMachineContextOf(fileInput.getFile().getProject()); - context.activateMachine(); - context.addActiveMachineListener(activeMachineListener); - recreateContextDependentControls(); - - setPartName(fileInput.getName()); - open(fileInput.getFile()); - } else - throw new IllegalArgumentException("SimulationViewEditor can only be used with Files"); - - setSite(site); - setInput(input); - } - - @Override - public void doSave(IProgressMonitor monitor) - { - IEditorInput input = getEditorInput(); - if (input instanceof IFileEditorInput) - SafeRunnable.getRunner().run(() -> save(((IFileEditorInput) input).getFile(), monitor)); - else - throw new IllegalArgumentException("SimulationViewEditor can only be used with Files"); - } - - private void save(IFile file, IProgressMonitor monitor) throws CoreException - { - file.setContents(new ByteArrayInputStream("actual contents will go here".getBytes()), 0, monitor); - } - - private void open(IFile file) - { - // do nothing yet - } - - @Override - public void doSaveAs() - { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isDirty() - { - return false; - } - - @Override - public boolean isSaveAsAllowed() - { - return false; - } - - @Override - public void setFocus() - { - canvas.setFocus(); - } - - @Override - public void dispose() - { - stopExecAndDeregisterContextDependentListeners(); - context.removeActiveMachineListener(activeMachineListener); - super.dispose(); - } -} \ No newline at end of file diff --git a/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/util/OverlappingFillLayout.java b/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/util/OverlappingFillLayout.java new file mode 100644 index 00000000..839ba71d --- /dev/null +++ b/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/util/OverlappingFillLayout.java @@ -0,0 +1,37 @@ +package net.mograsim.plugin.util; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Layout; + +public class OverlappingFillLayout extends Layout +{ + @Override + protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) + { + Point size = new Point(wHint == SWT.DEFAULT ? 0 : wHint, hHint == SWT.DEFAULT ? 0 : hHint); + + Control[] children = composite.getChildren(); + for (Control child : children) + { + Point childSize = child.computeSize(wHint, hHint, flushCache); + size.x = Math.max(size.x, childSize.x); + size.y = Math.max(size.y, childSize.y); + } + + return size; + } + + @Override + protected void layout(Composite composite, boolean flushCache) + { + Rectangle bounds = composite.getClientArea(); + + Control[] children = composite.getChildren(); + for (Control child : children) + child.setBounds(bounds); + } +} \ No newline at end of file diff --git a/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/views/SimulationView.java b/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/views/SimulationView.java new file mode 100644 index 00000000..4d118a9f --- /dev/null +++ b/plugins/net.mograsim.plugin.core/src/net/mograsim/plugin/views/SimulationView.java @@ -0,0 +1,302 @@ +package net.mograsim.plugin.views; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +import org.eclipse.core.runtime.SafeRunner; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.contexts.IDebugContextListener; +import org.eclipse.debug.ui.contexts.IDebugContextManager; +import org.eclipse.debug.ui.contexts.IDebugContextService; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Scale; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; + +import net.haspamelodica.swt.helper.input.DoubleInput; +import net.haspamelodica.swt.helper.zoomablecanvas.helper.ZoomableCanvasUserInput; +import net.mograsim.logic.core.LogicObserver; +import net.mograsim.logic.core.components.CoreClock; +import net.mograsim.logic.model.LogicUICanvas; +import net.mograsim.machine.Machine; +import net.mograsim.machine.Memory.MemoryCellModifiedListener; +import net.mograsim.machine.mi.AssignableMicroInstructionMemory; +import net.mograsim.plugin.launch.MachineDebugTarget; +import net.mograsim.plugin.tables.DisplaySettings; +import net.mograsim.plugin.tables.mi.ActiveInstructionPreviewContentProvider; +import net.mograsim.plugin.tables.mi.InstructionTable; +import net.mograsim.plugin.util.OverlappingFillLayout; +import net.mograsim.preferences.Preferences; + +public class SimulationView extends ViewPart +{ + private static final int SIM_SPEED_SCALE_STEPS = 50; + private static final double SIM_SPEED_SCALE_STEP_FACTOR = 1.32; + private static final double SIM_SPEED_SCALE_STEP_FACTOR_LOG = Math.log(SIM_SPEED_SCALE_STEP_FACTOR); + + private final Set controlsToDisableWhenNoMachinePresent; + private Scale simSpeedScale; + private DoubleInput simSpeedInput; + private Composite contextDependentControlsParent; + private Composite canvasParent; + private InstructionTable instPreview; + private ActiveInstructionPreviewContentProvider contentProvider; + private Label noRunningMachineLabel; + + private MachineDebugTarget debugTarget; + private LogicUICanvas canvas; + + private final MemoryCellModifiedListener memCellListener; + private final LogicObserver clockObserver; + private final IDebugContextListener debugContextListener; + private final Consumer executionSpeedListener; + + public SimulationView() + { + controlsToDisableWhenNoMachinePresent = new HashSet<>(); + memCellListener = a -> instPreview.refresh(); + // TODO could this be a breakpoint? + clockObserver = o -> + { + if (((CoreClock) o).isOn()) + SafeRunner.run(() -> debugTarget.suspend()); + }; + debugContextListener = e -> debugContextChanged(e.getContext()); + executionSpeedListener = this::speedFactorChanged; + } + + @Override + public void createPartControl(Composite parent) + { + // initialize UI + parent.setLayout(new GridLayout()); + + addSimulationControlWidgets(parent); + + Composite contextDependentControlsParentParent = new Composite(parent, SWT.NONE); + contextDependentControlsParentParent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + contextDependentControlsParentParent.setLayout(new OverlappingFillLayout()); + + noRunningMachineLabel = new Label(contextDependentControlsParentParent, SWT.NONE); + noRunningMachineLabel.setText("No machine running && selected in the Debug view..."); + + contextDependentControlsParent = new Composite(contextDependentControlsParentParent, SWT.NONE); + GridLayout contexDependentControlsLayout = new GridLayout(); + contexDependentControlsLayout.marginWidth = 0; + contexDependentControlsLayout.marginHeight = 0; + contextDependentControlsParent.setLayout(contexDependentControlsLayout); + + canvasParent = new Composite(contextDependentControlsParent, SWT.NONE); + canvasParent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + canvasParent.setLayout(new FillLayout()); + + addInstructionPreviewControlWidgets(contextDependentControlsParent); + + IDebugContextManager debugCManager = DebugUITools.getDebugContextManager(); + IDebugContextService contextService = debugCManager.getContextService(PlatformUI.getWorkbench().getActiveWorkbenchWindow()); + contextService.addDebugContextListener(debugContextListener); + debugContextChanged(contextService.getActiveContext()); + } + + private void addSimulationControlWidgets(Composite parent) + { + Composite c = new Composite(parent, SWT.NONE); + c.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + c.setLayout(new GridLayout(7, false)); + + Button sbseButton = new Button(c, SWT.CHECK); + controlsToDisableWhenNoMachinePresent.add(sbseButton); + + sbseButton.setText("Step by step execution"); + sbseButton.addListener(SWT.Selection, e -> + { + CoreClock cl = debugTarget.getMachine().getClock(); + if (sbseButton.getSelection()) + cl.registerObserver(clockObserver); + else + cl.deregisterObserver(clockObserver); + }); + sbseButton.setSelection(false); + + Label simSpeedLabel = new Label(c, SWT.NONE); + controlsToDisableWhenNoMachinePresent.add(simSpeedLabel); + simSpeedLabel.setText("Simulation Speed: "); + + simSpeedScale = new Scale(c, SWT.NONE); + controlsToDisableWhenNoMachinePresent.add(simSpeedScale); + simSpeedScale.setMinimum(0); + simSpeedScale.setMaximum(SIM_SPEED_SCALE_STEPS); + simSpeedScale.setIncrement(1); + simSpeedScale.setSelection(0); + simSpeedScale.addListener(SWT.Selection, e -> + { + double speed = Math.pow(SIM_SPEED_SCALE_STEP_FACTOR, simSpeedScale.getSelection() - SIM_SPEED_SCALE_STEPS); + debugTarget.setExecutionSpeed(speed); + }); + + simSpeedInput = new DoubleInput(c, SWT.NONE); + controlsToDisableWhenNoMachinePresent.add(simSpeedInput); + simSpeedInput.setPrecision(Preferences.current().getInt("net.mograsim.plugin.core.simspeedprecision")); + simSpeedInput.addChangeListener(speed -> + { + if (speed != 0) + debugTarget.setExecutionSpeed(speed); + else + debugTarget.setExecutionSpeed(Math.pow(10, -simSpeedInput.getPrecision())); + }); + + c.layout(); + } + + private void speedFactorChanged(double speed) + { + simSpeedInput.setValue(speed); + int closestScalePos = (int) Math.round(Math.log(speed) / SIM_SPEED_SCALE_STEP_FACTOR_LOG + SIM_SPEED_SCALE_STEPS); + simSpeedScale.setSelection(Math.min(Math.max(closestScalePos, 0), SIM_SPEED_SCALE_STEPS)); + } + + private void addInstructionPreviewControlWidgets(Composite parent) + { + instPreview = new InstructionTable(parent, new DisplaySettings(), getSite().getWorkbenchWindow().getWorkbench().getThemeManager()); + instPreview.getTableViewer().getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + contentProvider = new ActiveInstructionPreviewContentProvider(instPreview.getTableViewer()); + instPreview.setContentProvider(contentProvider); + } + + private void debugContextChanged(ISelection selection) + { + if (selection != null && selection instanceof TreeSelection) + { + TreeSelection treeSelection = (TreeSelection) selection; + Object[] selectedElements = treeSelection.toArray(); + for (Object selectedElement : selectedElements) + { + MachineDebugTarget debugTarget; + if (selectedElement instanceof MachineDebugTarget) + debugTarget = (MachineDebugTarget) selectedElement; + else if (selectedElement instanceof ILaunch) + { + ILaunch launch = (ILaunch) selectedElement; + IDebugTarget genericDebugTarget = launch.getDebugTarget(); + if (genericDebugTarget instanceof MachineDebugTarget) + debugTarget = (MachineDebugTarget) genericDebugTarget; + else + continue; + } else + continue; + if (debugTarget.isTerminated()) + continue; + // we found a selected MachineDebugTarget + if (this.debugTarget != debugTarget) + bindToDebugTarget(debugTarget); + return; + } + } + // we didn't find a selected MachineDebugTarget + // call binToDebugTarget even if this.debugTarget==null + bindToDebugTarget(null); + } + + private void bindToDebugTarget(MachineDebugTarget debugTarget) + { + this.debugTarget = debugTarget; + + if (canvasParent == null) + // createPartControls has not been called yet + return; + + double offX; + double offY; + double zoom; + deregisterMachineDependentListeners(); + if (canvas != null) + { + offX = canvas.getOffX(); + offY = canvas.getOffY(); + zoom = canvas.getZoom(); + canvas.dispose(); + } else + { + offX = 0; + offY = 0; + zoom = -1; + } + + if (debugTarget != null) + { + noRunningMachineLabel.setVisible(false); + contextDependentControlsParent.setVisible(true); + controlsToDisableWhenNoMachinePresent.forEach(c -> c.setEnabled(true)); + + Machine machine = debugTarget.getMachine(); + + canvas = new LogicUICanvas(canvasParent, SWT.NONE, machine.getModel()); + canvas.addListener(SWT.MouseDown, e -> canvas.setFocus()); + ZoomableCanvasUserInput userInput = new ZoomableCanvasUserInput(canvas); + userInput.buttonDrag = Preferences.current().getInt("net.mograsim.logic.model.button.drag"); + userInput.buttonZoom = Preferences.current().getInt("net.mograsim.logic.model.button.zoom"); + userInput.enableUserInput(); + if (zoom > 0) + { + canvas.moveTo(offX, offY, zoom); + canvas.commitTransform(); + } + + AssignableMicroInstructionMemory mIMemory = machine.getMicroInstructionMemory(); + instPreview.bindMicroInstructionMemory(mIMemory); + mIMemory.registerCellModifiedListener(memCellListener); + + canvasParent.layout(); + + // update preview + contentProvider.setMachine(machine); + + // initialize executer + debugTarget.addExecutionSpeedListener(executionSpeedListener); + speedFactorChanged(debugTarget.getExecutionSpeed()); + } else + { + noRunningMachineLabel.setVisible(true); + contextDependentControlsParent.setVisible(false); + controlsToDisableWhenNoMachinePresent.forEach(c -> c.setEnabled(false)); + contentProvider.setMachine(null); + } + } + + private void deregisterMachineDependentListeners() + { + if (debugTarget != null) + { + debugTarget.removeExecutionSpeedListener(executionSpeedListener); + debugTarget.getMachine().getMicroInstructionMemory().deregisterCellModifiedListener(memCellListener); + debugTarget.getMachine().getClock().deregisterObserver(clockObserver); + } + } + + @Override + public void setFocus() + { + if (canvas != null && !canvas.isDisposed()) + canvas.setFocus(); + } + + @Override + public void dispose() + { + deregisterMachineDependentListeners(); + DebugUITools.getDebugContextManager().removeDebugContextListener(debugContextListener); + super.dispose(); + } +} \ No newline at end of file