diff --git a/libsrc/ffdec_lib/lexers/text_xml.flex b/libsrc/ffdec_lib/lexers/text_xml.flex new file mode 100644 index 000000000..9d3c5d4bf --- /dev/null +++ b/libsrc/ffdec_lib/lexers/text_xml.flex @@ -0,0 +1,189 @@ +/* + * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License + * at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jpexs.decompiler.flash.tags.text.xml; + +%% + +%public +%class XmlLexer +%final +%unicode +%char +%type XmlParsedSymbol +%throws XmlException + +%{ + public XmlLexer() { + super(); + } + + private static final byte TAG_OPEN = 1; + private static final byte TAG_CLOSE = -1; + + private static final byte INSTR_OPEN = 2; + private static final byte INSTR_CLOSE = -2; + + private static final byte CDATA_OPEN = 3; + private static final byte CDATA_CLOSE = -3; + + private static final byte COMMENT_OPEN = 4; + private static final byte COMMENT_CLOSE = -4; +%} + +%xstate COMMENT, CDATA, TAG, INSTR + +/* main character classes */ + +/* white space */ +S = (\u0020 | \u0009 | \u000D | \u000A)+ + +/* characters */ + +Char = \u0009 | \u000A | \u000D | [\u0020-\uD7FF] | [\uE000-\uFFFD] | [\u10000-\u10FFFF] + +SimpleCharacter = [^><&] + +/* comments */ +CommentStart = "" + +NameStartChar = ":" | [A-Z] | "_" | [a-z] +NameStartCharUnicode = [\u00C0-\u00D6] | + [\u00D8-\u00F6] | + [\u00F8-\u02FF] | + [\u0370-\u037D] | + [\u037F-\u1FFF] | + [\u200C-\u200D] | + [\u2070-\u218F] | + [\u2C00-\u2FEF] | + [\u3001-\uD7FF] | + [\uF900-\uFDCF] | + [\uFDF0-\uFFFD] | + [\u10000-\uEFFFF] + +NameChar = {NameStartChar} | "-" | "." | [0-9] | \u00B7 +NameCharUnicode = [\u0300-\u036F] | [\u0203F-\u2040] +Name = {NameStartChar} {NameChar}* +NameUnicode = ({NameStartChar}|{NameStartCharUnicode}) ({NameChar}|{NameCharUnicode})* + +/* XML Processing Instructions */ +InstrStart = "" + +/* CDATA */ +CDataStart = "" + +/* Tags */ +OpenTagStart = "<" {Name} +OpenTagClose = "/>" +OpenTagEnd = ">" + +CloseTag = "" + +/* attribute */ +Attribute = {Name} "=" + +/* string and character literals */ +DQuoteStringChar = [^\r\n\"] +SQuoteStringChar = [^\r\n\'] + +%% + + { + + "&" [a-z]+ ";" { return new XmlParsedSymbol(XmlSymbolType.ENTITY, yytext().substring(1, yytext().length() - 1), yytext(), yychar); } + "&#" [:digit:]+ ";" { return new XmlParsedSymbol(XmlSymbolType.ENTITY_NUMERIC, Integer.parseInt(yytext().substring(1, yytext().length() - 1)), yytext(), yychar); } + + {InstrStart} { + yybegin(INSTR); + return new XmlParsedSymbol(XmlSymbolType.INSTR_OPEN, yytext(), yychar); + } + {OpenTagStart} { + yybegin(TAG); + return new XmlParsedSymbol(XmlSymbolType.TAG_OPEN, yytext().substring(1), yytext(), yychar); + } + {CloseTag} { return new XmlParsedSymbol(XmlSymbolType.TAG_CLOSE, yytext().substring(2, yytext().length() - 1), yytext(), yychar); } + {CommentStart} { + yybegin(COMMENT); + return new XmlParsedSymbol(XmlSymbolType.COMMENT_OPEN, yytext(), yychar); + } + {CDataStart} { + yybegin(CDATA); + return new XmlParsedSymbol(XmlSymbolType.CDATA_OPEN, yytext(), yychar); + } + {SimpleCharacter}+ { return new XmlParsedSymbol(XmlSymbolType.CHARACTER, yytext(), yytext(), yychar);} +} + + { + {Attribute} { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE, yytext().substring(0, yytext().length() - 1).trim(), yytext(), yychar); } + + \"{DQuoteStringChar}*\" | + \'{SQuoteStringChar}*\' { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE_VALUE, yytext().substring(1, yytext().length() - 1), yytext(), yychar); } + + {InstrEnd} { + yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.INSTR_CLOSE, yytext(), yychar); + } + {S} { return new XmlParsedSymbol(XmlSymbolType.WHITESPACE, yytext(), yychar);} +} + + { + {Attribute} { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE, yytext().substring(0, yytext().length() - 1).trim(), yytext(), yychar); } + + \"{DQuoteStringChar}*\" | + \'{SQuoteStringChar}*\' { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE_VALUE, yytext().substring(1, yytext().length() - 1), yytext(), yychar); } + + + {OpenTagClose} { + yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.TAG_CLOSE, yytext(), yychar); + } + + {OpenTagEnd} { + yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.TAG_OPEN_END, yytext(), yychar); + } + {S} { return new XmlParsedSymbol(XmlSymbolType.WHITESPACE, yytext(), yychar);} +} + + { + {CommentEnd} { + yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.COMMENT_CLOSE, yytext(), yychar); + } + ~{CommentEnd} { + yypushback(3); + return new XmlParsedSymbol(XmlSymbolType.COMMENT, yytext().substring(yytext().length() - 3), yytext().substring(yytext().length() - 3), yychar); + } +} + + { + {CDataEnd} { + yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.CDATA_CLOSE, yytext(), yychar); + } + ~{CDataEnd} { + yypushback(3); + return new XmlParsedSymbol(XmlSymbolType.CDATA, yytext().substring(yytext().length() - 3), yytext().substring(yytext().length() - 3), yychar); + } +} + + { +/* error fallback */ + .|\n { throw new XmlException("Incorrect text: \"" + yytext() + "\""); } + <> { return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); } +} + diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java index a26ae9fb4..d576a694c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java @@ -16,7 +16,6 @@ */ package com.jpexs.decompiler.flash.tags; -import com.jpexs.decompiler.flash.IdentifiersDeobfuscation; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFInputStream; import com.jpexs.decompiler.flash.SWFOutputStream; @@ -43,10 +42,13 @@ import com.jpexs.decompiler.flash.tags.dynamictext.TextStyle; import com.jpexs.decompiler.flash.tags.dynamictext.Word; import com.jpexs.decompiler.flash.tags.enums.TextRenderMode; import com.jpexs.decompiler.flash.tags.text.ParsedSymbol; -import com.jpexs.decompiler.flash.tags.text.SymbolType; import com.jpexs.decompiler.flash.tags.text.TextAlign; import com.jpexs.decompiler.flash.tags.text.TextLexer; import com.jpexs.decompiler.flash.tags.text.TextParseException; +import com.jpexs.decompiler.flash.tags.text.xml.XmlException; +import com.jpexs.decompiler.flash.tags.text.xml.XmlLexer; +import com.jpexs.decompiler.flash.tags.text.xml.XmlParsedSymbol; +import com.jpexs.decompiler.flash.tags.text.xml.XmlSymbolType; import com.jpexs.decompiler.flash.types.BasicType; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry; @@ -64,28 +66,22 @@ import com.jpexs.decompiler.flash.types.annotations.SWFVersion; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.helpers.ByteArrayRange; import com.jpexs.helpers.SerializableImage; -import com.jpexs.helpers.utf8.Utf8Helper; import java.awt.Color; import java.awt.Font; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; /** * DefineEditText tag - defines an editable text field. @@ -217,6 +213,154 @@ public class DefineEditTextTag extends TextTag { readData(sis, data, 0, false, false, false); } + @Override + public void removeCharacterGlyph(int glyphPos) { + List recs = getTextRecords(getSwf()); + int pos = 0; + for (TEXTRECORD r : recs) { + if (r instanceof AdvancedTextRecord) { + AdvancedTextRecord atr = (AdvancedTextRecord) r; + for (int htmlSourcePos : atr.htmlSourcePositions) { + if (pos == glyphPos) { + char character = initialText.charAt(htmlSourcePos); + if (character == '&') { + int semiPos = initialText.indexOf(";", htmlSourcePos); + if (semiPos != -1) { + initialText = initialText.substring(0, htmlSourcePos) + initialText.substring(semiPos + 1); + return; + } + } + initialText = initialText.substring(0, htmlSourcePos) + initialText.substring(htmlSourcePos + 1); + packHtml(); + setModified(true); + return; + } + pos++; + } + } + } + } + + private void packHtml() { + try { + XmlLexer lexer = new XmlLexer(new StringReader(initialText)); + XmlParsedSymbol s = lexer.yylex(); + String tagName = null; + StringBuilder result = new StringBuilder(); + StringBuilder cached = new StringBuilder(); + boolean hasCharacter = false; + while (s.type != XmlSymbolType.EOF) { + if (s.type == XmlSymbolType.TAG_OPEN) { + if (tagName != null) { + result.append(cached.toString()); + } + tagName = (String) s.value; + cached = new StringBuilder(); + cached.append(s.rawText); + hasCharacter = false; + } else if (s.type == XmlSymbolType.TAG_CLOSE) { + if (tagName != null) { + if ("font".equals(tagName) && !hasCharacter) { + //ignore + } else { + cached.append(s.rawText); + result.append(cached.toString()); + } + } else { + result.append(s.rawText); + } + tagName = null; + } else if (s.type == XmlSymbolType.ATTRIBUTE + || s.type == XmlSymbolType.ATTRIBUTE_VALUE + || s.type == XmlSymbolType.TAG_OPEN_END) { + if (tagName != null) { + cached.append(s.rawText); + } else { + result.append(s.rawText); + } + } else if (s.type == XmlSymbolType.CHARACTER) { + hasCharacter = true; + if (tagName != null) { + cached.append(s.rawText); + } else { + result.append(s.rawText); + } + } else { + if (tagName != null) { + cached.append(s.rawText); + } else { + result.append(s.rawText); + } + } + s = lexer.yylex(); + } + initialText = result.toString(); + } catch (IOException | XmlException ex) { + //ignore + } + } + + @Override + public void insertCharacterGlyph(int glyphPos, char character) { + List recs = getTextRecords(getSwf()); + int pos = 0; + String str = "" + character; + switch (character) { + case '&': + str = "&"; + break; + case '<': + str = "<"; + break; + case '>': + str = ">"; + break; + case '\u00A0': + str = " "; + break; + } + /* + case '"': + str = """; + break; + case '\'': + str = "'"; + break; + */ + + int lastHtmlSourcePos = -1; + for (TEXTRECORD r : recs) { + if (r instanceof AdvancedTextRecord) { + AdvancedTextRecord atr = (AdvancedTextRecord) r; + for (int htmlSourcePos : atr.htmlSourcePositions) { + if (pos == glyphPos) { + initialText = initialText.substring(0, htmlSourcePos) + str + initialText.substring(htmlSourcePos); + setModified(true); + return; + } + lastHtmlSourcePos = htmlSourcePos; + pos++; + } + } + } + + if (lastHtmlSourcePos == -1) { + initialText = str + initialText; + return; + } + + int newHtmlSourcePos = lastHtmlSourcePos + 1; + char prevCharacter = initialText.charAt(lastHtmlSourcePos); + if (character == '&') { + int semiPos = initialText.indexOf(";", lastHtmlSourcePos); + if (semiPos != -1) { + newHtmlSourcePos = semiPos + 1; + } + } + + initialText = initialText.substring(0, newHtmlSourcePos) + str + initialText.substring(newHtmlSourcePos); + } + @Override public final void readData(SWFInputStream sis, ByteArrayRange data, int level, boolean parallel, boolean skipUnusualTags, boolean lazy) throws IOException { characterID = sis.readUI16("characterID"); @@ -402,107 +546,24 @@ public class DefineEditTextTag extends TextTag { style.leftMargin = leftMargin; final List ret = new ArrayList<>(); if (html) { - SAXParserFactory factory = SAXParserFactory.newInstance(); - SAXParser saxParser; + //SAXParserFactory factory = SAXParserFactory.newInstance(); + //SAXParser saxParser; final Stack styles = new Stack<>(); styles.add(style); - try { + /*try { saxParser = factory.newSAXParser(); DefaultHandler handler = new DefaultHandler() { + + private Locator locator; + @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - TextStyle style = styles.peek(); - switch (qName) { - case "p": - // todo: parse the following attribute: - // align - break; - case "a": - // todo: handle link - href, target attributes - break; - case "b": - style = style.clone(); - style.bold = true; - styles.add(style); - break; - case "i": - style = style.clone(); - style.italic = true; - styles.add(style); - break; - case "u": - style = style.clone(); - style.underlined = true; - styles.add(style); - break; - case "font": - style = style.clone(); - String color = unescape(attributes.getValue("color")); - if (color != null) { - if (color.startsWith("#")) { - try { - if (color.length() == 7) { //#rrggbb - style.textColor = new RGBA(Color.decode(color)); - } else if (color.length() == 9) { //#aarrggbb - style.textColor = RGBA.fromHexARGB(color); - style.textColor.alpha = 255; //no alpha is allowed - } - } catch (NumberFormatException ex) { - //do not change textColor - } - - } - } - String size = unescape(attributes.getValue("size")); - if (size != null && size.length() > 0) { - try { - char firstChar = size.charAt(0); - if (firstChar != '+' && firstChar != '-') { - int fontSize = Integer.parseInt(size); - style.fontHeight = (int) Math.round(fontSize * SWF.unitDivisor); - } else { - int fontSizeDelta = (int) Math.round(Integer.parseInt(size.substring(1)) * SWF.unitDivisor); - if (firstChar == '+') { - style.fontHeight = style.fontHeight + fontSizeDelta; - } else { - style.fontHeight = style.fontHeight - fontSizeDelta; - } - } - style.fontLeading = leading; - } catch (NumberFormatException nfe) { - //do not change fontHeight or leading - } - } - String face = unescape(attributes.getValue("face")); - - if (face != null && face.length() > 0) { - style.fontFace = face; - } - - String letterspacing = unescape(attributes.getValue("letterSpacing")); - if (letterspacing != null && letterspacing.length() > 0) { - try { - style.letterSpacing = Double.parseDouble(letterspacing); - } catch (NumberFormatException nfe) { - //do not change letterSpacing - } - } - - String kerning = unescape(attributes.getValue("kerning")); - if (kerning != null && kerning.length() > 0) { - style.kerning = kerning.equals("1"); - } - - styles.add(style); - break; - case "br": - case "sbr": // what's this? - CharacterWithStyle cs = new CharacterWithStyle(); - cs.character = '\n'; - cs.style = style; - ret.add(cs); - break; - } + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + @Override + public void startElement(String uri, String localName, String qName, Map attributes) throws SAXException { + } @Override @@ -555,33 +616,209 @@ public class DefineEditTextTag extends TextTag { addCharacters(ret, txt, style); } }; + */ - str = str.replace(" ", "/{entity-nbsp}"); - str = str.replace("<", "/{entity-lt}"); - str = str.replace(">", "/{entity-gt}"); - str = str.replace(""", "/{entity-quot}"); - str = str.replace("&", "/{entity-amp}"); - str = str.replace("'", "/{entity-apos}"); - str = str.replace("&", "&"); + XmlLexer lexer = new XmlLexer(new StringReader(str)); + try { + XmlParsedSymbol s = lexer.yylex(); + boolean inOpenTag = false; + String attributeName = null; + String tagName = null; + Map attributes = new LinkedHashMap<>(); + loops: + while (s.type != XmlSymbolType.EOF) { + switch (s.type) { + case TAG_OPEN: + inOpenTag = true; + attributeName = null; + tagName = (String) s.value; + attributes.clear(); + break; + case ATTRIBUTE: + attributeName = (String) s.value; + break; + case ATTRIBUTE_VALUE: + if (attributeName == null) { + //Error + break loops; + } + attributes.put(attributeName, unescape((String) s.value)); + break; + case TAG_OPEN_END: + style = styles.peek(); + switch (tagName) { + case "p": + // todo: parse the following attribute: + // align + break; + case "a": + // todo: handle link - href, target attributes + break; + case "b": + style = style.clone(); + style.bold = true; + styles.add(style); + break; + case "i": + style = style.clone(); + style.italic = true; + styles.add(style); + break; + case "u": + style = style.clone(); + style.underlined = true; + styles.add(style); + break; + case "font": + style = style.clone(); + String color = attributes.get("color"); + if (color != null) { + if (color.startsWith("#")) { + try { + if (color.length() == 7) { //#rrggbb + style.textColor = new RGBA(Color.decode(color)); + } else if (color.length() == 9) { //#aarrggbb + style.textColor = RGBA.fromHexARGB(color); + style.textColor.alpha = 255; //no alpha is allowed + } + } catch (NumberFormatException ex) { + //do not change textColor + } - str = "" + str + ""; + } + } + String size = attributes.get("size"); + if (size != null && size.length() > 0) { + try { + char firstChar = size.charAt(0); + if (firstChar != '+' && firstChar != '-') { + int fontSize = Integer.parseInt(size); + style.fontHeight = (int) Math.round(fontSize * SWF.unitDivisor); + } else { + int fontSizeDelta = (int) Math.round(Integer.parseInt(size.substring(1)) * SWF.unitDivisor); + if (firstChar == '+') { + style.fontHeight = style.fontHeight + fontSizeDelta; + } else { + style.fontHeight = style.fontHeight - fontSizeDelta; + } + } + style.fontLeading = leading; + } catch (NumberFormatException nfe) { + //do not change fontHeight or leading + } + } + String face = attributes.get("face"); + + if (face != null && face.length() > 0) { + style.fontFace = face; + } + + String letterspacing = attributes.get("letterSpacing"); + if (letterspacing != null && letterspacing.length() > 0) { + try { + style.letterSpacing = Double.parseDouble(letterspacing); + } catch (NumberFormatException nfe) { + //do not change letterSpacing + } + } + + String kerning = attributes.get("kerning"); + if (kerning != null && kerning.length() > 0) { + style.kerning = kerning.equals("1"); + } + + styles.add(style); + break; + case "br": + case "sbr": // what's this? + CharacterWithStyle cs = new CharacterWithStyle(); + cs.character = '\n'; + cs.style = style; + ret.add(cs); + break; + } + tagName = null; + break; + case TAG_CLOSE: + tagName = (String) s.value; + switch (tagName) { + case "b": + case "i": + case "u": + case "font": + styles.pop(); + break; + case "p": + style = styles.peek(); + CharacterWithStyle cs = new CharacterWithStyle(); + cs.character = '\n'; + cs.style = style; + ret.add(cs); + break; + } + tagName = null; + break; + + case ENTITY: + case CHARACTER: + String txt = (String) s.value; + if (s.type == XmlSymbolType.ENTITY) { + txt = unescape("&" + txt + ";"); + } + style = styles.peek(); + if (style.fontFace != null && useOutlines) { + CharacterTag ct = swf.getCharacterByExportName(style.fontFace); + if (ct != null && (ct instanceof FontTag)) { + style.font = (FontTag) ct; + } else { + style.font = swf.getFontByNameInTag(style.fontFace, style.bold, style.italic); + } + if (style.font == null) { + style.fontFace = null; + } + } + addCharacters(ret, txt, style, s.position); + break; + } + s = lexer.yylex(); + } + } catch (IOException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + } catch (XmlException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + //ex.printStackTrace(); + } + /* + str = str.replace(" ", "/{entity-nbsp}"); + str = str.replace("<", "/{entity-lt}"); + str = str.replace(">", "/{entity-gt}"); + str = str.replace(""", "/{entity-quot}"); + str = str.replace("&", "/{entity-amp}"); + str = str.replace("'", "/{entity-apos}"); + str = str.replace("&", "&"); + + str = "" + str + ""; saxParser.parse(new ByteArrayInputStream(str.getBytes(Utf8Helper.charset)), handler); } catch (ParserConfigurationException | SAXException | IOException ex) { Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, "Error parsing text " + getCharacterId(), ex); } + */ } else { - addCharacters(ret, str, style); + addCharacters(ret, str, style, 0); } return ret; } - private void addCharacters(List list, String str, TextStyle style) { + private void addCharacters(List list, String str, TextStyle style, int position) { for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); CharacterWithStyle cs = new CharacterWithStyle(); cs.character = ch; cs.style = style; + if (position > -1) { + cs.htmlSourcePosition = position + i; + } list.add(cs); } } @@ -705,7 +942,7 @@ public class DefineEditTextTag extends TextTag { switch (s.type) { case PARAMETER_IDENTIFIER: String paramName = (String) s.value; - s = lexer.yylex(); + s = lexer.yylex(); String paramValue = (String) s.value; switch (paramName) { case "xmin": @@ -1137,7 +1374,7 @@ public class DefineEditTextTag extends TextTag { ge.glyphAdvance += font.getCharKerningAdjustment(c, nextChar) / font.getDivider(); } } - textModel.addGlyph(c, ge); + textModel.addGlyph(c, ge, cs.htmlSourcePosition); if (Character.isWhitespace(c)) { lastWasWhiteSpace = true; } @@ -1330,8 +1567,10 @@ public class DefineEditTextTag extends TextTag { tr2.yOffset = yOffset; } tr2.glyphEntries = new ArrayList<>(tr.glyphEntries.size()); + tr2.htmlSourcePositions = new ArrayList<>(tr.glyphEntries.size()); for (GlyphCharacter ge : tr.glyphEntries) { tr2.glyphEntries.add(ge.glyphEntry); + tr2.htmlSourcePositions.add(ge.htmlSourcePosition); } allTextRecords.add(tr2); } @@ -1353,4 +1592,17 @@ public class DefineEditTextTag extends TextTag { public boolean isSingleFrame() { return true; } + + private String unescape(String txt) { + if (txt == null) { + return null; + } + txt = txt.replace(" ", "\u00A0"); + txt = txt.replace("<", "<"); + txt = txt.replace(">", ">"); + txt = txt.replace(""", "\""); + txt = txt.replace("&", "&"); + txt = txt.replace("'", "'"); + return txt; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineTextTag.java index fa8f0f53a..934c35cf6 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineTextTag.java @@ -75,7 +75,7 @@ public class DefineTextTag extends StaticTextTag { public DefineTextTag(SWFInputStream sis, ByteArrayRange data) throws IOException { super(sis.getSwf(), ID, NAME, data); readData(sis, data, 0, false, false, false); - } + } @Override public int getTextNum() { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/RenderContext.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/RenderContext.java index 1c9d30f91..7c29cddb4 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/RenderContext.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/RenderContext.java @@ -16,6 +16,7 @@ */ package com.jpexs.decompiler.flash.tags.base; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.timeline.DepthState; import com.jpexs.helpers.Cache; import com.jpexs.helpers.SerializableImage; @@ -55,11 +56,21 @@ public class RenderContext { */ public TextTag mouseOverText; + /** + * Absolute matrix of mouse over text position + */ + public Matrix mouseOverTextAbsMatrix; + /** * Selection text tag */ public TextTag selectionText; + /** + * Selection text abs matrix + */ + public Matrix selectionAbsMatrix; + /** * Text selection start */ diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java index 6f065571b..b5c18b01a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java @@ -125,6 +125,14 @@ public abstract class StaticTextTag extends TextTag { } } + @Override + public void insertCharacterGlyph(int glyphPos, char character) { + } + + @Override + public void removeCharacterGlyph(int glyphPos) { + } + /** * Gets data bytes * diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java index 10a0d1b75..a7fd5fd19 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java @@ -92,6 +92,19 @@ public abstract class TextTag extends DrawableTag { super(swf, id, name, data); } + /** + * Inserts character at the given position. + * @param glyphPos Glyph position + * @param character Character + */ + public abstract void insertCharacterGlyph(int glyphPos, char character); + + /** + * Removes character glyph at the given position + * @param glyphPos Glyph position + */ + public abstract void removeCharacterGlyph(int glyphPos); + /** * Gets text matrix. * @return Text matrix diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/AdvancedTextRecord.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/AdvancedTextRecord.java index 27ff94a2c..6a32fed31 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/AdvancedTextRecord.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/AdvancedTextRecord.java @@ -19,6 +19,7 @@ package com.jpexs.decompiler.flash.tags.dynamictext; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.types.TEXTRECORD; +import java.util.List; /** * Text record with font class. @@ -27,6 +28,8 @@ import com.jpexs.decompiler.flash.types.TEXTRECORD; public class AdvancedTextRecord extends TEXTRECORD { public String fontClass = null; + public List htmlSourcePositions = null; + @Override public FontTag getFont(SWF swf) { if (fontClass != null) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/CharacterWithStyle.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/CharacterWithStyle.java index cf19b2c58..b0320e064 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/CharacterWithStyle.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/CharacterWithStyle.java @@ -32,4 +32,9 @@ public class CharacterWithStyle { * Style */ public TextStyle style; + + /** + * Position in HTML source + */ + public int htmlSourcePosition = -1; } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/DynamicTextModel.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/DynamicTextModel.java index 744168dcd..38ad3b308 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/DynamicTextModel.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/DynamicTextModel.java @@ -58,14 +58,15 @@ public class DynamicTextModel { * Add glyph. * @param character Character * @param glyphEntry Glyph entry + * @param htmlSourcePosition Position in HTML source */ - public void addGlyph(char character, GLYPHENTRY glyphEntry) { + public void addGlyph(char character, GLYPHENTRY glyphEntry, int htmlSourcePosition) { if (paragraph == null) { paragraph = new Paragraph(this); paragraphs.add(paragraph); } - paragraph.addGlyph(character, glyphEntry); + paragraph.addGlyph(character, glyphEntry, htmlSourcePosition); } /** diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/GlyphCharacter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/GlyphCharacter.java index c47076ee5..a624f741e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/GlyphCharacter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/GlyphCharacter.java @@ -34,16 +34,23 @@ public class GlyphCharacter { * Character */ public char character; + + /** + * Position in HTML source + */ + public int htmlSourcePosition = -1; /** * Constructor. * * @param character Character * @param glyphEntry Glyph entry + * @param htmlSourcePosition Position in HTML source */ - public GlyphCharacter(char character, GLYPHENTRY glyphEntry) { + public GlyphCharacter(char character, GLYPHENTRY glyphEntry, int htmlSourcePosition) { this.character = character; this.glyphEntry = glyphEntry; + this.htmlSourcePosition = htmlSourcePosition; } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Paragraph.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Paragraph.java index 0b30cc002..dcfd4c8a7 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Paragraph.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Paragraph.java @@ -60,14 +60,15 @@ public class Paragraph { * Add glyph. * @param character Character * @param glyphEntry Glyph entry + * @param htmlSourcePosition Position in HTML source */ - public void addGlyph(char character, GLYPHENTRY glyphEntry) { + public void addGlyph(char character, GLYPHENTRY glyphEntry, int htmlSourcePosition) { if (word == null) { word = new Word(model); words.add(word); } - word.addGlyph(character, glyphEntry); + word.addGlyph(character, glyphEntry, htmlSourcePosition); } /** diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Word.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Word.java index 55cc7c2de..cad1db0e6 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Word.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/dynamictext/Word.java @@ -60,15 +60,16 @@ public class Word { * Adds glyph. * @param character Character * @param glyphEntry Glyph entry + * @param htmlSourcePosition Position in HTML source */ - public void addGlyph(char character, GLYPHENTRY glyphEntry) { + public void addGlyph(char character, GLYPHENTRY glyphEntry, int htmlSourcePosition) { if (record == null) { record = new SameStyleTextRecord(); record.style = model.style; records.add(record); } - record.glyphEntries.add(new GlyphCharacter(character, glyphEntry)); + record.glyphEntries.add(new GlyphCharacter(character, glyphEntry, htmlSourcePosition)); } /** diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlException.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlException.java new file mode 100644 index 000000000..df6aff6c5 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010-2025 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.text.xml; + +/** + * + * @author JPEXS + */ +public class XmlException extends Exception { + public XmlException(String msg) { + super(msg); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlLexer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlLexer.java new file mode 100644 index 000000000..8893ffc0d --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlLexer.java @@ -0,0 +1,744 @@ +/* The following code was generated by JFlex 1.6.0 */ + +/* + * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License + * at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jpexs.decompiler.flash.tags.text.xml; + + +/** + * This class is a scanner generated by + * JFlex 1.6.0 + * from the specification file C:/Dropbox/Programovani/JavaSE/FFDec/libsrc/ffdec_lib/lexers/text_xml.flex + */ +public final class XmlLexer { + + /** This character denotes the end of file */ + public static final int YYEOF = -1; + + /** initial size of the lookahead buffer */ + private static final int ZZ_BUFFERSIZE = 16384; + + /** lexical states */ + public static final int YYINITIAL = 0; + public static final int COMMENT = 2; + public static final int CDATA = 4; + public static final int TAG = 6; + public static final int INSTR = 8; + + /** + * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l + * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l + * at the beginning of a line + * l is of the form l = 2*k, k a non negative integer + */ + private static final int ZZ_LEXSTATE[] = { + 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 + }; + + /** + * Translates characters to character classes + */ + private static final String ZZ_CMAP_PACKED = + "\11\0\1\1\1\3\1\33\1\33\1\2\22\0\1\1\1\7\1\26"+ + "\1\31\2\0\1\4\1\27\5\0\1\10\1\13\1\24\12\14\1\11"+ + "\1\30\1\6\1\25\1\5\1\15\1\0\1\21\1\11\1\17\1\20"+ + "\1\11\16\11\1\22\6\11\1\16\1\0\1\23\1\0\1\11\1\0"+ + "\32\12\12\0\1\33\61\0\1\13\10\0\27\0\1\0\37\0\1\0"+ + "\u0208\0\160\0\16\0\1\0\u02e1\0\12\32\206\0\12\32\306\0\12\32"+ + "\u019c\0\12\32\166\0\12\32\166\0\12\32\166\0\12\32\166\0\12\32"+ + "\166\0\12\32\166\0\12\32\166\0\12\32\166\0\12\32\166\0\12\32"+ + "\140\0\12\32\166\0\12\32\106\0\12\32\u0116\0\12\32\106\0\12\32"+ + "\146\0\u06e0\0\12\32\46\0\12\32\u012c\0\12\32\200\0\12\32\246\0"+ + "\12\32\6\0\12\32\266\0\12\32\126\0\12\32\206\0\12\32\6\0"+ + "\12\32\u03a6\0\14\0\2\0\32\0\1\33\1\33\27\0\57\0\u0120\0"+ + "\u0a70\0\u03f0\0\21\0\u761f\0\12\32\u02a6\0\12\32\46\0\12\32\306\0"+ + "\12\32\26\0\12\32\126\0\12\32\u0196\0\12\32\u2c06\0\u0800\0\u1000\0"+ + "\u0900\0\u04d0\0\40\0\u0120\0\12\32\344\0\u04a2\0\12\32\u0bbc\0\12\32"+ + "\200\0\12\32\74\0\12\32\220\0\12\32\u0116\0\12\32\u01d6\0\12\32"+ + "\u0176\0\12\32\146\0\12\32\u0216\0\12\32\u5176\0\12\32\346\0\12\32"+ + "\u6c74\0\62\32\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\u280f\0"; + + /** + * Translates characters to character classes + */ + private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED); + + /** + * Translates DFA states to action switch labels. + */ + private static final int [] ZZ_ACTION = zzUnpackAction(); + + private static final String ZZ_ACTION_PACKED_0 = + "\5\0\1\1\4\2\1\0\2\2\1\0\1\2\1\3"+ + "\1\4\5\2\3\0\1\5\7\0\1\6\1\7\1\0"+ + "\1\10\1\0\1\11\1\12\3\0\1\13\2\0\1\14"+ + "\1\0\1\15\1\16\1\17\2\0\1\20\1\21\1\22"+ + "\4\0\1\23"; + + private static int [] zzUnpackAction() { + int [] result = new int[61]; + int offset = 0; + offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAction(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /** + * Translates a state to a row index in the transition table + */ + private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); + + private static final String ZZ_ROWMAP_PACKED_0 = + "\0\0\0\34\0\70\0\124\0\160\0\214\0\250\0\304"+ + "\0\340\0\374\0\374\0\u0118\0\u0134\0\u0134\0\u0150\0\u016c"+ + "\0\304\0\u0188\0\u01a4\0\u01c0\0\u01dc\0\u01f8\0\u0214\0\u0230"+ + "\0\u024c\0\u0268\0\u0284\0\u02a0\0\u02bc\0\u02d8\0\u02f4\0\u0310"+ + "\0\u0188\0\304\0\304\0\u01c0\0\304\0\u01dc\0\304\0\304"+ + "\0\u032c\0\u0348\0\u0364\0\u0380\0\u039c\0\u03b8\0\304\0\u03d4"+ + "\0\304\0\304\0\304\0\u03f0\0\u040c\0\304\0\304\0\304"+ + "\0\u0428\0\u0444\0\u0460\0\u047c\0\304"; + + private static int [] zzUnpackRowMap() { + int [] result = new int[61]; + int offset = 0; + offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackRowMap(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int high = packed.charAt(i++) << 16; + result[j++] = high | packed.charAt(i++); + } + return j; + } + + /** + * The transition table of the DFA + */ + private static final int [] ZZ_TRANS = zzUnpackTrans(); + + private static final String ZZ_TRANS_PACKED_0 = + "\4\6\1\7\1\10\1\11\25\6\2\12\1\13\5\12"+ + "\1\14\22\12\1\13\2\15\1\16\20\15\1\17\7\15"+ + "\1\16\1\10\3\20\1\10\1\21\3\10\2\22\4\10"+ + "\4\22\1\10\1\23\1\10\1\24\1\25\3\10\1\0"+ + "\1\10\3\20\5\10\2\22\2\10\1\26\1\10\4\22"+ + "\3\10\1\24\1\25\3\10\1\0\4\6\3\0\25\6"+ + "\12\0\1\27\16\0\1\30\45\0\1\31\1\0\2\32"+ + "\2\0\1\33\1\0\4\32\1\0\1\34\7\0\10\13"+ + "\1\35\33\13\1\36\23\13\23\16\1\37\33\16\1\40"+ + "\10\16\1\0\3\20\40\0\5\41\2\0\4\41\2\0"+ + "\1\42\13\0\1\43\26\0\2\44\2\0\22\44\1\45"+ + "\5\44\2\46\2\0\23\46\1\45\4\46\5\0\1\47"+ + "\40\0\1\27\15\0\1\50\17\0\1\51\15\0\1\51"+ + "\11\0\1\52\5\0\1\53\25\0\5\32\2\0\4\32"+ + "\22\0\2\54\4\0\4\54\22\0\2\55\4\0\4\55"+ + "\11\0\10\13\1\56\30\13\1\57\2\13\1\56\23\13"+ + "\23\16\1\60\15\16\1\61\15\16\1\60\10\16\14\0"+ + "\1\51\13\0\1\62\1\0\1\51\11\0\1\63\42\0"+ + "\1\64\24\0\5\54\2\0\4\54\12\0\3\65\1\0"+ + "\1\66\2\0\5\55\2\0\4\55\11\0\5\13\1\67"+ + "\2\13\1\56\23\13\5\16\1\70\15\16\1\60\10\16"+ + "\20\0\1\71\14\0\3\65\1\0\1\66\47\0\1\72"+ + "\34\0\1\73\32\0\1\74\30\0\1\75\15\0"; + + private static int [] zzUnpackTrans() { + int [] result = new int[1176]; + int offset = 0; + offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackTrans(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + value--; + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /* error codes */ + private static final int ZZ_UNKNOWN_ERROR = 0; + private static final int ZZ_NO_MATCH = 1; + private static final int ZZ_PUSHBACK_2BIG = 2; + + /* error messages for the codes above */ + private static final String ZZ_ERROR_MSG[] = { + "Unkown internal scanner error", + "Error: could not match input", + "Error: pushback value was too large" + }; + + /** + * ZZ_ATTRIBUTE[aState] contains the attributes of state aState + */ + private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); + + private static final String ZZ_ATTRIBUTE_PACKED_0 = + "\5\0\2\1\1\11\2\1\1\0\2\1\1\0\2\1"+ + "\1\11\5\1\3\0\1\1\7\0\2\11\1\0\1\11"+ + "\1\0\2\11\3\0\1\1\2\0\1\11\1\0\3\11"+ + "\2\0\3\11\4\0\1\11"; + + private static int [] zzUnpackAttribute() { + int [] result = new int[61]; + int offset = 0; + offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAttribute(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + /** the input device */ + private java.io.Reader zzReader; + + /** the current state of the DFA */ + private int zzState; + + /** the current lexical state */ + private int zzLexicalState = YYINITIAL; + + /** this buffer contains the current text to be matched and is + the source of the yytext() string */ + private char zzBuffer[] = new char[ZZ_BUFFERSIZE]; + + /** the textposition at the last accepting state */ + private int zzMarkedPos; + + /** the current text position in the buffer */ + private int zzCurrentPos; + + /** startRead marks the beginning of the yytext() string in the buffer */ + private int zzStartRead; + + /** endRead marks the last character in the buffer, that has been read + from input */ + private int zzEndRead; + + /** number of newlines encountered up to the start of the matched text */ + private int yyline; + + /** the number of characters up to the start of the matched text */ + private int yychar; + + /** + * the number of characters from the last newline up to the start of the + * matched text + */ + private int yycolumn; + + /** + * zzAtBOL == true <=> the scanner is currently at the beginning of a line + */ + private boolean zzAtBOL = true; + + /** zzAtEOF == true <=> the scanner is at the EOF */ + private boolean zzAtEOF; + + /** denotes if the user-EOF-code has already been executed */ + private boolean zzEOFDone; + + /** + * The number of occupied positions in zzBuffer beyond zzEndRead. + * When a lead/high surrogate has been read from the input stream + * into the final zzBuffer position, this will have a value of 1; + * otherwise, it will have a value of 0. + */ + private int zzFinalHighSurrogate = 0; + + /* user code: */ + public XmlLexer() { + super(); + } + + private static final byte TAG_OPEN = 1; + private static final byte TAG_CLOSE = -1; + + private static final byte INSTR_OPEN = 2; + private static final byte INSTR_CLOSE = -2; + + private static final byte CDATA_OPEN = 3; + private static final byte CDATA_CLOSE = -3; + + private static final byte COMMENT_OPEN = 4; + private static final byte COMMENT_CLOSE = -4; + + + /** + * Creates a new scanner + * + * @param in the java.io.Reader to read input from. + */ + public XmlLexer(java.io.Reader in) { + this.zzReader = in; + } + + + /** + * Unpacks the compressed character translation table. + * + * @param packed the packed character translation table + * @return the unpacked character translation table + */ + private static char [] zzUnpackCMap(String packed) { + char [] map = new char[0x110000]; + int i = 0; /* index in packed string */ + int j = 0; /* index in unpacked array */ + while (i < 376) { + int count = packed.charAt(i++); + char value = packed.charAt(i++); + do map[j++] = value; while (--count > 0); + } + return map; + } + + + /** + * Refills the input buffer. + * + * @return false, iff there was new input. + * + * @exception java.io.IOException if any I/O-Error occurs + */ + private boolean zzRefill() throws java.io.IOException { + + /* first: make room (if you can) */ + if (zzStartRead > 0) { + zzEndRead += zzFinalHighSurrogate; + zzFinalHighSurrogate = 0; + System.arraycopy(zzBuffer, zzStartRead, + zzBuffer, 0, + zzEndRead-zzStartRead); + + /* translate stored positions */ + zzEndRead-= zzStartRead; + zzCurrentPos-= zzStartRead; + zzMarkedPos-= zzStartRead; + zzStartRead = 0; + } + + /* is the buffer big enough? */ + if (zzCurrentPos >= zzBuffer.length - zzFinalHighSurrogate) { + /* if not: blow it up */ + char newBuffer[] = new char[zzBuffer.length*2]; + System.arraycopy(zzBuffer, 0, newBuffer, 0, zzBuffer.length); + zzBuffer = newBuffer; + zzEndRead += zzFinalHighSurrogate; + zzFinalHighSurrogate = 0; + } + + /* fill the buffer with new input */ + int requested = zzBuffer.length - zzEndRead; + int totalRead = 0; + while (totalRead < requested) { + int numRead = zzReader.read(zzBuffer, zzEndRead + totalRead, requested - totalRead); + if (numRead == -1) { + break; + } + totalRead += numRead; + } + + if (totalRead > 0) { + zzEndRead += totalRead; + if (totalRead == requested) { /* possibly more input available */ + if (Character.isHighSurrogate(zzBuffer[zzEndRead - 1])) { + --zzEndRead; + zzFinalHighSurrogate = 1; + } + } + return false; + } + + // totalRead = 0: End of stream + return true; + } + + + /** + * Closes the input stream. + */ + public final void yyclose() throws java.io.IOException { + zzAtEOF = true; /* indicate end of file */ + zzEndRead = zzStartRead; /* invalidate buffer */ + + if (zzReader != null) + zzReader.close(); + } + + + /** + * Resets the scanner to read from a new input stream. + * Does not close the old reader. + * + * All internal variables are reset, the old input stream + * cannot be reused (internal buffer is discarded and lost). + * Lexical state is set to ZZ_INITIAL. + * + * Internal scan buffer is resized down to its initial length, if it has grown. + * + * @param reader the new input stream + */ + public final void yyreset(java.io.Reader reader) { + zzReader = reader; + zzAtBOL = true; + zzAtEOF = false; + zzEOFDone = false; + zzEndRead = zzStartRead = 0; + zzCurrentPos = zzMarkedPos = 0; + zzFinalHighSurrogate = 0; + yyline = yychar = yycolumn = 0; + zzLexicalState = YYINITIAL; + if (zzBuffer.length > ZZ_BUFFERSIZE) + zzBuffer = new char[ZZ_BUFFERSIZE]; + } + + + /** + * Returns the current lexical state. + */ + public final int yystate() { + return zzLexicalState; + } + + + /** + * Enters a new lexical state + * + * @param newState the new lexical state + */ + public final void yybegin(int newState) { + zzLexicalState = newState; + } + + + /** + * Returns the text matched by the current regular expression. + */ + public final String yytext() { + return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead ); + } + + + /** + * Returns the character at position pos from the + * matched text. + * + * It is equivalent to yytext().charAt(pos), but faster + * + * @param pos the position of the character to fetch. + * A value from 0 to yylength()-1. + * + * @return the character at position pos + */ + public final char yycharat(int pos) { + return zzBuffer[zzStartRead+pos]; + } + + + /** + * Returns the length of the matched text region. + */ + public final int yylength() { + return zzMarkedPos-zzStartRead; + } + + + /** + * Reports an error that occured while scanning. + * + * In a wellformed scanner (no or only correct usage of + * yypushback(int) and a match-all fallback rule) this method + * will only be called with things that "Can't Possibly Happen". + * If this method is called, something is seriously wrong + * (e.g. a JFlex bug producing a faulty scanner etc.). + * + * Usual syntax/scanner level error handling should be done + * in error fallback rules. + * + * @param errorCode the code of the errormessage to display + */ + private void zzScanError(int errorCode) { + String message; + try { + message = ZZ_ERROR_MSG[errorCode]; + } + catch (ArrayIndexOutOfBoundsException e) { + message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; + } + + throw new Error(message); + } + + + /** + * Pushes the specified amount of characters back into the input stream. + * + * They will be read again by then next call of the scanning method + * + * @param number the number of characters to be read again. + * This number must not be greater than yylength()! + */ + public void yypushback(int number) { + if ( number > yylength() ) + zzScanError(ZZ_PUSHBACK_2BIG); + + zzMarkedPos -= number; + } + + + /** + * Resumes scanning until the next regular expression is matched, + * the end of input is encountered or an I/O-Error occurs. + * + * @return the next token + * @exception java.io.IOException if any I/O-Error occurs + */ + public XmlParsedSymbol yylex() throws java.io.IOException, XmlException { + int zzInput; + int zzAction; + + // cached fields: + int zzCurrentPosL; + int zzMarkedPosL; + int zzEndReadL = zzEndRead; + char [] zzBufferL = zzBuffer; + char [] zzCMapL = ZZ_CMAP; + + int [] zzTransL = ZZ_TRANS; + int [] zzRowMapL = ZZ_ROWMAP; + int [] zzAttrL = ZZ_ATTRIBUTE; + + while (true) { + zzMarkedPosL = zzMarkedPos; + + yychar+= zzMarkedPosL-zzStartRead; + + zzAction = -1; + + zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; + + zzState = ZZ_LEXSTATE[zzLexicalState]; + + // set up zzAction for empty match case: + int zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + } + + + zzForAction: { + while (true) { + + if (zzCurrentPosL < zzEndReadL) { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); + zzCurrentPosL += Character.charCount(zzInput); + } + else if (zzAtEOF) { + zzInput = YYEOF; + break zzForAction; + } + else { + // store back cached positions + zzCurrentPos = zzCurrentPosL; + zzMarkedPos = zzMarkedPosL; + boolean eof = zzRefill(); + // get translated positions and possibly new buffer + zzCurrentPosL = zzCurrentPos; + zzMarkedPosL = zzMarkedPos; + zzBufferL = zzBuffer; + zzEndReadL = zzEndRead; + if (eof) { + zzInput = YYEOF; + break zzForAction; + } + else { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); + zzCurrentPosL += Character.charCount(zzInput); + } + } + int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ]; + if (zzNext == -1) break zzForAction; + zzState = zzNext; + + zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + zzMarkedPosL = zzCurrentPosL; + if ( (zzAttributes & 8) == 8 ) break zzForAction; + } + + } + } + + // store back cached position + zzMarkedPos = zzMarkedPosL; + + switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { + case 1: + { return new XmlParsedSymbol(XmlSymbolType.CHARACTER, yytext(), yytext(), yychar); + } + case 20: break; + case 2: + { throw new XmlException("Incorrect text: \"" + yytext() + "\""); + } + case 21: break; + case 3: + { return new XmlParsedSymbol(XmlSymbolType.WHITESPACE, yytext(), yychar); + } + case 22: break; + case 4: + { yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.TAG_OPEN_END, yytext(), yychar); + } + case 23: break; + case 5: + { yybegin(TAG); + return new XmlParsedSymbol(XmlSymbolType.TAG_OPEN, yytext().substring(1), yytext(), yychar); + } + case 24: break; + case 6: + { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE, yytext().substring(0, yytext().length() - 1).trim(), yytext(), yychar); + } + case 25: break; + case 7: + { yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.TAG_CLOSE, yytext(), yychar); + } + case 26: break; + case 8: + { return new XmlParsedSymbol(XmlSymbolType.ATTRIBUTE_VALUE, yytext().substring(1, yytext().length() - 1), yytext(), yychar); + } + case 27: break; + case 9: + { yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.INSTR_CLOSE, yytext(), yychar); + } + case 28: break; + case 10: + { return new XmlParsedSymbol(XmlSymbolType.ENTITY, yytext().substring(1, yytext().length() - 1), yytext(), yychar); + } + case 29: break; + case 11: + { yybegin(INSTR); + return new XmlParsedSymbol(XmlSymbolType.INSTR_OPEN, yytext(), yychar); + } + case 30: break; + case 12: + { yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.COMMENT_CLOSE, yytext(), yychar); + } + case 31: break; + case 13: + { yybegin(YYINITIAL); + return new XmlParsedSymbol(XmlSymbolType.CDATA_CLOSE, yytext(), yychar); + } + case 32: break; + case 14: + { return new XmlParsedSymbol(XmlSymbolType.ENTITY_NUMERIC, Integer.parseInt(yytext().substring(1, yytext().length() - 1)), yytext(), yychar); + } + case 33: break; + case 15: + { yybegin(COMMENT); + return new XmlParsedSymbol(XmlSymbolType.COMMENT_OPEN, yytext(), yychar); + } + case 34: break; + case 16: + { return new XmlParsedSymbol(XmlSymbolType.TAG_CLOSE, yytext().substring(2, yytext().length() - 1), yytext(), yychar); + } + case 35: break; + case 17: + { yypushback(3); + return new XmlParsedSymbol(XmlSymbolType.COMMENT, yytext().substring(yytext().length() - 3), yytext().substring(yytext().length() - 3), yychar); + } + case 36: break; + case 18: + { yypushback(3); + return new XmlParsedSymbol(XmlSymbolType.CDATA, yytext().substring(yytext().length() - 3), yytext().substring(yytext().length() - 3), yychar); + } + case 37: break; + case 19: + { yybegin(CDATA); + return new XmlParsedSymbol(XmlSymbolType.CDATA_OPEN, yytext(), yychar); + } + case 38: break; + default: + if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { + zzAtEOF = true; + switch (zzLexicalState) { + case YYINITIAL: { + return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); + } + case 62: break; + case COMMENT: { + return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); + } + case 63: break; + case CDATA: { + return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); + } + case 64: break; + case TAG: { + return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); + } + case 65: break; + case INSTR: { + return new XmlParsedSymbol(XmlSymbolType.EOF, "", -1); + } + case 66: break; + default: + return null; + } + } + else { + zzScanError(ZZ_NO_MATCH); + } + } + } + } + + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlParsedSymbol.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlParsedSymbol.java new file mode 100644 index 000000000..4d70a2b11 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlParsedSymbol.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010-2025 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.text.xml; + +/** + * + * @author JPEXS + */ +public class XmlParsedSymbol { + public Object value; + + public XmlSymbolType type; + + public String rawText; + + public int position; + + public XmlParsedSymbol(XmlSymbolType type, String rawText, int position) { + this.type = type; + this.value = ""; + this.rawText = rawText; + this.position = position; + } + + public XmlParsedSymbol(XmlSymbolType type, Object value, String rawText, int position) { + this.type = type; + this.value = value; + this.rawText = rawText; + this.position = position; + } + + @Override + public String toString() { + return type.toString() + " " + value.toString(); + } + + public boolean isType(XmlSymbolType... types) { + for (XmlSymbolType t : types) { + if (type == t) { + return true; + } + } + return false; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlSymbolType.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlSymbolType.java new file mode 100644 index 000000000..be73cf866 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/text/xml/XmlSymbolType.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010-2025 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.text.xml; + +/** + * + * @author JPEXS + */ +public enum XmlSymbolType { + ENTITY, + ENTITY_NUMERIC, + INSTR_OPEN, + TAG_OPEN, + TAG_OPEN_END, + TAG_CLOSE, + COMMENT_OPEN, + COMMENT, + COMMENT_CLOSE, + CDATA_OPEN, + CDATA, + CDATA_CLOSE, + CHARACTER, + ATTRIBUTE, + ATTRIBUTE_VALUE, + INSTR_CLOSE, + WHITESPACE, + EOF +} 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 58dd319f5..00bd5da23 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 @@ -1231,28 +1231,30 @@ public class Timeline { } if (drawable instanceof TextTag) { - if (renderContext.cursorPosition != null && renderContext.enableTexts) { + TextTag textTag = (TextTag) drawable; + Matrix textMatrix = new Matrix(textTag.getTextMatrix()); + if (drawable == renderContext.selectionText) { + renderContext.selectionAbsMatrix = absMat.concatenate(textMatrix); + } + if (renderContext.enableTexts) { int dx = (int) (viewRect.xMin * unzoom); int dy = (int) (viewRect.yMin * unzoom); - Point cursorPositionInView = new Point((int) Math.round(renderContext.cursorPosition.x * unzoom) - dx, (int) Math.round(renderContext.cursorPosition.y * unzoom) - dy); + Point cursorPositionInView = renderContext.cursorPosition == null ? new Point(0, 0) : new Point((int) Math.round(renderContext.cursorPosition.x * unzoom) - dx, (int) Math.round(renderContext.cursorPosition.y * unzoom) - dy); Shape textShape = ((TextTag) drawable).getOutline(true, 0, 0, 0, renderContext, absMat, true, viewRect, unzoom); - TextTag textTag = (TextTag) drawable; + if (textShape.contains(cursorPositionInView)) { renderContext.mouseOverText = textTag; + renderContext.mouseOverTextAbsMatrix = absMat; } + if (textShape.contains(cursorPositionInView) || (drawable == renderContext.selectionText && renderContext.mouseButton == 1)) { Rectangle textBounds = textShape.getBounds(); List textRecords = new ArrayList<>(); - Matrix textMatrix = new Matrix(); if (textTag instanceof StaticTextTag) { - textRecords = ((StaticTextTag) textTag).textRecords; - MATRIX tm = ((StaticTextTag) textTag).textMatrix; - if (tm != null) { - textMatrix = new Matrix(tm); - } + textRecords = ((StaticTextTag) textTag).textRecords; } if (textTag instanceof DefineEditTextTag) { textRecords = ((DefineEditTextTag) textTag).getTextRecords(textTag.getSwf()); @@ -1264,6 +1266,8 @@ public class Timeline { int closestPos = -1; double closestDistance = Double.MAX_VALUE; Point cursorPosNoTrans = absMat.concatenate(textMatrix).inverse().transform(cursorPositionInView); + + pos = 0; for (RECT gp : glyphPositions) { /*Rectangle2D r = new Rectangle2D.Double( textBounds.x + gp.Xmin * unzoom, @@ -1301,6 +1305,12 @@ public class Timeline { break; } + /*if (drawable == renderContext.selectionText && pos == renderContext.selectionStart) { + closestPos = pos; + closestDistance = 0; + break; + }*/ + if (cursorPosNoTrans.y >= r.getY() && cursorPosNoTrans.y <= r.getMaxY()) { double tx = Math.max(r.getMinX() - cursorPosNoTrans.getX(), 0); tx = Math.max(tx, cursorPosNoTrans.getX() - r.getMaxX()); @@ -1331,12 +1341,12 @@ public class Timeline { pos++; } - if (closestPos == -1) { + if (closestPos == -1 && renderContext.mouseButton == 1) { if (!glyphPositions.isEmpty()) { RECT gp = glyphPositions.get(0); Rectangle2D r = new Rectangle2D.Double( - gp.Xmin * unzoom, - gp.Ymin * unzoom, + gp.Xmin, + gp.Ymin, gp.Xmax - gp.Xmin, gp.Ymax - gp.Ymin ); @@ -1346,8 +1356,8 @@ public class Timeline { gp = glyphPositions.get(glyphPositions.size() - 1); r = new Rectangle2D.Double( - gp.Xmin * unzoom, - gp.Ymin * unzoom, + gp.Xmin, + gp.Ymin, gp.Xmax - gp.Xmin, gp.Ymax - gp.Ymin ); @@ -1360,8 +1370,8 @@ public class Timeline { if (closestPos > -1) { RECT gp = glyphPositions.get(closestPos); Rectangle2D r = new Rectangle2D.Double( - gp.Xmin * unzoom, - gp.Ymin * unzoom, + gp.Xmin, + gp.Ymin, gp.Xmax - gp.Xmin, gp.Ymax - gp.Ymin ); diff --git a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index 27273e641..500da72a2 100644 --- a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -46,6 +46,7 @@ import com.jpexs.decompiler.flash.tags.base.DrawableTag; import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag; import com.jpexs.decompiler.flash.tags.base.RenderContext; import com.jpexs.decompiler.flash.tags.base.SoundTag; +import com.jpexs.decompiler.flash.tags.base.StaticTextTag; import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.timeline.DepthState; import com.jpexs.decompiler.flash.timeline.Frame; @@ -56,6 +57,7 @@ import com.jpexs.decompiler.flash.types.ConstantColorColorTransform; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.RGB; import com.jpexs.decompiler.flash.types.SOUNDINFO; +import com.jpexs.decompiler.flash.types.TEXTRECORD; import com.jpexs.decompiler.flash.types.filters.BlendComposite; import com.jpexs.helpers.ByteArrayRange; import com.jpexs.helpers.Cache; @@ -149,6 +151,8 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private final List listeners = new ArrayList<>(); + private final List textChangedListeners = new ArrayList<>(); + private Timelined timelined; private boolean stillFrame = false; @@ -285,7 +289,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private boolean mutable = false; private boolean alwaysDisplay = false; - + private boolean allowSelectAllTextTypes = false; private RegistrationPointPosition registrationPointPosition = RegistrationPointPosition.CENTER; @@ -393,15 +397,73 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private SWF guidesSwf = null; private int guidesCharacterId = -1; - - private int textSelectionStart = 0; - private int textSelectionEnd = 0; + + private Rectangle2D textSelectionStartGlyphRect = null; + private Double textSelectionStartGlyphXPosition = null; private Double textSelectionStartPrecise = null; private Double textSelectionEndPrecise = null; private TextTag lastMouseOverText = null; private TextTag textSelectionText = null; private boolean selectingText = false; + private boolean textCursorBlinkOn = false; + + /** + * This was a test to edit texts inline, but it failed horribly. + * You can try to enable it, but the results are bad, very bad. + */ + private boolean editTexts = false; + + private Timer textCursorBlinkTimer; + + private void changeTextSelection(int delta) { + TextTag text = textSelectionText; + if (text == null) { + return; + } + int selStart = getSelectionStartInt(); + List textRecords = new ArrayList<>(); + if (text instanceof StaticTextTag) { + textRecords = ((StaticTextTag) text).textRecords; + } + if (text instanceof DefineEditTextTag) { + textRecords = ((DefineEditTextTag) text).getTextRecords(text.getSwf()); + } + + List glyphPositions = TextTag.getGlyphEntriesPositions(textRecords, text.getSwf()); + + selStart += delta; + + if (selStart < 0) { + selStart = 0; + } + if (selStart > glyphPositions.size()) { + selStart = glyphPositions.size(); + } + + if (glyphPositions.size() == 0) { + textSelectionStartPrecise = (double) selStart; + textSelectionEndPrecise = (double) selStart; + return; + } + + RECT gp = glyphPositions.get(selStart == glyphPositions.size() ? selStart - 1 : selStart); + Rectangle2D r = new Rectangle2D.Double( + gp.Xmin, + gp.Ymin, + gp.Xmax - gp.Xmin, + gp.Ymax - gp.Ymin + ); + textSelectionStartGlyphRect = r; + textSelectionStartGlyphXPosition = r.getX(); + if (selStart == glyphPositions.size()) { + textSelectionStartGlyphXPosition = r.getMaxX(); + } + + textSelectionStartPrecise = (double) selStart; + textSelectionEndPrecise = (double) selStart; + } + private static int getSnapGuidesDistance() { return Configuration.guidesSnapAccuracy.get().getDistance(); } @@ -899,6 +961,20 @@ public final class ImagePanel extends JPanel implements MediaDisplay { listeners.remove(listener); } + public void addTextChangedListener(Runnable listener) { + textChangedListeners.add(listener); + } + + public void removeTextChangedListener(Runnable listener) { + textChangedListeners.remove(listener); + } + + private void fireTextChanged() { + for (Runnable r : textChangedListeners) { + r.run(); + } + } + @Override public Color getBackgroundColor() { if (swf != null && swf.getBackgroundColor() != null) { @@ -998,13 +1074,15 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private ButtonTag mouseOverButton = null; private TextTag mouseOverText = null; - + + private Matrix selectionAbsMatrix = null; + private int glyphPosUnderCursor = -1; - + private Rectangle2D glyphUnderCursorRect = null; - + private double glyphUnderCursorXPosition = 0; - + private boolean autoFit = false; private boolean allowMove = true; @@ -1344,6 +1422,25 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } g2.draw(gp); g2.setComposite(AlphaComposite.SrcOver); + + Rectangle2D curRect = textSelectionStartGlyphRect; + Matrix mat = iconPanel.selectionAbsMatrix; + Double xPos = textSelectionStartGlyphXPosition; + if (textCursorBlinkOn && curRect != null && mat != null && textSelectionStartPrecise != null && xPos != null) { + double rectPos = (xPos - curRect.getX()) / curRect.getWidth(); + if (rectPos < 0.7) { + curRect = new Rectangle2D.Double(curRect.getX(), curRect.getY(), 1 * SWF.unitDivisor, curRect.getHeight()); + } else { + curRect = new Rectangle2D.Double(curRect.getMaxX(), curRect.getY(), 1 * SWF.unitDivisor, curRect.getHeight()); + } + + Matrix matScale = Matrix.getScaleInstance(1 / SWF.unitDivisor); + Shape shape = mat.preConcatenate(matScale).toTransform().createTransformedShape(curRect); + g2.setColor(Color.black); + g2.fill(shape); + /*g2.setStroke(new BasicStroke(2, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL)); + g2.draw(shape);*/ + } } } catch (InternalError ie) { //On some devices like Linux X11 - BlendComposite.Invert is not available @@ -1442,8 +1539,58 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } + @Override + public void keyTyped(KeyEvent e) { + + if (!editTexts) { + return; + } + + if (e.getKeyChar() == KeyEvent.VK_DELETE) { + if (textSelectionText != null) { + TextTag text = textSelectionText; + int selStart = getSelectionStartInt(); + if (text != null && selStart > -1) { + text.removeCharacterGlyph(selStart); + fireTextChanged(); + } + } + return; + } + + if (e.getKeyChar() == KeyEvent.VK_BACK_SPACE) { + if (textSelectionText != null) { + TextTag text = textSelectionText; + int selStart = getSelectionStartInt(); + if (text != null && selStart > 0) { + changeTextSelection(-1); + text.removeCharacterGlyph(selStart - 1); + fireTextChanged(); + } + } + return; + } + char c = e.getKeyChar(); + TextTag text = textSelectionText; + int selStart = getSelectionStartInt(); + if (text != null && selStart > -1) { + text.insertCharacterGlyph(selStart, c); + fireTextChanged(); + changeTextSelection(+1); + } + } + @Override public void keyPressed(KeyEvent e) { + if (textSelectionText != null) { + if (e.getKeyCode() == KeyEvent.VK_RIGHT) { + changeTextSelection(+1); + } + if (e.getKeyCode() == KeyEvent.VK_LEFT) { + changeTextSelection(-1); + } + } + if (hilightedPoints != null) { if (e.getKeyCode() == KeyEvent.VK_DELETE) { List selectedPointsDesc = new ArrayList<>(selectedPoints); @@ -4065,7 +4212,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { mouseButton = e.getButton(); lastMouseEvent = e; redraw(); - + TextTag text = iconPanel.mouseOverText; if (text == null || (!allowSelectAllTextTypes && (!(text instanceof DefineEditTextTag) || ((DefineEditTextTag) text).noSelect))) { text = null; @@ -4078,6 +4225,8 @@ public final class ImagePanel extends JPanel implements MediaDisplay { textSelectionStartPrecise = iconPanel.glyphPosUnderCursor + (glyphRect.getWidth() == 0 ? 0 : (iconPanel.glyphUnderCursorXPosition - glyphRect.getX()) / glyphRect.getWidth()); textSelectionEndPrecise = null; selectingText = true; + textSelectionStartGlyphRect = iconPanel.glyphUnderCursorRect; + textSelectionStartGlyphXPosition = iconPanel.glyphUnderCursorXPosition; } } else { textSelectionStartPrecise = null; @@ -4085,7 +4234,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } else { textSelectionStartPrecise = null; } - + ButtonTag button = iconPanel.mouseOverButton; if (button != null && !doFreeTransform && !frozenButtons) { DefineButtonSoundTag sounds = button.getSounds(); @@ -4130,7 +4279,19 @@ public final class ImagePanel extends JPanel implements MediaDisplay { synchronized (ImagePanel.this) { mouseButton = 0; lastMouseEvent = e; - redraw(); + redraw(); + + TextTag text = iconPanel.mouseOverText; + if (text == null || (!allowSelectAllTextTypes && (!(text instanceof DefineEditTextTag) || ((DefineEditTextTag) text).noSelect))) { + text = null; + } + if (selectingText && textSelectionStartPrecise != null && text != null && !doFreeTransform && SwingUtilities.isLeftMouseButton(e)) { + Rectangle2D glyphRect = iconPanel.glyphUnderCursorRect; + if (iconPanel.glyphPosUnderCursor > -1 && glyphRect != null) { + textSelectionEndPrecise = iconPanel.glyphPosUnderCursor + (glyphRect.getWidth() == 0 ? 0 : (iconPanel.glyphUnderCursorXPosition - glyphRect.getX()) / glyphRect.getWidth()); + } + } + ButtonTag button = iconPanel.mouseOverButton; if (!muted && button != null && !doFreeTransform && !frozenButtons) { DefineButtonSoundTag sounds = button.getSounds(); @@ -4159,7 +4320,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { synchronized (ImagePanel.this) { lastMouseEvent = e; redraw(); - + TextTag text = iconPanel.mouseOverText; if (text == null || (!allowSelectAllTextTypes && (!(text instanceof DefineEditTextTag) || ((DefineEditTextTag) text).noSelect))) { text = null; @@ -4173,6 +4334,16 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } }); + textCursorBlinkTimer = new Timer(); + textCursorBlinkTimer.schedule(new TimerTask() { + @Override + public void run() { + if (textSelectionStartPrecise != null && editTexts) { + textCursorBlinkOn = !textCursorBlinkOn; + repaint(); + } + } + }, 500, 500); //*/ } @@ -4549,7 +4720,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { hilightedEdge = null; hilightedPoints = null; selectedDepths = new ArrayList<>(); - selectedPoints = new ArrayList<>(); + selectedPoints = new ArrayList<>(); pointEditPanel.setVisible(false); this.showObjectsUnderCursor = showObjectsUnderCursor; this.registrationPointPosition = RegistrationPointPosition.CENTER; @@ -5262,6 +5433,29 @@ public final class ImagePanel extends JPanel implements MediaDisplay { return viewRect; } + private int getSelectionStartInt() { + Double selStart = textSelectionStartPrecise; + Double selEnd = textSelectionEndPrecise; + + if (selStart == null) { + return -1; + } + + if (selStart != null && selEnd != null) { + if (selStart > selEnd) { + double tmp = selStart; + selStart = selEnd; + selEnd = tmp; + } + } + int selStartInt = (int) Math.floor(selStart); + double startFract = selStart - selStartInt; + if (startFract > 0.7) { + selStartInt++; + } + return selStartInt; + } + private void drawFrame(Timer thisTimer, boolean display) { Timelined timelined; MouseEvent lastMouseEvent; @@ -5323,7 +5517,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { renderContext.mouseButton = mouseButton; renderContext.stateUnderCursor = new ArrayList<>(); renderContext.enableButtons = !frozenButtons; - + Double selStart = textSelectionStartPrecise; Double selEnd = textSelectionEndPrecise; if (selStart != null && selEnd != null) { @@ -5335,18 +5529,18 @@ public final class ImagePanel extends JPanel implements MediaDisplay { int selStartInt = (int) Math.floor(selStart); double startFract = selStart - selStartInt; int selEndInt = (int) Math.floor(selEnd); - double endFract = selEnd - selEndInt; - - if (endFract > 0.3) { + double endFract = selEnd - selEndInt; + + if (endFract > 0.7) { selEndInt++; } if (startFract > 0.7) { selStartInt++; } - + renderContext.selectionStart = selStartInt; renderContext.selectionEnd = selEndInt; - renderContext.selectionText = textSelectionText; + renderContext.selectionText = textSelectionText; } SerializableImage img; @@ -5542,6 +5736,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { lastMouseOverText = iconPanel.mouseOverText; iconPanel.mouseOverButton = renderContext.mouseOverButton; iconPanel.mouseOverText = renderContext.mouseOverText; + iconPanel.selectionAbsMatrix = renderContext.selectionAbsMatrix; iconPanel.glyphUnderCursorRect = renderContext.glyphUnderCursorRect; iconPanel.glyphUnderCursorXPosition = renderContext.glyphUnderCursorXPosition; iconPanel.glyphPosUnderCursor = renderContext.glyphPosUnderCursor; @@ -5578,7 +5773,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { newCursor = guideYCursor; } else if (iconPanel.isAltDown() && !selectionMode && !doFreeTransform) { if (depthStateUnderCursor == null) { - newCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); + newCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); } else { newCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); } @@ -5589,7 +5784,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } else if (zoomAvailable && iconPanel.hasAllowMove()) { newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); } else { - newCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); + newCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR); } if (iconPanel.getCursor() != newCursor) { //call setcursor only when needed to avoid cursor flickering when dragging in the tree iconPanel.setCursor(newCursor); @@ -5598,14 +5793,14 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } ); - + if (lastMouseOverText != renderContext.mouseOverText) { /*textSelectionStart = 0; textSelectionEnd = 0; textSelectionStartPrecise = null; textSelectionEndPrecise = null;*/ - } - + } + if (!muted) { if (lastMouseOverButton != renderContext.mouseOverButton) { ButtonTag b = renderContext.mouseOverButton; diff --git a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java index f641fa562..464022ae0 100644 --- a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java @@ -590,6 +590,13 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel JPanel previewCnt = new JPanel(new BorderLayout()); imagePanel = new ImagePanel(); + imagePanel.addTextChangedListener(new Runnable() { + @Override + public void run() { + textPanel.refresh(); + } + }); + imagePanel.addPlaceObjectSelectedListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { diff --git a/src/com/jpexs/decompiler/flash/gui/TextPanel.java b/src/com/jpexs/decompiler/flash/gui/TextPanel.java index 7b185ca1a..1beda6b69 100644 --- a/src/com/jpexs/decompiler/flash/gui/TextPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/TextPanel.java @@ -167,9 +167,11 @@ public class TextPanel extends JPanel implements TagEditorPanel { public SearchPanel getSearchPanel() { return textSearchPanel; } - - public void setText(TextTag textTag) { - this.textTag = textTag; + + public void refresh() { + if (this.textTag == null) { + return; + } String formattedText; try { formattedText = textTag.getFormattedText(false).text; @@ -178,6 +180,11 @@ public class TextPanel extends JPanel implements TagEditorPanel { } textValue.setText(formattedText); + } + + public void setText(TextTag textTag) { + this.textTag = textTag; + refresh(); textValue.setCaretPosition(0); setModified(false); setEditText(false);