/*
* Copyright (C) 2013 Alex Kuiper
*
* This file is part of PageTurner
*
* PageTurner 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.
*
* PageTurner 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 PageTurner. If not, see <http://www.gnu.org/licenses/>.*
*/
package net.nightwhistler.pageturner.view.bookview;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.content.Context;
import android.graphics.Color;
import android.text.*;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.view.View;
import com.google.inject.Inject;
import jedi.option.Option;
import net.nightwhistler.pageturner.Configuration;
import net.nightwhistler.pageturner.R;
import net.nightwhistler.pageturner.dto.HighLight;
import net.nightwhistler.pageturner.epub.PageTurnerSpine;
import android.graphics.Canvas;
import android.widget.TextView;
import net.nightwhistler.pageturner.view.HighlightManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.util.Collections.emptyList;
import static jedi.option.Options.none;
import static jedi.option.Options.option;
import static jedi.option.Options.some;
public class FixedPagesStrategy implements PageChangeStrategy {
private Configuration config;
private StaticLayoutFactory layoutFactory;
private HighlightManager highlightManager;
private static final Logger LOG = LoggerFactory.getLogger("FixedPagesStrategy");
private Spanned text;
private int pageNum;
private List<Integer> pageOffsets = new ArrayList<>();
private BookView bookView;
private TextView childView;
private int storedPosition = -1;
@Override
public void setBookView(BookView bookView) {
this.bookView = bookView;
this.childView = bookView.getInnerView();
}
@Inject
public void setHighlightManager( HighlightManager highlightManager ) {
this.highlightManager = highlightManager;
}
@Inject
public void setLayoutFactory(StaticLayoutFactory layoutFactory) {
this.layoutFactory = layoutFactory;
}
@Inject
public void setConfig( Configuration config ) {
this.config = config;
}
@Override
public void clearStoredPosition() {
this.pageNum = 0;
this.storedPosition = 0;
}
@Override
public void clearText() {
this.text = new SpannableStringBuilder("");
this.childView.setText(text);
this.pageOffsets = new ArrayList<Integer>();
}
/**
* Returns the current page INSIDE THE SECTION.
*
* @return
*/
public int getCurrentPage() {
return this.pageNum;
}
public List<Integer> getPageOffsets() {
return new ArrayList<>(this.pageOffsets);
}
public List<Integer> getPageOffsets(CharSequence text, boolean includePageNumbers ) {
if ( text == null ) {
return emptyList();
}
List<Integer> pageOffsets = new ArrayList<Integer>();
TextPaint textPaint = bookView.getInnerView().getPaint();
int boundedWidth = bookView.getInnerView().getMeasuredWidth();
LOG.debug( "Page width: " + boundedWidth );
StaticLayout layout = layoutFactory.create(text, textPaint, boundedWidth, bookView.getLineSpacing() );
if ( layout == null ) {
return emptyList();
}
LOG.debug( "Layout height: " + layout.getHeight() );
layout.draw(new Canvas());
//Subtract the height of the top margin
int pageHeight = bookView.getMeasuredHeight() - bookView.getVerticalMargin();
if ( includePageNumbers ) {
String bottomSpace = "0\n";
StaticLayout numLayout = layoutFactory.create(bottomSpace, textPaint, boundedWidth , bookView.getLineSpacing());
numLayout.draw(new Canvas());
//Subtract the height needed to show page numbers, or the
//height of the margin, whichever is more
pageHeight = pageHeight - Math.max(numLayout.getHeight(), bookView.getVerticalMargin());
} else {
//Just subtract the bottom margin
pageHeight = pageHeight - bookView.getVerticalMargin();
}
LOG.debug("Got pageHeight " + pageHeight );
int totalLines = layout.getLineCount();
int topLineNextPage = -1;
int pageStartOffset = 0;
while ( topLineNextPage < totalLines -1 ) {
LOG.debug( "Processing line " + topLineNextPage + " / " + totalLines );
int topLine = layout.getLineForOffset(pageStartOffset);
topLineNextPage = layout.getLineForVertical( layout.getLineTop( topLine ) + pageHeight);
LOG.debug( "topLine " + topLine + " / " + topLineNextPage );
if ( topLineNextPage == topLine ) { //If lines are bigger than can fit on a page
topLineNextPage = topLine + 1;
}
int pageEnd = layout.getLineEnd(topLineNextPage -1);
LOG.debug("pageStartOffset=" + pageStartOffset + ", pageEnd=" + pageEnd );
if (pageEnd > pageStartOffset ) {
if ( text.subSequence(pageStartOffset, pageEnd).toString().trim().length() > 0) {
pageOffsets.add(pageStartOffset);
}
pageStartOffset = layout.getLineStart(topLineNextPage);
}
}
return pageOffsets;
}
@Override
public void reset() {
clearStoredPosition();
this.pageOffsets.clear();
clearText();
}
private void updatePageNumber() {
for ( int i=0; i < this.pageOffsets.size(); i++ ) {
if ( this.pageOffsets.get(i) > this.storedPosition ) {
this.pageNum = i -1;
return;
}
}
this.pageNum = this.pageOffsets.size() - 1;
}
@Override
public void updatePosition() {
if ( pageOffsets.isEmpty() || text.length() == 0 || this.pageNum == -1) {
return;
}
if ( storedPosition != -1 ) {
updatePageNumber();
}
CharSequence sequence = getTextForPage(this.pageNum).getOrElse( "" );
if ( sequence.length() > 0 ) {
// #555 Remove \n at the end of sequence which get InnerView size changed
int endIndex = sequence.length();
while (sequence.charAt(endIndex - 1) == '\n') {
endIndex--;
}
sequence = sequence.subSequence(0, endIndex);
}
try {
this.childView.setText( sequence );
//If we get an error setting the formatted text,
//strip formatting and try again.
} catch ( IndexOutOfBoundsException ie ) {
this.childView.setText( sequence.toString() );
}
}
private Option<CharSequence> getTextForPage( int page ) {
if ( pageOffsets.size() < 1 || page < 0 ) {
return none();
} else if ( page >= pageOffsets.size() -1 ) {
int startOffset = pageOffsets.get(pageOffsets.size() -1);
if ( startOffset >= 0 && startOffset <= text.length() -1 ) {
return some(applySpans(this.text.subSequence(startOffset, text.length()), startOffset));
} else {
return some(applySpans(text, 0));
}
} else {
int start = this.pageOffsets.get(page);
int end = this.pageOffsets.get(page +1 );
return some(applySpans( this.text.subSequence(start, end), start ));
}
}
private CharSequence applySpans(CharSequence text, int offset) {
List<HighLight> highLights = highlightManager.getHighLights( bookView.getFileName() );
int end = offset + text.length() -1;
for ( final HighLight highLight: highLights ) {
if ( highLight.getIndex() == bookView.getIndex() &&
highLight.getStart() >= offset && highLight.getStart() < end ) {
LOG.debug("Got highlight from " + highLight.getStart() + " to " + highLight.getEnd() + " with offset " + offset );
int highLightEnd = Math.min(end, highLight.getEnd() );
( (Spannable) text).setSpan(new HighlightSpan(highLight),
highLight.getStart() - offset, highLightEnd - offset,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return text;
}
@Override
public void setPosition(int pos) {
this.storedPosition = pos;
}
@Override
public void setRelativePosition(double position) {
int intPosition = (int) (this.text.length() * position);
setPosition(intPosition);
}
public int getTopLeftPosition() {
if ( pageOffsets.isEmpty() ) {
return 0;
}
if ( this.pageNum >= this.pageOffsets.size() ) {
return this.pageOffsets.get( this.pageOffsets.size() -1 );
}
return this.pageOffsets.get(this.pageNum);
}
public int getProgressPosition() {
if ( storedPosition > 0 || this.pageOffsets.isEmpty() || this.pageNum == -1 ) {
return this.storedPosition;
}
return getTopLeftPosition();
}
public Option<android.text.Spanned> getText() {
return option(text);
}
public boolean isAtEnd() {
return pageNum == this.pageOffsets.size() - 1;
}
public boolean isAtStart() {
return this.pageNum == 0;
}
public boolean isScrolling() {
return false;
}
@Override
public Option<CharSequence> getNextPageText() {
if ( isAtEnd() ) {
return none();
}
return getTextForPage( this.pageNum + 1);
}
@Override
public Option<CharSequence> getPreviousPageText() {
if ( isAtStart() ) {
return none();
}
return getTextForPage( this.pageNum - 1);
}
@Override
public void pageDown() {
this.storedPosition = -1;
if ( isAtEnd() ) {
PageTurnerSpine spine = bookView.getSpine();
if ( spine == null || ! spine.navigateForward() ) {
return;
}
this.clearText();
this.pageNum = 0;
bookView.loadText();
} else {
this.pageNum = Math.min(pageNum +1, this.pageOffsets.size() -1 );
updatePosition();
}
}
@Override
public void pageUp() {
this.storedPosition = -1;
if ( isAtStart() ) {
PageTurnerSpine spine = bookView.getSpine();
if ( spine == null || ! spine.navigateBack() ) {
return;
}
this.clearText();
this.storedPosition = Integer.MAX_VALUE;
this.bookView.loadText();
} else {
this.pageNum = Math.max(pageNum -1, 0);
updatePosition();
}
}
@Override
public void loadText(Spanned text) {
this.text = text;
this.pageNum = 0;
this.pageOffsets = getPageOffsets(text, config.isShowPageNumbers() );
}
@Override
public void updateGUI() {
updatePosition();
}
}