package org.skylion.mangareader.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.skylion.mangareader.mangaengine.MangaEdenAPI;
import org.skylion.mangareader.mangaengine.MangaEngine;
import org.skylion.mangareader.mangaengine.MangaHereAPI;
import org.skylion.mangareader.mangaengine.MangaPandaAPI;
import org.skylion.mangareader.mangaengine.MangaReaderAPI;
import org.skylion.mangareader.mangaengine.Prefetcher;
import org.skylion.mangareader.util.AutoSuggestor;
import org.skylion.mangareader.util.HashMapComboBoxModel;
import org.skylion.mangareader.util.ImagePanel;
import org.skylion.mangareader.util.Logger;
import org.skylion.mangareader.util.StringUtil;
//TODO Re-compose code to remove unnecessary JFrame extension
/**
*
* A GUI designed for Janga
* @author Skylion
*
*/
public class MainGUI extends JFrame {
/**
*
*/
private static final long serialVersionUID = -6351329658980316434L;
private Container pane;//Place holder
/**
* User Interface Buttons
*/
private JButton next;
private JButton previous;
private JPanel toolbar;
private JComboBox<String> chapterSel;
private JComboBox<String> pageSel;
private JComboBox<String> engineSel;
private JButton toggleFullScreen;
//private JToggleButton toolbarLock;
/**
* User commandline
*/
private JTextField mangaSelect;//MangaSelect
private AutoSuggestor autoSelect;//My Autosuggestion Decorator
private ImagePanel page; //That page currently displayed
private boolean fullScreen = false;//Is in Fullscreen?
private static final String instructions = "<html><center>" +
"<p><font size =\"32\">Just type the name of the manga you wish to read in the search bar above."+
" Use the dropdown menus to navigate between different manga sources, chapters," +
" and pages.<font size =\"32\"></p>" + "</html></center>";
private JPanel pageUI;
/**
* Used to create and store Global Keystroke
*/
private HashMap<KeyStroke, Action> actionMap = new HashMap<KeyStroke, Action>();
/**
* Used to store different Manga Engines
*/
private LinkedHashMap<String, MangaEngine> mangaEngineMap = new LinkedHashMap<String, MangaEngine>();
/**
* The Engine used to fetch content from the Manga Website. In this case MangaHere
*/
private MangaEngine mangaEngine;
public MainGUI(){
super();
initGUI();
}
private void initGUI(){
this.setTitle("Janga Manga Reader");
this.setBackground(Color.BLACK);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//Exits the program on close
pane = getContentPane();
pane.setLayout(new BorderLayout());
pane.setBackground(Color.BLACK);
previous = new JButton("Previous Page");
previous.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
loadPage(mangaEngine.getPreviousPage());
}
});
next = new JButton("Next Page");
next.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt){
loadPage(mangaEngine.getNextPage());
}
});
mangaSelect = new JTextField("Type your manga into here");
mangaSelect.setEditable(true);
mangaSelect.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
disableNavigation(false);
loadPage(mangaEngine.getMangaURL(mangaSelect.getText()));
refreshLists();
}
});
//Loads MangaEngines in a separate Thread to reduce start up time
engineSel = new JComboBox<String>(
new HashMapComboBoxModel<String, MangaEngine>(mangaEngineMap));
new Thread(new Runnable(){
private boolean engineLoadingFailed = false;
@Override
public void run(){
try {
mangaEngineMap.put("MangaHere", new MangaHereAPI());
} catch (IOException ex) {
catcher(ex);
}
try {
mangaEngineMap.put("MangaReader", new MangaReaderAPI());
} catch(Exception ex){
catcher(ex);
}
try {
mangaEngineMap.put("MangaPanda", new MangaPandaAPI());
} catch(Exception ex){
catcher(ex);
}
try {
mangaEngineMap.put("MangaEden", new MangaEdenAPI());
} catch(IOException ex){
catcher(ex);
}
if(engineLoadingFailed){
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run(){
JOptionPane.showMessageDialog(MainGUI.this, "ERROR",
"Some MangaEngine could not be loaded", JOptionPane.ERROR_MESSAGE);
}
});
}
}
private void catcher(Exception e){
Toolkit.getDefaultToolkit().beep();
Logger.log(e);
engineLoadingFailed = true;
}
}).start();
//Loads the first Manga Engine that loads succesfully
while(mangaEngine == null){
try {
if(mangaEngineMap.keySet().isEmpty()){
Thread.sleep(100);
} else {
String first = mangaEngineMap.keySet().toArray()[0].toString();
engineSel.setSelectedItem(first);
mangaEngine = new Prefetcher(this, mangaEngineMap.get(first));
}
} catch (InterruptedException e) {
System.exit(ABORT);
}
}
//Wraps the TextField with my custom autosuggestion box
autoSelect = new AutoSuggestor(mangaSelect, this, mangaEngine.getMangaList(),
Color.WHITE.brighter(), Color.BLUE, Color.MAGENTA.darker(), 0.75f);
//Sets up the page
page = new ImagePanel();
page.setPreferredSize(getEffectiveScreenSize());
page.setBackground(Color.BLACK);
page.setForeground(next.getForeground());
page.setFont(next.getFont().deriveFont(72f));
page.setDoubleBuffered(true);
//User Welcome Screen!
page.setText("<html><center><p> Welcome to Janga!</p> " +
"<h1>A Java Manga Reading Application</h1>" + instructions +
"<p>Enjoy!</p></center>");
////////////////////////////////////////
//Constructs components in the toolbar//
////////////////////////////////////////
chapterSel = new JComboBox<String>(StringUtil.formatChapterNames(
mangaEngine.getChapterNames()));
chapterSel.setToolTipText("Chapter Navigation");
chapterSel.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
if (evt.getModifiers() != 0 && evt.getSource() instanceof JComboBox
&& ((JComboBox<?>)evt.getSource()).isPopupVisible()){
int index = chapterSel.getSelectedIndex();
try {
loadPage(mangaEngine.loadImg(mangaEngine.getChapterList()[index]));
refreshLists();
autoSelect.setDictionary(mangaEngine.getMangaList());
updateStatus();
} catch (IOException e) {
Logger.log(e);
}
}
}
});
pageSel = generateComboBox("Pg: ", mangaEngine.getPageList().length);
pageSel.setToolTipText("Page Navigation");
pageSel.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
//Checks if it wasn't programmatically fired.
if (evt.getModifiers() != 0 && evt.getSource() instanceof JComboBox
&& ((JComboBox<?>)evt.getSource()).isPopupVisible()){
int index = pageSel.getSelectedIndex();
try {
loadPage(mangaEngine.loadImg(mangaEngine.getPageList()[index]));
updateStatus();
} catch (IOException e) {
Logger.log(e);
}
}
}
});
//Initializes mangaEngineSel
engineSel.setToolTipText("Manga Source Selection");
engineSel.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
if (evt.getModifiers() != 0) {//Checks if it wasn't programmatically fired.
String engineName = (String)engineSel.getSelectedItem();
try {
if(mangaEngine instanceof Closeable){
((Closeable) mangaEngine).close();
}
mangaEngine = new Prefetcher(MainGUI.this, mangaEngineMap.get(engineName));
loadPage((BufferedImage)null);
page.setText(instructions + "</center></html>");
disableNavigation(true);
refreshLists();
autoSelect.setDictionary(mangaEngine.getMangaList());//Resets the list
} catch (IOException e) {
Logger.log(e);
}
}
}
});
toggleFullScreen = new JButton("F");
toggleFullScreen.setToolTipText("Toggles FullScreen (CTRL-F)");
toggleFullScreen.setForeground(Color.WHITE);
toggleFullScreen.setBackground(Color.MAGENTA.darker().darker());
toggleFullScreen.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
setFullScreen(!isFullScreen());
}
});
/*
toolbarLock = new JToggleButton("T");
toolbarLock.setToolTipText("Locks toolbar");
toolbarLock.setFont(next.getFont());
toolbarLock.setForeground(Color.WHITE);
toolbarLock.setBackground(Color.LIGHT_GRAY);
toggleFullScreen.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
}
});
*/
//Instantionates and sets up asthetics for toolbar
toolbar = new JPanel() {
/**
* Default Generated Serial UID
*/
private static final long serialVersionUID = -2560322159063958032L;
@Override
protected void paintComponent(Graphics graphics) { // Adds a gradient to the JPanel
super.paintComponent(graphics);
Graphics2D g2d = (Graphics2D) graphics;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
GradientPaint gp = new GradientPaint(0, 0,
getBackground().brighter(), 0, getHeight(),
getBackground().darker());
g2d.setPaint(gp);
g2d.fillRect(0, 0, getWidth(), getHeight());
}
};
toolbar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
toolbar.setBackground(Color.DARK_GRAY.brighter());
//toolbar.setBackground(next.getBackground().brighter());
//Adds Items to toolbar
toolbar.setLayout(new BoxLayout(toolbar, BoxLayout.X_AXIS));
toolbar.add(mangaSelect);
toolbar.add(engineSel);
toolbar.add(chapterSel);
toolbar.add(pageSel);
toolbar.add(toggleFullScreen);
//End of Toolbar setup
next.setPreferredSize(previous.getPreferredSize());////Makes the buttons the same size
//Sets up the User Interface
pageUI = new JPanel(new BorderLayout(0,0));
pageUI.setBackground(Color.BLACK);
pageUI.add(next, BorderLayout.EAST);
pageUI.add(previous, BorderLayout.WEST);
pageUI.add(page, BorderLayout.CENTER);
pane.add(toolbar, BorderLayout.NORTH);
pane.add(pageUI, BorderLayout.CENTER);
//toolbar.setBackground(toolbar.getParent().getBackground());
/*
// Experimental auto-hide functionality
final JToggleButton lockToolbar = new JToggleButton("V");
lockToolbar.setSelected(true);
lockToolbar.setForeground(Color.WHITE);
lockToolbar.setBackground(Color.GREEN.darker());
lockToolbar.setFont(next.getFont());
toolbar.add(lockToolbar);
pane.addMouseMotionListener(new MouseAdapter() {
public void mouseMoved (MouseEvent me) {
boolean was = toolbar.isVisible();
if (!was && toolbar.getBounds().contains(me.getPoint())) {//If mouseOver
toolbar.setVisible(true);
} else if(!lockToolbar.isSelected()){
toolbar.setVisible(false);
}
if(was != toolbar.isVisible()){
revalidate();
}
}
});
*/
disableNavigation(true);
initKeyboard();
}
private void initKeyboard(){
Action nextPageAction = new AbstractAction(){
/**
* AutoGenerated UID
*/
private static final long serialVersionUID = -3381019543157339629L;
@Override
public void actionPerformed(ActionEvent e) {
if(pageSel.isEnabled()){
next.doClick();
}
}
};
Action previousPageAction = new AbstractAction(){
/**
* AutoGenerated UID
*/
private static final long serialVersionUID = 1148536792558547221L;
@Override
public void actionPerformed(ActionEvent e) {
if(pageSel.isEnabled()){
previous.doClick();
}
}
};
Action toggleTaskbarAction = new AbstractAction(){
/**
* AutoGenerated UID
*/
private static final long serialVersionUID = 1148566792558547221L;
@Override
public void actionPerformed(ActionEvent e) {
toolbar.setVisible(!toolbar.isVisible());
}
};
Action toggleFullScreenAction = new AbstractAction(){
/**
* AutoGenerated UID
*/
private static final long serialVersionUID = 1148566792585247221L;
@Override
public void actionPerformed(ActionEvent e) {
setFullScreen(!isFullScreen());
}
};
Action escapeFullScreenAction = new AbstractAction(){
/**
* AutoGenerated UID
*/
private static final long serialVersionUID = 1148563792585247221L;
@Override
public void actionPerformed(ActionEvent e) {
if(isFullScreen()){
setFullScreen(false);
}
}
};
//Loads Bindings into JFrame
this.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "NEXT");
this.getRootPane().getActionMap().put("NEXT", nextPageAction);
this.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "PREV");
this.getRootPane().getActionMap().put("PREV", previousPageAction);
//Loads Keyboard Commands
actionMap.put(KeyStroke.getKeyStroke("ctrl released UP"), nextPageAction);
actionMap.put(KeyStroke.getKeyStroke("ctrl released DOWN"), previousPageAction);
actionMap.put(KeyStroke.getKeyStroke("released PAGE_UP"), nextPageAction);
actionMap.put(KeyStroke.getKeyStroke("released PAGE_DOWN"), previousPageAction);
actionMap.put(KeyStroke.getKeyStroke("control T"), toggleTaskbarAction);
actionMap.put(KeyStroke.getKeyStroke("control F"), toggleFullScreenAction);
actionMap.put(KeyStroke.getKeyStroke("pressed ESCAPE"), escapeFullScreenAction);
//Overrides the KeyboardFocusManager to allow Global key commands.
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
kfm.addKeyEventDispatcher( new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
//System.out.println(keyStroke);
if ( actionMap.containsKey(keyStroke) ) {
final Action a = actionMap.get(keyStroke);
final ActionEvent ae = new ActionEvent(e.getSource(), e.getID(), null );
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
a.actionPerformed(ae);
}
} );
return true;
}
return false;
}
});
}
/**
* Toggles full screen mode. Requires a lot of references to the JFrame.
*/
public void setFullScreen(boolean fullScreen){
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice dev = env.getDefaultScreenDevice();//Gets the main screen
if(!fullScreen){//Checks if a full screen application isn't open
this.dispose();//Restarts the JFrame
this.setVisible(false);
this.setResizable(true);//Re-enables resize-ability.
this.setUndecorated(false);//Adds title bar back
this.setVisible(true);//Shows restarted JFrame
this.removeMouseListener(macWorkAround);
this.pack();
this.setExtendedState(this.getExtendedState()|JFrame.MAXIMIZED_BOTH);//Returns to maximized state
this.fullScreen = false;
}
else{
this.dispose();//Restarts the JFrame
this.setResizable(false);//Disables resizing else causes bugs
this.setUndecorated(true);//removes title bar
this.setVisible(true);//Makes it visible again
this.revalidate();
this.setSize(Toolkit.getDefaultToolkit().getScreenSize());
try{
dev.setFullScreenWindow(this);//Makes it full screen
if(System.getProperty("os.name").contains("Mac OS X")){
this.setVisible(false);
this.setVisible(true);
this.addMouseListener(macWorkAround);
}
this.repaint();
this.revalidate();
}
catch(Exception e){
dev.setFullScreenWindow(null);//Fall back behavior
}
this.requestFocus();
this.fullScreen = true;
}
}
private final MouseAdapter macWorkAround = new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e){
MainGUI.this.setVisible(false);
MainGUI.this.setVisible(true);
}
};
public boolean isFullScreen(){
return fullScreen;
}
/*
// Alternate fullscreen
@Override
public void setExtendedState(int state){
if(state == JFrame.MAXIMIZED_BOTH){
this.dispose();
this.setUndecorated(true);
this.setVisible(true);
}
else{
this.dispose();
this.setUndecorated(false);
this.setVisible(true);
}
super.setExtendedState(state);
}
*/
/**
* Loads the page of the Manga specified by the URL
* @param newURL
*/
private void loadPage(String URL){
int attempts = 0;
while(attempts < 3)
try {
if(mangaEngine.getCurrentURL().equals(URL)){
return;//No need to waste time reloading a page.
}
loadPage(mangaEngine.loadImg(URL));
page.setText(null);
updateStatus();
return;
} catch (IOException e) {
Logger.log(e);
if(e instanceof SocketTimeoutException) {
// Do nothing allow reattempt.
// This doesn't do nothing...
}
page.loadImage(null);
page.setText("<html><p><center>An error has occured :(</p>" +
"<h1>Sorry, the currently requested title or page number could not be found. " +
"Please try a different page, chapter, or manga source. " +
"If you encountered this error while searching for a manga title, " +
"the manga you currently have requested is most likely licensed; " +
"hence, not available in your country and/or region. " +
"We apologize for this inconvenience.</h1> " +
"<h1>Thank you for your cooperation!</h1></center></html>");
Toolkit.getDefaultToolkit().beep();
return;
}
}
private void loadPage(BufferedImage image){
page.loadImage(image);
}
/**
* Gets the effective screen size: the screen size without toolbar
* @return
*/
private Dimension getEffectiveScreenSize(){
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Insets scnMax = Toolkit.getDefaultToolkit().getScreenInsets(getGraphicsConfiguration());
int taskBarSize = scnMax.bottom;
return new Dimension(screenSize.width, screenSize.height-taskBarSize);
}
/**
* Spawns a combo box
* @param prefix the prefix you want to have before the combobox
* @param size The size of the ComboBox
* @return The ComboBox
*/
private JComboBox<String> generateComboBox(String prefix, int size){
String[] out = new String[size];
for(int i = 0; i<size; i++){
out[i] = (prefix + (i+1));
}
return new JComboBox<String>(out);
}
private void disableNavigation(boolean disabled){
boolean enabled = !disabled;
pageSel.setEnabled(enabled);
chapterSel.setEnabled(enabled);
next.setVisible(enabled);
previous.setVisible(enabled);
}
/**
* Updates the ComboBoxes for chapters and pages
*/
private void refreshLists(){
//Work around to forcefully refresh
chapterSel.setModel(new DefaultComboBoxModel<String>(mangaEngine.getChapterNames()));
String[] pages = new String[mangaEngine.getPageList().length];
for(int i = 0; i<mangaEngine.getPageList().length; i++){
pages[i] = ("Pg: " + (i+1));
}
System.out.print("UPDATED ");
pageSel.setModel(new DefaultComboBoxModel<String>(pages));
}
private void updateStatus(){
String chapter = "Ch: " + mangaEngine.getCurrentChapNum();
String currentPage = "Pg: " + mangaEngine.getCurrentPageNum();
chapterSel.getModel().setSelectedItem(chapter);//Work around to forcefully change combobox
pageSel.getModel().setSelectedItem(currentPage);
}
}