Java tutorial
/* * Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.character; import com.lowagie.text.pdf.DefaultFontMapper; import com.lowagie.text.pdf.PdfContentByte; import com.lowagie.text.pdf.PdfTemplate; import com.lowagie.text.pdf.PdfWriter; import com.trollworks.gcs.advantage.Advantage; import com.trollworks.gcs.advantage.AdvantageOutline; import com.trollworks.gcs.app.GCSApp; import com.trollworks.gcs.app.GCSFonts; import com.trollworks.gcs.equipment.Equipment; import com.trollworks.gcs.equipment.EquipmentColumn; import com.trollworks.gcs.equipment.EquipmentOutline; import com.trollworks.gcs.notes.Note; import com.trollworks.gcs.notes.NoteOutline; import com.trollworks.gcs.page.Page; import com.trollworks.gcs.page.PageField; import com.trollworks.gcs.page.PageOwner; import com.trollworks.gcs.preferences.OutputPreferences; import com.trollworks.gcs.preferences.SheetPreferences; import com.trollworks.gcs.skill.Skill; import com.trollworks.gcs.skill.SkillDefault; import com.trollworks.gcs.skill.SkillDefaultType; import com.trollworks.gcs.skill.SkillOutline; import com.trollworks.gcs.spell.Spell; import com.trollworks.gcs.spell.SpellOutline; import com.trollworks.gcs.weapon.MeleeWeaponStats; import com.trollworks.gcs.weapon.RangedWeaponStats; import com.trollworks.gcs.weapon.WeaponDisplayRow; import com.trollworks.gcs.weapon.WeaponStats; import com.trollworks.gcs.widgets.outline.ListRow; import com.trollworks.toolkit.annotation.Localize; import com.trollworks.toolkit.io.Log; import com.trollworks.toolkit.ui.Fonts; import com.trollworks.toolkit.ui.GraphicsUtilities; import com.trollworks.toolkit.ui.Selection; import com.trollworks.toolkit.ui.UIUtilities; import com.trollworks.toolkit.ui.image.StdImage; import com.trollworks.toolkit.ui.layout.ColumnLayout; import com.trollworks.toolkit.ui.layout.RowDistribution; import com.trollworks.toolkit.ui.print.PrintManager; import com.trollworks.toolkit.ui.scale.Scale; import com.trollworks.toolkit.ui.scale.ScaleRoot; import com.trollworks.toolkit.ui.scale.Scales; import com.trollworks.toolkit.ui.widget.Wrapper; import com.trollworks.toolkit.ui.widget.dock.Dockable; import com.trollworks.toolkit.ui.widget.outline.Column; import com.trollworks.toolkit.ui.widget.outline.Outline; import com.trollworks.toolkit.ui.widget.outline.OutlineHeader; import com.trollworks.toolkit.ui.widget.outline.OutlineModel; import com.trollworks.toolkit.ui.widget.outline.OutlineSyncer; import com.trollworks.toolkit.ui.widget.outline.Row; import com.trollworks.toolkit.ui.widget.outline.RowIterator; import com.trollworks.toolkit.ui.widget.outline.RowSelection; import com.trollworks.toolkit.utility.BundleInfo; import com.trollworks.toolkit.utility.FileType; import com.trollworks.toolkit.utility.Localization; import com.trollworks.toolkit.utility.PathUtils; import com.trollworks.toolkit.utility.Preferences; import com.trollworks.toolkit.utility.PrintProxy; import com.trollworks.toolkit.utility.notification.BatchNotifierTarget; import com.trollworks.toolkit.utility.notification.NotifierTarget; import com.trollworks.toolkit.utility.text.Numbers; import com.trollworks.toolkit.utility.undo.StdUndoManager; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.Rectangle; import java.awt.Transparency; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.print.PageFormat; import java.awt.print.Paper; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.JPanel; import javax.swing.RepaintManager; import javax.swing.Scrollable; import javax.swing.SwingConstants; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** The character sheet. */ public class CharacterSheet extends JPanel implements ChangeListener, Scrollable, BatchNotifierTarget, PageOwner, PrintProxy, ActionListener, Runnable, DropTargetListener, ScaleRoot { @Localize("Page {0} of {1}") @Localize(locale = "de", value = "Seite {0} von {1}") @Localize(locale = "ru", value = ". {0} {1}") @Localize(locale = "es", value = "Pgina {0} de {1}") private static String PAGE_NUMBER; @Localize("Visit us at %s") @Localize(locale = "de", value = "Besucht uns auf %s") @Localize(locale = "ru", value = "? ? %s") @Localize(locale = "es", value = "Visitanos en %s") private static String ADVERTISEMENT; @Localize("Melee Weapons") @Localize(locale = "de", value = "Nahkampfwaffen") @Localize(locale = "ru", value = " ?") @Localize(locale = "es", value = "Armas de cuerpo a cuerpo") private static String MELEE_WEAPONS; @Localize("Ranged Weapons") @Localize(locale = "de", value = "Fernkampfwaffen") @Localize(locale = "ru", value = "? ?") @Localize(locale = "es", value = "Armas de distancia") private static String RANGED_WEAPONS; @Localize("Advantages, Disadvantages & Quirks") @Localize(locale = "de", value = "Vorteile, Nachteile und Marotten") @Localize(locale = "ru", value = "?, ? ") @Localize(locale = "es", value = "Ventajas, Desventajas y Singularidades") private static String ADVANTAGES; @Localize("Skills") @Localize(locale = "de", value = "Fhigkeiten") @Localize(locale = "ru", value = "?") @Localize(locale = "es", value = "Habilidades") private static String SKILLS; @Localize("Spells") @Localize(locale = "de", value = "Zauber") @Localize(locale = "ru", value = "?") @Localize(locale = "es", value = "Sortilegios") private static String SPELLS; @Localize("Equipment") @Localize(locale = "de", value = "Ausrstung") @Localize(locale = "ru", value = "?") @Localize(locale = "es", value = "Equuipo") private static String EQUIPMENT; @Localize("{0} (continued)") @Localize(locale = "de", value = "{0} (fortgesetzt)") @Localize(locale = "ru", value = "{0} (??)") @Localize(locale = "es", value = "{0} (continua)") private static String CONTINUED; @Localize("Natural") @Localize(locale = "de", value = "Angeboren") @Localize(locale = "ru", value = "") @Localize(locale = "es", value = "Natural") private static String NATURAL; @Localize("Punch") @Localize(locale = "de", value = "Schlag") @Localize(locale = "ru", value = "") @Localize(locale = "es", value = "Puetazo") private static String PUNCH; @Localize("Kick") @Localize(locale = "de", value = "Tritt") @Localize(locale = "ru", value = "") @Localize(locale = "es", value = "Patada") private static String KICK; @Localize("Kick w/Boots") @Localize(locale = "de", value = "Tritt mit Schuh") @Localize(locale = "ru", value = " ()") @Localize(locale = "es", value = "Patada con botas") private static String BOOTS; @Localize("Notes") @Localize(locale = "de", value = "Notizen") @Localize(locale = "ru", value = "") @Localize(locale = "es", value = "Notas") private static String NOTES; static { Localization.initialize(); } private static final String BOXING_SKILL_NAME = "Boxing"; //$NON-NLS-1$ private static final String KARATE_SKILL_NAME = "Karate"; //$NON-NLS-1$ private static final String BRAWLING_SKILL_NAME = "Brawling"; //$NON-NLS-1$ private static final int GAP = 2; private Scale mScale; private GURPSCharacter mCharacter; private int mLastPage; private boolean mBatchMode; private AdvantageOutline mAdvantageOutline; private SkillOutline mSkillOutline; private SpellOutline mSpellOutline; private EquipmentOutline mEquipmentOutline; private NoteOutline mNoteOutline; private Outline mMeleeWeaponOutline; private Outline mRangedWeaponOutline; private boolean mRebuildPending; private Set<Outline> mRootsToSync; private PrintManager mPrintManager; private Scale mSavedScale; private boolean mOkToPaint = true; private boolean mIsPrinting; private boolean mSyncWeapons; private boolean mDisposed; /** * Creates a new character sheet display. {@link #rebuild()} must be called prior to the first * display of this panel. * * @param character The character to display the data for. */ public CharacterSheet(GURPSCharacter character) { super(); setLayout(new CharacterSheetLayout(this)); setOpaque(false); mScale = SheetPreferences.getInitialUIScale().getScale(); mCharacter = character; mLastPage = -1; mRootsToSync = new HashSet<>(); if (!GraphicsUtilities.inHeadlessPrintMode()) { setDropTarget(new DropTarget(this, this)); } Preferences.getInstance().getNotifier().add(this, SheetPreferences.OPTIONAL_DICE_RULES_PREF_KEY, Fonts.FONT_NOTIFICATION_KEY, SheetPreferences.WEIGHT_UNITS_PREF_KEY, SheetPreferences.GURPS_METRIC_RULES_PREF_KEY, SheetPreferences.OPTIONAL_STRENGTH_RULES_PREF_KEY, SheetPreferences.OPTIONAL_REDUCED_SWING_PREF_KEY); } /** Call when the sheet is no longer in use. */ public void dispose() { Preferences.getInstance().getNotifier().remove(this); mCharacter.resetNotifier(); mDisposed = true; } /** @return Whether the sheet has had {@link #dispose()} called on it. */ public boolean hasBeenDisposed() { return mDisposed; } @Override public Scale getScale() { return mScale; } @Override public void setScale(Scale scale) { if (mScale.getScale() != scale.getScale()) { mScale = scale; markForRebuild(); } } /** @return Whether a rebuild is pending. */ public boolean isRebuildPending() { return mRebuildPending; } /** Synchronizes the display with the underlying model. */ public void rebuild() { KeyboardFocusManager focusMgr = KeyboardFocusManager.getCurrentKeyboardFocusManager(); Component focus = focusMgr.getPermanentFocusOwner(); int firstRow = 0; String focusKey = null; PageAssembler pageAssembler; if (UIUtilities.getSelfOrAncestorOfType(focus, CharacterSheet.class) == this) { if (focus instanceof PageField) { focusKey = ((PageField) focus).getConsumedType(); focus = null; } else if (focus instanceof Outline) { Outline outline = (Outline) focus; Selection selection = outline.getModel().getSelection(); firstRow = outline.getFirstRowToDisplay(); int selRow = selection.nextSelectedIndex(firstRow); if (selRow >= 0) { firstRow = selRow; } focus = outline.getRealOutline(); } focusMgr.clearFocusOwner(); } else { focus = null; } // Make sure our primary outlines exist createAdvantageOutline(); createSkillOutline(); createSpellOutline(); createMeleeWeaponOutline(); createRangedWeaponOutline(); createEquipmentOutline(); createNoteOutline(); // Clear out the old pages removeAll(); List<NotifierTarget> targets = new ArrayList<>(); targets.add(PrerequisitesThread.getThread(mCharacter)); SheetDockable sheetDockable = UIUtilities.getAncestorOfType(this, SheetDockable.class); if (sheetDockable != null) { targets.add(sheetDockable); } mCharacter.resetNotifier(targets.toArray(new NotifierTarget[targets.size()])); // Create the first page, which holds stuff that has a fixed vertical size. pageAssembler = new PageAssembler(this); pageAssembler.addToContent(hwrap(new PortraitPanel(this), vwrap(hwrap(new IdentityPanel(this), new PlayerInfoPanel(this)), new DescriptionPanel(this)), new PointsPanel(this)), null, null); pageAssembler.addToContent( hwrap(new AttributesPanel(this), vwrap(new EncumbrancePanel(this), new LiftPanel(this)), new HitLocationPanel(this), new HitPointsPanel(this)), null, null); // Add our outlines if (mAdvantageOutline.getModel().getRowCount() > 0 && mSkillOutline.getModel().getRowCount() > 0) { addOutline(pageAssembler, mAdvantageOutline, ADVANTAGES, mSkillOutline, SKILLS); } else { addOutline(pageAssembler, mAdvantageOutline, ADVANTAGES); addOutline(pageAssembler, mSkillOutline, SKILLS); } addOutline(pageAssembler, mSpellOutline, SPELLS); addOutline(pageAssembler, mMeleeWeaponOutline, MELEE_WEAPONS); addOutline(pageAssembler, mRangedWeaponOutline, RANGED_WEAPONS); addOutline(pageAssembler, mEquipmentOutline, EQUIPMENT); addOutline(pageAssembler, mNoteOutline, NOTES); pageAssembler.finish(); // Ensure everything is laid out and register for notification validate(); OutlineSyncer.remove(mAdvantageOutline); OutlineSyncer.remove(mSkillOutline); OutlineSyncer.remove(mSpellOutline); OutlineSyncer.remove(mEquipmentOutline); OutlineSyncer.remove(mNoteOutline); OutlineSyncer.remove(mMeleeWeaponOutline); OutlineSyncer.remove(mRangedWeaponOutline); mCharacter.addTarget(this, GURPSCharacter.CHARACTER_PREFIX); mCharacter.calculateWeightAndWealthCarried(true); if (focusKey != null) { restoreFocusToKey(focusKey, this); } else if (focus instanceof Outline) { ((Outline) focus).getBestOutlineForRowIndex(firstRow).requestFocusInWindow(); } else if (focus != null) { focus.requestFocusInWindow(); } setSize(getPreferredSize()); repaint(); } private boolean restoreFocusToKey(String key, Component panel) { if (key != null) { if (panel instanceof PageField) { if (key.equals(((PageField) panel).getConsumedType())) { panel.requestFocusInWindow(); return true; } } else if (panel instanceof Container) { Container container = (Container) panel; if (container.getComponentCount() > 0) { for (Component child : container.getComponents()) { if (restoreFocusToKey(key, child)) { return true; } } } } } return false; } private void addOutline(PageAssembler pageAssembler, Outline outline, String title) { if (outline.getModel().getRowCount() > 0) { OutlineInfo info = new OutlineInfo(outline, pageAssembler.getContentWidth()); boolean useProxy = false; while (pageAssembler.addToContent(new SingleOutlinePanel(mScale, outline, title, useProxy), info, null)) { if (!useProxy) { title = MessageFormat.format(CONTINUED, title); useProxy = true; } } } } private void addOutline(PageAssembler pageAssembler, Outline leftOutline, String leftTitle, Outline rightOutline, String rightTitle) { int width = pageAssembler.getContentWidth() / 2 - 1; OutlineInfo infoLeft = new OutlineInfo(leftOutline, width); OutlineInfo infoRight = new OutlineInfo(rightOutline, width); boolean useProxy = false; while (pageAssembler.addToContent( new DoubleOutlinePanel(mScale, leftOutline, leftTitle, rightOutline, rightTitle, useProxy), infoLeft, infoRight)) { if (!useProxy) { leftTitle = MessageFormat.format(CONTINUED, leftTitle); rightTitle = MessageFormat.format(CONTINUED, rightTitle); useProxy = true; } } } /** * Prepares the specified outline for embedding in the sheet. * * @param outline The outline to prepare. */ public static void prepOutline(Outline outline) { OutlineHeader header = outline.getHeaderPanel(); outline.setDynamicRowHeight(true); outline.setAllowColumnDrag(false); outline.setAllowColumnResize(false); outline.setAllowColumnContextMenu(false); header.setIgnoreResizeOK(true); header.setBackground(Color.black); header.setTopDividerColor(Color.black); } /** @return The outline containing the Advantages, Disadvantages & Quirks. */ public AdvantageOutline getAdvantageOutline() { return mAdvantageOutline; } private void createAdvantageOutline() { if (mAdvantageOutline == null) { mAdvantageOutline = new AdvantageOutline(mCharacter); initOutline(mAdvantageOutline); } else { resetOutline(mAdvantageOutline); } } /** @return The outline containing the skills. */ public SkillOutline getSkillOutline() { return mSkillOutline; } private void createSkillOutline() { if (mSkillOutline == null) { mSkillOutline = new SkillOutline(mCharacter); initOutline(mSkillOutline); } else { resetOutline(mSkillOutline); } } /** @return The outline containing the spells. */ public SpellOutline getSpellOutline() { return mSpellOutline; } private void createSpellOutline() { if (mSpellOutline == null) { mSpellOutline = new SpellOutline(mCharacter); initOutline(mSpellOutline); } else { resetOutline(mSpellOutline); } } /** @return The outline containing the notes. */ public NoteOutline getNoteOutline() { return mNoteOutline; } private void createNoteOutline() { if (mNoteOutline == null) { mNoteOutline = new NoteOutline(mCharacter); initOutline(mNoteOutline); } else { resetOutline(mNoteOutline); } } /** @return The outline containing the equipment. */ public EquipmentOutline getEquipmentOutline() { return mEquipmentOutline; } private void createEquipmentOutline() { if (mEquipmentOutline == null) { mEquipmentOutline = new EquipmentOutline(mCharacter); initOutline(mEquipmentOutline); } else { resetOutline(mEquipmentOutline); } } /** @return The outline containing the melee weapons. */ public Outline getMeleeWeaponOutline() { return mMeleeWeaponOutline; } private void createMeleeWeaponOutline() { if (mMeleeWeaponOutline == null) { OutlineModel outlineModel; String sortConfig; mMeleeWeaponOutline = new WeaponOutline(MeleeWeaponStats.class); outlineModel = mMeleeWeaponOutline.getModel(); sortConfig = outlineModel.getSortConfig(); for (WeaponDisplayRow row : collectWeapons(MeleeWeaponStats.class)) { outlineModel.addRow(row); } outlineModel.applySortConfig(sortConfig); initOutline(mMeleeWeaponOutline); } else { resetOutline(mMeleeWeaponOutline); } } /** @return The outline containing the ranged weapons. */ public Outline getRangedWeaponOutline() { return mRangedWeaponOutline; } private void createRangedWeaponOutline() { if (mRangedWeaponOutline == null) { OutlineModel outlineModel; String sortConfig; mRangedWeaponOutline = new WeaponOutline(RangedWeaponStats.class); outlineModel = mRangedWeaponOutline.getModel(); sortConfig = outlineModel.getSortConfig(); for (WeaponDisplayRow row : collectWeapons(RangedWeaponStats.class)) { outlineModel.addRow(row); } outlineModel.applySortConfig(sortConfig); initOutline(mRangedWeaponOutline); } else { resetOutline(mRangedWeaponOutline); } } private void addBuiltInWeapons(Class<? extends WeaponStats> weaponClass, HashMap<HashedWeapon, WeaponDisplayRow> map) { if (weaponClass == MeleeWeaponStats.class) { boolean savedModified = mCharacter.isModified(); ArrayList<SkillDefault> defaults = new ArrayList<>(); Advantage phantom; MeleeWeaponStats weapon; StdUndoManager mgr = mCharacter.getUndoManager(); mCharacter.setUndoManager(new StdUndoManager()); phantom = new Advantage(mCharacter, false); phantom.setName(NATURAL); if (mCharacter.includePunch()) { defaults.add(new SkillDefault(SkillDefaultType.DX, null, null, 0)); defaults.add(new SkillDefault(SkillDefaultType.Skill, BOXING_SKILL_NAME, null, 0)); defaults.add(new SkillDefault(SkillDefaultType.Skill, BRAWLING_SKILL_NAME, null, 0)); defaults.add(new SkillDefault(SkillDefaultType.Skill, KARATE_SKILL_NAME, null, 0)); weapon = new MeleeWeaponStats(phantom); weapon.setUsage(PUNCH); weapon.setDefaults(defaults); weapon.setDamage("thr-1 cr"); //$NON-NLS-1$ weapon.setReach("C"); //$NON-NLS-1$ weapon.setParry("0"); //$NON-NLS-1$ map.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); defaults.clear(); } defaults.add(new SkillDefault(SkillDefaultType.DX, null, null, -2)); defaults.add(new SkillDefault(SkillDefaultType.Skill, BRAWLING_SKILL_NAME, null, -2)); defaults.add(new SkillDefault(SkillDefaultType.Skill, KARATE_SKILL_NAME, null, -2)); if (mCharacter.includeKick()) { weapon = new MeleeWeaponStats(phantom); weapon.setUsage(KICK); weapon.setDefaults(defaults); weapon.setDamage("thr cr"); //$NON-NLS-1$ weapon.setReach("C,1"); //$NON-NLS-1$ weapon.setParry("No"); //$NON-NLS-1$ map.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } if (mCharacter.includeKickBoots()) { weapon = new MeleeWeaponStats(phantom); weapon.setUsage(BOOTS); weapon.setDefaults(defaults); weapon.setDamage("thr+1 cr"); //$NON-NLS-1$ weapon.setReach("C,1"); //$NON-NLS-1$ weapon.setParry("No"); //$NON-NLS-1$ map.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } mCharacter.setUndoManager(mgr); mCharacter.setModified(savedModified); } } private ArrayList<WeaponDisplayRow> collectWeapons(Class<? extends WeaponStats> weaponClass) { HashMap<HashedWeapon, WeaponDisplayRow> weaponMap = new HashMap<>(); ArrayList<WeaponDisplayRow> weaponList; addBuiltInWeapons(weaponClass, weaponMap); for (Advantage advantage : mCharacter.getAdvantagesIterator(false)) { for (WeaponStats weapon : advantage.getWeapons()) { if (weaponClass.isInstance(weapon)) { weaponMap.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } } } for (Equipment equipment : mCharacter.getEquipmentIterator()) { if (equipment.getQuantity() > 0 && equipment.isEquipped()) { for (WeaponStats weapon : equipment.getWeapons()) { if (weaponClass.isInstance(weapon)) { weaponMap.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } } } } for (Spell spell : mCharacter.getSpellsIterator()) { for (WeaponStats weapon : spell.getWeapons()) { if (weaponClass.isInstance(weapon)) { weaponMap.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } } } for (Skill skill : mCharacter.getSkillsIterator()) { for (WeaponStats weapon : skill.getWeapons()) { if (weaponClass.isInstance(weapon)) { weaponMap.put(new HashedWeapon(weapon), new WeaponDisplayRow(weapon)); } } } weaponList = new ArrayList<>(weaponMap.values()); return weaponList; } private void initOutline(Outline outline) { outline.addActionListener(this); } private static void resetOutline(Outline outline) { outline.clearProxies(); } private static Container hwrap(Component left, Component right) { Wrapper wrapper = new Wrapper(new ColumnLayout(2, GAP, GAP)); wrapper.add(left); wrapper.add(right); wrapper.setAlignmentY(-1f); return wrapper; } private static Container hwrap(Component left, Component center, Component right) { Wrapper wrapper = new Wrapper(new ColumnLayout(3, GAP, GAP)); wrapper.add(left); wrapper.add(center); wrapper.add(right); wrapper.setAlignmentY(-1f); return wrapper; } private static Container hwrap(Component left, Component center, Component center2, Component right) { Wrapper wrapper = new Wrapper(new ColumnLayout(4, GAP, GAP)); wrapper.add(left); wrapper.add(center); wrapper.add(center2); wrapper.add(right); wrapper.setAlignmentY(-1f); return wrapper; } private static Container vwrap(Component top, Component bottom) { Wrapper wrapper = new Wrapper(new ColumnLayout(1, GAP, GAP, RowDistribution.GIVE_EXCESS_TO_LAST)); wrapper.add(top); wrapper.add(bottom); wrapper.setAlignmentY(-1f); return wrapper; } /** @return The number of pages in this character sheet. */ public int getPageCount() { return getComponentCount(); } @Override public void paint(Graphics g) { if (mOkToPaint) { super.paint(g); } } @Override public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) { if (pageIndex >= getComponentCount()) { mLastPage = -1; return NO_SUCH_PAGE; } // We do the following trick to avoid going through the work twice, // as we are called twice for each page, the first of which doesn't // seem to be used. if (mLastPage != pageIndex) { mLastPage = pageIndex; } else { Component comp = getComponent(pageIndex); RepaintManager mgr = RepaintManager.currentManager(comp); boolean saved = mgr.isDoubleBufferingEnabled(); mgr.setDoubleBufferingEnabled(false); mOkToPaint = true; comp.print(graphics); mOkToPaint = false; mgr.setDoubleBufferingEnabled(saved); } return PAGE_EXISTS; } @Override public void enterBatchMode() { mBatchMode = true; } @Override public void leaveBatchMode() { mBatchMode = false; validate(); } @Override public void handleNotification(Object producer, String type, Object data) { if (SheetPreferences.OPTIONAL_DICE_RULES_PREF_KEY.equals(type) || Fonts.FONT_NOTIFICATION_KEY.equals(type) || SheetPreferences.WEIGHT_UNITS_PREF_KEY.equals(type) || SheetPreferences.GURPS_METRIC_RULES_PREF_KEY.equals(type) || Profile.ID_BODY_TYPE.equals(type) || SheetPreferences.OPTIONAL_STRENGTH_RULES_PREF_KEY.equals(type) || SheetPreferences.OPTIONAL_REDUCED_SWING_PREF_KEY.equals(type)) { markForRebuild(); } else { if (type.startsWith(Advantage.PREFIX)) { OutlineSyncer.add(mAdvantageOutline); } else if (type.startsWith(Skill.PREFIX)) { OutlineSyncer.add(mSkillOutline); } else if (type.startsWith(Spell.PREFIX)) { OutlineSyncer.add(mSpellOutline); } else if (type.startsWith(Equipment.PREFIX)) { OutlineSyncer.add(mEquipmentOutline); } else if (type.startsWith(Note.PREFIX)) { OutlineSyncer.add(mNoteOutline); } if (GURPSCharacter.ID_LAST_MODIFIED.equals(type)) { int count = getComponentCount(); for (int i = 0; i < count; i++) { Page page = (Page) getComponent(i); Rectangle bounds = page.getBounds(); Insets insets = page.getInsets(); bounds.y = bounds.y + bounds.height - insets.bottom; bounds.height = insets.bottom; repaint(bounds); } } else if (Advantage.ID_DISABLED.equals(type) || Equipment.ID_STATE.equals(type) || Equipment.ID_QUANTITY.equals(type) || Equipment.ID_WEAPON_STATUS_CHANGED.equals(type) || Advantage.ID_WEAPON_STATUS_CHANGED.equals(type) || Spell.ID_WEAPON_STATUS_CHANGED.equals(type) || Skill.ID_WEAPON_STATUS_CHANGED.equals(type) || GURPSCharacter.ID_INCLUDE_PUNCH.equals(type) || GURPSCharacter.ID_INCLUDE_KICK.equals(type) || GURPSCharacter.ID_INCLUDE_BOOTS.equals(type)) { mSyncWeapons = true; markForRebuild(); } else if (GURPSCharacter.ID_PARRY_BONUS.equals(type) || Skill.ID_LEVEL.equals(type)) { OutlineSyncer.add(mMeleeWeaponOutline); OutlineSyncer.add(mRangedWeaponOutline); } else if (GURPSCharacter.ID_CARRIED_WEIGHT.equals(type) || GURPSCharacter.ID_CARRIED_WEALTH.equals(type)) { Column column = mEquipmentOutline.getModel().getColumnWithID(EquipmentColumn.DESCRIPTION.ordinal()); column.setName(EquipmentColumn.DESCRIPTION.toString(mCharacter)); } else if (!mBatchMode) { validate(); } } } @Override public void drawPageAdornments(Page page, Graphics gc) { Rectangle bounds = page.getBounds(); Insets insets = page.getInsets(); bounds.width -= insets.left + insets.right; bounds.height -= insets.top + insets.bottom; bounds.x = insets.left; bounds.y = insets.top; int pageNumber = 1 + UIUtilities.getIndexOf(this, page); String pageString = MessageFormat.format(PAGE_NUMBER, Numbers.format(pageNumber), Numbers.format(getPageCount())); BundleInfo bundleInfo = BundleInfo.getDefault(); String copyright1 = bundleInfo.getCopyright(); String copyright2 = bundleInfo.getReservedRights(); Font font1 = mScale.scale(UIManager.getFont(GCSFonts.KEY_SECONDARY_FOOTER)); Font font2 = mScale.scale(UIManager.getFont(GCSFonts.KEY_PRIMARY_FOOTER)); FontMetrics fm1 = gc.getFontMetrics(font1); FontMetrics fm2 = gc.getFontMetrics(font2); int y = bounds.y + bounds.height + fm2.getAscent(); String left; String right; if (pageNumber % 2 == 1) { left = copyright1; right = mCharacter.getLastModified(); } else { left = mCharacter.getLastModified(); right = copyright1; } Font savedFont = gc.getFont(); gc.setColor(Color.BLACK); gc.setFont(font1); gc.drawString(left, bounds.x, y); gc.drawString(right, bounds.x + bounds.width - (int) fm1.getStringBounds(right, gc).getWidth(), y); gc.setFont(font2); String center = mCharacter.getDescription().getName(); gc.drawString(center, bounds.x + (bounds.width - (int) fm2.getStringBounds(center, gc).getWidth()) / 2, y); if (pageNumber % 2 == 1) { left = copyright2; right = pageString; } else { left = pageString; right = copyright2; } y += fm2.getDescent() + fm1.getAscent(); gc.setFont(font1); gc.drawString(left, bounds.x, y); gc.drawString(right, bounds.x + bounds.width - (int) fm1.getStringBounds(right, gc).getWidth(), y); // Trim off the leading URI scheme and authority path component. (http://, https://, ...) String advertisement = String.format(ADVERTISEMENT, GCSApp.WEB_SITE.replaceAll(".*://", "")); //$NON-NLS-1$ //$NON-NLS-2$ gc.drawString(advertisement, bounds.x + (bounds.width - (int) fm1.getStringBounds(advertisement, gc).getWidth()) / 2, y); gc.setFont(savedFont); } @Override public Insets getPageAdornmentsInsets(Page page) { FontMetrics fm1 = Fonts.getFontMetrics(UIManager.getFont(GCSFonts.KEY_SECONDARY_FOOTER)); FontMetrics fm2 = Fonts.getFontMetrics(UIManager.getFont(GCSFonts.KEY_PRIMARY_FOOTER)); return new Insets(0, 0, fm1.getAscent() + fm1.getDescent() + fm2.getAscent() + fm2.getDescent(), 0); } @Override public PrintManager getPageSettings() { return mCharacter.getPageSettings(); } /** @return The character being displayed. */ public GURPSCharacter getCharacter() { return mCharacter; } @Override public void actionPerformed(ActionEvent event) { String command = event.getActionCommand(); if (Outline.CMD_POTENTIAL_CONTENT_SIZE_CHANGE.equals(command)) { mRootsToSync.add(((Outline) event.getSource()).getRealOutline()); markForRebuild(); } } /** Marks the sheet for a rebuild in the near future. */ public void markForRebuild() { if (!mRebuildPending) { mRebuildPending = true; EventQueue.invokeLater(this); } } @Override public void run() { syncRoots(); rebuild(); mRebuildPending = false; } private void syncRoots() { if (mSyncWeapons || mRootsToSync.contains(mEquipmentOutline) || mRootsToSync.contains(mAdvantageOutline) || mRootsToSync.contains(mSpellOutline) || mRootsToSync.contains(mSkillOutline)) { OutlineModel outlineModel = mMeleeWeaponOutline.getModel(); String sortConfig = outlineModel.getSortConfig(); outlineModel.removeAllRows(); for (WeaponDisplayRow row : collectWeapons(MeleeWeaponStats.class)) { outlineModel.addRow(row); } outlineModel.applySortConfig(sortConfig); outlineModel = mRangedWeaponOutline.getModel(); sortConfig = outlineModel.getSortConfig(); outlineModel.removeAllRows(); for (WeaponDisplayRow row : collectWeapons(RangedWeaponStats.class)) { outlineModel.addRow(row); } outlineModel.applySortConfig(sortConfig); } mSyncWeapons = false; mRootsToSync.clear(); } @Override public void stateChanged(ChangeEvent event) { Dimension size = getLayout().preferredLayoutSize(this); if (!getSize().equals(size)) { invalidate(); repaint(); setSize(size); } } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return orientation == SwingConstants.VERTICAL ? visibleRect.height : visibleRect.width; } @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 10; } @Override public boolean getScrollableTracksViewportHeight() { return false; } @Override public boolean getScrollableTracksViewportWidth() { return false; } private boolean mDragWasAcceptable; private ArrayList<Row> mDragRows; @Override public void dragEnter(DropTargetDragEvent dtde) { mDragWasAcceptable = false; try { if (dtde.isDataFlavorSupported(RowSelection.DATA_FLAVOR)) { Row[] rows = (Row[]) dtde.getTransferable().getTransferData(RowSelection.DATA_FLAVOR); if (rows != null && rows.length > 0) { mDragRows = new ArrayList<>(rows.length); for (Row element : rows) { if (element instanceof ListRow) { mDragRows.add(element); } } if (!mDragRows.isEmpty()) { mDragWasAcceptable = true; dtde.acceptDrag(DnDConstants.ACTION_MOVE); } } } } catch (Exception exception) { Log.error(exception); } if (!mDragWasAcceptable) { dtde.rejectDrag(); } } @Override public void dragOver(DropTargetDragEvent dtde) { if (mDragWasAcceptable) { dtde.acceptDrag(DnDConstants.ACTION_MOVE); } else { dtde.rejectDrag(); } } @Override public void dropActionChanged(DropTargetDragEvent dtde) { if (mDragWasAcceptable) { dtde.acceptDrag(DnDConstants.ACTION_MOVE); } else { dtde.rejectDrag(); } } @Override public void drop(DropTargetDropEvent dtde) { dtde.acceptDrop(dtde.getDropAction()); UIUtilities.getAncestorOfType(this, SheetDockable.class).addRows(mDragRows); mDragRows = null; dtde.dropComplete(true); } @Override public void dragExit(DropTargetEvent dte) { mDragRows = null; } private static PageFormat createDefaultPageFormat() { Paper paper = new Paper(); PageFormat format = new PageFormat(); format.setOrientation(PageFormat.PORTRAIT); paper.setSize(8.5 * 72.0, 11.0 * 72.0); paper.setImageableArea(0.5 * 72.0, 0.5 * 72.0, 7.5 * 72.0, 10 * 72.0); format.setPaper(paper); return format; } private HashSet<Row> expandAllContainers() { HashSet<Row> changed = new HashSet<>(); expandAllContainers(mCharacter.getAdvantagesIterator(true), changed); expandAllContainers(mCharacter.getSkillsIterator(), changed); expandAllContainers(mCharacter.getSpellsIterator(), changed); expandAllContainers(mCharacter.getEquipmentIterator(), changed); if (mRebuildPending) { syncRoots(); rebuild(); } return changed; } private static void expandAllContainers(RowIterator<? extends Row> iterator, HashSet<Row> changed) { for (Row row : iterator) { if (!row.isOpen()) { row.setOpen(true); changed.add(row); } } } private void closeContainers(HashSet<Row> rows) { for (Row row : rows) { row.setOpen(false); } if (mRebuildPending) { syncRoots(); rebuild(); } } /** * @param file The file to save to. * @return <code>true</code> on success. */ public boolean saveAsPDF(File file) { HashSet<Row> changed = expandAllContainers(); try { PrintManager settings = mCharacter.getPageSettings(); PageFormat format = settings != null ? settings.createPageFormat() : createDefaultPageFormat(); Paper paper = format.getPaper(); float width = (float) paper.getWidth(); float height = (float) paper.getHeight(); adjustToPageSetupChanges(true); setPrinting(true); com.lowagie.text.Document pdfDoc = new com.lowagie.text.Document( new com.lowagie.text.Rectangle(width, height)); try (FileOutputStream out = new FileOutputStream(file)) { PdfWriter writer = PdfWriter.getInstance(pdfDoc, out); int pageNum = 0; PdfContentByte cb; pdfDoc.open(); cb = writer.getDirectContent(); while (true) { PdfTemplate template = cb.createTemplate(width, height); Graphics2D g2d = template.createGraphics(width, height, new DefaultFontMapper()); if (print(g2d, format, pageNum) == NO_SUCH_PAGE) { g2d.dispose(); break; } if (pageNum != 0) { pdfDoc.newPage(); } g2d.setClip(0, 0, (int) width, (int) height); print(g2d, format, pageNum++); g2d.dispose(); cb.addTemplate(template, 0, 0); } pdfDoc.close(); } return true; } catch (Exception exception) { return false; } finally { setPrinting(false); closeContainers(changed); } } /** * @param file The file to save to. * @param createdFiles The files that were created. * @return <code>true</code> on success. */ public boolean saveAsPNG(File file, ArrayList<File> createdFiles) { HashSet<Row> changed = expandAllContainers(); try { int dpi = OutputPreferences.getPNGResolution(); PrintManager settings = mCharacter.getPageSettings(); PageFormat format = settings != null ? settings.createPageFormat() : createDefaultPageFormat(); Paper paper = format.getPaper(); int width = (int) (paper.getWidth() / 72.0 * dpi); int height = (int) (paper.getHeight() / 72.0 * dpi); StdImage buffer = StdImage.create(width, height, Transparency.OPAQUE); int pageNum = 0; String name = PathUtils.getLeafName(file.getName(), false); file = file.getParentFile(); adjustToPageSetupChanges(true); setPrinting(true); while (true) { File pngFile; Graphics2D gc = buffer.getGraphics(); if (print(gc, format, pageNum) == NO_SUCH_PAGE) { gc.dispose(); break; } gc.setClip(0, 0, width, height); gc.setBackground(Color.WHITE); gc.clearRect(0, 0, width, height); gc.scale(dpi / 72.0, dpi / 72.0); print(gc, format, pageNum++); gc.dispose(); pngFile = new File(file, PathUtils.enforceExtension(name + (pageNum > 1 ? " " + pageNum : ""), //$NON-NLS-1$//$NON-NLS-2$ FileType.PNG_EXTENSION)); if (!StdImage.writePNG(pngFile, buffer, dpi)) { throw new IOException(); } createdFiles.add(pngFile); } return true; } catch (Exception exception) { return false; } finally { setPrinting(false); closeContainers(changed); } } @Override public int getNotificationPriority() { return 0; } @Override public PrintManager getPrintManager() { if (mPrintManager == null) { try { mPrintManager = mCharacter.getPageSettings(); } catch (Exception exception) { // Ignore } } return mPrintManager; } @Override public String getPrintJobTitle() { Dockable dockable = UIUtilities.getAncestorOfType(this, Dockable.class); if (dockable != null) { return dockable.getTitle(); } Frame frame = UIUtilities.getAncestorOfType(this, Frame.class); if (frame != null) { return frame.getTitle(); } return mCharacter.getDescription().getName(); } @Override public void adjustToPageSetupChanges(boolean willPrint) { OutputPreferences.setDefaultPageSettings(getPrintManager()); if (willPrint) { mSavedScale = mScale; mScale = Scales.ACTUAL_SIZE.getScale(); mOkToPaint = false; } rebuild(); } @Override public boolean isPrinting() { return mIsPrinting; } @Override public void setPrinting(boolean printing) { mIsPrinting = printing; if (!printing) { mOkToPaint = true; if (mSavedScale != null && mSavedScale.getScale() != mScale.getScale()) { mScale = mSavedScale; rebuild(); } else { repaint(); } } } }