diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df246f58..830a37c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added - Configurable tab size (formatting must be set to use tabs) - default matches indent size of 3 +- [#2100] Copy/paste frames (same SWF only) ### Fixed - [#2021], [#2000] Caret position in editors when using tabs and / or unicode @@ -3260,6 +3261,7 @@ Major version of SWF to XML export changed to 2. [alpha 9]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha8...alpha9 [alpha 8]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha7...alpha8 [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 +[#2100]: https://www.free-decompiler.com/flash/issues/2100 [#2021]: https://www.free-decompiler.com/flash/issues/2021 [#2000]: https://www.free-decompiler.com/flash/issues/2000 [#2116]: https://www.free-decompiler.com/flash/issues/2116 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java index b2d65e7b8..649ea0893 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -4574,4 +4574,9 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { } return uninitializedAs2ClassTraits; } + + @Override + public SWF getSwf() { + return this; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java index 30a2eb679..00253c758 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java @@ -261,7 +261,7 @@ public class DefineButton2Tag extends ButtonTag implements ASMSourceContainer { if (swf.getCyclicCharacters().contains(r.characterId)) { continue; } - DepthState layer = new DepthState(swf, null); + DepthState layer = new DepthState(swf, null, null); layer.colorTransForm = r.colorTransform; layer.blendMode = r.blendMode; layer.filters = r.filterList; @@ -272,16 +272,16 @@ public class DefineButton2Tag extends ButtonTag implements ASMSourceContainer { } if (r.buttonStateUp) { - frameUp.layers.put(r.placeDepth, new DepthState(layer, frameUp, false)); + frameUp.layers.put(r.placeDepth, new DepthState(layer, frameUp, frameUp, false)); } if (r.buttonStateDown) { - frameDown.layers.put(r.placeDepth, new DepthState(layer, frameDown, false)); + frameDown.layers.put(r.placeDepth, new DepthState(layer, frameDown, frameDown, false)); } if (r.buttonStateOver) { - frameOver.layers.put(r.placeDepth, new DepthState(layer, frameOver, false)); + frameOver.layers.put(r.placeDepth, new DepthState(layer, frameOver, frameOver, false)); } if (r.buttonStateHitTest) { - frameHit.layers.put(r.placeDepth, new DepthState(layer, frameHit, false)); + frameHit.layers.put(r.placeDepth, new DepthState(layer, frameHit, frameHit, false)); } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java index e077871d9..19b263a57 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java @@ -251,7 +251,7 @@ public class DefineButtonTag extends ButtonTag implements ASMSourceContainer { if (swf.getCyclicCharacters().contains(r.characterId)) { continue; } - DepthState layer = new DepthState(swf, null); + DepthState layer = new DepthState(swf, null, null); layer.colorTransForm = clrTrans; layer.blendMode = r.blendMode; layer.filters = r.filterList; @@ -262,16 +262,16 @@ public class DefineButtonTag extends ButtonTag implements ASMSourceContainer { } if (r.buttonStateUp) { - frameUp.layers.put(r.placeDepth, new DepthState(layer, frameUp, false)); + frameUp.layers.put(r.placeDepth, new DepthState(layer, frameUp, frameUp, false)); } if (r.buttonStateDown) { - frameDown.layers.put(r.placeDepth, new DepthState(layer, frameDown, false)); + frameDown.layers.put(r.placeDepth, new DepthState(layer, frameDown, frameDown, false)); } if (r.buttonStateOver) { - frameOver.layers.put(r.placeDepth, new DepthState(layer, frameOver, false)); + frameOver.layers.put(r.placeDepth, new DepthState(layer, frameOver, frameOver, false)); } if (r.buttonStateHitTest) { - frameHit.layers.put(r.placeDepth, new DepthState(layer, frameHit, false)); + frameHit.layers.put(r.placeDepth, new DepthState(layer, frameHit, frameHit, false)); } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/DepthState.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/DepthState.java index 011fef44f..0ec2fea04 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/DepthState.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/DepthState.java @@ -31,7 +31,9 @@ import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RGBA; import com.jpexs.decompiler.flash.types.filters.FILTER; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; /** @@ -77,6 +79,8 @@ public class DepthState { private final SWF swf; public Frame frame; + + public Frame placeFrame; public PlaceObjectTypeTag placeObjectTag; @@ -94,14 +98,16 @@ public class DepthState { return lastInstanceId.addAndGet(1); } - public DepthState(SWF swf, Frame frame) { + public DepthState(SWF swf, Frame frame, Frame placeFrame) { this.swf = swf; this.frame = frame; + this.placeFrame = placeFrame; this.instanceId = getNewInstanceId(); } - public DepthState(DepthState obj, Frame frame, boolean sameInstance) { + public DepthState(DepthState obj, Frame frame, Frame placeFrame, boolean sameInstance) { this.frame = frame; + this.placeFrame = placeFrame; swf = obj.swf; characterId = obj.characterId; matrix = obj.matrix; @@ -166,4 +172,87 @@ public class DepthState { return swf.getCharacter(characterId); } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + this.depth; + hash = 29 * hash + this.characterId; + hash = 29 * hash + Objects.hashCode(this.matrix); + hash = 29 * hash + Objects.hashCode(this.instanceName); + hash = 29 * hash + Objects.hashCode(this.className); + hash = 29 * hash + Objects.hashCode(this.colorTransForm); + hash = 29 * hash + (this.cacheAsBitmap ? 1 : 0); + hash = 29 * hash + this.blendMode; + hash = 29 * hash + Objects.hashCode(this.filters); + hash = 29 * hash + (this.isVisible ? 1 : 0); + hash = 29 * hash + Objects.hashCode(this.backGroundColor); + hash = 29 * hash + Objects.hashCode(this.clipActions); + hash = 29 * hash + Arrays.hashCode(this.amfData); + hash = 29 * hash + this.ratio; + hash = 29 * hash + this.clipDepth; + hash = 29 * hash + this.time; + hash = 29 * hash + (this.hasImage ? 1 : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DepthState other = (DepthState) obj; + if (this.depth != other.depth) { + return false; + } + if (this.characterId != other.characterId) { + return false; + } + if (this.cacheAsBitmap != other.cacheAsBitmap) { + return false; + } + if (this.blendMode != other.blendMode) { + return false; + } + if (this.isVisible != other.isVisible) { + return false; + } + if (this.ratio != other.ratio) { + return false; + } + if (this.clipDepth != other.clipDepth) { + return false; + } + if (this.hasImage != other.hasImage) { + return false; + } + if (!Objects.equals(this.instanceName, other.instanceName)) { + return false; + } + if (!Objects.equals(this.className, other.className)) { + return false; + } + if (!Objects.equals(this.matrix, other.matrix)) { + return false; + } + if (!Objects.equals(this.colorTransForm, other.colorTransForm)) { + return false; + } + if (!Objects.equals(this.filters, other.filters)) { + return false; + } + if (!Objects.equals(this.backGroundColor, other.backGroundColor)) { + return false; + } + if (!Objects.equals(this.clipActions, other.clipActions)) { + return false; + } + return Arrays.equals(this.amfData, other.amfData); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Frame.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Frame.java index 187070758..2d3caa3fa 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Frame.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Frame.java @@ -41,6 +41,9 @@ import java.util.TreeMap; */ public class Frame implements TreeItem, Exportable { + /** + * Zero based frame index + */ public final int frame; public TreeMap layers = new TreeMap<>(); @@ -67,9 +70,9 @@ public class Frame implements TreeItem, Exportable { public boolean layersChanged; - public String label = null; + public List labels = new ArrayList<>(); - public boolean namedAnchor = false; + public List namedAnchors = new ArrayList<>(); public Frame(Timeline timeline, int frame) { this.timeline = timeline; @@ -82,7 +85,7 @@ public class Frame implements TreeItem, Exportable { backgroundColor = obj.backgroundColor; timeline = obj.timeline; for (int depth : obj.layers.keySet()) { - layers.put(depth, new DepthState(obj.layers.get(depth), this, true)); + layers.put(depth, new DepthState(obj.layers.get(depth), this, obj.layers.get(depth).placeFrame, true)); } //Do not copy sounds } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java index be6a56622..57c538fb3 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timeline.java @@ -144,6 +144,11 @@ public class Timeline { return frames; } + /** + * + * @param index 0-based frame index + * @return + */ public synchronized Frame getFrame(int index) { ensureInitialized(); if (index >= frames.size()) { @@ -296,9 +301,10 @@ public class Timeline { } if (t instanceof FrameLabelTag) { - frame.label = ((FrameLabelTag) t).getLabelName(); - frame.namedAnchor = ((FrameLabelTag) t).isNamedAnchor(); - labelToFrame.put(frame.label, frames.size()); + String labelName = ((FrameLabelTag) t).getLabelName(); + frame.labels.add(labelName); + frame.namedAnchors.add(((FrameLabelTag) t).isNamedAnchor()); + labelToFrame.put(labelName, frames.size()); } else if (t instanceof StartSoundTag) { frame.sounds.add(((StartSoundTag) t).soundId); frame.soundClasses.add(null); @@ -314,11 +320,12 @@ public class Timeline { int depth = po.getDepth(); DepthState fl = frame.layers.get(depth); if (fl == null) { - frame.layers.put(depth, fl = new DepthState(swf, frame)); + frame.layers.put(depth, fl = new DepthState(swf, frame, frame)); fl.depth = depth; } frame.layersChanged = true; fl.placeObjectTag = po; + fl.placeFrame = frame; fl.minPlaceObjectNum = Math.max(fl.minPlaceObjectNum, po.getPlaceObjectNum()); boolean wasEmpty = fl.characterId == -1 && fl.className == null; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java index 954f6b12a..dd2789eea 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/Timelined.java @@ -17,6 +17,7 @@ package com.jpexs.decompiler.flash.timeline; import com.jpexs.decompiler.flash.ReadOnlyTagList; +import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.BoundedTag; @@ -26,6 +27,8 @@ import com.jpexs.decompiler.flash.tags.base.BoundedTag; */ public interface Timelined extends BoundedTag { + public SWF getSwf(); + public Timeline getTimeline(); public void resetTimeline(); diff --git a/src/com/jpexs/decompiler/flash/gui/ClipboardPanel.java b/src/com/jpexs/decompiler/flash/gui/ClipboardPanel.java index e77b757af..b12045883 100644 --- a/src/com/jpexs/decompiler/flash/gui/ClipboardPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/ClipboardPanel.java @@ -48,6 +48,7 @@ import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils; public class ClipboardPanel extends JPanel { private JLabel label; + private JLabel clearButton; private MainPanel mainPanel; @@ -56,14 +57,12 @@ public class ClipboardPanel extends JPanel { public ClipboardPanel(MainPanel mainPanel) { this.mainPanel = mainPanel; label = new JLabel("", View.getIcon("clipboard16"), JLabel.CENTER); - label.setToolTipText(AppStrings.translate("clipboard.hint")); label.setBorder(new EmptyBorder(0, 0, 0, 10)); int scrollBarSize = ((Integer) UIManager.get("ScrollBar.width")).intValue(); setBorder(new EmptyBorder(0, 0, 0, scrollBarSize)); setLayout(new FlowLayout(FlowLayout.RIGHT)); - JLabel clearButton = new JLabel(View.getIcon("cancel16")); - clearButton.setToolTipText(AppStrings.translate("clipboard.clear")); + clearButton = new JLabel(View.getIcon("cancel16")); clearButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); clearButton.addMouseListener(new MouseAdapter() { @Override @@ -127,6 +126,13 @@ public class ClipboardPanel extends JPanel { } else { label.setText(AppStrings.translate("clipboard.items").replace("%count%", "" + clipboardSize)); } + if (mainPanel.getClipboardType() == ClipboardType.FRAME) { + label.setToolTipText(AppStrings.translate("clipboard.hint.frame")); + clearButton.setToolTipText(AppStrings.translate("clipboard.clear.frame")); + } else { + label.setToolTipText(AppStrings.translate("clipboard.hint")); + clearButton.setToolTipText(AppStrings.translate("clipboard.clear")); + } setVisible(clipboardSize > 0); } diff --git a/src/com/jpexs/decompiler/flash/gui/ClipboardType.java b/src/com/jpexs/decompiler/flash/gui/ClipboardType.java new file mode 100644 index 000000000..daa905007 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/ClipboardType.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022-2023 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui; + +/** + * + * @author JPEXS + */ +public enum ClipboardType { + TAG, FRAME, NONE +} diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 297f95d21..596992479 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -654,12 +654,24 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se } if ((e.getKeyCode() == 'C' || e.getKeyCode() == 'X') && (e.isControlDown())) { List tagItems = new ArrayList<>(); + List frameItems = new ArrayList<>(); + Timelined frameTimelined = null; for (TreeItem item : items) { if (item instanceof TagScript) { tagItems.add(((TagScript) item).getTag()); } else if (item instanceof Tag) { tagItems.add((Tag) item); } + if (item instanceof Frame) { + frameItems.add(item); + Frame frame = (Frame) item; + if (frameTimelined != null && frameTimelined != frame.timeline.timelined) { + tagItems.clear(); + frameItems.clear(); + break; + } + frameTimelined = frame.timeline.timelined; + } } boolean allWritable = true; for (TreeItem item : tagItems) { @@ -670,18 +682,27 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se } if (e.getKeyCode() == 'C') { - if (e.isShiftDown()) { - contextPopupMenu.copyTagToClipboardWithDependenciesActionPerformed(null, tagItems); - } else { - contextPopupMenu.copyTagToClipboardActionPerformed(null, tagItems); + if (!frameItems.isEmpty()) { + contextPopupMenu.copyTagOrFrameToClipboardActionPerformed(null, frameItems); + } else { + if (e.isShiftDown()) { + contextPopupMenu.copyTagToClipboardWithDependenciesActionPerformed(null, tagItems); + } else { + contextPopupMenu.copyTagOrFrameToClipboardActionPerformed(null, tagItems); + } } } if (e.getKeyCode() == 'X' && allWritable) { - contextPopupMenu.update(tagItems); - if (e.isShiftDown()) { - contextPopupMenu.cutTagToClipboardWithDependenciesActionPerformed(null); + if (!frameItems.isEmpty()) { + contextPopupMenu.update(frameItems); + contextPopupMenu.cutTagOrFrameToClipboardActionPerformed(null); } else { - contextPopupMenu.cutTagToClipboardActionPerformed(null); + contextPopupMenu.update(tagItems); + if (e.isShiftDown()) { + contextPopupMenu.cutTagToClipboardWithDependenciesActionPerformed(null); + } else { + contextPopupMenu.cutTagOrFrameToClipboardActionPerformed(null); + } } } repaintTree(); @@ -691,8 +712,17 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se return; } TreeItem firstItem = items.get(0); - if (!((firstItem instanceof Tag) || (firstItem instanceof Frame))) { - return; + if (getClipboardType() == ClipboardType.FRAME) { + if (!(firstItem instanceof Frame)) { + return; + } + if (firstItem.getOpenable() != getClipboardContents().iterator().next().getOpenable()) { + return; + } + } else { + if (!((firstItem instanceof Tag) || (firstItem instanceof Frame))) { + return; + } } contextPopupMenu.update(items); if (e.isShiftDown()) { @@ -751,6 +781,17 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se public boolean clipboardEmpty() { return clipboard.isEmpty(); } + + public ClipboardType getClipboardType() { + if (clipboard.isEmpty()) { + return ClipboardType.NONE; + } + TreeItem item = clipboard.keySet().iterator().next(); + if (item instanceof Frame) { + return ClipboardType.FRAME; + } + return ClipboardType.TAG; + } public int getClipboardSize() { return clipboard.size(); @@ -6117,7 +6158,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se int framesCnt = (int) (timeline.frameRate * PreviewExporter.MORPH_SHAPE_ANIMATION_LENGTH); for (int i = 0; i < framesCnt; i++) { Frame f = new Frame(timeline, i); - DepthState ds = new DepthState(tag.getSwf(), f); + DepthState ds = new DepthState(tag.getSwf(), f, f); ds.characterId = ((CharacterTag) tag).getCharacterId(); ds.matrix = new MATRIX(); ds.ratio = i * 65535 / framesCnt; @@ -6126,7 +6167,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se timeline.addFrame(f); } Frame f = new Frame(timeline, framesCnt); - DepthState ds = new DepthState(tag.getSwf(), f); + DepthState ds = new DepthState(tag.getSwf(), f, f); ds.characterId = ((CharacterTag) tag).getCharacterId(); ds.matrix = new MATRIX(); ds.ratio = 65535; @@ -6143,7 +6184,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se //We do not want to draw fonts directly added to stage as //Fonts are really added to stage in some corner cases like for vertical text. Frame f = new Frame(timeline, 0); - DepthState ds = new DepthState(tag.getSwf(), f); + DepthState ds = new DepthState(tag.getSwf(), f, f); ds.characterId = ((CharacterTag) tag).getCharacterId(); ds.matrix = new MATRIX(); f.layers.put(1, ds); @@ -6152,7 +6193,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se timeline.fontFrameNum = frame; } else { Frame f = new Frame(timeline, 0); - DepthState ds = new DepthState(tag.getSwf(), f); + DepthState ds = new DepthState(tag.getSwf(), f, f); ds.characterId = ((CharacterTag) tag).getCharacterId(); ds.matrix = new MATRIX(); f.layers.put(1, ds); @@ -6232,6 +6273,11 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se public int getFrameCount() { return getTimeline().getFrameCount(); } + + @Override + public SWF getSwf() { + return tag.getSwf(); + } }; } diff --git a/src/com/jpexs/decompiler/flash/gui/SelectFramePositionDialog.java b/src/com/jpexs/decompiler/flash/gui/SelectFramePositionDialog.java new file mode 100644 index 000000000..4c83d9a90 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/SelectFramePositionDialog.java @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2022-2023 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.gui.tagtree.TagTree; +import com.jpexs.decompiler.flash.tags.DefineSpriteTag; +import com.jpexs.decompiler.flash.tags.DoInitActionTag; +import com.jpexs.decompiler.flash.tags.FrameLabelTag; +import com.jpexs.decompiler.flash.tags.ShowFrameTag; +import com.jpexs.decompiler.flash.tags.Tag; +import com.jpexs.decompiler.flash.timeline.Timelined; +import com.jpexs.decompiler.flash.treeitems.TreeItem; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.geom.GeneralPath; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTree; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.plaf.basic.BasicLabelUI; +import javax.swing.plaf.basic.BasicTreeUI; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +/** + * + * @author JPEXS + */ +public class SelectFramePositionDialog extends AppDialog { + + private final JButton okButton = new JButton(translate("button.ok")); + + private final JButton cancelButton = new JButton(translate("button.cancel")); + + private JTree positionTree; + + private PreviewPanel previewPanel; + + private final SWF swf; + + private int result = ERROR_OPTION; + + private int selectedFrame = -1; + private Timelined selectedTimelined = null; + + private boolean selectNext; + + private static class MyTreeNode implements TreeNode { + + private final List children = new ArrayList<>(); + private TreeNode parent; + private Object data; + + @Override + public String toString() { + if (data instanceof DoInitActionTag) { + DoInitActionTag doinit = (DoInitActionTag) data; + String exportName = doinit.getSwf().getExportName(doinit.spriteId); + if (exportName != null && !exportName.isEmpty()) { + return DoInitActionTag.NAME + " (" + doinit.spriteId + ") : " + exportName; + } + } + return data.toString(); + } + + public void setData(Object data) { + this.data = data; + } + + public Object getData() { + return data; + } + + public void addChild(TreeNode node) { + children.add(node); + } + + public void setParent(TreeNode parent) { + this.parent = parent; + } + + @Override + public TreeNode getChildAt(int childIndex) { + return children.get(childIndex); + } + + @Override + public int getChildCount() { + return children.size(); + } + + @Override + public TreeNode getParent() { + return parent; + } + + @Override + public int getIndex(TreeNode node) { + return children.indexOf(node); + } + + @Override + public boolean getAllowsChildren() { + return true; + } + + @Override + public boolean isLeaf() { + return children.isEmpty(); + } + + @Override + public Enumeration children() { + return Collections.enumeration(children); + } + } + + private static class MyTimelineEnd { + @Override + public String toString() { + return AppDialog.translateForDialog("timeline.end", SelectFramePositionDialog.class); + } + } + + private static class MySprites { + + @Override + public String toString() { + return AppStrings.translate("node.sprites"); + } + } + + private static class MyFrame { + + private final int frame; + private boolean invalid; + private List labels; + + public MyFrame(int frame, List labels) { + this.frame = frame; + this.labels = labels; + } + + public int getFrame() { + return frame; + } + + public void setInvalid(boolean invalid) { + this.invalid = invalid; + } + + public boolean isInvalid() { + return invalid; + } + + @Override + public String toString() { + String name = "frame " + frame; + if (!labels.isEmpty()) { + name += " (" + String.join(", ", labels) + ")"; + } + return name; + } + } + + + private void populateSprites(MyTreeNode root, Timelined tim) { + for (Tag t : tim.getTags()) { + if (t instanceof DefineSpriteTag) { + MyTreeNode node = new MyTreeNode(); + node.setData(t); + root.addChild(node); + populateSprites(root, (DefineSpriteTag) t); + DefineSpriteTag sprite = (DefineSpriteTag) t; + populateFrames(node, sprite); + } + } + } + + private void populateFrames(MyTreeNode parent, Timelined tim) { + MyTreeNode frameNode = new MyTreeNode(); + List labels = new ArrayList<>(); + frameNode.setData(new MyFrame(1, labels)); + frameNode.setParent(parent); + int f = 1; + parent.addChild(frameNode); + int numtags = 0; + for (Tag t : tim.getTags()) { + if (t instanceof FrameLabelTag) { + labels.add(((FrameLabelTag) t).name); + } + numtags++; + if (t instanceof ShowFrameTag) { + f++; + frameNode = new MyTreeNode(); + labels = new ArrayList<>(); + frameNode.setData(new MyFrame(f, labels)); + frameNode.setParent(parent); + parent.addChild(frameNode); + numtags = 0; + } + } + if (numtags == 0) { + parent.children.remove(parent.children.size() - 1); + } + MyTimelineEnd end = new MyTimelineEnd(); + MyTreeNode endNode = new MyTreeNode(); + endNode.setData(end); + parent.addChild(endNode); + } + + private void populateNodes(MyTreeNode root, Timelined tim) { + MyTreeNode spritesNode = new MyTreeNode(); + spritesNode.setData(new MySprites()); + root.addChild(spritesNode); + populateSprites(spritesNode, tim); + populateFrames(root, tim); + } + + private void selectPath(List path) { + Object[] pathArray = path.toArray(new Object[path.size()]); + TreePath tpath = new TreePath(pathArray); + positionTree.setSelectionPath(tpath); + int row = positionTree.getRowForPath(tpath); + if (row != -1) { + Rectangle rect = positionTree.getRowBounds(row); + rect.width += rect.x; + rect.x = 0; + positionTree.scrollRectToVisible(rect); + } + } + + private void selectCurrent(MyTreeNode root, Timelined timelined, List path) { + for (int i = 0; i < root.getChildCount(); i++) { + MyTreeNode node = (MyTreeNode) root.getChildAt(i); + + List subPath = new ArrayList<>(path); + subPath.add(node); + + List nextPath = new ArrayList<>(path); + if (i + 1 < root.getChildCount()) { + nextPath.add(root.getChildAt(i + 1)); + } + + if (timelined == selectedTimelined && ((node.getData() instanceof MyFrame) && (((MyFrame) node.getData()).frame == selectedFrame))) { + selectPath(selectNext ? nextPath : subPath); + return; + } + if (timelined == selectedTimelined && (node.getData() instanceof MyTimelineEnd) && selectedFrame == -1) { + selectPath(subPath); + return; + } + /*if ((selectedTimelined instanceof DefineSpriteTag) && !allowInsideSprites && node.getData() == selectedTimelined) { + selectPath(nextPath); + return; + }*/ + + if (node.getData() instanceof DefineSpriteTag) { + selectCurrent(node, (DefineSpriteTag) node.getData(), subPath); + } else { + selectCurrent(node, timelined, subPath); + } + } + } + + public SelectFramePositionDialog(Window parent, SWF swf) { + this(parent, swf, -1, null, false); + } + + private static class PositionTreeCellRenderer extends DefaultTreeCellRenderer { + + private boolean selected; + + public PositionTreeCellRenderer() { + if (View.isOceanic()) { + setUI(new BasicLabelUI()); + setOpaque(false); + setBackgroundNonSelectionColor(Color.white); + } + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + Component renderer = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + this.selected = sel; + if (renderer instanceof JLabel) { + JLabel lab = (JLabel) renderer; + + Object subValue = value; + if (value instanceof MyTreeNode) { + subValue = ((MyTreeNode) value).getData(); + } + if (subValue instanceof MyTimelineEnd) { + lab.setIcon(TagTree.getIconForType(TreeNodeType.END)); + } + if (subValue instanceof MySprites) { + lab.setIcon(View.getIcon("foldersprites16")); + } + + if (subValue instanceof MyFrame) { + lab.setIcon(TagTree.getIconForType(TreeNodeType.FRAME)); + } + if (subValue instanceof TreeItem) { + lab.setIcon(TagTree.getIconForType(TagTree.getTreeNodeType((TreeItem) subValue))); + } + } + return renderer; + } + } + + public SelectFramePositionDialog(Window parent, SWF swf, int selectedFrame, Timelined selectedTimelined, boolean selectNext) { + super(parent); + this.swf = swf; + this.selectedFrame = selectedFrame; + this.selectedTimelined = selectedTimelined; + this.selectNext = selectNext; + setTitle(translate("dialog.title").replace("%filetitle%", swf.getShortPathTitle())); + setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + Container cnt = getContentPane(); + cnt.setLayout(new BorderLayout()); + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + + okButton.addActionListener(this::okButtonActionPerformed); + cancelButton.addActionListener(this::cancelButtonActionPerformed); + + buttonsPanel.add(okButton); + buttonsPanel.add(cancelButton); + cnt.add(buttonsPanel, BorderLayout.SOUTH); + + MyTreeNode root = new MyTreeNode(); + root.setData("root"); + + populateNodes(root, swf); + + positionTree = new JTree(root) { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + int[] rows = getSelectionRows(); + if (rows.length == 1) { + int row = rows[0]; + + Object selection = getLastSelectedPathComponent(); + if (selection != null + && (((MyTreeNode) selection).getData() instanceof DefineSpriteTag) + || (((MyTreeNode) selection).getData() instanceof MySprites) + ) { // && !isCollapsed(row)) { + return; + } + Rectangle rect = this.getRowBounds(row); + int sideWidth = 6; + int sideHeight = 6; + int offsetX = -5; + + int lineStartX = offsetX + rect.x; + int backStartX = getWidth() + offsetX; + g.fillRect(lineStartX, rect.y, getWidth() - lineStartX, 1); + + Graphics2D g2d = (Graphics2D) g; + g2d.setPaint(getForeground()); + GeneralPath path = new GeneralPath(); + + path.moveTo(lineStartX - 6, rect.y + 6); + path.lineTo(lineStartX, rect.y); + path.lineTo(lineStartX + 6, rect.y + 6); + + path.closePath(); + g2d.fill(path); + } + } + + }; + if (View.isOceanic()) { + positionTree.setBackground(Color.white); + positionTree.setUI(new BasicTreeUI() { + { + setHashColor(Color.gray); + } + }); + } + positionTree.setCellRenderer(new PositionTreeCellRenderer()); + positionTree.setRootVisible(false); + positionTree.setShowsRootHandles(true); + positionTree.addTreeSelectionListener(this::spriteValueChanged); + positionTree.addTreeSelectionListener(this::positionTreeValueChanged); + positionTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + + previewPanel = new PreviewPanel(Main.getMainFrame().getPanel(), null); + previewPanel.setReadOnly(true); + previewPanel.setPreferredSize(new Dimension(300, 1)); + previewPanel.showEmpty(); + previewPanel.setParametersPanelVisible(false); + + JScrollPane positionTreeScrollPane = new FasterScrollPane(positionTree); + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, positionTreeScrollPane, previewPanel); + splitPane.setDividerLocation(400); + cnt.add(splitPane, BorderLayout.CENTER); + + List path = new ArrayList<>(); + path.add(root); + selectCurrent(root, swf, path); + + setSize(1024, 600); + setModal(true); + setResizable(true); + View.centerScreen(this); + View.setWindowIcon(this); + + calculateEnabled(); + } + + public void positionTreeValueChanged(TreeSelectionEvent e) { + calculateEnabled(); + } + + private void calculateEnabled() { + MyTreeNode node = (MyTreeNode) positionTree.getLastSelectedPathComponent(); + boolean enabled = node != null && ((node.getData() instanceof MyFrame) || (node.getData() instanceof MyTimelineEnd)); + okButton.setEnabled(enabled); + } + + private int getCurrentSelectedFrame() { + TreePath path = positionTree.getSelectionPath(); + for (int i = path.getPathCount() - 1; i >= 0; i--) { + MyTreeNode node = (MyTreeNode) path.getPathComponent(i); + if (node.getData() instanceof MyFrame) { + MyFrame frame = (MyFrame) node.getData(); + return frame.frame; + } + } + return -1; + } + + private Timelined getCurrentSelectedTimelined() { + TreePath path = positionTree.getSelectionPath(); + for (int i = path.getPathCount() - 1 - 1 /*sprite can be last, use its parent*/; i >= 0; i--) { + MyTreeNode node = (MyTreeNode) path.getPathComponent(i); + if ("root".equals(node.getData())) { + return swf; + } + if (node.getData() instanceof DefineSpriteTag) { + return (Timelined) node.getData(); + } + } + return null; + } + + private void spriteValueChanged(TreeSelectionEvent e) { + + TreePath selection = positionTree.getSelectionPath(); + if (selection == null) { + previewPanel.showEmpty(); + return; + } + MyTreeNode tnode = (MyTreeNode) selection.getLastPathComponent(); + if (tnode.getData() instanceof Tag) { + MainPanel.showPreview((TreeItem) tnode.getData(), previewPanel, getCurrentSelectedFrame() - 1, getCurrentSelectedTimelined()); + } else if (tnode.getData() instanceof MyFrame) { + int f = ((MyFrame) tnode.getData()).frame; + Object parent = ((MyTreeNode) tnode.getParent()).getData(); + if (parent instanceof DefineSpriteTag) { + previewPanel.showImagePanel((DefineSpriteTag) parent, swf, f - 1, true, true, !Configuration.animateSubsprites.get(), false, !Configuration.playFrameSounds.get(), true, false); + } else { + previewPanel.showImagePanel(swf, swf, f - 1, true, true, !Configuration.animateSubsprites.get(), false, !Configuration.playFrameSounds.get(), true, false); + } + } else { + previewPanel.showEmpty(); + } + } + + private void cancelButtonActionPerformed(ActionEvent evt) { + result = CANCEL_OPTION; + previewPanel.clear(); + setVisible(false); + } + + private void okButtonActionPerformed(ActionEvent evt) { + + selectedTimelined = getCurrentSelectedTimelined(); + + MyTreeNode node = (MyTreeNode) positionTree.getLastSelectedPathComponent(); + + if (node.getData() instanceof MyFrame) { + selectedFrame = ((MyFrame) node.getData()).frame; + } else if (node.getData() instanceof MyTimelineEnd) { + selectedFrame = selectedTimelined.getFrameCount() + 1; + } else { + return; + } + + + + result = OK_OPTION; + previewPanel.clear(); + setVisible(false); + } + + public int showDialog() { + setVisible(true); + return result; + } + + /** + * Gets current selected frame. -1 = end of timeline + * position + * + */ + public int getSelectedFrame() { + return selectedFrame; + } + + /** + * Gets selected timelined + */ + public Timelined getSelectedTimelined() { + return selectedTimelined; + } + +} diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index e64a0376b..a7115ff4b 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1221,4 +1221,12 @@ shaperecords.edge.style.fillstyle0 = FillStyle0 = %value% shaperecords.edge.style.fillstyle1 = FillStyle1 = %value% shaperecords.edge.end = Shape end +#after 20.0.0 +contextmenu.copyFrame = Copy frame to +contextmenu.copyFrame.clipboard = Copy to frame clipboard +contextmenu.cutFrame = Cut to frame clipboard +contextmenu.moveFrame = Move frame to +contextmenu.clipboard.frame = Frame clipboard +clipboard.hint.frame = Number of items in the frame clipboard +clipboard.clear.frame = Clear the frame clipboard \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties index aa1c4fb73..9168217b7 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -1196,4 +1196,14 @@ shaperecords.edge.style.move = P\u0159esun na %x%, %y% shaperecords.edge.style.newstyles = Nov\u00e9 styly - %numfillstyles%x fillstyle + %numlinestyles%x linestyle shaperecords.edge.style.fillstyle0 = FillStyle0 = %value% shaperecords.edge.style.fillstyle1 = FillStyle1 = %value% -shaperecords.edge.end = Konec tvaru \ No newline at end of file +shaperecords.edge.end = Konec tvaru + +#after 20.0.0 +contextmenu.copyFrame = Kop\u00edrovat sn\u00edmek do +contextmenu.copyFrame.clipboard = Kop\u00edrovat do sn\u00edmkov\u00e9 schr\u00e1nky +contextmenu.cutFrame = Vyjmout do sn\u00edmkov\u00e9 schr\u00e1nky +contextmenu.moveFrame = P\u0159esunout sn\u00edmek do +contextmenu.clipboard.frame = Sn\u00edmkov\u00e1 schr\u00e1nka + +clipboard.hint.frame = Po\u010det polo\u017eek ve sn\u00edmkov\u00e9 schr\u00e1nce +clipboard.clear.frame = Vy\u010distit sn\u00edmkovou schr\u00e1nku \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog.properties new file mode 100644 index 000000000..a76ed4647 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog.properties @@ -0,0 +1,19 @@ +# Copyright (C) 2022 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +dialog.title = Select frame position in %filetitle% +button.ok = OK +button.cancel = Cancel +timeline.end = end of timeline diff --git a/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog_cs.properties new file mode 100644 index 000000000..531802485 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/SelectFramePositionDialog_cs.properties @@ -0,0 +1,19 @@ +# Copyright (C) 2022 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +dialog.title = Vyberte pozici sn\u00edmku v %filetitle% +button.ok = OK +button.cancel = Storno +timeline.end = konec timeliny diff --git a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java index cf8f41f74..92c0fecb0 100644 --- a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java @@ -33,9 +33,11 @@ import com.jpexs.decompiler.flash.configuration.CustomConfigurationKeys; import com.jpexs.decompiler.flash.configuration.SwfSpecificCustomConfiguration; import com.jpexs.decompiler.flash.gui.AppDialog; import com.jpexs.decompiler.flash.gui.AppStrings; +import com.jpexs.decompiler.flash.gui.ClipboardType; import com.jpexs.decompiler.flash.gui.Main; import com.jpexs.decompiler.flash.gui.MainPanel; import com.jpexs.decompiler.flash.gui.ReplaceCharacterDialog; +import com.jpexs.decompiler.flash.gui.SelectFramePositionDialog; import com.jpexs.decompiler.flash.gui.SelectTagPositionDialog; import com.jpexs.decompiler.flash.gui.TreeNodeType; import com.jpexs.decompiler.flash.gui.View; @@ -59,14 +61,19 @@ import com.jpexs.decompiler.flash.tags.DoABC2Tag; import com.jpexs.decompiler.flash.tags.DoActionTag; import com.jpexs.decompiler.flash.tags.DoInitActionTag; import com.jpexs.decompiler.flash.tags.ExportAssetsTag; +import com.jpexs.decompiler.flash.tags.FileAttributesTag; +import com.jpexs.decompiler.flash.tags.MetadataTag; import com.jpexs.decompiler.flash.tags.PlaceObject2Tag; import com.jpexs.decompiler.flash.tags.PlaceObject3Tag; import com.jpexs.decompiler.flash.tags.PlaceObject4Tag; import com.jpexs.decompiler.flash.tags.PlaceObjectTag; +import com.jpexs.decompiler.flash.tags.RemoveObject2Tag; +import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.ShowFrameTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.TagTypeInfo; import com.jpexs.decompiler.flash.tags.UnknownTag; +import com.jpexs.decompiler.flash.tags.VideoFrameTag; import com.jpexs.decompiler.flash.tags.base.ASMSource; import com.jpexs.decompiler.flash.tags.base.ButtonTag; import com.jpexs.decompiler.flash.tags.base.CharacterIdTag; @@ -74,14 +81,18 @@ import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.decompiler.flash.tags.base.MorphShapeTag; import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag; +import com.jpexs.decompiler.flash.tags.base.RemoveTag; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundTag; import com.jpexs.decompiler.flash.tags.base.TextTag; +import com.jpexs.decompiler.flash.tags.gfx.ExporterInfo; import com.jpexs.decompiler.flash.timeline.AS2Package; import com.jpexs.decompiler.flash.timeline.AS3Package; +import com.jpexs.decompiler.flash.timeline.DepthState; import com.jpexs.decompiler.flash.timeline.Frame; import com.jpexs.decompiler.flash.timeline.FrameScript; import com.jpexs.decompiler.flash.timeline.TagScript; +import com.jpexs.decompiler.flash.timeline.Timeline; import com.jpexs.decompiler.flash.timeline.Timelined; import com.jpexs.decompiler.flash.treeitems.AS3ClassTreeItem; import com.jpexs.decompiler.flash.treeitems.FolderItem; @@ -111,12 +122,14 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; @@ -201,6 +214,8 @@ public class TagTreeContextMenu extends JPopupMenu { private JMenu moveTagToWithDependenciesMenu; + private JMenu moveFrameToMenu; + private JMenuItem moveUpMenuItem; private JMenuItem moveDownMenuItem; @@ -209,10 +224,16 @@ public class TagTreeContextMenu extends JPopupMenu { private JMenu copyTagToWithDependenciesMenu; + private JMenu copyFrameToMenu; + + private JMenuItem copyFrameToClipboardMenuItem; + private JMenuItem cutTagToClipboardMenuItem; private JMenuItem cutTagToClipboardWithDependenciesMenuItem; + private JMenuItem cutFrameToClipboardMenuItem; + private JMenuItem pasteBeforeMenuItem; private JMenuItem pasteAfterMenuItem; @@ -265,10 +286,12 @@ public class TagTreeContextMenu extends JPopupMenu { private List items = new ArrayList<>(); - private static final int KIND_MOVETO = 0; - private static final int KIND_MOVETODEPS = 1; - private static final int KIND_COPYTO = 2; - private static final int KIND_COPYTODEPS = 3; + private static final int KIND_TAG_MOVETO = 0; + private static final int KIND_TAG_MOVETODEPS = 1; + private static final int KIND_TAG_COPYTO = 2; + private static final int KIND_TAG_COPYTODEPS = 3; + private static final int KIND_FRAME_MOVETO = 4; + private static final int KIND_FRAME_COPYTO = 5; private TreeItem getCurrentItem() { if (items.isEmpty()) { @@ -353,7 +376,7 @@ public class TagTreeContextMenu extends JPopupMenu { replaceNoFillMenuItem = new JMenuItem(mainPanel.translate("button.replaceNoFill")); replaceNoFillMenuItem.addActionListener(new ActionListener() { @Override - public void actionPerformed(ActionEvent e) { + public void actionPerformed(ActionEvent e) { mainPanel.replaceNoFillButtonActionPerformed(getCurrentItem()); } }); @@ -514,6 +537,10 @@ public class TagTreeContextMenu extends JPopupMenu { moveTagToWithDependenciesMenu.setIcon(View.getIcon("move16")); add(moveTagToWithDependenciesMenu); + moveFrameToMenu = new JMenu(mainPanel.translate("contextmenu.moveFrame")); + moveFrameToMenu.setIcon(View.getIcon("move16")); + add(moveFrameToMenu); + moveUpMenuItem = new JMenuItem(mainPanel.translate("contextmenu.moveUp") + " (ALT + UP)"); moveUpMenuItem.setIcon(View.getIcon("arrowup16")); moveUpMenuItem.addActionListener(this::moveUpActionPerformed); @@ -532,9 +559,18 @@ public class TagTreeContextMenu extends JPopupMenu { copyTagToWithDependenciesMenu.setIcon(View.getIcon("copy16")); add(copyTagToWithDependenciesMenu); + copyFrameToMenu = new JMenu(mainPanel.translate("contextmenu.copyFrame")); + copyFrameToMenu.setIcon(View.getIcon("copy16")); + add(copyFrameToMenu); + + copyFrameToClipboardMenuItem = new JMenuItem(mainPanel.translate("contextmenu.copyFrame.clipboard") + " (CTRL+C)"); + copyFrameToClipboardMenuItem.setIcon(View.getIcon("copy16")); + copyFrameToClipboardMenuItem.addActionListener(this::copyTagOrFrameToClipboardActionPerformed); + add(copyFrameToClipboardMenuItem); + cutTagToClipboardMenuItem = new JMenuItem(mainPanel.translate("contextmenu.cutTag") + " (CTRL+X)"); cutTagToClipboardMenuItem.setIcon(View.getIcon("cut16")); - cutTagToClipboardMenuItem.addActionListener(this::cutTagToClipboardActionPerformed); + cutTagToClipboardMenuItem.addActionListener(this::cutTagOrFrameToClipboardActionPerformed); add(cutTagToClipboardMenuItem); cutTagToClipboardWithDependenciesMenuItem = new JMenuItem(mainPanel.translate("contextmenu.cutTagWithDependencies") + " (CTRL+SHIFT+X)"); @@ -542,6 +578,11 @@ public class TagTreeContextMenu extends JPopupMenu { cutTagToClipboardWithDependenciesMenuItem.addActionListener(this::cutTagToClipboardWithDependenciesActionPerformed); add(cutTagToClipboardWithDependenciesMenuItem); + cutFrameToClipboardMenuItem = new JMenuItem(mainPanel.translate("contextmenu.cutFrame") + " (CTRL+X)"); + cutFrameToClipboardMenuItem.setIcon(View.getIcon("cut16")); + cutFrameToClipboardMenuItem.addActionListener(this::cutTagOrFrameToClipboardActionPerformed); + add(cutFrameToClipboardMenuItem); + pasteBeforeMenuItem = new JMenuItem(mainPanel.translate("contextmenu.pasteBefore") + " (CTRL+V)"); pasteBeforeMenuItem.setIcon(View.getIcon("paste16")); pasteBeforeMenuItem.addActionListener(this::pasteBeforeActionPerformed); @@ -696,11 +737,18 @@ public class TagTreeContextMenu extends JPopupMenu { } public boolean canRemove(final List items) { + boolean wasNotFrame = false; + boolean wasFrame = false; + Timelined frameTimelined = null; for (TreeItem item : items) { if (item instanceof Tag) { + if (wasFrame) { + return false; + } if (((Tag) item).isReadOnly()) { return false; } + wasNotFrame = true; } else if (item instanceof Frame) { Frame frame = (Frame) item; if (frame.timeline.timelined instanceof DefineSpriteTag) { @@ -708,7 +756,19 @@ public class TagTreeContextMenu extends JPopupMenu { return false; } } + if (wasNotFrame) { + return false; + } + if (frameTimelined != null && frame.timeline.timelined != frameTimelined) { + return false; + } + frameTimelined = frame.timeline.timelined; + wasFrame = true; } else { + if (wasFrame) { + return false; + } + wasNotFrame = true; if (item instanceof TagScript) { if (((TagScript) item).getTag().isReadOnly()) { return false; @@ -832,6 +892,20 @@ public class TagTreeContextMenu extends JPopupMenu { boolean allSelectedIsTag = true; boolean allSelectedIsWritable = true; boolean allSelectedIsNotImported = true; + boolean allSelectedIsFrameInSameTimeline = true; + Timelined tim = null; + for (TreeItem item : items) { + if (!(item instanceof Frame)) { + allSelectedIsFrameInSameTimeline = false; + break; + } + Frame fr = (Frame) item; + if (tim != null && tim != fr.timeline.timelined) { + allSelectedIsFrameInSameTimeline = false; + break; + } + tim = fr.timeline.timelined; + } for (TreeItem item : items) { if (item instanceof Tag) { Tag tag = (Tag) item; @@ -982,12 +1056,16 @@ public class TagTreeContextMenu extends JPopupMenu { addTagAfterMenu.setVisible(false); moveTagToMenu.setVisible(false); moveTagToWithDependenciesMenu.setVisible(false); + moveFrameToMenu.setVisible(false); moveUpMenuItem.setVisible(false); moveDownMenuItem.setVisible(false); copyTagToMenu.setVisible(false); copyTagToWithDependenciesMenu.setVisible(false); + copyFrameToMenu.setVisible(false); + copyFrameToClipboardMenuItem.setVisible(false); cutTagToClipboardMenuItem.setVisible(false); cutTagToClipboardWithDependenciesMenuItem.setVisible(false); + cutFrameToClipboardMenuItem.setVisible(false); pasteAfterMenuItem.setVisible(false); pasteBeforeMenuItem.setVisible(false); pasteInsideMenuItem.setVisible(false); @@ -1043,12 +1121,12 @@ public class TagTreeContextMenu extends JPopupMenu { replaceMenuItem.setVisible(true); replaceNoFillMenuItem.setVisible(true); } - + if (canReplace.test(it -> it instanceof MorphShapeTag)) { replaceMenuItem.setVisible(true); replaceNoFillMenuItem.setVisible(true); } - + if (canReplace.test(it -> it instanceof DefineBinaryDataTag)) { replaceMenuItem.setVisible(true); } @@ -1258,7 +1336,7 @@ public class TagTreeContextMenu extends JPopupMenu { importScriptsMenuItem.setVisible(true); } - if (!mainPanel.clipboardEmpty()) { + if (mainPanel.getClipboardType() == ClipboardType.TAG) { if ((firstItem instanceof SWF) || (firstItem instanceof DefineSpriteTag) || (firstItem instanceof Frame)) { pasteInsideMenuItem.setVisible(true); } @@ -1267,6 +1345,19 @@ public class TagTreeContextMenu extends JPopupMenu { pasteBeforeMenuItem.setVisible(true); } } + if (mainPanel.getClipboardType() == ClipboardType.FRAME) { + Openable clipboardOpenable = mainPanel.getClipboardContents().iterator().next().getOpenable(); + + if (clipboardOpenable == firstItem.getOpenable()) { + if ((firstItem instanceof SWF) || (firstItem instanceof DefineSpriteTag)) { + pasteInsideMenuItem.setVisible(true); + } + if (firstItem instanceof Frame) { + pasteAfterMenuItem.setVisible(true); + pasteBeforeMenuItem.setVisible(true); + } + } + } if ((firstItem instanceof Tag) && (getTree() == mainPanel.tagListTree)) { moveUpMenuItem.setVisible(true); @@ -1295,8 +1386,10 @@ public class TagTreeContextMenu extends JPopupMenu { moveTagToMenu.removeAll(); moveTagToWithDependenciesMenu.removeAll(); + moveFrameToMenu.removeAll(); copyTagToMenu.removeAll(); copyTagToWithDependenciesMenu.removeAll(); + copyFrameToMenu.removeAll(); List tagItems = new ArrayList<>(); if (allSelectedIsTag) { @@ -1307,15 +1400,15 @@ public class TagTreeContextMenu extends JPopupMenu { tagItems.add((Tag) item); } } - JMenuItem copyToClipboardMenuItem = new JMenuItem(AppStrings.translate("contextmenu.clipboard") + " (CTRL+C)", View.getIcon("clipboard16")); - copyToClipboardMenuItem.addActionListener(new ActionListener() { + JMenuItem copyTagToClipboardMenuItem = new JMenuItem(AppStrings.translate("contextmenu.clipboard") + " (CTRL+C)", View.getIcon("clipboard16")); + copyTagToClipboardMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - copyTagToClipboardActionPerformed(e, tagItems); + copyTagOrFrameToClipboardActionPerformed(e, tagItems); } }); - copyTagToMenu.add(copyToClipboardMenuItem); + copyTagToMenu.add(copyTagToClipboardMenuItem); JMenuItem copyToClipboardWithDependenciesMenuItem = new JMenuItem(AppStrings.translate("contextmenu.clipboard") + " (CTRL+SHIFT+C)", View.getIcon("clipboard16")); copyToClipboardWithDependenciesMenuItem.addActionListener(new ActionListener() { @@ -1333,15 +1426,43 @@ public class TagTreeContextMenu extends JPopupMenu { copyTagToMenu.setVisible(true); copyTagToWithDependenciesMenu.setVisible(true); } + if (allSelectedIsFrameInSameTimeline) { + /*JMenuItem copyFrameToSubClipboardMenuItem = new JMenuItem(AppStrings.translate("contextmenu.clipboard.frame") + " (CTRL+C)", View.getIcon("clipboard16")); + copyFrameToSubClipboardMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + copyTagOrFrameToClipboardActionPerformed(e, items); + } + }); + copyFrameToMenu.add(copyFrameToSubClipboardMenuItem); + copyFrameToMenu.setVisible(true);*/ + + copyFrameToClipboardMenuItem.setVisible(true); + cutFrameToClipboardMenuItem.setVisible(true); + + /*if (swfs.size() > 1) { + for (OpenableList targetSwfList : swfs) { + if ((targetSwfList.size() == 1) && (targetSwfList.get(0) == singleSwf)) { + continue; + } + addCopyMoveToMenusSwfList(KIND_FRAME_MOVETO, singleSwf, targetSwfList, moveFrameToMenu, items); + addCopyMoveToMenusSwfList(KIND_FRAME_COPYTO, singleSwf, targetSwfList, copyFrameToMenu, items); + } + moveFrameToMenu.setVisible(true); + copyFrameToMenu.setVisible(true); + } + */ + } + if (allSelectedIsInTheSameSwf && allSelectedIsTag && swfs.size() > 1) { for (OpenableList targetSwfList : swfs) { if ((targetSwfList.size() == 1) && (targetSwfList.get(0) == singleSwf)) { continue; } - addCopyMoveToMenusSwfList(KIND_MOVETO, singleSwf, targetSwfList, moveTagToMenu, tagItems); - addCopyMoveToMenusSwfList(KIND_MOVETODEPS, singleSwf, targetSwfList, moveTagToWithDependenciesMenu, tagItems); - addCopyMoveToMenusSwfList(KIND_COPYTO, singleSwf, targetSwfList, copyTagToMenu, tagItems); - addCopyMoveToMenusSwfList(KIND_COPYTODEPS, singleSwf, targetSwfList, copyTagToWithDependenciesMenu, tagItems); + addCopyMoveToMenusSwfList(KIND_TAG_MOVETO, singleSwf, targetSwfList, moveTagToMenu, tagItems); + addCopyMoveToMenusSwfList(KIND_TAG_MOVETODEPS, singleSwf, targetSwfList, moveTagToWithDependenciesMenu, tagItems); + addCopyMoveToMenusSwfList(KIND_TAG_COPYTO, singleSwf, targetSwfList, copyTagToMenu, tagItems); + addCopyMoveToMenusSwfList(KIND_TAG_COPYTODEPS, singleSwf, targetSwfList, copyTagToWithDependenciesMenu, tagItems); } moveTagToMenu.setVisible(true); moveTagToWithDependenciesMenu.setVisible(true); @@ -1407,6 +1528,7 @@ public class TagTreeContextMenu extends JPopupMenu { } private interface AddTagActionListener { + void call(ActionEvent evt, TreeItem item, Class cl, TreeNodeType createNodeType); } @@ -1425,7 +1547,7 @@ public class TagTreeContextMenu extends JPopupMenu { if (allowedTagTypes.isEmpty() && mappedTagTypes.isEmpty()) { return; } - addAddTagMenuItems(allowedTagTypes, folderMenu, item, listener, folder); + addAddTagMenuItems(allowedTagTypes, folderMenu, item, listener, folder); if (!allowedTagTypes.isEmpty() && !mappedTagTypes.isEmpty()) { folderMenu.addSeparator(); } @@ -1594,7 +1716,7 @@ public class TagTreeContextMenu extends JPopupMenu { }); addTagMenu.add(tagItem); } - + if (parentFolder == null) { return; } @@ -1669,7 +1791,7 @@ public class TagTreeContextMenu extends JPopupMenu { createMorphShapeItem.addActionListener((ActionEvent ae) -> { listener.call(ae, item, DefineMorphShape2Tag.class, TreeNodeType.MORPH_SHAPE); }); - addTagMenu.add(createMorphShapeItem); + addTagMenu.add(createMorphShapeItem); break; } } @@ -1835,6 +1957,16 @@ public class TagTreeContextMenu extends JPopupMenu { copyOrMoveTags(new LinkedHashSet(items), true, timelined, position); } + private void moveFrameToActionPerformed(ActionEvent evt, List items, SWF targetSwf) { + SelectFramePositionDialog selectPositionDialog = new SelectFramePositionDialog(mainPanel.getMainFrame().getWindow(), targetSwf); + if (selectPositionDialog.showDialog() != AppDialog.OK_OPTION) { + return; + } + int position = selectPositionDialog.getSelectedFrame(); + Timelined timelined = selectPositionDialog.getSelectedTimelined(); + copyOrMoveFrames(new LinkedHashSet(items), true, timelined, position); + } + private void copyTagToActionPerformed(ActionEvent evt, List items, SWF targetSwf) { SelectTagPositionDialog selectPositionDialog = new SelectTagPositionDialog(mainPanel.getMainFrame().getWindow(), targetSwf, true); if (selectPositionDialog.showDialog() != AppDialog.OK_OPTION) { @@ -1845,6 +1977,16 @@ public class TagTreeContextMenu extends JPopupMenu { copyOrMoveTags(new LinkedHashSet(items), false, timelined, position); } + private void copyFrameToActionPerformed(ActionEvent evt, List items, SWF targetSwf) { + SelectFramePositionDialog selectPositionDialog = new SelectFramePositionDialog(mainPanel.getMainFrame().getWindow(), targetSwf); + if (selectPositionDialog.showDialog() != AppDialog.OK_OPTION) { + return; + } + int position = selectPositionDialog.getSelectedFrame(); + Timelined timelined = selectPositionDialog.getSelectedTimelined(); + copyOrMoveFrames(new LinkedHashSet(items), false, timelined, position); + } + private void copyTagWithDependenciesToActionPerformed(ActionEvent evt, List items, SWF targetSwf) { SelectTagPositionDialog selectPositionDialog = new SelectTagPositionDialog(mainPanel.getMainFrame().getWindow(), targetSwf, true); if (selectPositionDialog.showDialog() != AppDialog.OK_OPTION) { @@ -2082,8 +2224,8 @@ public class TagTreeContextMenu extends JPopupMenu { && !(scriptsPath.getLastPathComponent() instanceof ABC) && !(scriptsPath.getLastPathComponent() instanceof ABCContainerTag)) { scriptsPath = scriptsPath.getParentPath(); - } - + } + ABCContainerTag preselectedContainer = null; TreeItem scriptsNode = (TreeItem) scriptsPath.getLastPathComponent(); @@ -2186,7 +2328,7 @@ public class TagTreeContextMenu extends JPopupMenu { } } } - mainPanel.setTagTreeSelectedNode(mainPanel.getCurrentTree(), (TreeItem) item); + mainPanel.setTagTreeSelectedNode(mainPanel.getCurrentTree(), (TreeItem) item); } } @@ -2738,6 +2880,7 @@ public class TagTreeContextMenu extends JPopupMenu { } List tagsToRemove = new ArrayList<>(); + List framesToRemove = new ArrayList<>(); List itemsToRemove = new ArrayList<>(); List itemsToRemoveParents = new ArrayList<>(); List itemsToRemoveSprites = new ArrayList<>(); @@ -2779,12 +2922,13 @@ public class TagTreeContextMenu extends JPopupMenu { } else if (item instanceof Frame) { Frame frameNode = (Frame) item; Frame frame = frameNode.timeline.getFrame(frameNode.frame); - if (frame.showFrameTag != null) { + /*if (frame.showFrameTag != null) { tagsToRemove.add(frame.showFrameTag); } else { // this should be the last frame, so remove the inner tags tagsToRemove.addAll(frame.innerTags); - } + }*/ + framesToRemove.add(frame); } else if (item instanceof BUTTONCONDACTION) { itemsToRemove.add(item); itemsToRemoveParents.add(((TagScript) path.getParentPath().getLastPathComponent()).getTag()); @@ -2810,12 +2954,14 @@ public class TagTreeContextMenu extends JPopupMenu { } } - if (tagsToRemove.size() > 0 || itemsToRemove.size() > 0) { + if (tagsToRemove.size() > 0 || itemsToRemove.size() > 0 || framesToRemove.size() > 0) { String confirmationMessage; - if (tagsToRemove.size() + itemsToRemove.size() == 1) { + if (tagsToRemove.size() + itemsToRemove.size() + framesToRemove.size() == 1) { Object toRemove; if (tagsToRemove.size() == 1) { toRemove = tagsToRemove.get(0); + } else if (framesToRemove.size() == 1) { + toRemove = framesToRemove.get(0); } else { toRemove = itemsToRemove.get(0); } @@ -3002,6 +3148,17 @@ public class TagTreeContextMenu extends JPopupMenu { for (SWF swf : swfsToClearCache) { swf.clearAllCache(); } + + if (!framesToRemove.isEmpty()) { + Timelined tim = framesToRemove.get(0).timeline.timelined; + SWF swf = tim.getSwf(); + removeFrames(new LinkedHashSet<>(framesToRemove)); + + swf.resetTimelines(tim); + tim.setFrameCount(tim.getTimeline().getFrameCount()); + + mainPanel.refreshTree(swf); + } } }; @@ -3341,18 +3498,49 @@ public class TagTreeContextMenu extends JPopupMenu { JMenuItem swfItem = new JMenuItem(name); swfItem.addActionListener((ActionEvent ae) -> { switch (kind) { - case KIND_MOVETO: + case KIND_TAG_MOVETO: moveTagToActionPerformed(ae, items, targetSwf); break; - case KIND_MOVETODEPS: + case KIND_TAG_MOVETODEPS: moveTagWithDependenciesToActionPerformed(ae, items, targetSwf); break; - case KIND_COPYTO: + case KIND_TAG_COPYTO: copyTagToActionPerformed(ae, items, targetSwf); break; - case KIND_COPYTODEPS: + case KIND_TAG_COPYTODEPS: copyTagWithDependenciesToActionPerformed(ae, items, targetSwf); break; + case KIND_FRAME_MOVETO: + moveFrameToActionPerformed(ae, items, targetSwf); + break; + case KIND_FRAME_COPYTO: + copyFrameToActionPerformed(ae, items, targetSwf); + break; + } + }); + swfItem.setIcon(View.getIcon("flash16")); + menu.add(swfItem); + + for (Tag t : targetSwf.getTags()) { + if (t instanceof DefineBinaryDataTag) { + DefineBinaryDataTag binaryData = (DefineBinaryDataTag) t; + if (binaryData.innerSwf != null) { + addCopyMoveToMenus(kind, menu, items, name + " / " + t.getTagName() + " (" + ((DefineBinaryDataTag) t).getCharacterId() + ")", binaryData.innerSwf); + } + } + } + } + + private void addCopyMoveToFramesMenus(int kind, JMenuItem menu, List items, String name, SWF targetSwf) { + JMenuItem swfItem = new JMenuItem(name); + swfItem.addActionListener((ActionEvent ae) -> { + switch (kind) { + case KIND_TAG_MOVETO: + moveFrameToActionPerformed(ae, items, targetSwf); + break; + case KIND_TAG_COPYTO: + copyFrameToActionPerformed(ae, items, targetSwf); + break; } }); swfItem.setIcon(View.getIcon("flash16")); @@ -3400,7 +3588,12 @@ public class TagTreeContextMenu extends JPopupMenu { } } - public void copyTagToClipboardActionPerformed(ActionEvent evt, List items) { + public void copyTagOrFrameToClipboardActionPerformed(ActionEvent evt) { + List items = getSelectedItems(); + copyTagOrFrameToClipboardActionPerformed(evt, items); + } + + public void copyTagOrFrameToClipboardActionPerformed(ActionEvent evt, List items) { mainPanel.copyToClipboard(items); } @@ -3443,7 +3636,7 @@ public class TagTreeContextMenu extends JPopupMenu { mainPanel.copyToClipboard(getDependenciesSet(items)); } - public void cutTagToClipboardActionPerformed(ActionEvent evt) { + public void cutTagOrFrameToClipboardActionPerformed(ActionEvent evt) { List items = getSelectedItems(); mainPanel.cutToClipboard(items); mainPanel.repaintTree(); @@ -3457,21 +3650,28 @@ public class TagTreeContextMenu extends JPopupMenu { public void pasteBeforeActionPerformed(ActionEvent evt) { TreeItem item = getCurrentItem(); - Timelined timelined; - Tag position; - int positionInt; - if (item instanceof Frame) { - Frame frame = (Frame) item; - timelined = frame.timeline.timelined; - positionInt = calcFramePositionToAdd(frame, timelined, true, new Reference<>(false), false); - } else { - timelined = ((Tag) item).getTimelined(); - positionInt = timelined.indexOfTag((Tag) item); - } - ReadOnlyTagList tags = timelined.getTags(); - position = positionInt < tags.size() ? tags.get(positionInt) : null; - copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); + if (mainPanel.getClipboardType() == ClipboardType.FRAME) { + Frame frame = (Frame) item; + Timelined timelined = frame.timeline.timelined; + int position = frame.frame + 1; //Frame.frame is zero based + copyOrMoveFrames(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); + } else { + Timelined timelined; + Tag position; + int positionInt; + if (item instanceof Frame) { + Frame frame = (Frame) item; + timelined = frame.timeline.timelined; + positionInt = calcFramePositionToAdd(frame, timelined, true, new Reference<>(false), false); + } else { + timelined = ((Tag) item).getTimelined(); + positionInt = timelined.indexOfTag((Tag) item); + } + ReadOnlyTagList tags = timelined.getTags(); + position = positionInt < tags.size() ? tags.get(positionInt) : null; + copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); + } if (mainPanel.isClipboardCut()) { mainPanel.emptyClipboard(); } @@ -3479,21 +3679,27 @@ public class TagTreeContextMenu extends JPopupMenu { public void pasteAfterActionPerformed(ActionEvent evt) { TreeItem item = getCurrentItem(); - Timelined timelined; - Tag position; - int positionInt; - if (item instanceof Frame) { + if (mainPanel.getClipboardType() == ClipboardType.FRAME) { Frame frame = (Frame) item; - timelined = frame.timeline.timelined; - positionInt = calcFramePositionToAdd(frame, timelined, false, new Reference<>(false), false); + Timelined timelined = frame.timeline.timelined; + int position = frame.frame + 2; //Frame.frame is zero based + copyOrMoveFrames(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); } else { - timelined = ((Tag) item).getTimelined(); - positionInt = timelined.indexOfTag((Tag) item) + 1; + Timelined timelined; + Tag position; + int positionInt; + if (item instanceof Frame) { + Frame frame = (Frame) item; + timelined = frame.timeline.timelined; + positionInt = calcFramePositionToAdd(frame, timelined, false, new Reference<>(false), false); + } else { + timelined = ((Tag) item).getTimelined(); + positionInt = timelined.indexOfTag((Tag) item) + 1; + } + ReadOnlyTagList tags = timelined.getTags(); + position = positionInt < tags.size() ? tags.get(positionInt) : null; + copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); } - ReadOnlyTagList tags = timelined.getTags(); - position = positionInt < tags.size() ? tags.get(positionInt) : null; - copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); - if (mainPanel.isClipboardCut()) { mainPanel.emptyClipboard(); } @@ -3501,23 +3707,449 @@ public class TagTreeContextMenu extends JPopupMenu { private void pasteInsideActionPerformed(ActionEvent evt) { TreeItem item = getCurrentItem(); - Timelined timelined; - Tag position; - if (item instanceof Frame) { - Frame frame = (Frame) item; - position = frame.allInnerTags.get(frame.allInnerTags.size() - 1); - timelined = frame.timeline.timelined; + + if (mainPanel.getClipboardType() == ClipboardType.FRAME) { + Timelined timelined = (Timelined) item; + int position = timelined.getFrameCount() + 1; + copyOrMoveFrames(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); } else { - timelined = (Timelined) item; - position = null; + Timelined timelined; + Tag position; + if (item instanceof Frame) { + Frame frame = (Frame) item; + position = frame.allInnerTags.get(frame.allInnerTags.size() - 1); + timelined = frame.timeline.timelined; + } else { + timelined = (Timelined) item; + position = null; + } + copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); } - copyOrMoveTags(mainPanel.getClipboardContents(), mainPanel.isClipboardCut(), timelined, position); if (mainPanel.isClipboardCut()) { mainPanel.emptyClipboard(); } } + /** + * + * @param items + * @param move + * @param targetTimelined + * @param position 1-based position + */ + public void copyOrMoveFrames(Set items, boolean move, Timelined targetTimelined, int position) { + if (items.isEmpty()) { + return; + } + + final boolean USE_REMOVE_TAG = false; + + Frame targetFrame = targetTimelined.getTimeline().getFrame(position - 1); + Frame prevTargetFrame; + if (position == 1) { + prevTargetFrame = null; + } else { + prevTargetFrame = targetTimelined.getTimeline().getFrame(position - 2); + } + int f = 1; + int fc = 0; + boolean endsWithShowframe = false; + int prevShowFrameIndex = -1; + ShowFrameTag prevShowFrame = null; + for (int i = 0; i < targetTimelined.getTags().size(); i++) { + Tag t = targetTimelined.getTags().get(i); + endsWithShowframe = false; + if (t instanceof ShowFrameTag) { + f++; + endsWithShowframe = true; + if (f == position) { + prevShowFrameIndex = i; + prevShowFrame = (ShowFrameTag) t; + } + } + } + + + if (move) { + removeFrames(items); + } + if (targetFrame == null) { + prevShowFrameIndex = targetTimelined.getTags().size() - 1; + } else { + prevShowFrameIndex = targetTimelined.getTags().indexOf(prevShowFrame); + } + + + + SWF swf = targetTimelined.getSwf(); + List newFrameTags = new ArrayList<>(); + Frame frame = null; + for (TreeItem item : items) { + frame = (Frame) item; + Set allDepths = new TreeSet<>(frame.layers.keySet()); + if (prevTargetFrame != null) { + allDepths.addAll(prevTargetFrame.layers.keySet()); + } + for (int depth : allDepths) { + DepthState sourceState = frame.layers.get(depth); + DepthState prevTargetState = prevTargetFrame == null ? null : prevTargetFrame.layers.get(depth); + + if (prevTargetState != null && sourceState != null && prevTargetState.equals(sourceState)) { + //empty + } else if (sourceState != null) { + PlaceObjectTypeTag place = sourceState.toPlaceObjectTag(depth); + if (prevTargetState != null && !prevTargetState.equals(sourceState)) { + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } else { + place.setPlaceFlagMove(true); + } + } + newFrameTags.add(place); + } else if (prevTargetState != null && sourceState == null) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } + } + + for (DoActionTag doa : frame.actions) { + if (move) { + newFrameTags.add(doa); + } else { + try { + newFrameTags.add(doa.cloneTag()); + } catch (InterruptedException | IOException ex) { + Logger.getLogger(TagTreeContextMenu.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + ShowFrameTag showFrame = new ShowFrameTag(swf); + newFrameTags.add(showFrame); + prevTargetFrame = frame; + } + + if (targetFrame != null) { + Set allDepths = new TreeSet<>(frame.layers.keySet()); + allDepths.addAll(targetFrame.layers.keySet()); + for (int depth : allDepths) { + DepthState sourceState = frame.layers.get(depth); + DepthState targetState = targetFrame.layers.get(depth); + if (sourceState == null && targetState == null) { + continue; + } + boolean changed = false; + if (sourceState != null && !sourceState.equals(targetState) + || (targetState.placeFrame.frame < targetFrame.frame)) { + changed = true; + } + if (sourceState != null && targetState != null + && sourceState.equals(targetState) + && targetState.placeFrame.frame == targetFrame.frame + ) { + targetTimelined.removeTag(targetState.placeObjectTag); + } else if (targetState == null) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } else if (targetState.placeFrame.frame < targetFrame.frame + && sourceState != null + && !sourceState.equals(targetState) + ) { + PlaceObjectTypeTag fullPlace = targetState.toPlaceObjectTag(depth); + if (changed) { + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } else { + fullPlace.setPlaceFlagMove(true); + } + } + newFrameTags.add(fullPlace); + } else if (targetState.placeFrame.frame == targetFrame.frame) { + if (targetState.placeObjectTag.flagMove()) { + PlaceObjectTypeTag fullPlace = targetState.toPlaceObjectTag(depth); + fullPlace.setTimelined(targetTimelined); + if (changed) { + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } else { + fullPlace.setPlaceFlagMove(true); + } + } + targetTimelined.replaceTag(targetState.placeObjectTag, fullPlace); + } else { + if (changed) { + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + newFrameTags.add(rem); + } else { + targetState.placeObjectTag.setPlaceFlagMove(true); + } + } + } + } + } + } + + for (int i = prevShowFrameIndex + 1; i < targetTimelined.getTags().size(); i++) { + Tag t = targetTimelined.getTags().get(i); + if (t instanceof RemoveTag) { + targetTimelined.removeTag(i); + i--; + } + if (t instanceof ShowFrameTag) { + break; + } + } + + + int pos = prevShowFrameIndex + 1; + for (Tag t : newFrameTags) { + t.setTimelined(targetTimelined); + targetTimelined.addTag(pos, t); + pos++; + } + + fixDefineBeforeUsage(swf); + fixHeaderTags(swf); + swf.resetTimelines(targetTimelined); + + Timelined sourceTimelined = ((Frame) items.iterator().next()).timeline.timelined; + swf.resetTimelines(sourceTimelined); + sourceTimelined.setFrameCount(sourceTimelined.getTimeline().getFrameCount()); + + mainPanel.refreshTree(swf); + } + + private int moveCharacter(SWF swf, int usageIndex, int characterId) { + int i = 0; + for (int j = 0; j < swf.getTags().size(); j++) { + Tag t2 = swf.getTags().get(j); + if ((t2 instanceof CharacterIdTag) + && !(t2 instanceof PlaceObjectTypeTag) + && !(t2 instanceof RemoveTag)) { + CharacterIdTag chit = (CharacterIdTag) t2; + if (chit.getCharacterId() == characterId) { + swf.removeTag(j); + swf.addTag(usageIndex + i, (Tag) chit); + i++; + } + } + } + return i; + } + + /** + * Move tags usually placed in the first frame to the first frame + * @param swf + */ + private void fixHeaderTags(SWF swf) { + List headerTags = new ArrayList<>(); + for (int i = 0; i < swf.getTags().size(); i++) { + Tag t = swf.getTags().get(i); + if ((t instanceof FileAttributesTag) + || (t instanceof SetBackgroundColorTag) + || (t instanceof ExporterInfo) + || (t instanceof MetadataTag) + ) { + swf.removeTag(i); + headerTags.add(t); + i--; + } + } + for (int i = 0; i < headerTags.size(); i++) { + swf.addTag(i, headerTags.get(i)); + } + } + + private boolean fixDefineBeforeUsage(SWF swf) { + ReadOnlyTagList tags = swf.getTags(); + Set walkedSprites = new HashSet<>(); + boolean changed = false; + for (int i = 0; i < tags.size(); i++) { + Tag t = tags.get(i); + if (t instanceof PlaceObjectTypeTag) { + PlaceObjectTypeTag place = (PlaceObjectTypeTag) t; + if (place.getCharacterId() != -1) { + int chId = place.getCharacterId(); + CharacterTag ch = swf.getCharacter(chId); + if (ch != null) { + int defineIndex = tags.indexOf(ch); + int usageIndex = i; + if (usageIndex < defineIndex) { + i += moveCharacter(swf, i, chId); + changed = true; + } + } + if (ch instanceof DefineSpriteTag) { + DefineSpriteTag sprite = (DefineSpriteTag) ch; + if (walkedSprites.contains(sprite.getCharacterId())) { + continue; + } + walkedSprites.add(sprite.getCharacterId()); + for (int j = 0; j < sprite.getTags().size(); j++) { + Tag st = sprite.getTags().get(j); + if (st instanceof PlaceObjectTypeTag) { + place = (PlaceObjectTypeTag) st; + if (place.getCharacterId() != -1) { + chId = place.getCharacterId(); + ch = swf.getCharacter(chId); + if (ch != null) { + int defineIndex = tags.indexOf(ch); + int usageIndex = i; + if (usageIndex < defineIndex) { + i += moveCharacter(swf, swf.getTags().indexOf(sprite), chId); + changed = true; + } + } + } + } + } + } + } + } + } + return changed; + } + + private void removeFrames(Set items) { + if (items.isEmpty()) { + return; + } + final boolean USE_REMOVE_TAG = false; + + Timelined sourceTimelined = ((Frame) items.iterator().next()).timeline.timelined; + Timeline sourceTimeline = sourceTimelined.getTimeline(); + SWF swf = sourceTimelined.getSwf(); + + List frameTags = new ArrayList<>(); + Set framesToDelete = new TreeSet<>(); + boolean deleteLastFrame = false; + for (TreeItem item : items) { + Frame frameItem = (Frame) item; + framesToDelete.add(frameItem.frame + 1); + ShowFrameTag sft = frameItem.showFrameTag; + if (sft != null) { //can be null for last frame + frameTags.add(sft); + } else { + deleteLastFrame = true; + } + } + + int f = 1; + int lastNotDeletedFrameNum = 0; + int numtagsInFrame = 0; + for (int i = 0; i < sourceTimelined.getTags().size(); i++) { + Tag t = sourceTimelined.getTags().get(i); + numtagsInFrame++; + if (t instanceof ShowFrameTag) { + numtagsInFrame = 0; + if (frameTags.contains((ShowFrameTag) t)) { + sourceTimelined.removeTag(t); + for (i = i - 1; i >= 0; i--) { + Tag t2 = sourceTimelined.getTags().get(i); + if (t2 instanceof ShowFrameTag) { + break; + } + if ((t2 instanceof PlaceObjectTypeTag) + || (t2 instanceof RemoveObject2Tag)) { + sourceTimelined.removeTag(i); + } + } + } else { + lastNotDeletedFrameNum = f; + } + if (framesToDelete.contains(f) + && !framesToDelete.contains(f + 1) + && i + 1 + 1 < sourceTimelined.getTags().size()) { + Frame lastNotDeletedFrame = lastNotDeletedFrameNum == 0 ? null : sourceTimeline.getFrame(lastNotDeletedFrameNum - 1); + Frame nextFrame = sourceTimeline.getFrame(f + 1 - 1); + Set allDepths = new TreeSet<>(nextFrame.layers.keySet()); + if (lastNotDeletedFrame != null) { + allDepths.addAll(lastNotDeletedFrame.layers.keySet()); + } + for (int j = i + 1 + 1; j < sourceTimelined.getTags().size(); j++) { + Tag t2 = sourceTimelined.getTags().get(j); + if (t2 instanceof ShowFrameTag) { + break; + } + if (t2 instanceof RemoveTag) { + sourceTimelined.removeTag(j); + j--;; + } + } + for (int depth : allDepths) { + DepthState lastDepthstate = lastNotDeletedFrame == null ? null : lastNotDeletedFrame.layers.get(depth); + DepthState nextDepthstate = nextFrame.layers.get(depth); + if (nextDepthstate != null + && nextDepthstate.placeFrame.frame < nextDepthstate.frame.frame + && nextDepthstate.placeFrame.frame > lastNotDeletedFrameNum - 1) { + //the place was deleted + PlaceObjectTypeTag placeFull = nextDepthstate.toPlaceObjectTag(depth); + if (lastDepthstate != null) { + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + i++; + sourceTimelined.addTag(i, rem); + } else { + placeFull.setPlaceFlagMove(true); + } + } + i++; + sourceTimelined.addTag(i, placeFull); + } else if (lastDepthstate != null + && nextDepthstate != null + && !lastDepthstate.equals(nextDepthstate)) { + if (nextDepthstate.placeFrame.frame < nextDepthstate.frame.frame) { + PlaceObjectTypeTag placeFull = nextDepthstate.toPlaceObjectTag(depth); + if (USE_REMOVE_TAG) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + i++; + sourceTimelined.addTag(i, rem); + } else { + placeFull.setPlaceFlagMove(true); + } + i++; + sourceTimelined.addTag(i, placeFull); + } + } else if (lastDepthstate != null + && nextDepthstate == null) { + RemoveObject2Tag rem = new RemoveObject2Tag(swf); + rem.depth = depth; + i++; + sourceTimelined.addTag(i, rem); + } + } + } + f++; + } + } + if (numtagsInFrame > 0 && deleteLastFrame) { + for (int i = sourceTimelined.getTags().size(); i >= 0; i--) { + Tag t2 = sourceTimelined.getTags().get(i); + if (t2 instanceof ShowFrameTag) { + break; + } + if ((t2 instanceof PlaceObjectTypeTag) + || (t2 instanceof RemoveObject2Tag)) { + sourceTimelined.removeTag(i); + } + } + } + //WARNING: this method does not reset timeline and does not set frame count. Caller must do it. + } + public void copyOrMoveTags(Set items, boolean move, Timelined targetTimelined, Tag position) { Set sourceSwfs = new LinkedHashSet<>(); SWF targetSwf = (targetTimelined instanceof SWF) ? (SWF) targetTimelined : ((DefineSpriteTag) targetTimelined).getSwf(); @@ -3817,17 +4449,17 @@ public class TagTreeContextMenu extends JPopupMenu { } } } - + private void handleCreateFromFile(Tag tag, TreeNodeType createNodeType) { if (createNodeType == null) { return; - } + } boolean remove; switch (createNodeType) { case SPRITE: remove = !mainPanel.replaceSpriteWithGif(tag); break; - case SHAPE: + case SHAPE: remove = !mainPanel.replaceNoFill(tag); break; case MORPH_SHAPE: