package com.robonobo.gui.frames; import static com.robonobo.common.util.TextUtil.*; import static com.robonobo.gui.GuiUtil.*; import static javax.swing.SwingUtilities.*; import info.clearthought.layout.TableLayout; import java.awt.*; import java.awt.event.*; import java.io.File; import java.util.*; import java.util.List; import javax.swing.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.robonobo.Robonobo; import com.robonobo.common.concurrent.CatchingRunnable; import com.robonobo.common.exceptions.SeekInnerCalmException; import com.robonobo.common.util.FileUtil; import com.robonobo.common.util.NetUtil; import com.robonobo.core.Platform; import com.robonobo.core.RobonoboController; import com.robonobo.core.api.TrackListener; import com.robonobo.core.api.UserAdapter; import com.robonobo.core.api.model.*; import com.robonobo.core.metadata.UserConfigCallback; import com.robonobo.gui.*; import com.robonobo.gui.panels.LeftSidebar; import com.robonobo.gui.panels.MainPanel; import com.robonobo.gui.preferences.PrefDialog; import com.robonobo.gui.sheets.*; import com.robonobo.gui.tasks.ImportFilesTask; import com.robonobo.gui.tasks.ImportITunesTask; import com.robonobo.mina.external.HandoverHandler; @SuppressWarnings("serial") public class RobonoboFrame extends SheetableFrame implements TrackListener { public RobonoboController ctrl; List<String> cmdLineArgs = new ArrayList<String>(); boolean argsHandled = false; public JMenuBar menuBar; public MainPanel mainPanel; public LeftSidebar leftSidebar; Log log = LogFactory.getLog(RobonoboFrame.class); public GuiConfig guiCfg; public UriHandler uriHandler; private boolean tracksLoaded; private boolean shownLogin; static RobonoboFrame instance; public static RobonoboFrame getInstance() { return instance; } public RobonoboFrame(RobonoboController control, String[] args) { this.ctrl = control; cmdLineArgs.addAll(Arrays.asList(args)); guiCfg = (GuiConfig) control.getConfig("gui"); setTitle("robonobo"); setIconImage(getRobonoboIconImage()); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new CloseListener()); menuBar = Platform.getPlatform().getMenuBar(this); setJMenuBar(menuBar); JPanel contentPane = new JPanel(); double[][] cellSizen = { { 5, 200, 5, TableLayout.FILL, 5 }, { 3, TableLayout.FILL, 5 } }; contentPane.setLayout(new TableLayout(cellSizen)); setContentPane(contentPane); leftSidebar = new LeftSidebar(this); contentPane.add(leftSidebar, "1,1"); mainPanel = new MainPanel(this); contentPane.add(mainPanel, "3,1"); setPreferredSize(new Dimension(1024, 723)); pack(); leftSidebar.selectMyMusic(); addListeners(); uriHandler = new UriHandler(this); instance = this; } private void addListeners() { ctrl.addTrackListener(this); ctrl.addUserListener(new UserAdapter() { @Override public void allUsersAndPlaylistsLoaded() { handleArgs(); } }); // There's a chance the control might have loaded everything before we add ourselves as a listener, so // spawn a thread to check if this is so ctrl.getExecutor().execute(new CatchingRunnable() { public void doRun() throws Exception { checkTracksLoaded(); checkUsersLoaded(); } }); // Grab our events... KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventHandler()); } @Override public void setVisible(boolean visible) { super.setVisible(visible); if (visible) { // Log us the hell in Runnable onLogin = new CatchingRunnable() { public void doRun() throws Exception { // If the tracks haven't loaded yet, show the welcome when they have shownLogin = true; if (tracksLoaded) showWelcome(false); } }; final LoginSheet ls = new LoginSheet(RobonoboFrame.this, false, onLogin); showSheet(ls); if (isNonEmpty(ls.getEmailField().getText())) { invokeLater(new CatchingRunnable() { public void doRun() throws Exception { ls.tryLogin(); } }); } } } /** Once this is called, everything is up and running */ @Override public void allTracksLoaded() { tracksLoaded = true; setupHandoverHandler(); // If we haven't shown the login sheet yet, show the welcome later if (shownLogin) showWelcome(false); } private void checkTracksLoaded() { if (tracksLoaded) return; if (ctrl.haveAllSharesStarted()) allTracksLoaded(); } private void checkUsersLoaded() { if (ctrl.haveAllUsersAndPlaylistsLoaded()) handleArgs(); } private void handleArgs() { if (argsHandled) return; argsHandled = true; StringBuffer sb = new StringBuffer("Handling cmd line arguments: '"); for (String s : cmdLineArgs) { sb.append(s); sb.append(" "); } sb.append("'"); log.info(sb); // Handle everything that isn't the -console for (String arg : cmdLineArgs) { if (!"-console".equalsIgnoreCase(arg)) handleArg(arg); } } private void setupHandoverHandler() { ctrl.setHandoverHandler(new HandoverHandler() { @Override public String gotHandover(String arg) { handleArg(arg); SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { // Note: this doesn't bring the app to the front on OSX, but we don't care that much as the app // receives URL notifications directly anyway // If we need it at a subsequent stage, just run an applescript: // tell app "robonobo" // activate // end tell RobonoboFrame.this.setState(Frame.NORMAL); RobonoboFrame.this.toFront(); } }); log.debug("Got handover msg: " + arg); return "0:OK"; } }); } private void handleArg(String arg) { log.info("Handling cmdline arg: " + arg); if (isNonEmpty(arg)) { if (arg.startsWith("rbnb")) openRbnbUri(arg); else log.error("Received erroneous robonobo argument: " + arg); } } public void addRuntimeArg(String arg) { log.info("Adding additional runtime arg: " + arg); try { if (argsHandled) handleArg(arg); else cmdLineArgs.add(arg); } catch (Exception e) { log.error("Error adding additional arg", e); } } private void openRbnbUri(String uri) { uriHandler.handle(uri); } public void showWelcome(boolean forceShow) { // If we have no shares or no friends (or we're forcing it), show the welcome dialog boolean gotShares = (ctrl.getNumShares() > 0); boolean gotFriends = (ctrl.getMyUser().getFriendIds().size() > 0); boolean show = false; if (forceShow) show = true; else { if (!guiCfg.getShowWelcomePanel()) show = false; else show = (!gotShares) || (!gotFriends); } if (show) { SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { showSheet(new WelcomeSheet(RobonoboFrame.this)); } }); } } @Override public void trackUpdated(String streamId, Track t) { // Do nothing } @Override public void tracksUpdated(Collection<Track> trax) { // Do nothing } public void shareFilesOrDirectories(final List<File> files) { List<File> allFiles = new ArrayList<File>(); for (File selFile : files) if (selFile.isDirectory()) allFiles.addAll(FileUtil.getFilesWithinPath(selFile, "mp3")); else allFiles.add(selFile); if (allFiles.size() == 0) { runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { showSheet(new InfoSheet(RobonoboFrame.this, "No files added", "No importable files were found. At this time, robonobo can share only MP3 files.")); } }); return; } shareFiles(allFiles); return; } public void shareFiles(final List<File> files) { ImportFilesTask t = new ImportFilesTask(this, files); ctrl.runTask(t); } public void shareFromITunes() { ImportITunesTask t = new ImportITunesTask(this); ctrl.runTask(t); } public void showAddSharesDialog() { // Define this as a runnable as we might need to login first Runnable flarp = new CatchingRunnable() { @Override public void doRun() throws Exception { JFileChooser fc = new JFileChooser(); fc.setFileFilter(new javax.swing.filechooser.FileFilter() { public boolean accept(File f) { if (f.isDirectory()) return true; return "mp3".equalsIgnoreCase(FileUtil.getFileExtension(f)); } public String getDescription() { return "MP3 files"; } }); fc.setMultiSelectionEnabled(true); fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); int retVal = fc.showOpenDialog(RobonoboFrame.this); if (retVal == JFileChooser.APPROVE_OPTION) { final File[] selFiles = fc.getSelectedFiles(); ctrl.getExecutor().execute(new CatchingRunnable() { public void doRun() throws Exception { shareFilesOrDirectories(Arrays.asList(selFiles)); } }); } } }; if (ctrl.getMyUser() != null) flarp.run(); else showLogin(flarp); } /** @param onLogin * If the login is successful, this will be executed on the Swing GUI thread (so don't do too much in it) */ public void showLogin(final Runnable onLogin) { SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { LoginSheet lp = new LoginSheet(RobonoboFrame.this, true, onLogin); showSheet(lp); } }); } public void showAbout() { SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { AboutSheet ap = new AboutSheet(RobonoboFrame.this); showSheet(ap); } }); } public void showPreferences() { // showSheet(new PreferencesSheet(this)); PrefDialog prefDialog = new PrefDialog(this); prefDialog.setVisible(true); } public void showConsole() { ConsoleFrame consoleFrame = new ConsoleFrame(this); consoleFrame.setVisible(true); } public void showLogFrame() { Log4jMonitorFrame logFrame = new Log4jMonitorFrame(this); logFrame.setVisible(true); } public void showFacebookSignupSheet(String title, String msg) { showWebsitePageSheet(title, msg, ctrl.getConfig().getWebsiteUrlBase() + "before-facebook-attach"); } public void showTwitterSignupSheet(String title, String msg) { showWebsitePageSheet(title, msg, ctrl.getConfig().getWebsiteUrlBase() + "before-twitter-attach"); } public void showWebsitePageSheet(String title, String msg, final String url) { final Sheet sheet = new ConfirmSheet(RobonoboFrame.this, title, msg, "Go to robonobo website", new CatchingRunnable() { public void doRun() throws Exception { NetUtil.browse(url); ctrl.watchMyUserConfig(); } }); runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { showSheet(sheet); } }); } /** Call only from UI thread */ public void showAddFriendsSheet() { if (!SwingUtilities.isEventDispatchThread()) throw new SeekInnerCalmException(); UserConfig uc = ctrl.getMyUserConfig(); if (uc == null || uc.getItem("facebookId") == null) { // They don't seem to be registered for facebook - fetch a fresh copy of the usercfg from midas in // case they've recently added themselves to fb final Sheet waitSheet = new PleaseWaitSheet(this, "checking Facebook details"); showSheet(waitSheet); ctrl.fetchMyUserConfig(new UserConfigCallback() { public void success(UserConfig freshUc) { waitSheet.setVisible(false); final boolean haveFb = (freshUc.getItem("facebookId") != null); runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { showSheet(new AddFriendsSheet(RobonoboFrame.this, haveFb)); } }); } public void error(long userId, Exception e) { waitSheet.setVisible(false); } }); } else { // They are registered for fb already, reflect this in the sheet showSheet(new AddFriendsSheet(this, true)); } } // TODO Generalise fb/twitter into SocialNetwork or something /** Call only from UI thread */ public void postToFacebook(final Playlist p) { if (!SwingUtilities.isEventDispatchThread()) throw new SeekInnerCalmException(); UserConfig uc = ctrl.getMyUserConfig(); if (uc == null || uc.getItem("facebookId") == null) { // They don't seem to be registered for facebook - fetch a fresh copy of the usercfg from midas in // case they've recently added themselves to fb final Sheet waitSheet = new PleaseWaitSheet(this, "checking Facebook details"); showSheet(waitSheet); ctrl.fetchMyUserConfig(new UserConfigCallback() { public void success(UserConfig freshUc) { waitSheet.setVisible(false); final String title = "Post to Facebook"; if (freshUc.getItem("facebookId") == null) { // They haven't associated their facebook account with their rbnb one... open a browser window // on the page to do so String facebookBounceMsg = "Before you can post playlists to Facebook, you must add your Facebook details to your account on the robonobo website."; showFacebookSignupSheet(title, facebookBounceMsg); } else { runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { // Playlist must be public or friends-visible to post to fb if (p.getVisibility().equals(Playlist.VIS_ME)) { String msg = "This playlist is currently set to be visible to you only; it must be visible to your friends for you to post it to Facebook."; final Sheet sheet = new ConfirmSheet(RobonoboFrame.this, title, msg, "Make playlist visible", new CatchingRunnable() { public void doRun() throws Exception { p.setVisibility(Playlist.VIS_FRIENDS); ctrl.updatePlaylist(p); showSheet(new PostToFacebookSheet(RobonoboFrame.this, p)); } }); showSheet(sheet); } else showSheet(new PostToFacebookSheet(RobonoboFrame.this, p)); } }); } } public void error(long userId, Exception e) { waitSheet.setVisible(false); } }); } else { // Playlist must be public or friends-visible to post to fb if (p.getVisibility().equals(Playlist.VIS_ME)) { String msg = "This playlist is currently set to be visible to you only; it must be visible to your friends for you to post it to Facebook."; final Sheet sheet = new ConfirmSheet(RobonoboFrame.this, "Post to Facebook", msg, "Make playlist visible", new CatchingRunnable() { public void doRun() throws Exception { p.setVisibility(Playlist.VIS_FRIENDS); showSheet(new PostToFacebookSheet(RobonoboFrame.this, p)); ctrl.updatePlaylist(p); } }); showSheet(sheet); } else showSheet(new PostToFacebookSheet(this, p)); } } public void postToTwitter(final Playlist p) { if (!SwingUtilities.isEventDispatchThread()) throw new SeekInnerCalmException(); UserConfig uc = ctrl.getMyUserConfig(); if (uc == null || uc.getItem("twitterId") == null) { // They don't seem to be registered for twitter - fetch a fresh copy of the usercfg from midas in // case they've recently added themselves final Sheet waitSheet = new PleaseWaitSheet(this, "checking Twitter details"); showSheet(waitSheet); ctrl.fetchMyUserConfig(new UserConfigCallback() { public void success(UserConfig freshUc) { waitSheet.setVisible(false); final String title = "Post to Twitter"; if (freshUc.getItem("twitterId") == null) { // They haven't associated their twitter account with their rbnb one... open a browser window on // the page to do so String twitBounceMsg = "Before you can post playlists to Twitter, you must add your Twitter details to your account on the robonobo website."; showTwitterSignupSheet(title, twitBounceMsg); } else { runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { // Playlist must be public to post to twitter if (p.getVisibility().equals(Playlist.VIS_ALL)) showSheet(new PostToTwitterSheet(RobonoboFrame.this, p)); else { String msg = "This playlist must be publically-visible for you to post it to Twitter."; final Sheet sheet = new ConfirmSheet(RobonoboFrame.this, title, msg, "Make playlist public", new CatchingRunnable() { public void doRun() throws Exception { p.setVisibility(Playlist.VIS_ALL); showSheet(new PostToTwitterSheet(RobonoboFrame.this, p)); ctrl.updatePlaylist(p); } }); showSheet(sheet); } } }); } } public void error(long userId, Exception e) { waitSheet.setVisible(false); } }); } else { // Playlist must be public to post to twitter if (p.getVisibility().equals(Playlist.VIS_ALL)) showSheet(new PostToTwitterSheet(this, p)); else { String msg = "This playlist must be publically-visible to your friends for you to post it to Twitter."; final Sheet sheet = new ConfirmSheet(RobonoboFrame.this, "Post to Twitter", msg, "Make playlist public", new CatchingRunnable() { public void doRun() throws Exception { p.setVisibility(Playlist.VIS_ALL); showSheet(new PostToTwitterSheet(RobonoboFrame.this, p)); ctrl.updatePlaylist(p); } }); showSheet(sheet); } } } public static Image getRobonoboIconImage() { return GuiUtil.getImage("/rbnb-icon-128x128.png"); } public void shutdown() { setVisible(false); Thread shutdownThread = new Thread(new CatchingRunnable() { public void doRun() throws Exception { ctrl.shutdown(); System.exit(0); } }); shutdownThread.start(); } public void restart() { log.fatal("robonobo restarting"); Thread restartThread = new Thread(new CatchingRunnable() { public void doRun() throws Exception { // Show a message that we're restarting SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { String[] butOpts = { "Quit" }; int result = JOptionPane.showOptionDialog(RobonoboFrame.this, "robonobo is restarting, please wait...", "robonobo restarting", JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, butOpts, "Force Quit"); if (result >= 0) { // They pressed the button... just kill everything log.fatal("Emergency shutdown during restart... pressing Big Red Switch"); System.exit(1); } } }); // Shut down the controller - this will block until the // controller exits ctrl.shutdown(); // Hide this frame - don't dispose of it yet as this might make // the jvm exit SwingUtilities.invokeLater(new CatchingRunnable() { public void doRun() throws Exception { RobonoboFrame.this.setVisible(false); } }); // Startup a new frame and controller Robonobo.startup(null, (String[]) cmdLineArgs.toArray(), false); // Dispose of the old frame RobonoboFrame.this.dispose(); } }); restartThread.setName("Restart"); restartThread.start(); } public void confirmThenShutdown() { invokeLater(new CatchingRunnable() { public void doRun() throws Exception { // If we aren't sharing anything, just close if (ctrl.getNumShares() == 0) { shutdown(); return; } // Likewise, if they've asked us not to confirm if (!guiCfg.getConfirmExit()) { shutdown(); return; } showSheet(new ConfirmCloseSheet(RobonoboFrame.this)); } }); } /** For slow things that have to happen on the gui thread - shows a helpful message to mollify the user while their * ui is frozen * * @param pFetcher * This will be run on the gui thread */ public void runSlowTask(final String whatsHappening, final Runnable task) { runOnUiThread(new CatchingRunnable() { public void doRun() throws Exception { final PleaseWaitSheet sheet = new PleaseWaitSheet(RobonoboFrame.this, whatsHappening); showSheet(sheet); invokeLater(new CatchingRunnable() { public void doRun() throws Exception { task.run(); sheet.setVisible(false); } }); } }); } class CloseListener extends WindowAdapter { public void windowClosing(WindowEvent e) { confirmThenShutdown(); } } class KeyEventHandler implements KeyEventDispatcher { @Override public boolean dispatchKeyEvent(KeyEvent e) { int code = e.getKeyCode(); int modifiers = e.getModifiers(); if (code == KeyEvent.VK_ESCAPE) { if (isShowingSheet()) { // If this is the initial login sheet, don't let them escape it Sheet sh = getTopSheet(); if (sh instanceof LoginSheet) { LoginSheet lsh = (LoginSheet) sh; if (!lsh.getCancelAllowed()) return false; } discardTopSheet(); return true; } } if (code == KeyEvent.VK_Q && modifiers == Platform.getPlatform().getCommandModifierMask()) { confirmThenShutdown(); return true; } return false; } } }