diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index f1fda9b3a..5be229a07 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -23,7 +23,7 @@ jobs: sed -i 's/debug="false"/debug="true"/' build.xml ant -noinput -buildfile build.xml - name: Upload artifact - if: ${{ github.actor != 'NearInfinityBrowser' }} + if: ${{ github.repository == 'Argent77/NearInfinity' }} uses: pyTooling/Actions/releaser@r0 with: tag: nightly diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..fd85e7579 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,138 @@ +# This workflow will build a Java project with Apache Ant and upload the created artifacts +# on pushes to the master branch. + +name: Java CD with Apache Ant + +on: + push: + branches: [ "master" ] + workflow_dispatch: + branches: [ "master", "devel" ] + +permissions: + contents: read + +jobs: + # Build and upload NearInfinity.jar + deploy-jar: + if: ${{ github.repository == 'Argent77/NearInfinity' }} + runs-on: ubuntu-latest + name: Build NearInfinity.jar + outputs: + ni_version: ${{ steps.ni-build.outputs.NI_VERSION }} + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + + - name: Build with Ant + id: ni-build + run: | + ant -noinput -buildfile build.xml + echo "NI_VERSION=$(java -jar "NearInfinity.jar" -version 2>/dev/null | grep -Eo '[0-9]{8}')" >> "$GITHUB_OUTPUT" + + - name: Upload JAR artifact + uses: actions/upload-artifact@v3 + with: + name: NearInfinity-${{ steps.ni-build.outputs.NI_VERSION }} + path: NearInfinity.jar + + # Build and upload installer versions for Windows and macOS + deploy-installer: + if: ${{ github.repository == 'Argent77/NearInfinity' }} + needs: deploy-jar + strategy: + fail-fast: false + matrix: + os: [ windows-latest, macos-latest ] + java: [ '21' ] + runs-on: ${{ matrix.os }} + name: Create installer for ${{ matrix.os }}, JDK ${{ matrix.java }} + steps: + # Initializations + - name: Git checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + + - name: Echo JAVA_HOME (windows) + if: (matrix.os == 'windows-latest') + run: | + echo $env:JAVA_HOME + java -version + + - name: Echo JAVA_HOME (macos) + if: (matrix.os != 'windows-latest') + run: | + echo $JAVA_HOME + java -version + + # Preparations + - name: Download JAR artifact + uses: actions/download-artifact@v3 + with: + name: NearInfinity-${{ needs.deploy-jar.outputs.ni_version }} + path: jar + + - name: Set up installer data + uses: actions/checkout@v4 + with: + repository: NearInfinityBrowser/NearInfinity-assets + path: assets + + # Building + - name: Build portable archive and installer (windows) + if: (matrix.os == 'windows-latest') + run: | + move assets\redistributable\windows\package . + move assets\redistributable\windows\build-image.cmd . + move assets\redistributable\windows\build-installer.cmd . + .\build-image.cmd + .\build-installer.cmd + + - name: Build installer (macos) + if: (matrix.os == 'macos-latest') + run: | + mv assets/redistributable/macos/package . + mv assets/redistributable/macos/build.command . + chmod +x build.command + ./build.command + + # Validation + - name: List built files (windows) + if: (matrix.os == 'windows-latest') + run: dir + + - name: List built files (macos) + if: (matrix.os == 'macos-latest') + run: ls -l + + # Uploading + - name: Upload portable artifact (windows) + if: (matrix.os == 'windows-latest') + uses: actions/upload-artifact@v3 + with: + name: portable-windows + path: NearInfinity-*.zip + + - name: Upload exe artifact (windows) + if: (matrix.os == 'windows-latest') + uses: actions/upload-artifact@v3 + with: + name: installer-windows + path: NearInfinity-*.exe + + - name: Upload pkg artifact (macos) + if: (matrix.os == 'macos-latest') + uses: actions/upload-artifact@v3 + with: + name: installer-macos + path: NearInfinity-*.pkg diff --git a/src/org/infinity/AppOption.java b/src/org/infinity/AppOption.java index c693eaaf9..16034647b 100644 --- a/src/org/infinity/AppOption.java +++ b/src/org/infinity/AppOption.java @@ -206,6 +206,9 @@ public class AppOption { public static final AppOption BCS_INDENT = new AppOption(OptionsMenuItem.OPTION_BCS_INDENT, "Indentation", 1); // Category: Misc. Types + /** Menu Options > Misc. Types: AutoAlign2da (Integer, Default: 0) */ + public static final AppOption AUTO_ALIGN_2DA = new AppOption(OptionsMenuItem.OPTION_2DA_AUTOALIGN, + "Auto-Align 2DA Columns", 0); /** Menu Options > Misc. Types: GlslColorScheme (Integer, Default: 0) */ public static final AppOption GLSL_COLOR_SCHEME = new AppOption(OptionsMenuItem.OPTION_GLSL_COLORSCHEME, "GLSL Color Scheme", 0); diff --git a/src/org/infinity/NearInfinity.java b/src/org/infinity/NearInfinity.java index 716c9958e..f6517e91f 100644 --- a/src/org/infinity/NearInfinity.java +++ b/src/org/infinity/NearInfinity.java @@ -136,7 +136,7 @@ public final class NearInfinity extends JFrame implements ActionListener, ViewableContainer { // the current Near Infinity version - private static final String VERSION = "v2.4-20230729"; + private static final String VERSION = "v2.4-20231231"; // the minimum supported Java version private static final int JAVA_VERSION_MIN = 8; @@ -247,6 +247,7 @@ private static Path findKeyfile() { } else { chooser = new JFileChooser(Profile.getGameRoot().toFile()); } + chooser.setFileHidingEnabled(false); chooser.setDialogTitle("Open game: Locate keyfile"); chooser.setFileFilter(new FileFilter() { @Override @@ -888,7 +889,11 @@ public boolean removeViewable() { } public void showResourceEntry(ResourceEntry resourceEntry) { - tree.select(resourceEntry); + showResourceEntry(resourceEntry, null); + } + + public void showResourceEntry(ResourceEntry resourceEntry, Operation doneOperation) { + tree.select(resourceEntry, doneOperation); } public void quit() { diff --git a/src/org/infinity/check/BCSIDSChecker.java b/src/org/infinity/check/BCSIDSChecker.java index 30eb6a78d..dd931a119 100644 --- a/src/org/infinity/check/BCSIDSChecker.java +++ b/src/org/infinity/check/BCSIDSChecker.java @@ -65,9 +65,10 @@ public void actionPerformed(ActionEvent event) { int row = table.getSelectedRow(); if (row != -1) { ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - NearInfinity.getInstance().showResourceEntry(resourceEntry); - BcsResource bcsfile = (BcsResource) NearInfinity.getInstance().getViewable(); - bcsfile.highlightText(((Integer) table.getValueAt(row, 2)), null); + NearInfinity.getInstance().showResourceEntry(resourceEntry, () -> { + final BcsResource bcsfile = (BcsResource) NearInfinity.getInstance().getViewable(); + bcsfile.highlightText(((Integer) table.getValueAt(row, 2)), null); + }); } } else if (event.getSource() == bopennew) { int row = table.getSelectedRow(); diff --git a/src/org/infinity/check/CreInvChecker.java b/src/org/infinity/check/CreInvChecker.java index f15a0baaf..23a8aeecf 100644 --- a/src/org/infinity/check/CreInvChecker.java +++ b/src/org/infinity/check/CreInvChecker.java @@ -68,9 +68,9 @@ public void actionPerformed(ActionEvent event) { int row = table.getSelectedRow(); if (row != -1) { ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - NearInfinity.getInstance().showResourceEntry(resourceEntry); - ((AbstractStruct) NearInfinity.getInstance().getViewable()).getViewer() - .selectEntry(((Item) table.getValueAt(row, 2)).getName()); + NearInfinity.getInstance().showResourceEntry(resourceEntry, + () -> ((AbstractStruct)NearInfinity.getInstance().getViewable()).getViewer() + .selectEntry(((Item)table.getValueAt(row, 2)).getName())); } } else if (event.getSource() == bopennew) { int row = table.getSelectedRow(); diff --git a/src/org/infinity/check/DialogChecker.java b/src/org/infinity/check/DialogChecker.java index d8e5841f4..3d0915621 100644 --- a/src/org/infinity/check/DialogChecker.java +++ b/src/org/infinity/check/DialogChecker.java @@ -84,9 +84,10 @@ public void actionPerformed(ActionEvent event) { int row = table.getSelectedRow(); if (row != -1) { ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - NearInfinity.getInstance().showResourceEntry(resourceEntry); - ((AbstractStruct) NearInfinity.getInstance().getViewable()).getViewer() - .selectEntry((String) table.getValueAt(row, 1)); + final SortableTable tableCapture = table; + NearInfinity.getInstance().showResourceEntry(resourceEntry, + () -> ((AbstractStruct)NearInfinity.getInstance().getViewable()).getViewer() + .selectEntry((String)tableCapture.getValueAt(row, 1))); } } else if (event.getSource() == bopennew) { int row = table.getSelectedRow(); diff --git a/src/org/infinity/check/ScriptChecker.java b/src/org/infinity/check/ScriptChecker.java index 93d56cf35..096dc2b99 100644 --- a/src/org/infinity/check/ScriptChecker.java +++ b/src/org/infinity/check/ScriptChecker.java @@ -76,9 +76,10 @@ public void actionPerformed(ActionEvent event) { int row = table.getSelectedRow(); if (row != -1) { ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - NearInfinity.getInstance().showResourceEntry(resourceEntry); - ((BcsResource) NearInfinity.getInstance().getViewable()).highlightText(((Integer) table.getValueAt(row, 2)), - null); + final SortableTable tableCapture = table; + NearInfinity.getInstance().showResourceEntry(resourceEntry, + () -> ((BcsResource)NearInfinity.getInstance().getViewable()) + .highlightText(((Integer)tableCapture.getValueAt(row, 2)), null)); } } else if (event.getSource() == bopennew) { int row = table.getSelectedRow(); diff --git a/src/org/infinity/check/StructChecker.java b/src/org/infinity/check/StructChecker.java index f740bea03..15abd7a2b 100644 --- a/src/org/infinity/check/StructChecker.java +++ b/src/org/infinity/check/StructChecker.java @@ -11,6 +11,7 @@ import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -29,6 +30,7 @@ import org.infinity.datatype.IsNumeric; import org.infinity.datatype.IsReference; import org.infinity.datatype.IsTextual; +import org.infinity.datatype.SectionOffset; import org.infinity.datatype.StringRef; import org.infinity.datatype.TextString; import org.infinity.gui.Center; @@ -39,12 +41,16 @@ import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; +import org.infinity.resource.AddRemovable; +import org.infinity.resource.Profile; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.StructEntry; +import org.infinity.resource.Viewable; import org.infinity.resource.bcs.Compiler; import org.infinity.resource.bcs.ScriptMessage; import org.infinity.resource.bcs.ScriptType; +import org.infinity.resource.cre.CreResource; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.sto.ItemSale11; import org.infinity.resource.wed.Overlay; @@ -99,14 +105,12 @@ public void actionPerformed(ActionEvent event) { if (event.getSource() == bopen) { int row = table.getSelectedRow(); if (row != -1) { - ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - NearInfinity.getInstance().showResourceEntry(resourceEntry); + showInViewer((Corruption) table.getTableItemAt(row), false); } } else if (event.getSource() == bopennew) { int row = table.getSelectedRow(); if (row != -1) { - ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - new ViewFrame(resultFrame, ResourceFactory.getResource(resourceEntry)); + showInViewer((Corruption) table.getTableItemAt(row), true); } } else if (event.getSource() == bsave) { table.saveCheckResult(resultFrame, "Corrupted files"); @@ -170,10 +174,7 @@ public void mouseReleased(MouseEvent event) { if (event.getClickCount() == 2) { final int row = table.getSelectedRow(); if (row != -1) { - final ResourceEntry resourceEntry = (ResourceEntry) table.getValueAt(row, 0); - final Resource resource = ResourceFactory.getResource(resourceEntry); - new ViewFrame(resultFrame, resource); - ((AbstractStruct) resource).getViewer().selectEntry((String) table.getValueAt(row, 1)); + showInViewer((Corruption) table.getTableItemAt(row), true); } } } @@ -260,6 +261,67 @@ private void search(ResourceEntry entry, AbstractStruct struct) { } } + // Checking for valid substructure offsets and counts + // calculating size of static portion of the resource data + final HashSet> removableSet = new HashSet<>(); + int headerSize = 0; + if (Profile.hasProperty(Profile.Key.IS_SUPPORTED_CRE_V22) && entry.getExtension().equalsIgnoreCase("CRE")) { + // special: CRE V2.2 static size cannot be determined dynamically + final String version = ((TextString) struct.getAttribute(AbstractStruct.COMMON_VERSION)).getText(); + if ("V2.2".equalsIgnoreCase(version)) { + headerSize = 0x62e; + } + } + if (headerSize == 0) { + for (final StructEntry field : struct.getFields()) { + if (field instanceof SectionOffset) { + final Class cls = ((SectionOffset) field).getSection(); + removableSet.add(cls); + } + if (field instanceof AddRemovable || removableSet.contains(field.getClass())) { + headerSize = field.getOffset(); + break; + } else { + headerSize = field.getOffset() + field.getSize(); + } + } + } + removableSet.clear(); + + // CHR offset correction for embedded CRE data + int ofsOffset = 0; + if (entry.getExtension().equalsIgnoreCase("CHR")) { + final StructEntry se = struct.getAttribute(CreResource.CHR_SIGNATURE_2); + if (se != null) { + ofsOffset = se.getOffset(); + } + } + + // checking offsets + for (final StructEntry field : struct.getFields()) { + if (field.getOffset() >= headerSize) { + break; + } + if (field instanceof SectionOffset) { + final SectionOffset so = (SectionOffset) field; + if (so.getValue() + ofsOffset < headerSize) { + synchronized (table) { + table.addTableItem(new Corruption(entry, so.getOffset(), + "Offset field points to header data (field name: \"" + so.getName() + "\", offset: " + + Integer.toHexString(so.getValue()) + "h, header size: " + + Integer.toHexString(headerSize - ofsOffset) + "h)")); + } + } else if (so.getValue() + ofsOffset > struct.getSize()) { + synchronized (table) { + table.addTableItem(new Corruption(entry, so.getOffset(), + "Offset field value is out of range (field name: \"" + so.getName() + "\", offset: " + + Integer.toHexString(so.getValue()) + "h, resource size: " + + Integer.toHexString(struct.getSize() - ofsOffset) + "h)")); + } + } + } + } + // Type-specific checks if (entry.getExtension().equalsIgnoreCase("WED")) { List list = getWedCorruption(entry, struct); @@ -415,16 +477,71 @@ private List getWedCorruption(ResourceEntry entry, AbstractStruct st } return list; } + + /** + * Opens a view of the referenced resources and selects the field at the offset in question. + * + * @param corruption {@link Corruption} instance with error information. + * @param newWindow Whether to open the resource in a new window. + */ + private void showInViewer(Corruption corruption, boolean newWindow) { + if (corruption == null) { + return; + } + + if (newWindow) { + final ResourceEntry entry = corruption.getResourceEntry(); + final Resource res = ResourceFactory.getResource(entry); + final int offset = corruption.getOffset(); + new ViewFrame(resultFrame, res); + if (res instanceof AbstractStruct) { + try { + ((AbstractStruct) res).getViewer().selectEntry(offset); + } catch (Exception e) { + e.printStackTrace(); + } + } + } else { + final ResourceEntry entry = corruption.getResourceEntry(); + final int offset = corruption.getOffset(); + NearInfinity.getInstance().showResourceEntry(entry); + if (parent instanceof ViewFrame && parent.isVisible()) { + final Resource res = ResourceFactory.getResource(entry); + ((ViewFrame) parent).setViewable(res); + if (res instanceof AbstractStruct) { + try { + ((AbstractStruct) res).getViewer().selectEntry(offset); + } catch (Exception e) { + e.printStackTrace(); + } + } + } else { + NearInfinity.getInstance().showResourceEntry(entry, () -> { + final Viewable viewable = NearInfinity.getInstance().getViewable(); + if (viewable instanceof AbstractStruct) { + try { + ((AbstractStruct) viewable).getViewer().selectEntry(offset); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + } + } + // -------------------------- INNER CLASSES -------------------------- private static final class Corruption implements TableItem { private final ResourceEntry resourceEntry; - private final String offset; + private final int offset; + private final String offsetString; private final String errorMsg; private Corruption(ResourceEntry resourceEntry, int offset, String errorMsg) { this.resourceEntry = resourceEntry; - this.offset = Integer.toHexString(offset) + 'h'; + this.offset= offset; + this.offsetString = Integer.toHexString(offset) + 'h'; this.errorMsg = errorMsg; } @@ -433,15 +550,27 @@ public Object getObjectAt(int columnIndex) { if (columnIndex == 0) { return resourceEntry; } else if (columnIndex == 1) { - return offset; + return offsetString; } else { return errorMsg; } } + public ResourceEntry getResourceEntry() { + return resourceEntry; + } + + public int getOffset() { + return offset; + } + + public String getMessage() { + return errorMsg; + } + @Override public String toString() { - return "File: " + resourceEntry.getResourceName() + ", Offset: " + offset + ", Error: " + errorMsg; + return "File: " + resourceEntry.getResourceName() + ", Offset: " + offsetString + ", Error: " + errorMsg; } } diff --git a/src/org/infinity/datatype/ResourceRef.java b/src/org/infinity/datatype/ResourceRef.java index 99888d640..c84033364 100644 --- a/src/org/infinity/datatype/ResourceRef.java +++ b/src/org/infinity/datatype/ResourceRef.java @@ -4,6 +4,7 @@ package org.infinity.datatype; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -38,8 +39,11 @@ import org.infinity.gui.menu.BrowserMenuBar; import org.infinity.icon.Icons; import org.infinity.resource.AbstractStruct; +import org.infinity.resource.Closeable; +import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.key.ResourceEntry; +import org.infinity.resource.sound.SoundResource; import org.infinity.util.Misc; import org.infinity.util.io.StreamUtils; @@ -80,12 +84,18 @@ public class ResourceRef extends Datatype /** Button that used to open editor of current selected element in the list. */ private JButton bView; + /** Button that used to play sound of current selected element in the list. */ + private JButton bPlay; + /** * GUI component that lists all available resources that can be set to this resource reference and have edit field for * ability to enter resource reference manually. */ private TextListPanel list; + /** Contains the {@link Resource} of the currently selected resource reference. */ + private Resource currentResource; + /** * Returns a list of resource extensions that can be used to display associated icons. * @return String set with file extensions (without leading dot). @@ -117,6 +127,15 @@ public void actionPerformed(ActionEvent event) { if (isEditable(selected)) { new ViewFrame(list.getTopLevelAncestor(), ResourceFactory.getResource(selected.entry)); } + } else if (event.getSource() == bPlay) { + final ResourceRefEntry selected = list.getSelectedValue(); + if (isSound(selected)) { + // prevent overlapping sound playback + closeResource(currentResource); + SoundResource res = (SoundResource) ResourceFactory.getResource(selected.entry); + res.playSound(); + currentResource = res; + } } } @@ -182,21 +201,41 @@ public void mouseClicked(MouseEvent event) { bUpdate.setActionCommand(StructViewer.UPDATE_VALUE); bView = new JButton("View/Edit", Icons.ICON_ZOOM_16.getIcon()); bView.addActionListener(this); - bView.setEnabled(isEditable(list.getSelectedValue())); + bPlay = new JButton("Play", Icons.ICON_PLAY_16.getIcon()); + bPlay.addActionListener(this); list.addListSelectionListener(this); + setResourceEntryUpdated(list.getSelectedValue()); GridBagConstraints gbc = null; JPanel panel = new JPanel(new GridBagLayout()); - gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 2, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + gbc = ViewerUtil.setGBC(gbc, 0, 0, 1, 5, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0); panel.add(list, gbc); - gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.SOUTH, GridBagConstraints.HORIZONTAL, + + // spacer keeps controls in the center + final JPanel spacerTop = new JPanel(); + spacerTop.setMinimumSize(new Dimension()); + gbc = ViewerUtil.setGBC(gbc, 1, 0, 1, 1, 0.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0); + panel.add(spacerTop, gbc); + + gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(3, 6, 3, 0), 0, 0); panel.add(bUpdate, gbc); - gbc = ViewerUtil.setGBC(gbc, 1, 1, 1, 1, 0.0, 1.0, GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, + gbc = ViewerUtil.setGBC(gbc, 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(3, 6, 3, 0), 0, 0); panel.add(bView, gbc); + gbc = ViewerUtil.setGBC(gbc, 1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + new Insets(24, 6, 3, 0), 0, 0); + panel.add(bPlay, gbc); + + // spacer keeps controls in the center + final JPanel spacerBottom = new JPanel(); + spacerTop.setMinimumSize(new Dimension()); + gbc = ViewerUtil.setGBC(gbc, 1, 4, 1, 1, 0.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0); + panel.add(spacerBottom, gbc); panel.setMinimumSize(Misc.getScaledDimension(DIM_MEDIUM)); panel.setPreferredSize(panel.getMinimumSize()); @@ -251,7 +290,7 @@ public boolean updateValue(AbstractStruct struct) { @Override public void valueChanged(ListSelectionEvent e) { - bView.setEnabled(isEditable(list.getSelectedValue())); + setResourceEntryUpdated(list.getSelectedValue()); } @Override @@ -373,10 +412,35 @@ public boolean isLegalEntry(ResourceEntry entry) { void addExtraEntries(List entries) { } + private void setResourceEntryUpdated(ResourceRefEntry entry) { + closeResource(currentResource); + if (entry != null) { + bView.setEnabled(isEditable(entry)); + bPlay.setEnabled(isSound(entry)); + } else { + bView.setEnabled(false); + bPlay.setEnabled(false); + } + } + + private void closeResource(Resource resource) { + if (resource instanceof Closeable) { + try { + ((Closeable) resource).close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + private boolean isEditable(ResourceRefEntry ref) { return ref != null && ref != NONE && ref.entry != null; } + private boolean isSound(ResourceRefEntry ref) { + return ref != null && ref != NONE && ref.entry != null && ref.entry.isSound(); + } + private void setValue(String newValue) { final String oldValue = NONE.name.equals(resname) ? null : resname; resname = newValue; diff --git a/src/org/infinity/gui/PreferencesDialog.java b/src/org/infinity/gui/PreferencesDialog.java index 547e5fc1c..4d7465c49 100644 --- a/src/org/infinity/gui/PreferencesDialog.java +++ b/src/org/infinity/gui/PreferencesDialog.java @@ -95,6 +95,14 @@ * that were formerly presented as individual entries in the "Options" menu. */ public class PreferencesDialog extends JDialog { + /** + * Blacklisted L&F themes: These themes should not be listed in the Preferences dialog. + *

+ * List contains the fully qualified class names of L&F themes. + *

+ */ + private static final List LOOK_AND_FEEL_BLACKLIST = Arrays.asList("javax.swing.plaf.nimbus.NimbusLookAndFeel"); + /** Definition of category names. */ public enum Category { DEFAULT(""), @@ -277,6 +285,16 @@ public String toString() { ) ), OptionCategory.create(Category.MISC_RESOURCE_TYPES, + OptionGroup.create("2DA", + OptionGroupBox.create(AppOption.AUTO_ALIGN_2DA.getName(), AppOption.AUTO_ALIGN_2DA.getLabel(), + "Choose how to to automatically align 2DA table columns when the resource is opened.

" + + "Disabled: Table data is not modified.
" + + "Compact: Column widths are calculated individually.
" + + "Uniform: Column widths are calculated evenly (comparable to Weidu's PRETTY_PRINT_2DA.)" + + "

Note: Formatting is discarded when the resource is closed unless the changes " + + "are explicitly saved.

", + 0, OptionsMenuItem.AutoAlign2da.values(), AppOption.AUTO_ALIGN_2DA) + ), OptionGroup.create("GLSL", OptionGroupBox.create(AppOption.GLSL_COLOR_SCHEME.getName(), AppOption.GLSL_COLOR_SCHEME.getLabel(), "Select a color scheme for GLSL resources.

" @@ -455,9 +473,7 @@ public String toString() { OptionGroupBox.create(AppOption.LOOK_AND_FEEL_CLASS.getName(), AppOption.LOOK_AND_FEEL_CLASS.getLabel(), "Choose a Look & Feel theme for the GUI." + "

Metal is the default L&F theme and provides the most consistent user experience. " - + "It is available on all platforms.

" - + "

Note: It is not recommended to use the \"Nimbus\" L&F theme. The theme " - + "initializes an incomplete set of UI properties, which can result in display errors.

", + + "It is available on all platforms.

", 0, new DataItem[0], AppOption.LOOK_AND_FEEL_CLASS) .setOnInit(this::lookAndFeelClassOnInit).setOnAccept(this::lookAndFeelClassOnAccept), OptionGroupBox.create(AppOption.TEXT_FONT.getName(), AppOption.TEXT_FONT.getLabel(), @@ -1379,6 +1395,12 @@ private void lookAndFeelClassOnInit(OptionGroupBox gb) { LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels(); for (int i = 0, curIdx = 0; i < info.length; i++) { final LookAndFeelInfo lf = info[i]; + + // check if theme is black-listed + if (lf != null && LOOK_AND_FEEL_BLACKLIST.contains(lf.getClassName())) { + continue; + } + try { // L&F description is only available from class instance final Class cls = Class.forName(lf.getClassName()); diff --git a/src/org/infinity/gui/ResourceTree.java b/src/org/infinity/gui/ResourceTree.java index c1e61779d..05f751c4e 100644 --- a/src/org/infinity/gui/ResourceTree.java +++ b/src/org/infinity/gui/ResourceTree.java @@ -68,6 +68,7 @@ import org.infinity.resource.key.ResourceTreeFolder; import org.infinity.resource.key.ResourceTreeModel; import org.infinity.util.IconCache; +import org.infinity.util.Operation; import org.infinity.util.io.FileEx; import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; @@ -192,24 +193,39 @@ public void select(ResourceEntry entry) { } public void select(ResourceEntry entry, boolean forced) { + select(entry, forced, null); + } + + public void select(ResourceEntry entry, Operation doneOperation) { + select(entry, false, doneOperation); + } + + public void select(ResourceEntry entry, boolean forced, Operation doneOperation) { new SwingWorker() { @Override protected Void doInBackground() throws Exception { - if (entry == null) { - tree.clearSelection(); - } else if (forced || entry != shownResource) { - TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry); - try { - expandListener.treeWillExpand(new TreeExpansionEvent(tree, tp)); - tree.scrollPathToVisible(tp); - tree.addSelectionPath(tp); - tree.repaint(); - } finally { - expandListener.treeExpanded(new TreeExpansionEvent(tree, tp)); - } + if (entry == null) { + tree.clearSelection(); + } else if (forced || entry != shownResource) { + TreePath tp = ResourceFactory.getResourceTreeModel().getPathToNode(entry); + try { + expandListener.treeWillExpand(new TreeExpansionEvent(tree, tp)); + tree.scrollPathToVisible(tp); + tree.addSelectionPath(tp); + tree.repaint(); + } finally { + expandListener.treeExpanded(new TreeExpansionEvent(tree, tp)); } + } return null; } + + @Override + protected void done() { + if (doneOperation != null) { + doneOperation.perform(); + } + } }.execute(); } diff --git a/src/org/infinity/gui/TextListPanel.java b/src/org/infinity/gui/TextListPanel.java index 77b29feda..e749f5de6 100644 --- a/src/org/infinity/gui/TextListPanel.java +++ b/src/org/infinity/gui/TextListPanel.java @@ -41,6 +41,7 @@ import org.infinity.NearInfinity; import org.infinity.datatype.AbstractBitmap; +import org.infinity.datatype.IwdRef; import org.infinity.datatype.ResourceBitmap; import org.infinity.datatype.ResourceRef; import org.infinity.icon.Icons; @@ -345,7 +346,8 @@ public Component getListCellRendererComponent(JList list, Object value, int i } else if (value instanceof AbstractBitmap.FormattedData) { // resolving Resource Bitmap final AbstractBitmap.FormattedData fmt = (AbstractBitmap.FormattedData) value; - if (fmt.getParent() != null) { + // Limit icon preview to parent type: IwdRef + if (fmt.getParent() instanceof IwdRef) { final AbstractBitmap bmp = fmt.getParent(); Object o = bmp.getDataOf(fmt.getValue()); if (o instanceof ResourceBitmap.RefEntry) { diff --git a/src/org/infinity/gui/menu/OptionsMenuItem.java b/src/org/infinity/gui/menu/OptionsMenuItem.java index 86604f6f8..9a31ef589 100644 --- a/src/org/infinity/gui/menu/OptionsMenuItem.java +++ b/src/org/infinity/gui/menu/OptionsMenuItem.java @@ -58,6 +58,28 @@ * Handles Option menu items for the {@link BrowserMenuBar}. */ public class OptionsMenuItem extends JMenuItem implements ActionListener { + /** Alignment types available for 2DA resources. */ + public enum AutoAlign2da { + /** Do not align columns. */ + DISABLED("Disabled"), + /** Align columns individually. */ + COMPACT("Compact"), + /** Align columns evenly (comparable to WeiDU's PRETTY_PRINT_2DA). */ + UNIFORM("Uniform"), + ; + + private final String label; + + private AutoAlign2da(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + // Symbolic name for the default character set private static final String DEFAULT_CHARSET = "Auto"; @@ -151,6 +173,7 @@ public class OptionsMenuItem extends JMenuItem implements ActionListener { public static final String OPTION_BCS_CODEFOLDING = "BcsCodeFolding"; public static final String OPTION_BCS_AUTO_INDENT = "BcsAutoIndent"; public static final String OPTION_BCS_INDENT = "BcsIndent"; + public static final String OPTION_2DA_AUTOALIGN = "AutoAlign2da"; public static final String OPTION_GLSL_SYNTAXHIGHLIGHTING = "GlslSyntaxHighlighting"; public static final String OPTION_GLSL_COLORSCHEME = "GlslColorScheme"; public static final String OPTION_GLSL_CODEFOLDING = "GlslCodeFolding"; @@ -648,6 +671,14 @@ public String getWeiDUColorScheme() { return COLOR_SCHEME.get(idx).getPath(); } + public AutoAlign2da getAutoAlign2da() { + int idx = AppOption.AUTO_ALIGN_2DA.getIntValue(); + if (idx >= 0 && idx < AutoAlign2da.values().length) { + return AutoAlign2da.values()[idx]; + } + return AutoAlign2da.DISABLED; + } + /** Returns whether the dialog tree viewer shows icons in front of state and response entries. */ public boolean showDlgTreeIcons() { return AppOption.DLG_SHOW_ICONS.getBoolValue(); diff --git a/src/org/infinity/resource/AbstractStruct.java b/src/org/infinity/resource/AbstractStruct.java index f025e3462..920119d94 100644 --- a/src/org/infinity/resource/AbstractStruct.java +++ b/src/org/infinity/resource/AbstractStruct.java @@ -19,6 +19,8 @@ import java.util.ListIterator; import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.swing.JComponent; @@ -588,6 +590,7 @@ public int addDatatype(AddRemovable addedEntry, int index) { } setStructChanged(true); fireTableRowsInserted(index, index); + updateRemovableIndices(addedEntry.getClass()); return index; } @@ -947,6 +950,7 @@ public void removeDatatype(AddRemovable removedEntry, boolean removeRecurse) { superStruct.datatypeRemovedInChild(this, removedEntry); } fireTableRowsDeleted(index, index); + updateRemovableIndices(removedEntry.getClass()); setStructChanged(true); } @@ -1047,6 +1051,52 @@ public void setStructChanged(boolean changed) { } } + /** + * Ensures that all {@link AddRemovable} of the specified type are properly indexed in sequential order. + * + * @param cls Class type of the AddRemovables to update. + */ + public void updateRemovableIndices(Class cls) { + if (cls == null) { + return; + } + + final List fieldList = new ArrayList<>(getFields(cls)); + if (fieldList.isEmpty()) { + return; + } + fieldList.sort((f1, f2) -> f1.getOffset() - f2.getOffset()); + + int minIndex = Integer.MAX_VALUE; + int maxIndex = Integer.MIN_VALUE; + final Pattern patName = Pattern.compile("^(.+) ([0-9]+)$"); + for (int i = 0, size = fieldList.size(); i < size; i++) { + final StructEntry entry = fieldList.get(i); + final Matcher m = patName.matcher(entry.getName()); + final String name; + if (m.matches()) { + // existing or copy-pasted entry + name = m.group(1); +// System.out.printf("Existing/pasted: name=%s, newIndex=%d\n", entry.getName(), i); + } else { + // newly added entry + name = entry.getName(); +// System.out.printf("Newly added: name=%s, newIndex=%d\n", name, i); + } + + final String newName = name + " " + i; + entry.setName(newName); + + final int fieldIdx = fields.indexOf(entry); + minIndex = Math.min(minIndex, fieldIdx); + maxIndex = Math.max(maxIndex, fieldIdx); + } + + if (minIndex != Integer.MAX_VALUE && maxIndex != Integer.MIN_VALUE) { + fireTableRowsUpdated(minIndex, maxIndex); + } + } + public String toMultiLineString() { final StringBuilder sb = new StringBuilder(30 * fields.size()); for (final StructEntry e : fields) { diff --git a/src/org/infinity/resource/Profile.java b/src/org/infinity/resource/Profile.java index 809596f41..0a078e4e9 100644 --- a/src/org/infinity/resource/Profile.java +++ b/src/org/infinity/resource/Profile.java @@ -111,6 +111,8 @@ public enum Game { IWDHowTotLM(Engine.IWD, "Icewind Dale: Trials of the Luremaster"), /** Icewind Dale II */ IWD2(Engine.IWD2, "Icewind Dale II"), + /** Icewind Dale II: Enhanced Edition */ + IWD2EE(Engine.IWD2, "Icewind Dale II: Enhanced Edition"), /** Baldur's Gate: Enhanced Edition */ BG1EE(Engine.EE, "Baldur's Gate: Enhanced Edition"), /** Baldur's Gate: Siege of Dragonspear */ @@ -512,6 +514,7 @@ public enum Key { GAME_EXTRA_FOLDERS.put(Game.IWDHoW, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS))); GAME_EXTRA_FOLDERS.put(Game.IWDHowTotLM, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS))); GAME_EXTRA_FOLDERS.put(Game.IWD2, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS))); + GAME_EXTRA_FOLDERS.put(Game.IWD2EE, new ArrayList<>(Arrays.asList(BG_EXTRA_FOLDERS))); GAME_EXTRA_FOLDERS.put(Game.BG1EE, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS))); GAME_EXTRA_FOLDERS.put(Game.BG1SoD, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS))); GAME_EXTRA_FOLDERS.put(Game.BG2EE, new ArrayList<>(Arrays.asList(EE_EXTRA_FOLDERS))); @@ -533,6 +536,7 @@ public enum Key { GAME_SAVE_FOLDERS.put(Game.IWDHoW, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS))); GAME_SAVE_FOLDERS.put(Game.IWDHowTotLM, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS))); GAME_SAVE_FOLDERS.put(Game.IWD2, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS))); + GAME_SAVE_FOLDERS.put(Game.IWD2EE, new ArrayList<>(Arrays.asList(BG_SAVE_FOLDERS))); GAME_SAVE_FOLDERS.put(Game.BG1EE, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS))); GAME_SAVE_FOLDERS.put(Game.BG1SoD, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS))); GAME_SAVE_FOLDERS.put(Game.BG2EE, new ArrayList<>(Arrays.asList(EE_SAVE_FOLDERS))); @@ -1527,6 +1531,15 @@ private static void initDefaultGameBinaries() { osMap.put(Platform.OS.WINDOWS, list); DEFAULT_GAME_BINARIES.put(Game.IWD2, osMap); + // IWD2EE (Windows) + osMap = new EnumMap<>(Platform.OS.class); + osMap.put(Platform.OS.UNIX, emptyList); + osMap.put(Platform.OS.MAC_OS, emptyList); + list = new ArrayList<>(); + list.add("iwd2ee.exe"); + osMap.put(Platform.OS.WINDOWS, list); + DEFAULT_GAME_BINARIES.put(Game.IWD2EE, osMap); + // BG1EE (Linux, macOS, Windows) osMap = new EnumMap<>(Platform.OS.class); list = new ArrayList<>(); @@ -1828,7 +1841,9 @@ && getLuaValue(FileManager.query(gameRoots, "engine.lua"), "engine_mode", "0", f if (ini != null && FileEx.create(ini).isFile()) { addEntry(Key.GET_GAME_INI_FILE, Type.PATH, ini); } - } else if (game == Game.IWD2 || (FileEx.create(FileManager.query(gameRoots, "iwd2.exe")).isFile()) + } else if (game == Game.IWD2 || game == Game.IWD2EE + || (FileEx.create(FileManager.query(gameRoots, "iwd2.exe")).isFile() + || FileEx.create(FileManager.query(gameRoots, "iwd2ee.exe")).isFile()) && (FileEx.create(FileManager.query(gameRoots, "Data/Credits.mve")).isFile())) { if (game == null) { game = Game.IWD2; @@ -1965,6 +1980,8 @@ && getLuaValue(FileManager.query(gameRoots, "engine.lua"), "engine_mode", "0", f } } else if (!isForced && game == Game.BG1 && ResourceFactory.resourceExists("DURLAG.MVE")) { game = Game.BG1TotSC; + } else if (!isForced && game == Game.IWD2 && FileEx.create(FileManager.query(gameRoots, "IEex.dll")).isFile()) { + game = Game.IWD2EE; } // updating game type @@ -2287,7 +2304,7 @@ private void initResourceTypes() { addEntry(Key.IS_SUPPORTED_LOG, Type.BOOLEAN, true); - addEntry(Key.IS_SUPPORTED_LUA, Type.BOOLEAN, isEnhancedEdition()); + addEntry(Key.IS_SUPPORTED_LUA, Type.BOOLEAN, isEnhancedEdition() || game == Game.IWD2EE); addEntry(Key.IS_SUPPORTED_MAZE, Type.BOOLEAN, game == Game.PSTEE); diff --git a/src/org/infinity/resource/ResourceFactory.java b/src/org/infinity/resource/ResourceFactory.java index c6d793c3a..eb9ae616e 100644 --- a/src/org/infinity/resource/ResourceFactory.java +++ b/src/org/infinity/resource/ResourceFactory.java @@ -164,10 +164,11 @@ public static Class getResourceType(ResourceEntry entry, Str } else if (ext.equals("MUS")) { cls = MusResource.class; } else if (ext.equals("IDS") || ext.equals("2DA") || ext.equals("BIO") || ext.equals("RES") || ext.equals("TXT") - || ext.equals("LOG") || // WeiDU log files - (ext.equals("SRC") && Profile.getEngine() == Profile.Engine.IWD2) - || (Profile.isEnhancedEdition() && (ext.equals("SQL") || ext.equals("GUI") || ext.equals("LUA") - || ext.equals("MENU") || ext.equals("GLSL")))) { + || ext.equals("LOG") // WeiDU log files + || (ext.equals("SRC") && Profile.getEngine() == Profile.Engine.IWD2) + || (ext.equals("LUA") && (Profile.isEnhancedEdition() || Profile.getGame() == Profile.Game.IWD2EE)) + || (Profile.isEnhancedEdition() + && (ext.equals("SQL") || ext.equals("GUI") || ext.equals("MENU") || ext.equals("GLSL")))) { cls = PlainTextResource.class; } else if (ext.equals("INI")) { final boolean isPST = Profile.getEngine() == Profile.Engine.PST; diff --git a/src/org/infinity/resource/key/ResourceEntry.java b/src/org/infinity/resource/key/ResourceEntry.java index cdf316a13..e67976704 100644 --- a/src/org/infinity/resource/key/ResourceEntry.java +++ b/src/org/infinity/resource/key/ResourceEntry.java @@ -280,4 +280,10 @@ public boolean isVisible() { public abstract ResourceTreeFolder getTreeFolder(); public abstract boolean hasOverride(); + + public boolean isSound() { + String extension = getExtension().toUpperCase(); + + return extension.equals("WAV") || extension.equals("MUS") || extension.equals("ACM"); + } } diff --git a/src/org/infinity/resource/sound/SoundResource.java b/src/org/infinity/resource/sound/SoundResource.java index 83b3731a8..214a8ee14 100644 --- a/src/org/infinity/resource/sound/SoundResource.java +++ b/src/org/infinity/resource/sound/SoundResource.java @@ -168,8 +168,10 @@ public void searchReferences(Component parent) { @Override public void run() { - bPlay.setIcon(PLAY_ICONS.get(false)); - bStop.setEnabled(true); + if (bPlay != null) { + bPlay.setIcon(PLAY_ICONS.get(false)); + bStop.setEnabled(true); + } if (audioBuffer != null) { final TimerElapsedTask timerTask = new TimerElapsedTask(250L); try { @@ -182,8 +184,10 @@ public void run() { player.stopPlay(); timerTask.stop(); } - bStop.setEnabled(false); - bPlay.setIcon(PLAY_ICONS.get(true)); + if (bPlay != null) { + bStop.setEnabled(false); + bPlay.setIcon(PLAY_ICONS.get(true)); + } } // --------------------- End Interface Runnable --------------------- @@ -294,10 +298,11 @@ private synchronized void setLoaded(boolean b) { if (bPlay != null) { bPlay.setEnabled(b); bPlay.setIcon(PLAY_ICONS.get(true)); + + updateTimeLabel(0); + miConvert.setEnabled(b); + buttonPanel.getControlByType(PROPERTIES).setEnabled(true); } - updateTimeLabel(0); - miConvert.setEnabled(b); - buttonPanel.getControlByType(PROPERTIES).setEnabled(true); } private synchronized void setClosed(boolean b) { @@ -448,16 +453,22 @@ public void stop() { timer.cancel(); timer = null; paused = false; - updateTimeLabel(0L); + if (bPlay != null) { + updateTimeLabel(0L); + } } } @Override public void run() { - if (!paused && timer != null && player != null && player.getDataLine() != null) { + if (!paused && timer != null && player != null && player.getDataLine() != null && bPlay != null) { updateTimeLabel(player.getDataLine().getMicrosecondPosition() / 1000L); } } + } + public void playSound() { + loadAudio(); + new Thread(this).start(); } } diff --git a/src/org/infinity/resource/text/PlainTextResource.java b/src/org/infinity/resource/text/PlainTextResource.java index 5b0f99c77..fbe77f4c9 100644 --- a/src/org/infinity/resource/text/PlainTextResource.java +++ b/src/org/infinity/resource/text/PlainTextResource.java @@ -137,6 +137,26 @@ public static String trimSpaces(String text, boolean trailing, boolean leading) return retVal; } + /** + * Aligns table columns individually. + * + * @param text The text content with table columns. + * @return The aligned text. Returns {@code null} if {@code text} argument is {@code null}. + */ + public static String alignTableColumnsCompact(String text) { + return alignTableColumns(text, 2, true, 4); + } + + /** + * Aligns all table columns evenly, comparable to WeiDU's PRETTY_PRINT_2DA. + * + * @param text The text content with table columns. + * @return The aligned text. Returns {@code null} if {@code text} argument is {@code null}. + */ + public static String alignTableColumnsUniform(String text) { + return alignTableColumns(text, 1, false, 1); + } + /** * Aligns table columns to improve readability. * @@ -329,7 +349,7 @@ public PlainTextResource(ResourceEntry entry) throws Exception { buffer = StaticSimpleXorDecryptor.decrypt(buffer, 2); } final Charset cs = Misc.getCharsetFrom(BrowserMenuBar.getInstance().getOptions().getSelectedCharset()); - text = StreamUtils.readString(buffer, buffer.limit(), cs); + text = applyTransformText(StreamUtils.readString(buffer, buffer.limit(), cs)); } // --------------------- Begin Interface ActionListener --------------------- @@ -416,9 +436,9 @@ public void itemStateChanged(ItemEvent event) { if (bpmFormat.getSelectedItem() == miFormatTrim) { setText(trimSpaces(editor.getText(), true, false)); } else if (bpmFormat.getSelectedItem() == miFormatAlignCompact) { - setText(alignTableColumns(editor.getText(), 2, true, 4)); + setText(alignTableColumnsCompact(editor.getText())); } else if (bpmFormat.getSelectedItem() == miFormatAlignUniform) { - setText(alignTableColumns(editor.getText(), 1, false, 1)); + setText(alignTableColumnsUniform(editor.getText())); } else if (bpmFormat.getSelectedItem() == miFormatSort) { setText(sortTable(editor.getText(), true, entry.getResourceRef().equalsIgnoreCase("TRIGGER"))); } @@ -638,4 +658,30 @@ private void setSyntaxHighlightingEnabled(InfinityTextArea edit, InfinityScrollP pane.applyExtendedSettings(language); } } + + private String applyTransformText(String data) { + if (data == null) { + return data; + } + + final String ext = (entry != null) ? entry.getExtension() : ""; + if (ext.equals("2DA")) { + return applyAutoAlign2da(data); + } + + return data; + } + + private String applyAutoAlign2da(String data) { + switch (BrowserMenuBar.getInstance().getOptions().getAutoAlign2da()) { + case COMPACT: + return alignTableColumnsCompact(data); + case UNIFORM: + return alignTableColumnsUniform(data); + default: + } + + return data; + } + } diff --git a/src/org/infinity/resource/wmp/ViewerMap.java b/src/org/infinity/resource/wmp/ViewerMap.java index 1363294e7..7ba4e3c28 100644 --- a/src/org/infinity/resource/wmp/ViewerMap.java +++ b/src/org/infinity/resource/wmp/ViewerMap.java @@ -24,6 +24,8 @@ import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import javax.imageio.ImageIO; @@ -39,6 +41,7 @@ import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; +import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -167,6 +170,7 @@ private enum Direction { listPanel = (StructListPanel) ViewerUtil.makeListPanel("Areas", wmpMap, AreaEntry.class, AreaEntry.WMP_AREA_CURRENT, new WmpAreaListRenderer(mapIcons), listeners); + listPanel.getList().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); JScrollPane mapScroll = new JScrollPane(rcMap); mapScroll.getVerticalScrollBar().setUnitIncrement(16); mapScroll.getHorizontalScrollBar().setUnitIncrement(16); @@ -230,7 +234,7 @@ private void showOverlays(boolean showIcons, boolean showIconLabels, boolean sho showMapIconLabels(); } if (showDistances) { - showMapDistances(listPanel.getList().getSelectedIndex()); + showMapDistances(listPanel.getList().getSelectedIndices()); } } showDot((AreaEntry) listPanel.getList().getSelectedValue(), false); @@ -346,86 +350,122 @@ private void showMapIconLabels() { } } - /** Displays all map distances from the specified area (by index). */ - private void showMapDistances(int areaIndex) { - AreaEntry area = getAreaEntry(areaIndex, true); - if (area != null) { - final Direction[] srcDir = { Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; - final Color[] dirColor = { Color.GREEN, Color.RED, Color.CYAN, Color.YELLOW }; - final int[] links = new int[8]; - final int linkSize = 216; // size of a single area link structure - int ofsLinkBase = ((IsNumeric) getEntry().getAttribute(MapEntry.WMP_MAP_OFFSET_AREA_LINKS)).getValue(); - - links[0] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_NORTH)).getValue(); - links[1] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_NORTH)).getValue(); - links[2] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_WEST)).getValue(); - links[3] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_WEST)).getValue(); - links[4] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_SOUTH)).getValue(); - links[5] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_SOUTH)).getValue(); - links[6] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_EAST)).getValue(); - links[7] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_EAST)).getValue(); - for (int dir = 0; dir < srcDir.length; dir++) { - Direction curDir = srcDir[dir]; - Point ptOrigin = getMapIconCoordinate(areaIndex, curDir, true); - for (int dirIndex = 0, dirCount = links[dir * 2 + 1]; dirIndex < dirCount; dirIndex++) { - int ofsLink = ofsLinkBase + (links[dir * 2] + dirIndex) * linkSize; - AreaLink destLink = (AreaLink) area.getAttribute(ofsLink, false); - - if (destLink != null) { - int dstAreaIndex = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue(); - Flag flag = (Flag) destLink.getAttribute(AreaLink.WMP_LINK_DEFAULT_ENTRANCE); - Direction dstDir = Direction.NORTH; - if (flag.isFlagSet(1)) { - dstDir = Direction.EAST; - } else if (flag.isFlagSet(2)) { - dstDir = Direction.SOUTH; - } else if (flag.isFlagSet(3)) { - dstDir = Direction.WEST; + /** + * Displays all map distances from the specified area (by index). + * + * @param areaIndices Sequence of map indices for showing distances. Specify no parameters to show distances for all + * available maps. + */ + private void showMapDistances(int... areaIndices) { + final List areaIndicesList = new ArrayList<>(); + if (areaIndices.length == 0) { + int numAreas = ((IsNumeric) mapEntry.getAttribute(MapEntry.WMP_MAP_NUM_AREAS)).getValue(); + for (int i = 0; i < numAreas; i++) { + areaIndicesList.add(i); + } + } else { + for (final int idx : areaIndices) { + areaIndicesList.add(idx); + } + } + + for (final int curAreaIndex : areaIndicesList) { + AreaEntry area = getAreaEntry(curAreaIndex, true); + if (area != null) { + final Direction[] srcDir = { Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST }; + final Color[] dirColor = { Color.GREEN, Color.RED, Color.CYAN, Color.YELLOW }; + final int[] links = new int[8]; + final int linkSize = 216; // size of a single area link structure + int ofsLinkBase = ((IsNumeric) getEntry().getAttribute(MapEntry.WMP_MAP_OFFSET_AREA_LINKS)).getValue(); + + links[0] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_NORTH)).getValue(); + links[1] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_NORTH)).getValue(); + links[2] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_WEST)).getValue(); + links[3] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_WEST)).getValue(); + links[4] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_SOUTH)).getValue(); + links[5] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_SOUTH)).getValue(); + links[6] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_FIRST_LINK_EAST)).getValue(); + links[7] = ((IsNumeric) area.getAttribute(AreaEntry.WMP_AREA_NUM_LINKS_EAST)).getValue(); + for (int dir = 0; dir < srcDir.length; dir++) { + Direction curDir = srcDir[dir]; + Point ptOrigin = getMapIconCoordinate(curAreaIndex, curDir, true); + for (int dirIndex = 0, dirCount = links[dir * 2 + 1]; dirIndex < dirCount; dirIndex++) { + int ofsLink = ofsLinkBase + (links[dir * 2] + dirIndex) * linkSize; + AreaLink destLink = (AreaLink) area.getAttribute(ofsLink, false); + + // finding corresponding travel distances between selected areas + if (destLink != null && areaIndices.length > 1) { + final int destAreaIdx = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue(); + final AreaEntry destArea = getAreaEntry(destAreaIdx, false); + boolean found = false; + if (destArea != null) { + found = areaIndicesList + .stream() + .filter(idx -> curAreaIndex != idx && destArea.equals(getAreaEntry(idx, true))) + .findAny() + .isPresent(); + } + if (!found) { + destLink = null; + } } - Point ptTarget = getMapIconCoordinate(dstAreaIndex, dstDir, false); - - // checking for random encounters during travels - boolean hasRandomEncounters = false; - if (((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_PROBABILITY)).getValue() > 0) { - for (int rnd = 1; rnd < 6; rnd++) { - String rndArea = ((IsReference) destLink - .getAttribute(String.format(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_AREA_FMT, rnd))).getResourceName(); - if (ResourceFactory.resourceExists(rndArea)) { - hasRandomEncounters = true; - break; + + if (destLink != null) { + int dstAreaIndex = ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_TARGET_AREA)).getValue(); + Flag flag = (Flag) destLink.getAttribute(AreaLink.WMP_LINK_DEFAULT_ENTRANCE); + Direction dstDir = Direction.NORTH; + if (flag.isFlagSet(1)) { + dstDir = Direction.EAST; + } else if (flag.isFlagSet(2)) { + dstDir = Direction.SOUTH; + } else if (flag.isFlagSet(3)) { + dstDir = Direction.WEST; + } + Point ptTarget = getMapIconCoordinate(dstAreaIndex, dstDir, false); + + // checking for random encounters during travels + boolean hasRandomEncounters = false; + if (((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_PROBABILITY)).getValue() > 0) { + for (int rnd = 1; rnd < 6; rnd++) { + String rndArea = ((IsReference) destLink + .getAttribute(String.format(AreaLink.WMP_LINK_RANDOM_ENCOUNTER_AREA_FMT, rnd))).getResourceName(); + if (ResourceFactory.resourceExists(rndArea)) { + hasRandomEncounters = true; + break; + } } } - } - Graphics2D g = ((BufferedImage) rcMap.getImage()).createGraphics(); - g.setFont(g.getFont().deriveFont(g.getFont().getSize2D() * 0.8f)); - try { - // drawing line - g.setColor(dirColor[dir]); - if (hasRandomEncounters) { - g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.0f, - new float[] { 6.0f, 4.0f }, 0.0f)); - } else { - g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + Graphics2D g = ((BufferedImage) rcMap.getImage()).createGraphics(); + g.setFont(g.getFont().deriveFont(g.getFont().getSize2D() * 0.8f)); + try { + // drawing line + g.setColor(dirColor[dir]); + if (hasRandomEncounters) { + g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.0f, + new float[] { 6.0f, 4.0f }, 0.0f)); + } else { + g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + } + g.drawLine(ptOrigin.x, ptOrigin.y, ptTarget.x, ptTarget.y); + + // printing travel time (in hours) + String duration = String.format("%d h", + ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_DISTANCE_SCALE)).getValue() * 4); + LineMetrics lm = g.getFont().getLineMetrics(duration, g.getFontRenderContext()); + Rectangle2D rectText = g.getFont().getStringBounds(duration, g.getFontRenderContext()); + int textX = ptOrigin.x + ((ptTarget.x - ptOrigin.x) - rectText.getBounds().width) / 3; + int textY = ptOrigin.y + ((ptTarget.y - ptOrigin.y) - rectText.getBounds().height) / 3; + int textWidth = rectText.getBounds().width; + int textHeight = rectText.getBounds().height; + g.setColor(Color.LIGHT_GRAY); + g.fillRect(textX - 2, textY, textWidth + 4, textHeight); + g.setColor(Color.BLUE); + g.drawString(duration, textX, textY + lm.getAscent() + lm.getLeading()); + } finally { + g.dispose(); + g = null; } - g.drawLine(ptOrigin.x, ptOrigin.y, ptTarget.x, ptTarget.y); - - // printing travel time (in hours) - String duration = String.format("%d h", - ((IsNumeric) destLink.getAttribute(AreaLink.WMP_LINK_DISTANCE_SCALE)).getValue() * 4); - LineMetrics lm = g.getFont().getLineMetrics(duration, g.getFontRenderContext()); - Rectangle2D rectText = g.getFont().getStringBounds(duration, g.getFontRenderContext()); - int textX = ptOrigin.x + ((ptTarget.x - ptOrigin.x) - rectText.getBounds().width) / 2; - int textY = ptOrigin.y + ((ptTarget.y - ptOrigin.y) - rectText.getBounds().height) / 2; - int textWidth = rectText.getBounds().width; - int textHeight = rectText.getBounds().height; - g.setColor(Color.LIGHT_GRAY); - g.fillRect(textX - 2, textY, textWidth + 4, textHeight); - g.setColor(Color.BLUE); - g.drawString(duration, textX, textY + lm.getAscent() + lm.getLeading()); - } finally { - g.dispose(); - g = null; } } } diff --git a/src/org/infinity/search/ReferenceHitFrame.java b/src/org/infinity/search/ReferenceHitFrame.java index ae290c188..609b46248 100644 --- a/src/org/infinity/search/ReferenceHitFrame.java +++ b/src/org/infinity/search/ReferenceHitFrame.java @@ -130,12 +130,13 @@ public void actionPerformed(ActionEvent event) { ((ViewFrame) parent).toFront(); } } else { - NearInfinity.getInstance().showResourceEntry(entry); - Viewable viewable = NearInfinity.getInstance().getViewable(); - showEntryInViewer(row, viewable); - if (viewable instanceof DlgResource) { - NearInfinity.getInstance().toFront(); - } + NearInfinity.getInstance().showResourceEntry(entry, () -> { + Viewable viewable = NearInfinity.getInstance().getViewable(); + showEntryInViewer(row, viewable); + if (viewable instanceof DlgResource) { + NearInfinity.getInstance().toFront(); + } + }); } } } else if (event.getSource() == bopennew) { diff --git a/src/org/infinity/search/TextHitFrame.java b/src/org/infinity/search/TextHitFrame.java index 5c1a11f4a..08484ff87 100644 --- a/src/org/infinity/search/TextHitFrame.java +++ b/src/org/infinity/search/TextHitFrame.java @@ -119,11 +119,12 @@ public void actionPerformed(ActionEvent event) { ((TextResource) res).highlightText(((Integer) table.getValueAt(row, 2)), query); } } else { - NearInfinity.getInstance().showResourceEntry(entry); - Viewable viewable = NearInfinity.getInstance().getViewable(); - if (viewable instanceof TextResource) { - ((TextResource) viewable).highlightText(((Integer) table.getValueAt(row, 2)), query); - } + NearInfinity.getInstance().showResourceEntry(entry, () -> { + Viewable viewable = NearInfinity.getInstance().getViewable(); + if (viewable instanceof TextResource) { + ((TextResource) viewable).highlightText(((Integer) table.getValueAt(row, 2)), query); + } + }); } } } else if (event.getSource() == bopennew) { diff --git a/src/org/infinity/search/advanced/AdvancedSearch.java b/src/org/infinity/search/advanced/AdvancedSearch.java index ef82f8f22..5796bf657 100644 --- a/src/org/infinity/search/advanced/AdvancedSearch.java +++ b/src/org/infinity/search/advanced/AdvancedSearch.java @@ -819,12 +819,13 @@ public void actionPerformed(ActionEvent event) { if (row != -1) { ResourceEntry entry = (ResourceEntry) listResults.getValueAt(row, 0); if (entry != null) { - NearInfinity.getInstance().showResourceEntry(entry); - Viewable viewable = NearInfinity.getInstance().getViewable(); - showEntryInViewer(row, viewable); - if (viewable instanceof DlgResource) { - NearInfinity.getInstance().toFront(); - } + NearInfinity.getInstance().showResourceEntry(entry, () -> { + Viewable viewable = NearInfinity.getInstance().getViewable(); + showEntryInViewer(row, viewable); + if (viewable instanceof DlgResource) { + NearInfinity.getInstance().toFront(); + } + }); } } } else if (event.getSource() == bOpenNew) {