package org.korsakow.ide.ui.controller; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import org.apache.log4j.Logger; import org.dsrg.soenea.domain.MapperException; import org.korsakow.domain.interf.IImage; import org.korsakow.domain.interf.IMedia; import org.korsakow.domain.interf.ISound; import org.korsakow.domain.interf.ISnu.BackgroundSoundMode; import org.korsakow.domain.mapper.input.ImageInputMapper; import org.korsakow.domain.mapper.input.MediaInputMapper; import org.korsakow.domain.mapper.input.SoundInputMapper; import org.korsakow.ide.Application; import org.korsakow.ide.code.RuleParserException; import org.korsakow.ide.code.k5.K5Code; import org.korsakow.ide.code.k5.K5CodeGenerator2; import org.korsakow.ide.code.k5.K5RuleParser2; import org.korsakow.ide.code.k5.K5Symbol; import org.korsakow.ide.resources.ResourceType; import org.korsakow.ide.resources.media.MediaFactory; import org.korsakow.ide.resources.media.Playable; import org.korsakow.ide.rules.RuleType; import org.korsakow.ide.ui.ResourceEditor; import org.korsakow.ide.ui.components.code.CodeTable; import org.korsakow.ide.ui.components.code.CodeTableModel; import org.korsakow.ide.ui.model.RuleModel; import org.korsakow.ide.ui.resources.SnuResourceView; import org.korsakow.ide.util.UIUtil; public class SnuResourceEditorController { private final class MyWindowFocusListener implements WindowFocusListener { private final CodeTable codeTable; private MyWindowFocusListener(CodeTable codeTable) { this.codeTable = codeTable; } @Override public void windowLostFocus(WindowEvent e) { if (codeTable.getCellEditor() != null) codeTable.getCellEditor().stopCellEditing(); } @Override public void windowGainedFocus(WindowEvent e) { } } private class ValidateDocumentListener implements DocumentListener { public void changedUpdate(DocumentEvent e) { validate(); } public void insertUpdate(DocumentEvent e) { validate(); } public void removeUpdate(DocumentEvent e) { validate(); } } private class ValidateActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { validate(); } } private class UpdateCodeTimeFromMainMediaListener implements ChangeListener { public void stateChanged(ChangeEvent e) { Playable media = resourceView.getMainMediaPanel().getPlayable(); if (media == null) return; if (media.isPlaying()) return; final CodeTable codeTable = resourceView.getCodeTable(); int viewRowIndex = codeTable.getSelectedRow(); if (viewRowIndex != -1) { int viewColIndex = codeTable.getSelectedColumn(); if (viewColIndex == -1 || viewColIndex != codeTable.getColumnIndex(CodeTable.TIME_IDENTIFIER)) viewRowIndex = -1; if (viewRowIndex != -1 && null == codeTable.getValueAt(viewRowIndex, codeTable.getColumnIndex(CodeTable.TIME_IDENTIFIER))) return; } final int modelRowIndex; if (viewRowIndex == -1) return; // modelRowIndex = codeTable.getModel().getEmptyRow(); // else modelRowIndex = codeTable.convertRowIndexToModel(viewRowIndex); long time = media.getTime(); resourceView.getCodeTable().getModel().setTimeAt(time, modelRowIndex); } } private class UpdateVideoFromCodeTableListener implements ListSelectionListener, TableModelListener { private void update() { // sadly you would think the selected row could not be other than -1 if // the row count was 0, but empirical analysis proved otherwise final CodeTable codeTable = resourceView.getCodeTable(); if (codeTable.getRowCount() == 0) return; int viewColIndex = codeTable.getSelectedColumn(); if (viewColIndex == -1 || viewColIndex != codeTable.getColumnIndex(CodeTable.TIME_IDENTIFIER)) return; int row = codeTable.getSelectedRow(); if (row == -1) return; // this was a bug that should not have been if (row >= codeTable.getRowCount()) return; Long time = codeTable.getModel().getTimeAt(codeTable.convertRowIndexToModel(row)); if (time == null) return; Playable media = resourceView.getMainMediaPanel().getPlayable(); if (media == null) return; if (!media.isPlaying()) { media.setTime(time); } } public void valueChanged(ListSelectionEvent e) { update(); } public void tableChanged(TableModelEvent e) { final CodeTable codeTable = resourceView.getCodeTable(); if (e.getType()==TableModelEvent.UPDATE && e.getColumn() == codeTable.getColumnIndex(CodeTable.TIME_IDENTIFIER)) update(); } } private class CodeTableCanonicalFormListener implements TableModelListener { CodeTable codeTable; TableColumnModel columnModel; public CodeTableCanonicalFormListener(CodeTable codeTable, TableColumnModel columnModel) { this.codeTable = codeTable; this.columnModel = columnModel; } public void tableChanged(TableModelEvent e) { if (e.getType()==TableModelEvent.DELETE) return; int column = e.getColumn(); if (column == -1) return; if (codeTable.getColumnIndex(CodeTable.CODE_IDENTIFIER) == e.getColumn()) { // we wish to maintain the K3 rules in canonical form restoreCodeCanonicalForm(); // if you add code to a line with no time, it gets the timecode of the row before. // its coded for the general case, but its intended in particular for editing the last row CodeTableModel model = codeTable.getModel(); int row = e.getFirstRow(); if (model.getTimeAt(row) == null && model.getCodeAt(row).getRawCode().trim().length() > 0) { Long time = null; for (int i = row - 1; i > -1; --i) { if (model.getTimeAt(i) != null) { time = model.getTimeAt(i); break; } } if (time == null) time = 0L; model.setTimeAt(time, row); } } UIUtil.runUITaskLater(new Runnable() { public void run() { // remove empty rows for (int i = 0; i < codeTable.getModel().getRowCount()-1; ++i) { if (codeTable.getModel().getTimeAt(i) == null && (codeTable.getModel().getCodeAt(i) == null || codeTable.getModel().getCodeAt(i).getRawCode().trim().length()==0)) { codeTable.getModel().removeRow(i); --i; // compensate for removal } } } }); // bound to length of media if (codeTable.getColumnIndex(CodeTable.TIME_IDENTIFIER) == e.getColumn()) { final Playable media = resourceView.getMainMediaPanel().getPlayable(); if (media != null) { for (int i = e.getFirstRow(); i <= e.getLastRow() && i < codeTable.getRowCount(); ++i) { Long time = codeTable.getModel().getTimeAt(i); if (time != null && time > media.getDuration()) { final int I = i; codeTable.getModel().setTimeAt(media.getDuration(), I); } } } } } } /** * Reattaches listenrs to the model when it changes * @author d * */ private class CodeTableModelChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent event) { TableModel newModel = (TableModel)event.getNewValue(); newModel.addTableModelListener(codeTableModelListener); } } private class BackgroundSoundChangeListener implements ActionListener { public void actionPerformed( ActionEvent event ) { Long mediaId = resourceView.getMainMediaId(); if ( mediaId == null ) return; IMedia media; try { media = MediaInputMapper.map( mediaId ); } catch (MapperException e) { Logger.getLogger(getClass()).error("", e); return; } // this instanceof check is actually part of the business logic // we only adjust the media length if it is an image if ( !ResourceType.IMAGE.isInstance(media) ) return; IImage image; try { image = ImageInputMapper.map(media.getId()); } catch (MapperException e) { Logger.getLogger(getClass()).error("", e); return; } // this null check is actually part of the business logic // we only adjust the media length if it was not already manually set if ( image.getDuration() != null ) return; if (resourceView.getBackgroundSoundMode() != BackgroundSoundMode.SET) return; Long bgId = resourceView.getBackgroundSoundId(); ISound bg; try { bg = SoundInputMapper.map( bgId ); } catch (MapperException e) { Logger.getLogger(getClass()).error("", e); return; } if ( bg == null ) return; final long duration = MediaFactory.getMediaNoThrow( bg ).getDuration(); resourceView.setMainMediaCustomDuration( duration ); Application.getInstance().showOneTimeAlertDialog( "AdjustSnuDurationFromBackgroundSound", (Component)event.getSource(), "", "The SNU's duration has been adjusted to match the selected background sound.\nThis happened because the SNU's media is an image and you have not previously adjusted its duration."); } } private final ResourceEditor editor; private final SnuResourceView resourceView; private CodeTableCanonicalFormListener codeTableModelListener; /** * @param resourceId null for inserts, non-null for updates */ public SnuResourceEditorController(ResourceEditor editor, Long resourceId) { this.editor = editor; resourceView = (SnuResourceView)editor.getResourceView(); final CodeTable codeTable = resourceView.getCodeTable(); codeTable.addPropertyChangeListener("model", new CodeTableModelChangeListener()); codeTable.getModel().addTableModelListener(codeTableModelListener = new CodeTableCanonicalFormListener(codeTable, codeTable.getColumnModel())); UpdateVideoFromCodeTableListener updateVideoFromCodeTableListener = new UpdateVideoFromCodeTableListener(); codeTable.getSelectionModel().addListSelectionListener(updateVideoFromCodeTableListener); codeTable.getModel().addTableModelListener(updateVideoFromCodeTableListener); editor.addWindowFocusListener(new MyWindowFocusListener(codeTable)); resourceView.getMainMediaPanel().addSeekSliderChangeListener(new UpdateCodeTimeFromMainMediaListener()); resourceView.getKeywordArea().getDocument().addDocumentListener(new ValidateDocumentListener()); resourceView.getStarerCheck().addActionListener(new ValidateActionListener()); resourceView.getEnderCheck().addActionListener(new ValidateActionListener()); // resourceView.addEventActionListener(new ShowEventEditorAction(editor, resourceView)); resourceView.addBackgroundSoundChangeListener( new BackgroundSoundChangeListener() ); validate(); } public boolean restoreCodeCanonicalForm() { K5RuleParser2 parser = new K5RuleParser2(); K5CodeGenerator2 generator = new K5CodeGenerator2(); CodeTableModel codeModel = resourceView.getCodeTable().getModel(); boolean valid = true; for (int i = 0; i < codeModel.getRowCount(); ++i) { K5Code code = codeModel.getCodeAt(i); try { String rawCode = code.getRawCode(); if (i == 0) rawCode = generator.createClearPreviousLinks() + K5Symbol.DEFAULT_STATEMENT_SEPARATOR_STRING + " " + rawCode; else rawCode = generator.createKeepPreviousLinks() + K5Symbol.DEFAULT_STATEMENT_SEPARATOR_STRING + " " + rawCode; List<RuleModel> rules = parser.parse(rawCode); boolean haveClearScores = false; List<RuleModel> toRemove = new ArrayList<RuleModel>(); for (RuleModel rule : rules) if (rule.getType() == RuleType.ClearScores) { haveClearScores = true; toRemove.add(rule); } if (i == 0) rules.removeAll(toRemove); K5Code canonicalCode = generator.createK5CodeOmitUnsupported(rules, i==0); String canonicalRaw = canonicalCode.getRawCode(); if (i==0) { if (!haveClearScores) canonicalRaw = generator.createKeepPreviousLinks() + K5Symbol.DEFAULT_STATEMENT_SEPARATOR_STRING + " " + canonicalRaw; } code.setRawCode(canonicalRaw); code.setValid(true); } catch (RuleParserException e) { e.printStackTrace(); valid = false; code.setValid(false); break; } } resourceView.getCodeTable().repaint(); return valid; } public void validate() { try { validateMainMedia(); validateKeywords(); editor.getOKButton().setEnabled(true); if (_validate_wasPressed) { // this is a hack. on focus lost we set to disabled which cancels the button press // when clicking the ok button editor.getOKButton().getModel().setRollover(true); editor.getOKButton().getModel().setArmed(true); editor.getOKButton().getModel().setPressed(true); } _validate_wasPressed = false; resourceView.setStatusText(""); if (!restoreCodeCanonicalForm()) throw new ValidationException("Invalid rule"); } catch (ValidationException e) { _validate_wasPressed = editor.getOKButton().getModel().isPressed(); editor.getOKButton().setEnabled(false); resourceView.setStatusText(e.getMessage()); } } private boolean _validate_wasPressed = false; public void validateKeywords() throws ValidationException { if (resourceView.getStarter() || resourceView.getEnder()) return; Collection<String> tokens = resourceView.getKeywordArea().getTokens(); if (tokens.isEmpty()) { _validate_wasPressed = editor.getOKButton().getModel().isPressed(); throw new ValidationException("SNU must have keywords unless its a startfilm or endfilm"); } } private void validateMainMedia() throws ValidationException { if (resourceView.getMainMediaId() == null) throw new ValidationException("SNU must have a media"); } private static class ValidationException extends Exception { public ValidationException(String message) { super(message); } } }