/* * 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.widgets.outline; import com.trollworks.gcs.character.GURPSCharacter; import com.trollworks.gcs.character.names.Namer; import com.trollworks.gcs.common.DataFile; import com.trollworks.gcs.template.Template; import com.trollworks.toolkit.annotation.Localize; import com.trollworks.toolkit.collections.FilteredList; import com.trollworks.toolkit.ui.Selection; import com.trollworks.toolkit.ui.widget.outline.Outline; import com.trollworks.toolkit.ui.widget.outline.OutlineModel; import com.trollworks.toolkit.ui.widget.outline.OutlineProxy; import com.trollworks.toolkit.ui.widget.outline.Row; import com.trollworks.toolkit.utility.Localization; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import javax.swing.undo.StateEdit; /** Base outline class. */ public class ListOutline extends Outline implements Runnable, ActionListener { @Localize("Remove Rows") @Localize(locale = "de", value = "Zeilen entfernen") @Localize(locale = "ru", value = "Удалить строки") @Localize(locale = "es", value = "Eliminar Filas") private static String CLEAR_UNDO; static { Localization.initialize(); } /** The owning data file. */ protected DataFile mDataFile; private String mRowSetChangedID; /** * Create a new outline. * * @param dataFile The owning data file. * @param model The outline model to use. * @param rowSetChangedID The notification ID to use when the row set changes. */ public ListOutline(DataFile dataFile, OutlineModel model, String rowSetChangedID) { super(model); mDataFile = dataFile; mRowSetChangedID = rowSetChangedID; addActionListener(this); } /** @return The owning data file. */ public DataFile getDataFile() { return mDataFile; } @Override public void rowsAdded(OutlineModel model, Row[] rows) { super.rowsAdded(model, rows); mDataFile.notifySingle(mRowSetChangedID, null); } @Override public void rowsWereRemoved(OutlineModel model, Row[] rows) { super.rowsWereRemoved(model, rows); mDataFile.notifySingle(mRowSetChangedID, null); } /** @return The notification ID to use when the row set changes. */ public String getRowSetChangedID() { return mRowSetChangedID; } @Override public boolean canDeleteSelection() { OutlineModel model = getModel(); return !model.isLocked() && model.hasSelection(); } @Override public void deleteSelection() { if (canDeleteSelection()) { OutlineModel model = getModel(); StateEdit edit = new StateEdit(model, CLEAR_UNDO); Row[] rows = model.getSelectionAsList(true).toArray(new Row[0]); mDataFile.startNotify(); model.removeSelection(); for (int i = rows.length - 1; i >= 0; i--) { rows[i].removeFromParent(); } if (model.getRowCount() > 0) { updateAllRows(); } // Send it out again, since we have a few chicken-and-egg // scenarios to deal with... <sigh> mDataFile.notify(mRowSetChangedID, null); mDataFile.endNotify(); edit.end(); postUndo(edit); } } /** * Adds a row at the "best" place (i.e. looks at the selection). * * @param row The row to add. * @param name The name for the undo event. * @param sibling If the current selection is a container, whether to insert into it, or as a * sibling. * @return The index of the row that was added. */ public int addRow(ListRow row, String name, boolean sibling) { return addRow(new ListRow[] { row }, name, sibling); } /** * Adds rows at the "best" place (i.e. looks at the selection). * * @param rows The rows to add. * @param name The name for the undo event. * @param sibling If the current selection is a container, whether to insert into it, or as a * sibling. * @return The index of the first row that was added. */ public int addRow(ListRow[] rows, String name, boolean sibling) { OutlineModel model = getModel(); StateEdit edit = new StateEdit(model, name); Selection selection = model.getSelection(); int count = selection.getCount(); int insertAt; int i; Row parentRow; if (count > 0) { insertAt = count == 1 ? selection.firstSelectedIndex() : selection.lastSelectedIndex(); parentRow = model.getRowAtIndex(insertAt++); if (!parentRow.canHaveChildren() || !parentRow.isOpen()) { parentRow = parentRow.getParent(); } else if (sibling) { insertAt += parentRow.getChildCount(); parentRow = parentRow.getParent(); } if (parentRow != null && parentRow.canHaveChildren()) { for (ListRow row : rows) { parentRow.addChild(row); } } } else { insertAt = model.getRowCount(); } i = insertAt; for (ListRow row : rows) { model.addRow(i++, row, true); } updateAllRows(); edit.end(); postUndo(edit); revalidate(); return insertAt; } @Override public void actionPerformed(ActionEvent event) { Object source = event.getSource(); String command = event.getActionCommand(); if (source instanceof OutlineProxy) { source = ((OutlineProxy) source).getRealOutline(); } if (source == this && Outline.CMD_OPEN_SELECTION.equals(command)) { openDetailEditor(false); } } /** * Opens detailed editors for the current selection. * * @param later Whether to call {@link EventQueue#invokeLater(Runnable)} rather than immediately * opening the editor. */ public void openDetailEditor(boolean later) { mRowsToEdit = new FilteredList<>(getModel().getSelectionAsList(), ListRow.class); if (later) { EventQueue.invokeLater(this); } else { run(); } } private ArrayList<ListRow> mRowsToEdit; @Override public void run() { if (RowEditor.edit(this, mRowsToEdit)) { if (mDataFile instanceof GURPSCharacter || mDataFile instanceof Template) { Namer.name(this, mRowsToEdit); } updateRows(mRowsToEdit); updateRowHeights(mRowsToEdit); repaint(); mDataFile.notifySingle(mRowSetChangedID, null); } mRowsToEdit = null; } @Override public void undoDidHappen(OutlineModel model) { super.undoDidHappen(model); updateAllRows(); mDataFile.notifySingle(mRowSetChangedID, null); } @Override protected void rowsWereDropped() { updateAllRows(); mDataFile.notifySingle(mRowSetChangedID, null); requestFocusInWindow(); } private void updateAllRows() { updateRows(new FilteredList<>(getModel().getTopLevelRows(), ListRow.class)); } private void updateRows(Collection<ListRow> rows) { for (ListRow row : rows) { updateRow(row); } } private void updateRow(ListRow row) { if (row != null) { int count = row.getChildCount(); for (int i = 0; i < count; i++) { ListRow child = (ListRow) row.getChild(i); updateRow(child); } row.update(); } } /** * Adds the row to the list. Recursively descends the row's children and does the same with * them. * * @param list The list to add rows to. * @param row The row to check. */ protected void addRowsToBeProcessed(ArrayList<ListRow> list, ListRow row) { int count = row.getChildCount(); list.add(row); for (int i = 0; i < count; i++) { addRowsToBeProcessed(list, (ListRow) row.getChild(i)); } } @Override public void sorted(OutlineModel model, boolean restoring) { super.sorted(model, restoring); if (!restoring && mDataFile.sortingMarksDirty()) { mDataFile.setModified(true); } } }