/* * @copyright 2012 Philip Warner * @license GNU General Public License * * This file is part of Book Catalogue. * * Book Catalogue is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Book Catalogue is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>. */ package com.eleybourn.bookcatalogue.booklist; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import com.eleybourn.bookcatalogue.BookCatalogueApp; import com.eleybourn.bookcatalogue.CatalogueDBAdapter; import com.eleybourn.bookcatalogue.R; import com.eleybourn.bookcatalogue.database.DbUtils.DomainDefinition; import com.eleybourn.bookcatalogue.database.SerializationUtils; import com.eleybourn.bookcatalogue.database.SerializationUtils.DeserializationException; import com.eleybourn.bookcatalogue.properties.BooleanListProperty; import com.eleybourn.bookcatalogue.properties.BooleanProperty; import com.eleybourn.bookcatalogue.properties.IntegerListProperty; import com.eleybourn.bookcatalogue.properties.ListProperty.ItemEntries; import com.eleybourn.bookcatalogue.properties.Properties; import com.eleybourn.bookcatalogue.properties.Property; import com.eleybourn.bookcatalogue.properties.PropertyGroup; import com.eleybourn.bookcatalogue.properties.StringProperty; /** * Represents a specific style of book list (eg. authors/series). Individual BooklistGroup objects * are added to a style in order to describe the resulting list style. * * ENHANCE: Allow for style-based overrides of things currently stored in preferences * This should include thumbnail presence/size, book-in-each-series etc. as well as font sizes. * * How to add a new Group: * * - add it to RowKinds. Update ROW_KIND_MAX. * - add new domain to DatabaseDefinitions (if necessary) * - modify BooklistBuilder.build() to add the necessary grouped/sorted domains * - modify BooksMultitypeListHandler; if it is just a string field, then use a GenericHolder. Otherwise add a new holder. * Need to at least modify BooksMultitypeListHandler.newHolder(). * * @author Philip Warner */ public class BooklistStyle implements Iterable<BooklistGroup>, Serializable { private static final long serialVersionUID = 6615877148246388549L; private static final long realSerialVersion = 4; /** Extra book data to show at lowest level */ public static final int EXTRAS_BOOKSHELVES = 1; /** Extra book data to show at lowest level */ public static final int EXTRAS_LOCATION = 2; /** Extra book data to show at lowest level */ public static final int EXTRAS_PUBLISHER = 4; /** Extra book data to show at lowest level */ public static final int EXTRAS_AUTHOR = 8; /** Extra book data to show at lowest level */ public static final int EXTRAS_THUMBNAIL = 16; /** Extra book data to show at lowest level */ public static final int EXTRAS_THUMBNAIL_LARGE = 32; /** Extra book data to show at lowest level */ public static final int EXTRAS_ALL = EXTRAS_BOOKSHELVES|EXTRAS_LOCATION|EXTRAS_PUBLISHER|EXTRAS_AUTHOR|EXTRAS_THUMBNAIL|EXTRAS_THUMBNAIL_LARGE; public static final String SFX_SHOW_BOOKSHELVES = "ShowBookshelves"; public static final String SFX_SHOW_LOCATION = "ShowLocation"; public static final String SFX_SHOW_PUBLISHER = "ShowPublisher"; public static final String SFX_SHOW_AUTHOR = "ShowAuthor"; public static final String SFX_SHOW_THUMBNAILS = "ShowThumbnails"; public static final String SFX_LARGE_THUMBNAILS = "LargeThumbnails"; public static final String SFX_CONDENSED = "Condensed"; public static final String SFX_SHOW_HEADER_INFO = "ShowHeaderInfo"; /** Prefix for all prefs */ public static final String TAG = "BookList"; /** Show list of bookshelves for each book */ public static final String PREF_SHOW_EXTRAS_PREFIX = TAG + "."; /** Show header info in list */ public static final String PREF_SHOW_HEADER_INFO = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_HEADER_INFO; /** Show list of bookshelves for each book */ public static final String PREF_CONDENSED_TEXT = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_CONDENSED; /** Show list of bookshelves for each book */ public static final String PREF_SHOW_BOOKSHELVES = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_BOOKSHELVES; /** Show location for each book */ public static final String PREF_SHOW_LOCATION = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_LOCATION; /** Show author for each book */ public static final String PREF_SHOW_AUTHOR = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_AUTHOR; /** Show publisher for each book */ public static final String PREF_SHOW_PUBLISHER = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_PUBLISHER; /** Show thumbnail image for each book */ public static final String PREF_SHOW_THUMBNAILS = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_SHOW_THUMBNAILS; /** Show large thumbnail if thumbnails are shown */ public static final String PREF_LARGE_THUMBNAILS = PREF_SHOW_EXTRAS_PREFIX + BooklistStyle.SFX_LARGE_THUMBNAILS; /** ID if string representing name of this style. Used for standard system-defined styles */ private int mNameStringId; /** User-defined name of this style. Used for user-defined styles */ private String mName; // TODO: Legacy field designed for backward serialization compatibility private transient StringProperty mNameProperty; /** Extra fields to show at the book level */ //private int mExtras = 0; /** List of groups */ private final ArrayList<BooklistGroup> mGroups; /** Row id of database row from which this object comes */ private long mRowId = 0; /** Extra details to show on book rows */ private transient BooleanProperty mXtraShowThumbnails; /** Extra details to show on book rows */ private transient BooleanProperty mXtraLargeThumbnails; /** Extra details to show on book rows */ private transient BooleanProperty mXtraShowBookshelves; /** Extra details to show on book rows */ private transient BooleanProperty mXtraShowLocation; /** Extra details to show on book rows */ private transient BooleanProperty mXtraShowPublisher; /** Extra details to show on book rows */ private transient BooleanProperty mXtraShowAuthor; /** Extra details to show on book rows */ private transient IntegerListProperty mXtraReadUnreadAll; /** Show list using smaller text */ private transient BooleanListProperty mCondensed; /** Show list header info */ private transient IntegerListProperty mShowHeaderInfo; /** * Flag indicating this style was in the 'preferred' set when it was added to its Styles collection * The value is not dynamically checked. */ private boolean mIsPreferred; /** * Represents a collection of domains that make a unique key for a given group. * * @author Philip Warner */ public static class CompoundKey { /** Unique prefix used to represent a key in the hierarchy */ String prefix; /** List of domains in key */ DomainDefinition[] domains; /** Constructor */ CompoundKey(String prefix, DomainDefinition...domains) { this.prefix = prefix; this.domains = domains; } } public static final int FILTER_READ = 1; public static final int FILTER_UNREAD = 2; public static final int FILTER_READ_AND_UNREAD = 3; // ENHANCE: Add filters based on 'loaned', 'anthology' and (maybe) duplicate books /** Support for 'READ' filter */ private static ItemEntries<Integer> mReadFilterListItems = new ItemEntries<Integer>(); static { mReadFilterListItems.add(FILTER_UNREAD, R.string.select_unread_only); mReadFilterListItems.add(FILTER_READ, R.string.select_read_only); mReadFilterListItems.add(FILTER_READ_AND_UNREAD, R.string.all_books); } /** Support for 'Condensed' property */ private static ItemEntries<Boolean> mCondensedListItems = new ItemEntries<Boolean>(); static { mCondensedListItems.add(null, R.string.use_default_setting); mCondensedListItems.add(false, R.string.normal); mCondensedListItems.add(true, R.string.smaller); } public static final Integer SUMMARY_HIDE = 0; public static final Integer SUMMARY_SHOW_COUNT = 1; public static final Integer SUMMARY_SHOW_LEVEL_1 = 2; public static final Integer SUMMARY_SHOW_LEVEL_2 = 4; public static final Integer SUMMARY_SHOW_LEVEL_1_AND_COUNT = SUMMARY_SHOW_COUNT ^ SUMMARY_SHOW_LEVEL_1; public static final Integer SUMMARY_SHOW_ALL = 0xff; /** Support for 'Show List Header Info' property */ private static ItemEntries<Integer> mShowHeaderInfoListItems = new ItemEntries<Integer>(); static { mShowHeaderInfoListItems.add(null, R.string.use_default_setting); mShowHeaderInfoListItems.add(SUMMARY_HIDE, R.string.hide_summary_details); mShowHeaderInfoListItems.add(SUMMARY_SHOW_COUNT, R.string.show_book_count); mShowHeaderInfoListItems.add(SUMMARY_SHOW_LEVEL_1_AND_COUNT, R.string.show_first_level_and_book_count); mShowHeaderInfoListItems.add(SUMMARY_SHOW_ALL, R.string.show_all_summary_details); } /** * Constructor for system-defined styles. * * @param stringId */ BooklistStyle(int stringId) { mNameStringId = stringId; mGroups = new ArrayList<BooklistGroup>(); initProperties(); mNameProperty.set((String)null); } /** * Constructor for user-defined styles. * * @param name */ BooklistStyle(String name) { initProperties(); mNameStringId = 0; mGroups = new ArrayList<BooklistGroup>(); mNameProperty.set(name); } public int getReadFilter() { return mXtraReadUnreadAll.getResolvedValue(); } /** * Accessor for flag indicating style is among preferred styles. * * @return */ public boolean isPreferred() { return mIsPreferred; } /** * Accessor for flag indicating style is among preferred styles. * * @return */ public void setPreferred(boolean isPreferred) { mIsPreferred = isPreferred; } /** * Accessor. Returns system name or user-defined name based on kind of style this object defines. * * @return */ public String getDisplayName() { String s = mNameProperty.getResolvedValue(); if (!s.equals("")) return s; else return BookCatalogueApp.getResourceString(mNameStringId); } /** * Accessor. Sets user-defined name. * * @return */ public void setName(String name) { mNameProperty.set(name); mNameStringId = 0; } /** * Accessor. Returns a standarised form of the style name. This name is unique. * * @return */ public String getCanonicalName() { if (isUserDefined()) return getRowId() + "-u"; else { String name = getDisplayName().trim().toLowerCase(); return name + "-s"; } } public void addGroup(BooklistGroup group) { mGroups.add(group); } /** * Add a group to this style below any already added groups. * * @param kind Kind of group to add. * * @return Newly created group. */ public BooklistGroup addGroup(int kind) { BooklistGroup g = BooklistGroup.newGroup(kind); addGroup(g); return g; } /** * Remove a group from this style. * * @param kind Kind of group to add. * * @return Newly created group. */ public BooklistGroup removeGroup(int kind) { BooklistGroup toRemove = null; for(BooklistGroup g: mGroups) { if (g.kind == kind) { toRemove = g; break; } } if (toRemove != null) mGroups.remove(toRemove); return toRemove; } /** * Returns true if this style is user-defined. * * @return */ public boolean isUserDefined() { return (mNameStringId == 0 || mRowId != 0); } private void initProperties() { mXtraShowThumbnails = new BooleanProperty("XThumbnails", PropertyGroup.GRP_THUMBNAILS, R.string.show_thumbnails, PREF_SHOW_THUMBNAILS, true); mXtraShowThumbnails.setWeight(-100); mXtraLargeThumbnails = new BooleanProperty("XLargeThumbnails", PropertyGroup.GRP_THUMBNAILS, R.string.prefer_large_thumbnails, PREF_LARGE_THUMBNAILS, false); mXtraLargeThumbnails.setWeight(-99); mXtraShowBookshelves = new BooleanProperty("XBookshelves", PropertyGroup.GRP_EXTRA_BOOK_DETAILS, R.string.bookshelves, PREF_SHOW_BOOKSHELVES, false); mXtraShowLocation = new BooleanProperty("XLocation", PropertyGroup.GRP_EXTRA_BOOK_DETAILS, R.string.location, PREF_SHOW_LOCATION, false); mXtraShowPublisher = new BooleanProperty("XPublisher", PropertyGroup.GRP_EXTRA_BOOK_DETAILS, R.string.publisher, PREF_SHOW_PUBLISHER, false); mXtraShowAuthor = new BooleanProperty("XAuthor", PropertyGroup.GRP_EXTRA_BOOK_DETAILS, R.string.author, PREF_SHOW_AUTHOR, false); mXtraReadUnreadAll = new IntegerListProperty(mReadFilterListItems, "XReadUnreadAll", PropertyGroup.GRP_EXTRA_FILTERS, R.string.select_based_on_read_status, FILTER_READ_AND_UNREAD); mNameProperty = new StringProperty("StyleName", PropertyGroup.GRP_GENERAL, R.string.name); mNameProperty.setRequireNonBlank(true); // Put it at top of its group mNameProperty.setWeight(-100); mCondensed = new BooleanListProperty(mCondensedListItems, PREF_CONDENSED_TEXT, PropertyGroup.GRP_GENERAL, R.string.size_of_booklist_items, null, PREF_CONDENSED_TEXT, false); mShowHeaderInfo = new IntegerListProperty(mShowHeaderInfoListItems, PREF_SHOW_HEADER_INFO, PropertyGroup.GRP_GENERAL, R.string.summary_details_in_header, null, PREF_SHOW_HEADER_INFO, SUMMARY_SHOW_ALL); } /** * Get all of the properties of this Style and its groups. */ public Properties getProperties() { Properties props = new Properties(); props.add(mXtraShowThumbnails); props.add(mXtraLargeThumbnails); props.add(mXtraShowBookshelves); props.add(mXtraShowLocation); props.add(mXtraShowPublisher); props.add(mXtraShowAuthor); props.add(mXtraReadUnreadAll); props.add(mCondensed); props.add(mNameProperty); props.add(mShowHeaderInfo); for(BooklistGroup g: mGroups) { g.getStyleProperties(props); } return props; } /** * Passed a Properties object, update the properties of this style * based on the values of the passed properties. */ public void setProperties(Properties newProps) { Properties props = getProperties(); for(Property newVal: newProps) { Property thisProp = props.get(newVal.getUniqueName()); if (thisProp != null) { thisProp.set(newVal); } } } /** * Passed a template style, copy the group structure to this style. */ public void setGroups(BooklistStyle fromStyle) { Properties newProps = new Properties(); // Save the current groups Hashtable<Integer, BooklistGroup> oldGroups = new Hashtable<Integer, BooklistGroup>(); for(BooklistGroup g: this) { oldGroups.put(g.kind, g); } // Clear the current groups, and rebuild, reusing old values where possible mGroups.clear(); for(BooklistGroup g: fromStyle) { BooklistGroup saved = oldGroups.get(g.kind); if (saved != null) { this.addGroup(saved); } else { g.getStyleProperties(newProps); this.addGroup(g.kind); } } // Copy any properties from new groups. this.setProperties(newProps); } /** * Accessor. */ public int getExtras() { int extras = 0; if ( mXtraShowThumbnails.getResolvedValue() ) extras |= EXTRAS_THUMBNAIL; if ( mXtraLargeThumbnails.getResolvedValue() ) extras |= EXTRAS_THUMBNAIL_LARGE; if ( mXtraShowBookshelves.getResolvedValue() ) extras |= EXTRAS_BOOKSHELVES; if ( mXtraShowLocation.getResolvedValue() ) extras |= EXTRAS_LOCATION; if ( mXtraShowPublisher.getResolvedValue() ) extras |= EXTRAS_PUBLISHER; if ( mXtraShowAuthor.getResolvedValue() ) extras |= EXTRAS_AUTHOR; return extras; } /** * Check if ths style has the specified group */ public boolean hasKind(int kind) { for (BooklistGroup g : mGroups) { if (g.kind == kind) return true; } return false; } /** * Get the group at the passed index. */ public BooklistGroup getGroupAt(int index) { return mGroups.get(index); } /** * Get the number of groups in this style */ public int size() { return mGroups.size(); } /** * Accessor for underlying database row id, if this object is from a database. 0 if not from database. * * @return */ public long getRowId() { return mRowId; } /** * Accessor for underlying database row id, set by query that retrieves the object. * * @return */ public void setRowId(long rowId) { mRowId = rowId; } /** * Iterable support */ @Override public Iterator<BooklistGroup> iterator() { return mGroups.iterator(); } /** * Custom serialization support. */ private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject((Long)realSerialVersion); out.writeObject(mXtraShowThumbnails.get()); out.writeObject(mXtraLargeThumbnails.get()); out.writeObject(mXtraShowBookshelves.get()); out.writeObject(mXtraShowLocation.get()); out.writeObject(mXtraShowPublisher.get()); out.writeObject(mXtraShowAuthor.get()); out.writeObject(mXtraReadUnreadAll.get()); out.writeObject(mCondensed.get()); out.writeObject(mNameProperty.get()); out.writeObject(mShowHeaderInfo.get()); } /** * Pseudo-constructor for custom serialization support. */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); initProperties(); Object o = in.readObject(); long version = 0; if (o instanceof Long) { // Its the version version = ((Long)o); // Get the next object o = in.readObject(); } else { // Its a pre-version object...just use it } mXtraShowThumbnails.set((Boolean)o); mXtraLargeThumbnails.set((Boolean)in.readObject()); mXtraShowBookshelves.set((Boolean)in.readObject()); mXtraShowLocation.set((Boolean)in.readObject()); mXtraShowPublisher.set((Boolean)in.readObject()); mXtraShowAuthor.set((Boolean)in.readObject()); mXtraReadUnreadAll.set((Integer)in.readObject()); if (version > 0) mCondensed.set((Boolean)in.readObject()); if (version > 1) mNameProperty.set((String)in.readObject()); else mNameProperty.set(mName); // Added mShowHeaderInfo with version 3 if (version > 2) { // Changed it from Boolean to Integer in version 4 if (version == 3) { Boolean isSet = (Boolean)in.readObject(); if (isSet == null) { mShowHeaderInfo.set( (Integer)null ); } else { mShowHeaderInfo.set( isSet ? SUMMARY_SHOW_ALL : SUMMARY_HIDE); } } else { mShowHeaderInfo.set((Integer)in.readObject()); } } } /** * Accessor */ public boolean isCondensed() { return mCondensed.getResolvedValue(); } public void setCondensed(boolean condensed) { mCondensed.set(condensed); } /** * Accessor */ public boolean showThumbnails() { return mXtraShowThumbnails.getResolvedValue(); } public void setShowThumbnails(boolean show) { mXtraShowThumbnails.set(show); } /** * Accessor */ public Integer getReadUnreadAll() { return mXtraReadUnreadAll.getResolvedValue(); } public void setReadUnreadAll(Integer readUnreadAll) { mXtraReadUnreadAll.set(readUnreadAll); } /** * Accessor * * @return */ public int getShowHeaderInfo() { return mShowHeaderInfo.getResolvedValue(); } /** * Save this style as a custom user style to the database. * Either updates or creates as necessary */ public void saveToDb(CatalogueDBAdapter db) { if (getRowId() == 0) mRowId = db.insertBooklistStyle(this); else db.updateBooklistStyle(this); } /** * Delete this style from the database */ public void deleteFromDb(CatalogueDBAdapter db) { if (getRowId() == 0) throw new RuntimeException("Style is not stored in the database, can not be deleted"); db.deleteBooklistStyle(this.getRowId()); } /** * Convenience function to return a list of group names. */ public String getGroupListDisplayNames() { StringBuilder groups = new StringBuilder(); boolean first = true; for(BooklistGroup g: this) { if (first) first = false; else groups.append(" / "); groups.append(g.getName()); } return groups.toString(); } /** * Construct a deep clone of this object. */ public BooklistStyle getClone() throws DeserializationException { BooklistStyle newStyle = SerializationUtils.cloneObject(this); return newStyle; } /** * Accessor to allow setting of Extras value directly. * @param show */ public void setShowAuthor(boolean show) { mXtraShowAuthor.set(show); } }