eu.over9000.skadi.ui.MainWindow.java Source code

Java tutorial

Introduction

Here is the source code for eu.over9000.skadi.ui.MainWindow.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2015 s1mpl3x <jan[at]over9000.eu>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package eu.over9000.skadi.ui;

import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.StatusBar;

import de.jensd.fx.glyphs.GlyphsDude;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcons;
import eu.over9000.skadi.handler.ChannelHandler;
import eu.over9000.skadi.handler.ChatHandler;
import eu.over9000.skadi.handler.StreamHandler;
import eu.over9000.skadi.io.PersistenceHandler;
import eu.over9000.skadi.model.Channel;
import eu.over9000.skadi.model.StateContainer;
import eu.over9000.skadi.service.ForcedChannelUpdateService;
import eu.over9000.skadi.service.ImportFollowedService;
import eu.over9000.skadi.ui.cells.LiveCell;
import eu.over9000.skadi.ui.cells.RightAlignedCell;
import eu.over9000.skadi.ui.cells.UptimeCell;
import eu.over9000.skadi.ui.dialogs.SettingsDialog;
import eu.over9000.skadi.util.JavaFXUtil;
import eu.over9000.skadi.util.StringUtil;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.image.Image;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseButton;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;

public class MainWindow extends Application {

    private ChannelHandler channelHandler;
    private ChatHandler chatHandler;
    private StreamHandler streamHandler;
    private PersistenceHandler persistenceHandler;
    private StateContainer currentState;

    private ObjectProperty<Channel> detailChannel;

    private BorderPane bp;
    private SplitPane sp;
    private StatusBar sb;

    private ChannelDetailPane detailPane;
    private TableView<Channel> table;
    private TableColumn<Channel, Boolean> liveCol;
    private TableColumn<Channel, String> nameCol;
    private TableColumn<Channel, String> titleCol;
    private TableColumn<Channel, String> gameCol;
    private TableColumn<Channel, Integer> viewerCol;
    private TableColumn<Channel, Long> uptimeCol;
    private SortedList<Channel> sortedChannelList;
    private FilteredList<Channel> filteredChannelList;
    private Button add;
    private TextField addName;
    private Button imprt;
    private Button details;
    private Button remove;
    private Button refresh;
    private Button settings;
    private ToggleButton onlineOnly;
    private ToolBar tb;
    private TextField filterText;
    private HandlerControlButton chatAndStreamButton;

    private Stage stage;

    @Override
    public void init() throws Exception {
        this.persistenceHandler = new PersistenceHandler();
        this.currentState = this.persistenceHandler.loadState();
        this.channelHandler = new ChannelHandler(this.persistenceHandler);
        this.chatHandler = new ChatHandler();
        this.streamHandler = new StreamHandler(this.channelHandler);

        this.detailChannel = new SimpleObjectProperty<>();
    }

    @Override
    public void start(final Stage stage) throws Exception {
        this.stage = stage;

        this.detailPane = new ChannelDetailPane(this);

        this.bp = new BorderPane();
        this.sp = new SplitPane();
        this.sb = new StatusBar();

        this.setupTable();
        this.setupToolbar(stage);

        this.sp.getItems().add(this.table);

        this.bp.setTop(this.tb);
        this.bp.setCenter(this.sp);
        this.bp.setBottom(this.sb);

        final Scene scene = new Scene(this.bp, 1280, 720);
        scene.getStylesheets().add(this.getClass().getResource("/styles/copyable-label.css").toExternalForm());
        scene.setOnDragOver(event -> {
            final Dragboard d = event.getDragboard();
            if (d.hasUrl() || d.hasString()) {
                event.acceptTransferModes(TransferMode.COPY);
            } else {
                event.consume();
            }
        });
        scene.setOnDragDropped(event -> {
            final Dragboard d = event.getDragboard();
            boolean success = false;
            if (d.hasUrl()) {
                final String user = StringUtil.extractUsernameFromURL(d.getUrl());
                if (user != null) {
                    success = this.channelHandler.addChannel(user, this.sb);
                } else {
                    this.sb.setText("dragged url is no twitch stream");
                }
            } else if (d.hasString()) {
                success = this.channelHandler.addChannel(d.getString(), this.sb);
            }
            event.setDropCompleted(success);
            event.consume();

        });

        stage.setTitle("Skadi");
        stage.getIcons().add(new Image(this.getClass().getResourceAsStream("/icons/skadi.png")));
        stage.setScene(scene);
        stage.show();

        stage.iconifiedProperty().addListener((obs, oldV, newV) -> {
            if (this.currentState.isMinimizeToTray()) {
                if (newV) {
                    stage.hide();
                }
            }
        });
        stage.setOnCloseRequest(event -> Platform.exit());

        this.bindColumnWidths();

    }

    @Override
    public void stop() throws Exception {
        super.stop();
        ForcedChannelUpdateService.onShutdown();
    }

    private void setupToolbar(final Stage stage) {

        this.add = GlyphsDude.createIconButton(FontAwesomeIcons.PLUS);
        this.addName = new TextField();
        this.addName.setOnAction(event -> this.add.fire());

        this.add.setOnAction(event -> {
            final String name = this.addName.getText().trim();

            if (name.isEmpty()) {
                return;
            }

            final boolean result = this.channelHandler.addChannel(name, this.sb);

            if (result) {
                this.addName.clear();
            }

        });

        this.imprt = GlyphsDude.createIconButton(FontAwesomeIcons.DOWNLOAD);
        this.imprt.setOnAction(event -> {
            final TextInputDialog dialog = new TextInputDialog();
            dialog.initModality(Modality.APPLICATION_MODAL);
            dialog.initOwner(stage);
            dialog.setTitle("Import followed channels");
            dialog.setHeaderText("Import followed channels from Twitch");
            dialog.setGraphic(null);
            dialog.setContentText("Twitch username:");

            dialog.showAndWait().ifPresent(name -> {
                final ImportFollowedService ifs = new ImportFollowedService(this.channelHandler, name, this.sb);
                ifs.start();
            });
        });

        this.details = GlyphsDude.createIconButton(FontAwesomeIcons.INFO);
        this.details.setDisable(true);
        this.details.setOnAction(event -> {
            this.detailChannel.set(this.table.getSelectionModel().getSelectedItem());
            if (!this.sp.getItems().contains(this.detailPane)) {
                this.sp.getItems().add(this.detailPane);
                this.doDetailSlide(true);
            }
        });
        this.details.setTooltip(new Tooltip("Show channel information"));

        this.remove = GlyphsDude.createIconButton(FontAwesomeIcons.TRASH);
        this.remove.setDisable(true);
        this.remove.setOnAction(event -> {
            final Channel candidate = this.table.getSelectionModel().getSelectedItem();

            final Alert alert = new Alert(AlertType.CONFIRMATION);
            alert.initModality(Modality.APPLICATION_MODAL);
            alert.initOwner(stage);
            alert.setTitle("Delete channel");
            alert.setHeaderText("Delete " + candidate.getName());
            alert.setContentText("Do you really want to delete " + candidate.getName() + "?");

            final Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK) {
                this.channelHandler.getChannels().remove(candidate);
                this.sb.setText("Removed channel " + candidate.getName());
            }
        });

        this.refresh = GlyphsDude.createIconButton(FontAwesomeIcons.REFRESH);
        this.refresh.setTooltip(new Tooltip("Refresh all channels"));
        this.refresh.setOnAction(event -> {
            this.refresh.setDisable(true);
            final ForcedChannelUpdateService service = new ForcedChannelUpdateService(this.channelHandler, this.sb,
                    this.refresh);
            service.start();
        });

        this.settings = GlyphsDude.createIconButton(FontAwesomeIcons.COG);
        this.settings.setTooltip(new Tooltip("Settings"));
        this.settings.setOnAction(event -> {
            final SettingsDialog dialog = new SettingsDialog();
            dialog.initModality(Modality.APPLICATION_MODAL);
            dialog.initOwner(stage);
            final Optional<StateContainer> result = dialog.showAndWait();
            if (result.isPresent()) {
                this.persistenceHandler.saveState(result.get());
            }
        });

        this.onlineOnly = new ToggleButton("Live", GlyphsDude.createIcon(FontAwesomeIcons.FILTER));
        this.onlineOnly.setSelected(this.currentState.isOnlineFilterActive());

        this.onlineOnly.setOnAction(event -> {
            this.currentState.setOnlineFilterActive(this.onlineOnly.isSelected());
            this.persistenceHandler.saveState(this.currentState);
            this.updateFilterPredicate();
        });
        // TODO re-enable if 8u60 is released
        this.onlineOnly.setDisable(true);

        this.filterText = new TextField();
        this.filterText.textProperty().addListener((obs, oldV, newV) -> this.updateFilterPredicate());
        this.filterText.setTooltip(new Tooltip("Filter channels by name, status and game"));

        // TODO re-enable if 8u60 is released
        this.filterText.setDisable(true);

        this.tb = new ToolBar();
        this.tb.getItems().addAll(this.addName, this.add, this.imprt, new Separator(), this.refresh, this.settings,
                new Separator(), this.onlineOnly, this.filterText, new Separator(), this.details, this.remove);

        this.chatAndStreamButton = new HandlerControlButton(this.chatHandler, this.streamHandler, this.table,
                this.tb, this.sb);

        this.updateFilterPredicate();
    }

    private void updateFilterPredicate() {
        this.filteredChannelList.setPredicate(channel -> {

            boolean isOnlineResult;
            boolean containsTextResult;

            // isOnline returns a Boolean, can be null
            isOnlineResult = !this.onlineOnly.isSelected() || Boolean.TRUE.equals(channel.isOnline());

            final String filter = this.filterText.getText().trim();
            if (filter.isEmpty()) {
                containsTextResult = true;
            } else {
                final boolean nameContains = StringUtils.containsIgnoreCase(channel.getName(), filter);
                final boolean gameContains = StringUtils.containsIgnoreCase(channel.getGame(), filter);
                final boolean titleContains = StringUtils.containsIgnoreCase(channel.getTitle(), filter);
                containsTextResult = nameContains || gameContains || titleContains;
            }

            return isOnlineResult && containsTextResult;
        });
    }

    private void setupTable() {
        this.table = new TableView<>();

        this.liveCol = new TableColumn<>("Live");
        this.liveCol.setCellValueFactory(p -> p.getValue().onlineProperty());
        this.liveCol.setSortType(SortType.DESCENDING);
        this.liveCol.setCellFactory(p -> new LiveCell());

        this.nameCol = new TableColumn<>("Channel");
        this.nameCol.setCellValueFactory(p -> p.getValue().nameProperty());

        this.titleCol = new TableColumn<>("Status");
        this.titleCol.setCellValueFactory(p -> p.getValue().titleProperty());

        this.gameCol = new TableColumn<>("Game");
        this.gameCol.setCellValueFactory(p -> p.getValue().gameProperty());

        this.viewerCol = new TableColumn<>("Viewer");
        this.viewerCol.setCellValueFactory(p -> p.getValue().viewerProperty().asObject());
        this.viewerCol.setSortType(SortType.DESCENDING);
        this.viewerCol.setCellFactory(p -> new RightAlignedCell<>());

        this.uptimeCol = new TableColumn<>("Uptime");
        this.uptimeCol.setCellValueFactory((p) -> p.getValue().uptimeProperty().asObject());
        this.uptimeCol.setCellFactory(p -> new UptimeCell());

        this.table.setPlaceholder(new Label("no channels added/matching the filters"));

        this.table.getColumns().add(this.liveCol);
        this.table.getColumns().add(this.nameCol);
        this.table.getColumns().add(this.titleCol);
        this.table.getColumns().add(this.gameCol);
        this.table.getColumns().add(this.viewerCol);
        this.table.getColumns().add(this.uptimeCol);

        this.table.getSortOrder().add(this.liveCol);
        this.table.getSortOrder().add(this.viewerCol);
        this.table.getSortOrder().add(this.nameCol);

        this.filteredChannelList = new FilteredList<>(this.channelHandler.getChannels());
        this.sortedChannelList = new SortedList<>(this.filteredChannelList);
        this.sortedChannelList.comparatorProperty().bind(this.table.comparatorProperty());

        this.table.setItems(this.sortedChannelList);

        this.table.setRowFactory(tv -> {
            final TableRow<Channel> row = new TableRow<>();
            row.setOnMouseClicked(event -> {
                if ((event.getButton() == MouseButton.PRIMARY) && (event.getClickCount() == 2) && !row.isEmpty()) {
                    this.detailChannel.set(row.getItem());
                    if (!this.sp.getItems().contains(this.detailPane)) {
                        this.sp.getItems().add(this.detailPane);
                        this.doDetailSlide(true);
                    }
                }
            });
            return row;
        });
        this.table.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> {

            this.details.setDisable(newV == null);
            this.remove.setDisable(newV == null);
            this.chatAndStreamButton.setDisable(newV == null);
            this.chatAndStreamButton.resetQualities();
            if ((newV == null) && this.sp.getItems().contains(this.detailPane)) {
                this.doDetailSlide(false);
            }

        });
    }

    private void bindColumnWidths() {
        final ScrollBar tsb = JavaFXUtil.getVerticalScrollbar(this.table);
        final ReadOnlyDoubleProperty sbw = tsb.widthProperty();
        final DoubleBinding tcw = this.table.widthProperty().subtract(sbw);

        this.liveCol.prefWidthProperty().bind(tcw.multiply(0.05));
        this.nameCol.prefWidthProperty().bind(tcw.multiply(0.15));
        this.titleCol.prefWidthProperty().bind(tcw.multiply(0.4));
        this.gameCol.prefWidthProperty().bind(tcw.multiply(0.2));
        this.viewerCol.prefWidthProperty().bind(tcw.multiply(0.075));
        this.uptimeCol.prefWidthProperty().bind(tcw.multiply(0.125));
    }

    public void doDetailSlide(final boolean doOpen) {

        final KeyValue positionKeyValue = new KeyValue(this.sp.getDividers().get(0).positionProperty(),
                doOpen ? 0.15 : 1);
        final KeyValue opacityKeyValue = new KeyValue(this.detailPane.opacityProperty(), doOpen ? 1 : 0);
        final KeyFrame keyFrame = new KeyFrame(Duration.seconds(0.1), positionKeyValue, opacityKeyValue);
        final Timeline timeline = new Timeline(keyFrame);
        timeline.setOnFinished(evt -> {
            if (!doOpen) {
                MainWindow.this.sp.getItems().remove(MainWindow.this.detailPane);
                MainWindow.this.detailPane.setOpacity(1);
            }
        });
        timeline.play();
    }

    public StateContainer getCurrentState() {
        return this.currentState;
    }

    public ObjectProperty<Channel> getDetailChannel() {
        return this.detailChannel;
    }

    public HandlerControlButton getChatAndStreamButton() {
        return this.chatAndStreamButton;
    }
}