package chatty.gui;
import chatty.gui.components.textpane.UserMessage;
import chatty.gui.components.userinfo.UserInfo;
import chatty.gui.components.DebugWindow;
import chatty.gui.components.ChannelInfoDialog;
import chatty.gui.components.LinkLabelListener;
import chatty.gui.components.help.About;
import chatty.gui.components.HighlightedMessages;
import chatty.gui.components.TokenDialog;
import chatty.gui.components.admin.AdminDialog;
import chatty.gui.components.ConnectionDialog;
import chatty.gui.components.Channel;
import chatty.gui.components.TokenGetDialog;
import chatty.gui.components.FavoritesDialog;
import chatty.gui.components.JoinDialog;
import chatty.util.api.Emoticon;
import chatty.util.api.StreamInfo;
import chatty.util.api.TokenInfo;
import chatty.util.api.Emoticons;
import chatty.util.api.ChannelInfo;
import java.util.List;
import chatty.Chatty;
import chatty.TwitchClient;
import chatty.Helper;
import chatty.User;
import chatty.Irc;
import chatty.gui.components.admin.StatusHistory;
import chatty.UsercolorItem;
import chatty.util.api.usericons.Usericon;
import chatty.WhisperManager;
import chatty.gui.components.AddressbookDialog;
import chatty.gui.components.AutoModDialog;
import chatty.gui.components.ChatRulesDialog;
import chatty.gui.components.EmotesDialog;
import chatty.gui.components.ErrorMessage;
import chatty.gui.components.FollowersDialog;
import chatty.gui.components.LiveStreamsDialog;
import chatty.gui.components.LivestreamerDialog;
import chatty.gui.components.ModerationLog;
import chatty.gui.components.NewsDialog;
import chatty.gui.components.srl.SRL;
import chatty.gui.components.SearchDialog;
import chatty.gui.components.StreamChat;
import chatty.gui.components.UpdateMessage;
import chatty.gui.components.menus.CommandActionEvent;
import chatty.gui.components.menus.CommandMenuItems;
import chatty.gui.components.menus.ContextMenuHelper;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.gui.components.menus.EmoteContextMenu;
import chatty.gui.components.settings.SettingsDialog;
import chatty.gui.components.textpane.SubscriberMessage;
import chatty.gui.notifications.NotificationActionListener;
import chatty.gui.notifications.NotificationManager;
import chatty.util.CopyMessages;
import chatty.util.DateTime;
import chatty.util.ImageCache;
import chatty.util.MiscUtil;
import chatty.util.MsgTags;
import chatty.util.Sound;
import chatty.util.StringUtil;
import chatty.util.api.ChatInfo;
import chatty.util.api.CheerEmoticon;
import chatty.util.api.Emoticon.EmoticonImage;
import chatty.util.api.EmoticonUpdate;
import chatty.util.api.Emoticons.TagEmotes;
import chatty.util.api.FollowerInfo;
import chatty.util.api.TwitchApi.RequestResultCode;
import chatty.util.api.pubsub.ModeratorActionData;
import chatty.util.commands.CustomCommand;
import chatty.util.commands.Parameters;
import chatty.util.hotkeys.HotkeyManager;
import chatty.util.settings.Setting;
import chatty.util.settings.SettingChangeListener;
import chatty.util.settings.Settings;
import chatty.util.settings.SettingsListener;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.logging.LogRecord;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
/**
* The Main Hub for all GUI activity.
*
* @author tduva
*/
public class MainGui extends JFrame implements Runnable {
public static final Color COLOR_NEW_MESSAGE = new Color(200,0,0);
public static final Color COLOR_NEW_HIGHLIGHTED_MESSAGE = new Color(255,80,0);
public final Emoticons emoticons = new Emoticons();
// Reference back to the client to give back data etc.
TwitchClient client = null;
public volatile boolean guiCreated;
// Parts of the GUI
private Channels channels;
private ConnectionDialog connectionDialog;
private TokenDialog tokenDialog;
private TokenGetDialog tokenGetDialog;
private DebugWindow debugWindow;
private UserInfo userInfoDialog;
private About aboutDialog;
private ChannelInfoDialog channelInfoDialog;
private SettingsDialog settingsDialog;
private AdminDialog adminDialog;
private FavoritesDialog favoritesDialog;
private JoinDialog joinDialog;
private HighlightedMessages highlightedMessages;
private HighlightedMessages ignoredMessages;
private MainMenu menu;
private LiveStreamsDialog liveStreamsDialog;
private NotificationManager<String> notificationManager;
private ErrorMessage errorMessage;
private AddressbookDialog addressbookDialog;
private SRL srl;
private LivestreamerDialog livestreamerDialog;
private UpdateMessage updateMessage;
private NewsDialog newsDialog;
private EmotesDialog emotesDialog;
private FollowersDialog followerDialog;
private FollowersDialog subscribersDialog;
private StreamChat streamChat;
private ModerationLog moderationLog;
private AutoModDialog autoModDialog;
private ChatRulesDialog chatRulesDialog;
// Helpers
private final Highlighter highlighter = new Highlighter();
private final Highlighter ignoreChecker = new Highlighter();
private StyleManager styleManager;
private TrayIconManager trayIcon;
private final StateUpdater state = new StateUpdater();
private WindowStateManager windowStateManager;
private final IgnoredMessages ignoredMessagesHelper = new IgnoredMessages(this);
public final HotkeyManager hotkeyManager = new HotkeyManager(this);
// Listeners that need to be returned by methods
private ActionListener actionListener;
private final WindowListener windowListener = new MyWindowListener();
private final UserListener userListener = new MyUserListener();
private final LinkLabelListener linkLabelListener = new MyLinkLabelListener();
private final ContextMenuListener contextMenuListener = new MyContextMenuListener();
public MainGui(TwitchClient client) {
this.client = client;
SwingUtilities.invokeLater(this);
}
@Override
public void run() {
createGui();
}
private Image createImage(String name) {
return Toolkit.getDefaultToolkit().createImage(getClass().getResource(name));
}
/**
* Sets different sizes of the window icon.
*/
private void setWindowIcons() {
ArrayList<Image> windowIcons = new ArrayList<>();
windowIcons.add(createImage("app_16.png"));
windowIcons.add(createImage("app_64.png"));
this.setIconImages(windowIcons);
}
private void setLiveStreamsWindowIcons() {
ArrayList<Image> windowIcons = new ArrayList<>();
windowIcons.add(createImage("app_live_16.png"));
windowIcons.add(createImage("app_live_64.png"));
liveStreamsDialog.setIconImages(windowIcons);
}
private void setHelpWindowIcons() {
ArrayList<Image> windowIcons = new ArrayList<>();
windowIcons.add(createImage("app_help_16.png"));
windowIcons.add(createImage("app_help_64.png"));
aboutDialog.setIconImages(windowIcons);
}
/**
* Creates the gui, run in the EDT.
*/
private void createGui() {
setWindowIcons();
actionListener = new MyActionListener();
// Error/debug stuff
debugWindow = new DebugWindow(new DebugCheckboxListener());
errorMessage = new ErrorMessage(this, linkLabelListener);
// Dialogs and stuff
connectionDialog = new ConnectionDialog(this);
GuiUtil.installEscapeCloseOperation(connectionDialog);
tokenDialog = new TokenDialog(this);
tokenGetDialog = new TokenGetDialog(this);
userInfoDialog = new UserInfo(this, client.settings, contextMenuListener);
GuiUtil.installEscapeCloseOperation(userInfoDialog);
aboutDialog = new About();
setHelpWindowIcons();
channelInfoDialog = new ChannelInfoDialog(this);
channelInfoDialog.addContextMenuListener(contextMenuListener);
adminDialog = new AdminDialog(this, client.api);
favoritesDialog = new FavoritesDialog(this, contextMenuListener);
GuiUtil.installEscapeCloseOperation(favoritesDialog);
joinDialog = new JoinDialog(this);
GuiUtil.installEscapeCloseOperation(joinDialog);
liveStreamsDialog = new LiveStreamsDialog(contextMenuListener);
setLiveStreamsWindowIcons();
//GuiUtil.installEscapeCloseOperation(liveStreamsDialog);
EmoteContextMenu.setEmoteManager(emoticons);
emotesDialog = new EmotesDialog(this, emoticons, this, contextMenuListener);
GuiUtil.installEscapeCloseOperation(emotesDialog);
followerDialog = new FollowersDialog(FollowersDialog.Type.FOLLOWERS,
this, client.api, contextMenuListener);
subscribersDialog = new FollowersDialog(FollowersDialog.Type.SUBSCRIBERS,
this, client.api, contextMenuListener);
// Tray/Notifications
trayIcon = new TrayIconManager(createImage("app_16.png"));
trayIcon.addActionListener(new TrayMenuListener());
notificationManager = new NotificationManager<>(this);
notificationManager.setNotificationActionListener(new MyNotificationActionListener());
// Channels/Chat output
styleManager = new StyleManager(client.settings);
highlightedMessages = new HighlightedMessages(this, styleManager,
"Highlighted Messages","Highlighted", contextMenuListener);
ignoredMessages = new HighlightedMessages(this, styleManager,
"Ignored Messages", "Ignored", contextMenuListener);
channels = new Channels(this,styleManager, contextMenuListener);
channels.getComponent().setPreferredSize(new Dimension(600,300));
add(channels.getComponent(), BorderLayout.CENTER);
channels.setChangeListener(new ChannelChangeListener());
// Some newer stuff
addressbookDialog = new AddressbookDialog(this, client.addressbook);
srl = new SRL(this, client.speedrunsLive, contextMenuListener);
livestreamerDialog = new LivestreamerDialog(this, linkLabelListener, client.settings);
updateMessage = new UpdateMessage(this);
newsDialog = new NewsDialog(this, client.settings);
client.settings.addSettingChangeListener(new MySettingChangeListener());
client.settings.addSettingsListener(new MySettingsListener());
streamChat = new StreamChat(this, styleManager, contextMenuListener,
client.settings.getBoolean("streamChatBottom"));
moderationLog = new ModerationLog(this);
autoModDialog = new AutoModDialog(this, client.api, client);
chatRulesDialog = new ChatRulesDialog(this);
channels.setOnceOffEditListener(chatRulesDialog);
//this.getContentPane().setBackground(new Color(0,0,0,0));
getSettingsDialog();
// Main Menu
MainMenuListener menuListener = new MainMenuListener();
menu = new MainMenu(menuListener,menuListener, linkLabelListener);
setJMenuBar(menu);
state.update();
addListeners();
pack();
setLocationByPlatform(true);
// Load some stuff
client.api.setUserId(client.settings.getString("username"), client.settings.getString("userid"));
client.api.requestEmoticons(false);
//client.api.requestCheerEmoticons(false);
// TEST
// client.api.getUserIdAsap(null, "m_tt");
// client.api.getCheers("m_tt", false);
client.twitchemotes.requestEmotesets(false);
if (client.settings.getBoolean("bttvEmotes")) {
client.bttvEmotes.requestEmotes("$global$", false);
}
// Window states
windowStateManager = new WindowStateManager(this, client.settings);
windowStateManager.addWindow(this, "main", true, true);
windowStateManager.setPrimaryWindow(this);
windowStateManager.addWindow(highlightedMessages, "highlights", true, true);
windowStateManager.addWindow(ignoredMessages, "ignoredMessages", true, true);
windowStateManager.addWindow(channelInfoDialog, "channelInfo", true, true);
windowStateManager.addWindow(liveStreamsDialog, "liveStreams", true, true);
windowStateManager.addWindow(adminDialog, "admin", true, true);
windowStateManager.addWindow(addressbookDialog, "addressbook", true, true);
windowStateManager.addWindow(emotesDialog, "emotes", true, true);
windowStateManager.addWindow(followerDialog, "followers", true, true);
windowStateManager.addWindow(subscribersDialog, "subscribers", true, true);
windowStateManager.addWindow(moderationLog, "moderationLog", true, true);
windowStateManager.addWindow(streamChat, "streamChat", true, true);
windowStateManager.addWindow(userInfoDialog, "userInfo", true, false);
windowStateManager.addWindow(autoModDialog, "autoMod", true, true);
guiCreated = true;
}
protected void popoutCreated(JDialog popout) {
hotkeyManager.registerPopout(popout);
}
private SettingsDialog getSettingsDialog() {
if (settingsDialog == null) {
settingsDialog = new SettingsDialog(this,client.settings);
}
return settingsDialog;
}
/**
* Perform a command executed by a hotkey. This means that the channel
* context is the currently active channel and the selected user name is
* added as command parameter if present.
*
* @param command The name of the command, leading / is removed if necessary
* @param parameter2 Additional parameter after the username
* @param selectedUserRequired Whether the command should only be executed
* if a user is currently selected
*/
private void hotkeyCommand(String command, String parameter2,
boolean selectedUserRequired) {
Channel channel = channels.getLastActiveChannel();
User selectedUser = channel.getSelectedUser();
if (selectedUserRequired && selectedUser == null) {
return;
}
String selectedUserName = selectedUser != null ? selectedUser.getName() : "";
if (command.startsWith("/")) {
command = command.substring(1);
}
String parameter = null;
if (!selectedUserName.isEmpty() || parameter2 != null) {
parameter = selectedUserName+(parameter2 != null ? " "+parameter2 : "");
}
client.command(channels.getLastActiveChannel().getName(), command, parameter);
}
/**
* Adds an action that is also represented in the main menu.
*
* @param id The action id
* @param label The label to use for the action
* @param menuLabel The label to use for the action in the menu
* @param mnemonic The mnemonic for the action in the menu
* @param action The action to perform
*/
private void addMenuAction(String id, String label, String menuLabel, int mnemonic, Action action) {
action.putValue(Action.NAME, menuLabel);
action.putValue(Action.MNEMONIC_KEY, mnemonic);
menu.setAction(id, action);
hotkeyManager.registerAction(id, label, action);
}
private void addListeners() {
WindowManager manager = new WindowManager(this);
manager.addWindowOnTop(liveStreamsDialog);
MainWindowListener mainWindowListener = new MainWindowListener();
addWindowStateListener(mainWindowListener);
addWindowListener(mainWindowListener);
hotkeyManager.registerAction("custom.command", "Custom Command", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand(e.getActionCommand(), null, false);
}
});
hotkeyManager.registerAction("tabs.next", "Tabs: Switch to next tab", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
channels.switchToNextChannel();
}
});
hotkeyManager.registerAction("tabs.previous", "Tabs: Switch to previous tab", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
channels.switchToPreviousChannel();
}
});
hotkeyManager.registerAction("tabs.close", "Tabs: Close tab/popout", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
client.closeChannel(channels.getActiveChannel().getName());
}
});
addMenuAction("dialog.search", "Dialog: Open Search Dialog",
"Find text..", KeyEvent.VK_F, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openSearchDialog();
}
});
addMenuAction("dialog.addressbook", "Dialog: Addressbook (toggle)",
"Addressbook", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleAddressbook();
}
});
addMenuAction("dialog.autoModDialog", "Dialog: AutoMod Dialog (toggle)",
"AutoMod", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleAutoModDialog();
}
});
addMenuAction("dialog.moderationLog", "Dialog: Moderation Log (toggle)",
"Moderation Log", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleModerationLog();
}
});
addMenuAction("dialog.channelInfo", "Dialog: Channel Info Dialog (toggle)",
"Channel Info", KeyEvent.VK_C, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleChannelInfoDialog();
}
});
addMenuAction("dialog.channelAdmin", "Dialog: Channel Admin Dialog (toggle)",
"Channel Admin", KeyEvent.VK_A, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleChannelAdminDialog();
}
});
addMenuAction("dialog.chatRules", "Dialog: Chat Rules (toggle)",
"Chat Rules", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleChatRules();
}
});
addMenuAction("dialog.toggleEmotes", "Dialog: Emotes Dialog (toggle)",
"Emoticons", KeyEvent.VK_E, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleEmotesDialog();
}
});
addMenuAction("dialog.highlightedMessages", "Dialog: Highlighted Messages (toggle)",
"Highlights", KeyEvent.VK_H, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleHighlightedMessages();
}
});
addMenuAction("dialog.ignoredMessages", "Dialog: Ignored Messages (toggle)",
"Ignored", KeyEvent.VK_I, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleIgnoredMessages();
}
});
addMenuAction("dialog.streams", "Dialog: Live Channels Dialog (toggle)",
"Live Channels", KeyEvent.VK_L, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleLiveStreamsDialog();
}
});
addMenuAction("dialog.followers", "Dialog: Followers List (toggle)",
"Followers", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleFollowerDialog();
}
});
addMenuAction("dialog.subscribers", "Dialog: Subscriber List (toggle)",
"Subscribers", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleSubscriberDialog();
}
});
addMenuAction("dialog.joinChannel", "Dialog: Join Channel",
"Join Channel", KeyEvent.VK_J, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openJoinDialog();
}
});
hotkeyManager.registerAction("selection.toggle", "User Selection: Toggle", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Channel channel = channels.getLastActiveChannel();
channel.toggleUserSelection();
}
});
hotkeyManager.registerAction("selection.next", "User Selection: Next", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Channel channel = channels.getLastActiveChannel();
channel.selectNextUser();
}
});
hotkeyManager.registerAction("selection.nextExitAtBottom", "User Selection: Next (Exit at bottom)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Channel channel = channels.getLastActiveChannel();
channel.selectNextUserExitAtBottom();
}
});
hotkeyManager.registerAction("selection.previous", "User Selection: Previous", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Channel channel = channels.getLastActiveChannel();
channel.selectPreviousUser();
}
});
hotkeyManager.registerAction("selection.exit", "User Selection: Exit", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Channel channel = channels.getLastActiveChannel();
channel.exitUserSelection();
}
});
hotkeyManager.registerAction("selection.timeout30", "User Selection: Timeout (30s)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand("timeout", "30", true);
}
});
hotkeyManager.registerAction("selection.timeout600", "User Selection: Timeout (10m)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand("timeout", "600", true);
}
});
hotkeyManager.registerAction("selection.timeout30m", "User Selection: Timeout (30m)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand("timeout", "1800", true);
}
});
hotkeyManager.registerAction("selection.timeout24h", "User Selection: Timeout (24h)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand("timeout", "86400", true);
}
});
hotkeyManager.registerAction("commercial.30", "Run commercial (30s)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
runCommercial(30);
}
});
hotkeyManager.registerAction("commercial.60", "Run commercial (60s)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
runCommercial(60);
}
});
hotkeyManager.registerAction("commercial.90", "Run commercial (90s)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
runCommercial(90);
}
});
hotkeyManager.registerAction("stream.addhighlight", "Add Stream Highlight", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hotkeyCommand("addstreamhighlight", null, false);
}
});
hotkeyManager.registerAction("window.toggleCompact", "Window: Toggle Compact Mode", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleCompact(false);
}
});
hotkeyManager.registerAction("window.toggleCompactMaximized", "Window: Toggle Compact Mode (Maximized)", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleCompact(true);
}
});
hotkeyManager.registerAction("window.toggleInput", "Window: Toggle Input", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
channels.getActiveChannel().toggleInput();
}
});
hotkeyManager.registerAction("window.toggleUserlist", "Window: Toggle Userlist", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
channels.getActiveChannel().toggleUserlist();
}
});
hotkeyManager.registerAction("dialog.toggleHelp", "Window: Toggle Help", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
toggleHelp();
}
});
addMenuAction("dialog.streamchat", "Window: Open StreamChat",
"Open Dialog", KeyEvent.VK_O, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openStreamChat();
}
});
hotkeyManager.registerAction("notification.closeAll", "Close all shown/queued notifications", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
notificationManager.clearAll();
}
});
hotkeyManager.registerAction("notification.closeAllShown", "Close all shown notifications", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
notificationManager.clearAllShown();
}
});
addMenuAction("application.exit", "Exit Chatty",
"Exit", KeyEvent.VK_UNDEFINED, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
exit();
}
});
}
public void showGui() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!guiCreated) {
return;
}
setVisible(true);
// Should be done when the main window is already visible, so
// it can be centered on it correctly, if that is necessary
reopenWindows();
newsDialog.autoRequestNews(true);
client.init();
}
});
}
/**
* Toggle the main menubar. Also toggle between maximized/normal if maximize
* is true.
*
* @param maximize If true, also toggle between maximized/normal
*/
public void toggleCompact(final boolean maximize) {
if (!isVisible()) {
return;
}
final boolean hide = menu.isVisible();
menu.setVisible(!hide);
if (maximize) {
if (hide) {
setExtendedState(MAXIMIZED_BOTH);
} else {
setExtendedState(NORMAL);
}
}
}
/**
* Bring the main window into view by bringing it out of minimization (if
* necessary) and bringing it to the front.
*/
private void makeVisible() {
// Set visible was required to show it again after being minimized to tray
setVisible(true);
setState(NORMAL);
toFront();
//cleanupAfterRestoredFromTray();
//setExtendedState(JFrame.MAXIMIZED_BOTH);
}
/**
* Loads settings
*/
public void loadSettings() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (guiCreated) {
loadSettingsInternal();
}
}
});
}
/**
* Initiates the GUI with settings
*/
private void loadSettingsInternal() {
if (client.settings.getBoolean("bufferStrategy1")) {
createBufferStrategy(1);
}
setAlwaysOnTop(client.settings.getBoolean("ontop"));
setResizable(client.settings.getBoolean("mainResizable"));
streamChat.setResizable(client.settings.getBoolean("streamChatResizable"));
loadMenuSettings();
updateConnectionDialog(null);
userInfoDialog.setUserDefinedButtonsDef(client.settings.getString("timeoutButtons"));
debugWindow.getLogIrcCheckBox().setSelected(client.settings.getBoolean("debugLogIrc"));
updateLiveStreamsDialog();
windowStateManager.loadWindowStates();
windowStateManager.setAttachedWindowsEnabled(client.settings.getBoolean("attachedWindows"));
// Set window maximized state
if (client.settings.getBoolean("maximized")) {
setExtendedState(MAXIMIZED_BOTH);
}
updateHighlight();
updateIgnore();
updateHistoryRange();
updateNotificationSettings();
updateChannelsSettings();
updateHighlightNextMessages();
// This should be done before updatePopoutSettings() because that method
// will delete the attributes correctly depending on the setting
channels.setPopoutAttributes(client.settings.getList("popoutAttributes"));
updatePopoutSettings();
loadCommercialDelaySettings();
UrlOpener.setPrompt(client.settings.getBoolean("urlPrompt"));
UrlOpener.setCustomCommandEnabled(client.settings.getBoolean("urlCommandEnabled"));
UrlOpener.setCustomCommand(client.settings.getString("urlCommand"));
channels.setTabOrder(client.settings.getString("tabOrder"));
favoritesDialog.setSorting((int)client.settings.getLong("favoritesSorting"));
updateCustomContextMenuEntries();
emoticons.setIgnoredEmotes(client.settings.getList("ignoredEmotes"));
emoticons.loadFavoritesFromSettings(client.settings);
emoticons.loadCustomEmotes();
emoticons.addEmoji(client.settings.getString("emoji"));
emoticons.setCheerState(client.settings.getString("cheersType"));
emoticons.setCheerBackground(HtmlColors.decode(client.settings.getString("backgroundColor")));
client.api.setToken(client.settings.getString("token"));
userInfoDialog.setFontSize(client.settings.getLong("dialogFontSize"));
hotkeyManager.setGlobalHotkeysEnabled(client.settings.getBoolean("globalHotkeysEnabled"));
hotkeyManager.loadFromSettings(client.settings);
streamChat.setMessageTimeout((int)client.settings.getLong("streamChatMessageTimeout"));
emotesDialog.setEmoteScale((int)client.settings.getLong("emoteScaleDialog"));
emotesDialog.setCloseOnDoubleClick(client.settings.getBoolean("closeEmoteDialogOnDoubleClick"));
adminDialog.setStatusHistorySorting(client.settings.getString("statusHistorySorting"));
Sound.setDeviceName(client.settings.getString("soundDevice"));
}
private static final String[] menuBooleanSettings = new String[]{
"showJoinsParts", "ontop", "showModMessages", "attachedWindows",
"simpleTitle", "globalHotkeysEnabled", "mainResizable", "streamChatResizable",
"titleShowUptime", "titleShowViewerCount", "titleShowChannelState",
"titleLongerUptime"
};
/**
* Initiates the Main Menu with settings
*/
private void loadMenuSettings() {
for (String setting : menuBooleanSettings) {
loadMenuSetting(setting);
}
}
/**
* Initiates a single setting in the Main Menu
* @param name The name of the setting
*/
private void loadMenuSetting(String name) {
menu.setItemState(name,client.settings.getBoolean(name));
}
/**
* Tells the highlighter the current list of highlight-items from the settings.
*/
private void updateHighlight() {
highlighter.update(StringUtil.getStringList(client.settings.getList("highlight")));
}
private void updateIgnore() {
ignoreChecker.update(StringUtil.getStringList(client.settings.getList("ignore")));
}
private void updateCustomContextMenuEntries() {
CommandMenuItems.setCommands(CommandMenuItems.MenuType.CHANNEL, client.settings.getString("channelContextMenu"));
CommandMenuItems.setCommands(CommandMenuItems.MenuType.USER, client.settings.getString("userContextMenu"));
CommandMenuItems.setCommands(CommandMenuItems.MenuType.STREAMS, client.settings.getString("streamsContextMenu"));
ContextMenuHelper.livestreamerQualities = client.settings.getString("livestreamerQualities");
ContextMenuHelper.enableLivestreamer = client.settings.getBoolean("livestreamer");
ContextMenuHelper.settings = client.settings;
}
private void updateChannelsSettings() {
channels.setDefaultUserlistWidth(
(int)client.settings.getLong("userlistWidth"),
(int)client.settings.getLong("userlistMinWidth"));
channels.setChatScrollbarAlways(client.settings.getBoolean("chatScrollbarAlways"));
channels.setDefaultUserlistVisibleState(client.settings.getBoolean("userlistEnabled"));
}
/**
* Tells the highlighter the current username and whether it should be used
* for highlight. Used to initialize on connect, when the username is fixed
* for the duration of the connection.
*
* @param username The current username.
*/
public void updateHighlightSetUsername(String username) {
highlighter.setUsername(username);
highlighter.setHighlightUsername(client.settings.getBoolean("highlightUsername"));
}
/**
* Tells the highlighter whether the current username should be used for
* highlight. Used to set the setting when the setting is changed.
*
* @param highlight
*/
private void updateHighlightSetUsernameHighlighted(boolean highlight) {
highlighter.setHighlightUsername(highlight);
}
private void updateHighlightNextMessages() {
highlighter.setHighlightNextMessages(client.settings.getBoolean("highlightNextMessages"));
}
private void updateNotificationSettings() {
notificationManager.setDisplayTime((int)client.settings.getLong("nDisplayTime"));
notificationManager.setMaxDisplayTime((int)client.settings.getLong("nMaxDisplayTime"));
notificationManager.setMaxDisplayItems((int)client.settings.getLong("nMaxDisplayed"));
notificationManager.setMaxQueueSize((int)client.settings.getLong("nMaxQueueSize"));
int activityTime = client.settings.getBoolean("nActivity")
? (int)client.settings.getLong("nActivityTime") : -1;
notificationManager.setActivityTime(activityTime);
notificationManager.clearAll();
notificationManager.setScreen((int)client.settings.getLong("nScreen"));
notificationManager.setPosition((int)client.settings.getLong(("nPosition")));
}
private void updatePopoutSettings() {
channels.setSavePopoutAttributes(client.settings.getBoolean("popoutSaveAttributes"));
channels.setCloseLastChannelPopout(client.settings.getBoolean("popoutCloseLastChannel"));
}
/**
* Puts the updated state of the windows/dialogs/popouts into the settings.
*/
public void saveWindowStates() {
windowStateManager.saveWindowStates();
client.settings.putList("popoutAttributes", channels.getPopoutAttributes());
}
/**
* Reopen some windows if enabled.
*/
private void reopenWindows() {
for (Window window : windowStateManager.getWindows()) {
reopenWindow(window);
}
}
/**
* Open the given Component if enabled and if it was open before.
*
* @param window
*/
private void reopenWindow(Window window) {
if (windowStateManager.shouldReopen(window)) {
if (window == liveStreamsDialog) {
openLiveStreamsDialog();
} else if (window == highlightedMessages) {
openHighlightedMessages();
} else if (window == ignoredMessages) {
openIgnoredMessages();
} else if (window == channelInfoDialog) {
openChannelInfoDialog();
} else if (window == addressbookDialog) {
openAddressbook(null);
} else if (window == adminDialog) {
openChannelAdminDialog();
} else if (window == emotesDialog) {
openEmotesDialog();
} else if (window == followerDialog) {
openFollowerDialog();
} else if (window == subscribersDialog) {
openSubscriberDialog();
} else if (window == moderationLog) {
openModerationLog();
} else if (window == streamChat) {
openStreamChat();
} else if (window == autoModDialog) {
openAutoModDialog();
}
}
}
/**
* Saves whether the window is currently maximized.
*/
private void saveState(Component c) {
if (c == this) {
client.settings.setBoolean("maximized", isMaximized());
}
}
/**
* Returns if the window is currently maximized.
*
* @return true if the window is maximized, false otherwise
*/
private boolean isMaximized() {
return (getExtendedState() & MAXIMIZED_BOTH) == MAXIMIZED_BOTH;
}
/**
* Updates the connection dialog with current settings
*/
private void updateConnectionDialog(String channelPreset) {
connectionDialog.setUsername(client.settings.getString("username"));
if (channelPreset != null) {
connectionDialog.setChannel(channelPreset);
} else {
connectionDialog.setChannel(client.settings.getString("channel"));
}
String password = client.settings.getString("password");
String token = client.settings.getString("token");
boolean usePassword = client.settings.getBoolean("usePassword");
connectionDialog.update(password, token, usePassword);
connectionDialog.setAreChannelsOpen(channels.getChannelCount() > 0);
}
private void updateChannelInfoDialog() {
String stream = channels.getLastActiveChannel().getStreamName();
StreamInfo streamInfo = getStreamInfo(stream);
channelInfoDialog.set(streamInfo);
}
private void updateTokenDialog() {
String username = client.settings.getString("username");
String token = client.settings.getString("token");
tokenDialog.update(username, token);
tokenDialog.setForeignToken(client.settings.getBoolean("foreignToken"));
}
private void updateFavoritesDialog() {
Set<String> favorites = client.channelFavorites.getFavorites();
Map<String, Long> history = client.channelFavorites.getHistory();
favoritesDialog.setData(favorites, history);
}
private void updateFavoritesDialogWhenVisible() {
if (favoritesDialog.isVisible()) {
updateFavoritesDialog();
}
}
public void updateUserinfo(final User user) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateUserInfoDialog(user);
}
});
}
private void updateUserInfoDialog(User user) {
userInfoDialog.update(user, client.getUsername());
}
private void updateLiveStreamsDialog() {
liveStreamsDialog.setSorting(client.settings.getString("liveStreamsSorting"));
}
private void updateHistoryRange() {
int range = (int)client.settings.getLong("historyRange");
channelInfoDialog.setHistoryRange(range);
liveStreamsDialog.setHistoryRange(range);
}
private void openTokenDialog() {
updateTokenDialog();
updateTokenScopes();
if (connectionDialog.isVisible()) {
tokenDialog.setLocationRelativeTo(connectionDialog);
} else {
tokenDialog.setLocationRelativeTo(this);
}
tokenDialog.setVisible(true);
}
public void addStreamInfo(final StreamInfo streamInfo) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
liveStreamsDialog.addStream(streamInfo);
}
});
}
public ActionListener getActionListener() {
return actionListener;
}
public StatusHistory getStatusHistory() {
return client.statusHistory;
}
public boolean getSaveStatusHistorySetting() {
return client.settings.getBoolean("saveStatusHistory");
}
class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
// text input
Channel chan = channels.getChannelFromInput(event.getSource());
if (chan != null) {
client.textInput(chan.getName(), chan.getInputText());
}
Object source = event.getSource();
//---------------------------
// Connection Dialog actions
//---------------------------
if (source == connectionDialog.getCancelButton()) {
connectionDialog.setVisible(false);
channels.setInitialFocus();
} else if (source == connectionDialog.getConnectButton()
|| source == connectionDialog.getChannelInput()) {
String password = connectionDialog.getPassword();
String channel = connectionDialog.getChannel();
//client.settings.setString("username",name);
client.settings.setString("password", password);
client.settings.setString("channel", channel);
if (client.prepareConnection(connectionDialog.rejoinOpenChannels())) {
connectionDialog.setVisible(false);
channels.setInitialFocus();
}
} else if (event.getSource() == connectionDialog.getGetTokenButton()) {
openTokenDialog();
} else if (event.getSource() == connectionDialog.getFavoritesButton()) {
openFavoritesDialogFromConnectionDialog(connectionDialog.getChannel());
} //---------------------------
// Token Dialog actions
//---------------------------
else if (event.getSource() == tokenDialog.getDeleteTokenButton()) {
int result = JOptionPane.showConfirmDialog(tokenDialog,
"<html><body style='width:400px'>"
+ "This removes the login token from Chatty.<br><br>"
+ "It does not revoke access for the token, which "
+ "usually is no problem if the token isn't saved "
+ "anywhere else. If you suspect it may still be stored "
+ "in other places (or even compromised) you have to go "
+ "to <code>twitch.tv/settings/connections</code> and "
+ "click 'Disconnect' next to Chatty to revoke access.",
"Save Settings to file",
JOptionPane.OK_CANCEL_OPTION);
if (result == 0) {
client.settings.setString("token", "");
client.settings.setBoolean("foreignToken", false);
client.settings.setString("username", "");
client.settings.setString("userid", "");
resetTokenScopes();
updateConnectionDialog(null);
tokenDialog.update("", "");
updateTokenScopes();
}
} else if (event.getSource() == tokenDialog.getRequestTokenButton()) {
tokenGetDialog.setLocationRelativeTo(tokenDialog);
tokenGetDialog.reset();
client.startWebserver();
tokenGetDialog.setVisible(true);
} else if (event.getSource() == tokenDialog.getDoneButton()) {
tokenDialog.setVisible(false);
} else if (event.getSource() == tokenDialog.getVerifyTokenButton()) {
verifyToken(client.settings.getString("token"));
} // Get token Dialog
else if (event.getSource() == tokenGetDialog.getCloseButton()) {
tokenGetDialogClosed();
} //-----------------
// Userinfo Dialog
//-----------------
else if (userInfoDialog.getCommand(source) != null) {
CustomCommand command = userInfoDialog.getCommand(source);
User user = userInfoDialog.getUser();
String nick = user.getName();
String channel = userInfoDialog.getChannel();
String msgId = userInfoDialog.getTargetMsgId();
String reason = userInfoDialog.getBanReason();
if (!reason.isEmpty()) {
reason = " "+reason;
}
Parameters parameters = Parameters.create(nick+reason);
parameters.put("msg-id", msgId);
client.anonCustomCommand(channel, command, parameters);
// Favorites Dialog
} else if (favoritesDialog.getAction(source) == FavoritesDialog.BUTTON_ADD_FAVORITES) {
Set<String> channels = favoritesDialog.getChannels();
client.channelFavorites.addChannelsToFavorites(channels);
} else if (favoritesDialog.getAction(source) == FavoritesDialog.BUTTON_REMOVE_FAVORITES) {
Set<String> channels = favoritesDialog.getSelectedChannels();
client.channelFavorites.removeChannelsFromFavorites(channels);
} else if (favoritesDialog.getAction(source) == FavoritesDialog.BUTTON_REMOVE) {
Set<String> channels = favoritesDialog.getSelectedChannels();
client.channelFavorites.removeChannels(channels);
}
}
}
private class DebugCheckboxListener implements ItemListener {
@Override
public void itemStateChanged(ItemEvent e) {
boolean state = e.getStateChange() == ItemEvent.SELECTED;
if (e.getSource() == debugWindow.getLogIrcCheckBox()) {
client.settings.setBoolean("debugLogIrc", state);
}
}
}
private class MyLinkLabelListener implements LinkLabelListener {
@Override
public void linkClicked(String type, String ref) {
if (type.equals("help")) {
openHelp(ref);
} else if (type.equals("help-settings")) {
openHelp("help-settings.html", ref);
} else if (type.equals("help-commands")) {
openHelp("help-custom_commands.html", ref);
} else if (type.equals("help-admin")) {
openHelp("help-admin.html", ref);
} else if (type.equals("help-livestreamer")) {
openHelp("help-livestreamer.html", ref);
} else if (type.equals("help-whisper")) {
openHelp("help-whisper.html", ref);
} else if (type.equals("url")) {
UrlOpener.openUrlPrompt(MainGui.this, ref);
} else if (type.equals("update")) {
if (ref.equals("show")) {
openUpdateDialog();
}
} else if (type.equals("announcement")) {
if (ref.equals("show")) {
newsDialog.showDialog();
}
}
}
}
public LinkLabelListener getLinkLabelListener() {
return linkLabelListener;
}
public void clearHistory() {
client.channelFavorites.clearHistory();
}
private class TrayMenuListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd == null || cmd.equals("show")) {
makeVisible();
}
else if (cmd.equals("exit")) {
exit();
}
}
}
/**
* Listener for the Main Menu
*/
private class MainMenuListener implements ItemListener, ActionListener, MenuListener {
@Override
public void itemStateChanged(ItemEvent e) {
String setting = menu.getSettingByMenuItem(e.getSource());
boolean state = e.getStateChange() == ItemEvent.SELECTED;
if (setting != null) {
client.settings.setBoolean(setting, state);
}
}
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("debug")) {
if (!debugWindow.isShowing()) {
debugWindow.setLocationByPlatform(true);
debugWindow.setPreferredSize(new Dimension(500, 400));
}
debugWindow.setVisible(true);
} else if (cmd.equals("connect")) {
openConnectDialogInternal(null);
} else if (cmd.equals("disconnect")) {
client.disconnect();
} else if (cmd.equals("about")) {
openHelp("");
} else if (cmd.equals("news")) {
newsDialog.showDialog();
} else if (cmd.equals("settings")) {
getSettingsDialog().showSettings();
} else if (cmd.equals("saveSettings")) {
int result = JOptionPane.showConfirmDialog(MainGui.this,
"This manually saves settings to file.\n" +
"Settings are also automatically saved when you exit Chatty.",
"Save Settings to file",
JOptionPane.OK_CANCEL_OPTION);
if (result == 0) {
client.saveSettings(false);
}
} else if (cmd.equals("website")) {
UrlOpener.openUrlPrompt(MainGui.this, Chatty.WEBSITE, true);
} else if (cmd.equals("favoritesDialog")) {
openFavoritesDialogToJoin("");
} else if (cmd.equals("unhandledException")) {
String[] array = new String[0];
String a = array[1];
} else if (cmd.equals("addressbook")) {
openAddressbook(null);
} else if (cmd.equals("srlRaces")) {
openSrlRaces();
} else if (cmd.equals("srlRaceActive")) {
srl.searchRaceWithEntrant(channels.getActiveTab().getStreamName());
} else if (cmd.startsWith("srlRace4")) {
String stream = cmd.substring(8);
if (!stream.isEmpty()) {
srl.searchRaceWithEntrant(stream);
}
} else if (cmd.equals("livestreamer")) {
livestreamerDialog.open(null, null);
} else if (cmd.equals("configureLogin")) {
openTokenDialog();
} else if (cmd.equals("addStreamHighlight")) {
client.commandAddStreamHighlight(channels.getActiveChannel().getName(), null);
} else if (cmd.equals("openStreamHighlights")) {
client.commandOpenStreamHighlights(channels.getActiveChannel().getName());
} else if (cmd.equals("srcOpen")) {
client.speedruncom.openCurrentGame(channels.getActiveChannel());
}
}
@Override
public void menuSelected(MenuEvent e) {
if (e.getSource() == menu.srlStreams) {
ArrayList<String> popoutStreams = new ArrayList<>();
for (Channel channel : channels.getPopoutChannels().keySet()) {
popoutStreams.add(channel.getStreamName());
}
menu.updateSrlStreams(channels.getActiveTab().getStreamName(), popoutStreams);
} else if (e.getSource() == menu.view) {
menu.updateCount(highlightedMessages.getNewCount(),
highlightedMessages.getDisplayedCount(),
ignoredMessages.getNewCount(),
ignoredMessages.getDisplayedCount());
}
}
@Override
public void menuDeselected(MenuEvent e) {
}
@Override
public void menuCanceled(MenuEvent e) {
}
}
/**
* Listener for all kind of context menu events
*/
class MyContextMenuListener implements ContextMenuListener {
/**
* User context menu event.
*
* @param e
* @param user
*/
@Override
public void userMenuItemClicked(ActionEvent e, User user) {
String cmd = e.getActionCommand();
if (cmd.equals("userinfo")) {
openUserInfoDialog(user, null);
}
else if (cmd.equals("addressbookEdit")) {
openAddressbook(user.getName());
}
else if (cmd.equals("addressbookRemove")) {
client.addressbook.remove(user.getName());
updateUserInfoDialog(user);
}
else if (cmd.startsWith("cat")) {
if (e.getSource() instanceof JCheckBoxMenuItem) {
boolean selected = ((JCheckBoxMenuItem)e.getSource()).isSelected();
String catName = cmd.substring(3);
if (selected) {
client.addressbook.add(user.getName(), catName);
} else {
client.addressbook.remove(user.getName(), catName);
}
}
updateUserInfoDialog(user);
}
else if (cmd.equals("setcolor")) {
setColor(user.getName());
}
else if (cmd.equals("setname")) {
setCustomName(user.getName());
}
else if (cmd.startsWith("command")) {
customCommand(user.getChannel(), e, user.getRegularDisplayNick());
} else if (cmd.equals("copyNick")) {
MiscUtil.copyToClipboard(user.getName());
} else if (cmd.equals("copyDisplayNick")) {
MiscUtil.copyToClipboard(user.getDisplayNick());
} else if (cmd.equals("ignore")) {
client.commandSetIgnored(user.getName(), "chat", true);
} else if (cmd.equals("ignoreWhisper")) {
client.commandSetIgnored(user.getName(), "whisper", true);
} else if (cmd.equals("unignore")) {
client.commandSetIgnored(user.getName(), "chat", false);
} else if (cmd.equals("unignoreWhisper")) {
client.commandSetIgnored(user.getName(), "whisper", false);
} else {
nameBasedStuff(e, user.getName());
}
}
/**
* Event of an URL context menu.
*
* @param e
* @param url
*/
@Override
public void urlMenuItemClicked(ActionEvent e, String url) {
String cmd = e.getActionCommand();
if (cmd.equals("open")) {
UrlOpener.openUrlPrompt(MainGui.this, url);
}
else if (cmd.equals("copy")) {
MiscUtil.copyToClipboard(url);
}
else if (cmd.equals("join")) {
client.commandJoinChannel(url);
}
}
/**
* Context menu event without any channel context, which means it just
* uses the active one or performs some other action that doesn't
* immediately require one.
*
* @param e
*/
@Override
public void menuItemClicked(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("channelInfo")) {
openChannelInfoDialog();
}
else if (cmd.equals("channelAdmin")) {
openChannelAdminDialog();
}
else if (cmd.equals("chatRules")) {
openChatRules();
}
else if (cmd.equals("closeChannel")) {
client.closeChannel(channels.getActiveChannel().getName());
}
else if (cmd.startsWith("closeAllTabs")) {
Collection<Channel> chans = null;
if (cmd.equals("closeAllTabsButCurrent")) {
chans = channels.getTabsRelativeToCurrent(0);
} else if (cmd.equals("closeAllTabsToLeft")) {
chans = channels.getTabsRelativeToCurrent(-1);
} else if (cmd.equals("closeAllTabsToRight")) {
chans = channels.getTabsRelativeToCurrent(1);
} else if (cmd.equals("closeAllTabs")) {
chans = channels.getTabs();
}
if (chans != null) {
for (Channel c : chans) {
client.closeChannel(c.getName());
}
}
}
else if (cmd.equals("joinHostedChannel")) {
String chan = client.getHostedChannel(channels.getActiveChannel().getName());
if (chan == null) {
printLine("No channel is currently being hosted.");
} else {
client.joinChannel(chan);
}
}
else if (cmd.equals("srcOpen")) {
client.speedruncom.openCurrentGame(channels.getActiveChannel());
}
else if (cmd.equals("popoutChannel")) {
channels.popoutActiveChannel();
}
else if (cmd.startsWith("command")) {
customCommand(channels.getActiveChannel().getName(), e, channels.getActiveChannel().getStreamName());
}
else if (cmd.startsWith("range")) {
int range = -1;
switch (cmd) {
case "range1h":
range = 60;
break;
case "range2h":
range = 120;
break;
case "range4h":
range = 240;
break;
case "range8h":
range = 480;
break;
case "range12h":
range = 720;
break;
}
// Change here as well, because even if it's the same value,
// update may be needed. This will make it update twice often.
updateHistoryRange();
client.settings.setLong("historyRange", range);
} else {
nameBasedStuff(e, channels.getActiveChannel().getStreamName());
}
}
/**
* Context menu event associated with a list of stream or channel names.
*
* @param e
* @param streams
*/
@Override
public void streamsMenuItemClicked(ActionEvent e, Collection<String> streams) {
streamStuff(e, streams);
}
/**
* Goes through the {@code StreamInfo} objects and adds the stream names
* into a list, so it can be used by the more generic method.
*
* @param e The event
* @param streamInfos The list of {@code StreamInfo} objects associated
* with this event
* @see streamsMenuItemClicked(ActionEvent, Collection)
*/
@Override
public void streamInfosMenuItemClicked(ActionEvent e, Collection<StreamInfo> streamInfos) {
String cmd = e.getActionCommand();
String sorting = null;
if (cmd.equals("sortName")) {
sorting = "name";
} else if (cmd.equals("sortGame")) {
sorting = "game";
} else if (cmd.equals("sortRecent")) {
sorting = "recent";
} else if (cmd.equals("sortViewers")) {
sorting = "viewers";
}
if (sorting != null) {
client.settings.setString("liveStreamsSorting", sorting);
} else {
Collection<String> streams = new ArrayList<>();
for (StreamInfo info : streamInfos) {
streams.add(info.getCapitalizedName());
}
streamsMenuItemClicked(e, streams);
}
if (cmd.equals("manualRefreshStreams")) {
client.api.manualRefreshStreams();
state.update(true);
}
}
/**
* Handles context menu events with a single name (stream/channel). Just
* packs it into a list for use in another method.
*
* @param cmd
* @param name
*/
private void nameBasedStuff(ActionEvent e, String name) {
Collection<String> list = new ArrayList<>();
list.add(name);
streamStuff(e, list);
}
/**
* Any commands that are equal to these Strings is supposed to have a
* stream parameter.
*/
private final Set<String> streamCmds = new HashSet<>(
Arrays.asList("profile", "join", "hostchannel"));
/**
* Any commands starting with these Strings is supposed to have a stream
* parameter.
*/
private final Set<String> streamCmdsPrefix = new HashSet<>(
Arrays.asList("stream", "livestreamer"));
/**
* Check if this command requires at least one stream/channel parameter.
*
* @param cmd
* @return
*/
private boolean cmdRequiresStream(String cmd) {
for (String prefix : streamCmdsPrefix) {
if (cmd.startsWith(prefix)) {
return true;
}
}
return streamCmds.contains(cmd);
}
/**
* Handles context menu events that can be applied to one or more
* streams or channels. Checks if any valid stream parameters are
* present and outputs an error otherwise. Since this can also be called
* if it's not one of the commands that actually require a stream (other
* listeners may be registered), it also checks if it's actually one of
* the commands it handles.
*
* @param cmd The command
* @param streams The list of stream or channel names
*/
private void streamStuff(ActionEvent e, Collection<String> streams) {
String cmd = e.getActionCommand();
TwitchUrl.removeInvalidStreams(streams);
if (streams.isEmpty() && cmdRequiresStream(cmd)) {
JOptionPane.showMessageDialog(getActiveWindow(), "Can't perform action: No stream/channel.",
"Info", JOptionPane.INFORMATION_MESSAGE);
return;
}
String firstStream = null;
if (!streams.isEmpty()) {
firstStream = streams.iterator().next();
}
if (cmd.equals("stream") || cmd.equals("streamPopout")
|| cmd.equals("streamPopoutOld") || cmd.equals("profile")) {
List<String> urls = new ArrayList<>();
for (String stream : streams) {
String url;
switch (cmd) {
case "stream":
url = TwitchUrl.makeTwitchStreamUrl(stream, false);
break;
case "profile":
url = TwitchUrl.makeTwitchProfileUrl(stream);
break;
case "streamPopout":
url = TwitchUrl.makeTwitchPlayerUrl(stream);
break;
default:
url = TwitchUrl.makeTwitchStreamUrl(stream, true);
break;
}
urls.add(url);
}
UrlOpener.openUrlsPrompt(getActiveWindow(), urls, true);
} else if (cmd.equals("join")) {
Set<String> channels = new HashSet<>();
for (String stream : streams) {
channels.add(stream);
}
makeVisible();
client.joinChannels(channels);
} else if (cmd.startsWith("streams")) {
ArrayList<String> streams2 = new ArrayList<>();
for (String stream : streams) {
streams2.add(stream);
}
String type = TwitchUrl.MULTITWITCH;
switch (cmd) {
case "streamsSpeedruntv":
type = TwitchUrl.SPEEDRUNTV;
break;
case "streamsKadgar":
type = TwitchUrl.KADGAR;
break;
}
TwitchUrl.openMultitwitch(streams2, getActiveWindow(), type);
} else if (cmd.startsWith("livestreamer")) {
// quality null means select
String quality = null;
if (cmd.startsWith("livestreamerQ")) {
quality = StringUtil.toLowerCase(cmd.substring(13));
if (quality.equalsIgnoreCase("select")) {
quality = null;
}
}
for (String stream : streams) {
livestreamerDialog.open(stream.toLowerCase(), quality);
}
} else if (cmd.equals("showChannelEmotes")) {
if (firstStream != null) {
openEmotesDialogChannelEmotes(firstStream.toLowerCase());
}
} else if (cmd.equals("hostchannel")) {
if (firstStream != null && streams.size() == 1) {
client.command(null, "host2", firstStream.toLowerCase());
} else {
printLine("Can't host more than one channel.");
}
} else if (cmd.equals("follow")) {
for (String stream : streams) {
client.commandFollow(null, stream);
}
} else if (cmd.equals("unfollow")) {
for (String stream : streams) {
client.commandUnfollow(null, stream);
}
} else if (cmd.equals("copy") && !streams.isEmpty()) {
MiscUtil.copyToClipboard(StringUtil.join(streams, ", "));
} else if (cmd.startsWith("command")) {
customCommand(channels.getLastActiveChannel().getName(), e, StringUtil.join(streams, " "));
}
}
@Override
public void emoteMenuItemClicked(ActionEvent e, EmoticonImage emoteImage) {
Emoticon emote = emoteImage.getEmoticon();
String url = null;
if (e.getActionCommand().equals("code")) {
channels.getActiveChannel().insertText(emote.code, true);
} else if (e.getActionCommand().equals("cheer")) {
url = "http://help.twitch.tv/customer/portal/articles/2449458";
} else if (e.getActionCommand().equals("emoteImage")) {
url = emoteImage.getLoadedFrom();
} else if (e.getActionCommand().equals("ffzlink")) {
url = TwitchUrl.makeFFZUrl();
} else if (e.getActionCommand().equals("emoteId")) {
if (emote.type == Emoticon.Type.FFZ) {
url = TwitchUrl.makeFFZUrl(emote.numericId);
} else if (emote.type == Emoticon.Type.TWITCH) {
url = TwitchUrl.makeTwitchemotesUrl(emote.numericId);
}
} else if (e.getActionCommand().equals("emoteCreator")) {
if (emote.type == Emoticon.Type.FFZ) {
url = TwitchUrl.makeFFZUrl(emote.creator);
}
} else if (e.getActionCommand().equals("twitchturbolink")) {
url = TwitchUrl.makeTwitchTurboUrl();
} else if (e.getActionCommand().equals("bttvlink")) {
url = TwitchUrl.makeBttvUrl();
} else if (e.getActionCommand().equals("emoteDetails")) {
openEmotesDialogEmoteDetails(emote);
}
else if (e.getActionCommand().equals("ignoreEmote")) {
String code = emote.code;
if (emote instanceof CheerEmoticon) {
code = ((CheerEmoticon)emote).getSimpleCode();
}
int result = JOptionPane.showConfirmDialog(getActiveWindow(),
"<html><body style='width:200px'>Ignoring an emote "
+ "means showing just the code instead of turning "
+ "it into an image. The list of ignored emotes can be edited in "
+ "the Settings under 'Emoticons'.\n\nDo you want to "
+ "ignore '"+code+"' from now on?",
"Ignore Emote", JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) {
emoticons.addIgnoredEmote(code);
client.settings.setAdd("ignoredEmotes", code);
}
}
else if (e.getActionCommand().equals("favoriteEmote")) {
emoticons.addFavorite(emote);
client.settings.setAdd("favoriteEmotes", emote.code);
emotesDialog.favoritesUpdated();
}
else if (e.getActionCommand().equals("unfavoriteEmote")) {
emoticons.removeFavorite(emote);
client.settings.listRemove("favoriteEmotes", emote.code);
emotesDialog.favoritesUpdated();
}
if (emote.hasStreamSet()) {
nameBasedStuff(e, emote.getStream());
}
if (url != null) {
UrlOpener.openUrlPrompt(getActiveWindow(), url, true);
}
}
@Override
public void usericonMenuItemClicked(ActionEvent e, Usericon usericon) {
if (e.getActionCommand().equals("usericonUrl")) {
if (!usericon.metaUrl.isEmpty()) {
UrlOpener.openUrlPrompt(MainGui.this, usericon.metaUrl);
}
}
else if (e.getActionCommand().equals("copyBadgeType")) {
MiscUtil.copyToClipboard(usericon.badgeType.toString());
}
else if (e.getActionCommand().equals("addUsericonOfBadgeType")) {
getSettingsDialog().showSettings("addUsericonOfBadgeType", usericon.badgeType.toString());
}
else if (e.getActionCommand().equals("addUsericonOfBadgeTypeId")) {
getSettingsDialog().showSettings("addUsericonOfBadgeType", usericon.badgeType.id);
}
else if (e.getActionCommand().equals("badgeImage")) {
UrlOpener.openUrlPrompt(getActiveWindow(), usericon.url.toString(), true);
}
}
private void customCommand(String channel, ActionEvent e, String args) {
CommandActionEvent ce = (CommandActionEvent)e;
CustomCommand command = ce.getCommand();
client.anonCustomCommand(channel, command, Parameters.create(args));
}
}
private class ChannelChangeListener implements ChangeListener {
/**
* When the focus changes to a different channel (either by changing
* a tab in the main window or changing focus to a different popout
* dialog).
*
* @param e
*/
@Override
public void stateChanged(ChangeEvent e) {
state.update(true);
updateChannelInfoDialog();
emotesDialog.updateStream(channels.getLastActiveChannel().getStreamName());
moderationLog.setChannel(channels.getLastActiveChannel().getStreamName());
autoModDialog.setChannel(channels.getLastActiveChannel().getStreamName());
}
}
private class MyUserListener implements UserListener {
@Override
public void userClicked(User user, String messageId, MouseEvent e) {
if (e == null || (!e.isControlDown() && !e.isAltDown())) {
openUserInfoDialog(user, messageId);
return;
}
String command = client.settings.getString("commandOnCtrlClick");
if (command.startsWith("/")) {
command = command.substring(1);
}
if (e.isControlDown() && !command.isEmpty()) {
client.command(user.getChannel(), command, user.getRegularDisplayNick());
} else if (!e.isAltDown()) {
openUserInfoDialog(user, messageId);
}
}
@Override
public void emoteClicked(Emoticon emote, MouseEvent e) {
openEmotesDialogEmoteDetails(emote);
}
@Override
public void usericonClicked(Usericon usericon, MouseEvent e) {
if (!usericon.metaUrl.isEmpty()) {
UrlOpener.openUrlPrompt(MainGui.this, usericon.metaUrl);
}
}
}
private class MyNotificationActionListener implements NotificationActionListener<String> {
/**
* Right-clicked on a notification.
*
* @param data
*/
@Override
public void notificationAction(String data) {
if (data != null) {
makeVisible();
client.joinChannel(data);
}
}
}
public UserListener getUserListener() {
return userListener;
}
public java.util.List<UsercolorItem> getUsercolorData() {
return client.usercolorManager.getData();
}
public void setUsercolorData(java.util.List<UsercolorItem> data) {
client.usercolorManager.setData(data);
}
public java.util.List<Usericon> getUsericonData() {
return client.usericonManager.getData();
}
public Set<String> getTwitchBadgeTypes() {
return client.usericonManager.getTwitchBadgeTypes();
}
public void setUsericonData(java.util.List<Usericon> data) {
client.usericonManager.setData(data);
}
/**
* Should only be called out of EDT. All commands have to be defined
* lowercase, because they are made lowercase when entered.
*
* @param command
* @param parameter
* @return
*/
public boolean commandGui(String command, String parameter) {
if (command.equals("settings")) {
getSettingsDialog().showSettings();
} else if (command.equals("customemotes")) {
printLine(emoticons.getCustomEmotesInfo());
} else if (command.equals("reloadcustomemotes")) {
printLine("Reloading custom emotes from file..");
emoticons.loadCustomEmotes();
printLine(emoticons.getCustomEmotesInfo());
} else if (command.equals("livestreams")) {
openLiveStreamsDialog();
} else if (command.equals("channeladmin")) {
openChannelAdminDialog();
} else if (command.equals("channelinfo")) {
openChannelInfoDialog();
} else if (command.equals("search")) {
openSearchDialog();
} else if (command.equals("insert")) {
insert(parameter, false);
} else if (command.equals("insertword")) {
insert(parameter, true);
} else if (command.equals("openurl")) {
if (!UrlOpener.openUrl(parameter)) {
printLine("Failed to open URL (none specified or invalid).");
}
} else if (command.equals("openurlprompt")) {
// Could do in invokeLater() so command isn't visible in input box
// while the dialog is open, but probably doesn't matter since this
// is mainly for custom commands put in a context menu anyway.
if (!UrlOpener.openUrlPrompt(getActiveWindow(), parameter, true)) {
printLine("Failed to open URL (none specified or invalid).");
}
} else if (command.equals("openfollowers")) {
openFollowerDialog();
} else if (command.equals("opensubscribers")) {
openSubscriberDialog();
} else if (command.equals("openrules")) {
if (parameter != null) {
openChatRules("#"+parameter);
} else {
openChatRules();
}
} else if (command.equals("openstreamchat")) {
openStreamChat();
} else if (command.equals("clearstreamchat")) {
streamChat.clear();
} else if (command.equals("streamchattest")) {
String message = "A bit longer chat message with emotes and stuff "
+ "FrankerZ ZreknarF MiniK ("+(int)(Math.random()*10)+")";
if (parameter != null && !parameter.isEmpty()) {
message = parameter;
}
UserMessage m = new UserMessage(client.getSpecialUser(), message, null, null, 0);
streamChat.printMessage(m);
} else if (command.equals("livestreamer")) {
String stream = null;
String quality = null;
if (parameter != null && !parameter.trim().isEmpty()) {
String[] split = parameter.trim().split(" ");
stream = split[0];
if (stream.equals("$active")) {
stream = channels.getActiveChannel().getStreamName();
if (stream == null) {
printLine("Livestreamer: No channel open.");
return true;
}
}
if (split.length > 1) {
quality = split[1];
}
}
printLine("Livestreamer: Opening stream..");
livestreamerDialog.open(stream, quality);
} else if (command.equals("help")) {
openHelp(null);
} else if (command.equals("setstreamchatsize")) {
Dimension size = Helper.getDimensionFromParameter(parameter);
if (size != null) {
setStreamChatSize(size.width, size.height);
printSystem("Set StreamChat size to " + size.width + "x" + size.height);
return true;
}
printSystem("Invalid parameters.");
} else if (command.equals("getstreamchatsize")) {
Dimension d = streamChat.getSize();
printSystem("StreamChat size: "+d.width+"x"+d.height);
} else if (command.equals("setsize")) {
Dimension size = Helper.getDimensionFromParameter(parameter);
if (size != null) {
setSize(size);
printSystem(String.format("Set Window size to %dx%d", size.width, size.height));
return true;
}
printSystem("Invalid parameters.");
} else {
return false;
}
return true;
}
public void insert(final String text, final boolean spaces) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (text != null) {
channels.getLastActiveChannel().insertText(text, spaces);
}
}
});
}
private void openLiveStreamsDialog() {
windowStateManager.setWindowPosition(liveStreamsDialog);
liveStreamsDialog.setAlwaysOnTop(client.settings.getBoolean("ontop"));
liveStreamsDialog.setState(JFrame.NORMAL);
liveStreamsDialog.setVisible(true);
}
private void toggleLiveStreamsDialog() {
if (liveStreamsDialog.isVisible()) {
liveStreamsDialog.setVisible(false);
} else {
openLiveStreamsDialog();
}
}
/**
* Only call out of the EDT.
*
* @param user
* @param messageId
*/
public void openUserInfoDialog(User user, String messageId) {
windowStateManager.setWindowPosition(userInfoDialog, getActiveWindow());
userInfoDialog.show(getActiveWindow(), user, messageId, client.getUsername());
}
private void openChannelInfoDialog() {
windowStateManager.setWindowPosition(channelInfoDialog, getActiveWindow());
channelInfoDialog.setVisible(true);
}
private void toggleChannelInfoDialog() {
if (!closeDialog(channelInfoDialog)) {
openChannelInfoDialog();
}
}
private boolean closeDialog(JDialog dialog) {
if (dialog.isVisible()) {
dialog.setVisible(false);
return true;
}
return false;
}
private void openChannelAdminDialog() {
windowStateManager.setWindowPosition(adminDialog, getActiveWindow());
updateTokenScopes();
String stream = channels.getActiveChannel().getStreamName();
if (stream == null) {
stream = client.settings.getString("username");
}
adminDialog.open(stream);
}
private void toggleChannelAdminDialog() {
if (!closeDialog(adminDialog)) {
openChannelAdminDialog();
}
}
private void openHelp(String ref) {
openHelp(null, ref, false);
}
public void openHelp(String page, String ref) {
openHelp(page, ref, false);
}
public void openHelp(String page, String ref, boolean keepPage) {
if (!aboutDialog.isVisible()) {
aboutDialog.setLocationRelativeTo(this);
}
if (!keepPage) {
aboutDialog.open(page, ref);
}
// Set ontop setting, so it won't be hidden behind the main window
aboutDialog.setAlwaysOnTop(client.settings.getBoolean("ontop"));
aboutDialog.setModalExclusionType(Dialog.ModalExclusionType.APPLICATION_EXCLUDE);
aboutDialog.toFront();
aboutDialog.setState(NORMAL);
aboutDialog.setVisible(true);
}
private void toggleHelp() {
if (aboutDialog.isVisible()) {
aboutDialog.setVisible(false);
} else {
openHelp(null, null, true);
}
}
/**
* Opens the release info in the help.
*/
public void openReleaseInfo() {
/**
* Use invokeLater() twice to run definitely when everything is ready so it
* jumps to the correct reference (#latest). Otherwise it didn't really
* seem to work. This may also help to open it after other stuff (like
* the Connection Dialog) is opened.
*
* This was previously implicitly achieved by having it in showGui(),
* which already runs in invokeLater(), and then calling this which also
* ran in invokeLater().
*/
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
openHelp("help-releases.html", "latest");
}
});
}
});
}
protected void openSearchDialog() {
// searchDialog.setLocationRelativeTo(getActiveWindow());
// searchDialog.setVisible(true);
SearchDialog.showSearchDialog(channels.getActiveChannel(), this, getActiveWindow());
}
private void openEmotesDialog() {
openEmotesDialog(channels.getLastActiveChannel().getStreamName());
}
private void openEmotesDialog(String channel) {
windowStateManager.setWindowPosition(emotesDialog, getActiveWindow());
emotesDialog.showDialog(client.getSpecialUser().getEmoteSet(), channel);
}
private void openEmotesDialogChannelEmotes(String channel) {
client.requestChannelEmotes(channel);
openEmotesDialog(channel);
emotesDialog.showChannelEmotes();
}
private void openEmotesDialogEmoteDetails(Emoticon emote) {
openEmotesDialog(null);
emotesDialog.showEmoteDetails(emote);
}
protected void toggleEmotesDialog() {
if (!closeDialog(emotesDialog)) {
openEmotesDialog();
}
}
private void openFollowerDialog() {
windowStateManager.setWindowPosition(followerDialog);
String stream = channels.getLastActiveChannel().getStreamName();
if (stream == null || stream.isEmpty()) {
stream = client.settings.getString("username");
}
if (stream != null && !stream.isEmpty()) {
followerDialog.showDialog(stream);
}
}
private void toggleFollowerDialog() {
if (!closeDialog(followerDialog)) {
openFollowerDialog();
}
}
private void openSubscriberDialog() {
windowStateManager.setWindowPosition(subscribersDialog);
String stream = client.settings.getString("username");
if (stream != null && !stream.isEmpty()) {
subscribersDialog.showDialog(stream);
}
}
private void toggleSubscriberDialog() {
if (!closeDialog(subscribersDialog)) {
openSubscriberDialog();
}
}
private void openModerationLog() {
windowStateManager.setWindowPosition(moderationLog);
moderationLog.showDialog();
}
private void toggleModerationLog() {
if (!closeDialog(moderationLog)) {
openModerationLog();
}
}
private void openAutoModDialog() {
windowStateManager.setWindowPosition(autoModDialog);
autoModDialog.showDialog();
}
private void toggleAutoModDialog() {
if (!closeDialog(autoModDialog)) {
openAutoModDialog();
}
}
private void openChatRules() {
openChatRules(channels.getLastActiveChannel().getName());
}
private void openChatRules(String channel) {
chatRulesDialog.showRules(channel);
}
private void toggleChatRules() {
if (!closeDialog(chatRulesDialog)) {
openChatRules();
}
}
private void openUpdateDialog() {
updateMessage.setLocationRelativeTo(this);
updateMessage.showDialog();
}
private void openFavoritesDialogFromConnectionDialog(String channel) {
Set<String> channels = chooseFavorites(this, channel);
if (!channels.isEmpty()) {
connectionDialog.setChannel(Helper.buildStreamsString(channels));
}
}
public Set<String> chooseFavorites(Component owner, String channel) {
updateFavoritesDialog();
favoritesDialog.setLocationRelativeTo(owner);
int result = favoritesDialog.showDialog(channel, "Use chosen channels",
"Use chosen channel");
if (result == FavoritesDialog.ACTION_DONE) {
return favoritesDialog.getChannels();
}
return new HashSet<>();
}
private void openFavoritesDialogToJoin(String channel) {
updateFavoritesDialog();
favoritesDialog.setLocationRelativeTo(this);
int result = favoritesDialog.showDialog(channel, "Join chosen channels",
"Join chosen channel");
if (result == FavoritesDialog.ACTION_DONE) {
Set<String> selectedChannels = favoritesDialog.getChannels();
client.joinChannels(selectedChannels);
}
}
private void openJoinDialog() {
joinDialog.setLocationRelativeTo(this);
Set<String> chans = joinDialog.showDialog();
client.joinChannels(chans);
}
private void openHighlightedMessages() {
windowStateManager.setWindowPosition(highlightedMessages);
highlightedMessages.setVisible(true);
}
private void toggleHighlightedMessages() {
if (!closeDialog(highlightedMessages)) {
openHighlightedMessages();
}
}
private void openIgnoredMessages() {
windowStateManager.setWindowPosition(ignoredMessages);
ignoredMessages.setVisible(true);
}
private void toggleIgnoredMessages() {
if (!closeDialog(ignoredMessages)) {
openIgnoredMessages();
}
}
/**
* Opens the addressbook, opening an edit dialog for the given name if it
* is non-null.
*
* @param name The name to edit or null.
*/
private void openAddressbook(String name) {
if (!addressbookDialog.isVisible()) {
windowStateManager.setWindowPosition(addressbookDialog);
}
addressbookDialog.showDialog(name);
}
private void toggleAddressbook() {
if (!closeDialog(addressbookDialog)) {
openAddressbook(null);
}
}
private void openSrlRaces() {
srl.openRaceList();
}
/*
* Channel Management
*/
public void removeChannel(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.removeChannel(channel);
state.update();
}
});
}
public void switchToChannel(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.switchToChannel(channel);
}
});
}
private void messageSound(String channel) {
playSound("message", channel);
}
private void playHighlightSound(String channel) {
playSound("highlight", channel);
}
public void followerSound(String channel) {
playSound("follower", channel);
}
/**
* Plays the sound for the given sound identifier (highlight, status, ..),
* if the requirements are met.
*
* @param id The id of the sound
* @param channel The channel this event originated from, to check
* requirements
*/
public void playSound(final String id, final String channel) {
if (SwingUtilities.isEventDispatchThread()) {
playSoundInternal(id, channel);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
playSoundInternal(id, channel);
}
});
}
}
/**
* Plays the sound for the given sound identifier (highlight, status, ..),
* if the requirements are met. For use in EDT, because it may have to check
* which channel is currently selected, but not sure if that has to be in
* the EDT.
*
* @param id The id of the sound
* @param channel The channel this event originated from, to check
* requirements
*/
private void playSoundInternal(String id, String channel) {
if (client.settings.getBoolean("sounds")
&& checkRequirements(client.settings.getString(id + "Sound"), channel)) {
playSound(id);
}
}
/**
* Plays the sound for the given sound identifier (highlight, status, ..).
*
* @param id
*/
private void playSound(String id) {
String fileName = client.settings.getString(id + "SoundFile");
long volume = client.settings.getLong(id + "SoundVolume");
int delay = ((Long) client.settings.getLong(id + "SoundDelay")).intValue();
Sound.play(fileName, volume, id, delay);
}
private void showHighlightNotification(String channel, User user, String text) {
String setting = client.settings.getString("highlightNotification");
if (checkRequirements(setting, channel)) {
long displayNamesMode = getSettings().getLong("displayNamesMode");
String name = Helper.makeDisplayNick(user, displayNamesMode);
String title = String.format("[Highlight] %s in %s", name, channel);
showNotification(title, text, channel);
}
}
public void setChannelNewStatus(final String channel, final String newStatus) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.setChannelNewStatus(channels.getChannel(channel));
}
});
}
public void statusNotification(final String channel, final String status) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (checkRequirements(client.settings.getString("statusNotification"), channel)) {
showNotification("[Status] " + channel, status, channel);
}
playSound("status", channel);
}
});
}
private void showNotification(String title, String message, String channel) {
if (client.settings.getBoolean("useCustomNotifications")) {
notificationManager.showMessage(title, message, channel);
} else {
trayIcon.displayInfo(title, message);
}
}
public void showTestNotification(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (client.settings.getString("username").equalsIgnoreCase("joshimuz")) {
showNotification("[Test] It works!", "Now you have your notifications Josh.. Kappa", channel);
} else if (channel == null) {
showNotification("[Test] It works!", "This is where the text goes.", null);
} else {
showNotification("[Status] "+Helper.toValidChannel(channel), "Test Notification (this would pop up when a stream status changes)", channel);
}
}
});
}
/**
* Checks the requirements that depend on whether the app and/or the given
* channel is active.
*
* @param setting What the requirements are
* @param channel The channel to check the requirement against
* @return true if the requirements are met, false otherwise
*/
private boolean checkRequirements(String setting, String channel) {
boolean channelActive = channels.getLastActiveChannel().getName().equals(channel);
boolean appActive = isAppActive();
// These conditions check when the requirements are NOT met
if (setting.equals("off")) {
return false;
}
if (setting.equals("both") && (channelActive || appActive)) {
return false;
}
if (setting.equals("channel") && channelActive) {
return false;
}
if (setting.equals("app") && appActive) {
return false;
}
if (setting.equals("either") && (channelActive && appActive)) {
return false;
}
if (setting.equals("channelActive") && !channelActive) {
return false;
}
return true;
}
private boolean isAppActive() {
for (Window frame : Window.getWindows()) {
if (frame.isActive()) {
return true;
}
}
return false;
}
private Window getActiveWindow() {
for (Window frame : Window.getWindows()) {
if (frame.isActive()) {
return frame;
}
}
return this;
}
/* ############
* # Messages #
*/
public void printMessage(String toChan, User user, String text, boolean action,
String emotes, int bits) {
printMessage(toChan, user, text, action, emotes, bits, null);
}
public void printMessage(final String toChan, final User user,
final String text, final boolean action, final String emotes,
final int bits2, final String id) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel chan;
String channel = toChan;
boolean whisper = false;
// Disable Cheer emotes altogether if disabled in the settings
int bits = bits2;
if (client.settings.getString("cheersType").equals("none")) {
bits = 0;
}
/**
* Check if special channel and change target according to
* settings
*/
if (channel.equals(WhisperManager.WHISPER_CHANNEL)) {
int whisperSetting = (int)client.settings.getLong("whisperDisplayMode");
if (whisperSetting == WhisperManager.DISPLAY_ONE_WINDOW) {
chan = channels.getChannel(channel);
} else if (whisperSetting == WhisperManager.DISPLAY_PER_USER) {
if (!userIgnored(user, true)) {
chan = channels.getChannel("$"+user.getName());
} else {
chan = channels.getActiveChannel();
}
} else {
chan = channels.getActiveChannel();
}
whisper = true;
} else {
chan = channels.getChannel(channel);
}
// If channel was changed from the given one, change accordingly
channel = chan.getName();
client.chatLog.message(chan.getName(), user, text, action);
boolean isOwnMessage = isOwnUsername(user.getName()) || (whisper && action);
boolean ignored = checkHighlight(user, text, ignoreChecker, "ignore", isOwnMessage)
|| (userIgnored(user, whisper) && !isOwnMessage);
boolean highlighted = false;
if ((client.settings.getBoolean("highlightIgnored") || !ignored)
&& !client.settings.listContains("noHighlightUsers", user.getName())) {
highlighted = checkHighlight(user, text, highlighter, "highlight", isOwnMessage);
}
TagEmotes tagEmotes = Emoticons.parseEmotesTag(emotes);
// Do stuff if highlighted, without printing message
if (highlighted) {
highlightedMessages.addMessage(channel, user, text, action,
tagEmotes, bits, whisper);
if (!highlighter.getLastMatchNoSound()) {
playHighlightSound(channel);
}
if (!highlighter.getLastMatchNoNotification()) {
showHighlightNotification(channel, user, (whisper ? "[Whisper] " : "")+text);
channels.setChannelHighlighted(chan);
} else {
channels.setChannelNewMessage(chan);
}
} else if (!ignored) {
messageSound(channel);
channels.setChannelNewMessage(chan);
}
// Do stuff if ignored, without printing message
if (ignored) {
ignoredMessages.addMessage(channel, user, text, action,
tagEmotes, bits, whisper);
ignoredMessagesHelper.ignoredMessage(channel);
}
long ignoreMode = client.settings.getLong("ignoreMode");
// Print or don't print depending on ignore
if (ignored && (ignoreMode <= IgnoredMessages.MODE_COUNT ||
!showIgnoredInfo())) {
// Don't print message
if (isOwnMessage) {
printLine(channel, "Own message ignored.");
}
} else {
// Print message, but determine how exactly
UserMessage message = new UserMessage(user, text, tagEmotes, id, bits);
message.color = highlighter.getLastMatchColor();
message.whisper = whisper;
message.action = action;
if (highlighted) {
message.highlighted = highlighted;
} else if (ignored && ignoreMode == IgnoredMessages.MODE_COMPACT) {
message.ignored_compact = true;
}
chan.printMessage(message);
if (client.settings.listContains("streamChatChannels", channel)) {
streamChat.printMessage(message);
}
}
CopyMessages.copyMessage(client.settings, user, text, highlighted);
// Stuff independent of highlight/ignore
user.addMessage(processMessage(text), action, id);
if (highlighted) {
user.setHighlighted();
}
updateUserInfoDialog(user);
}
});
}
public void printSubscriberMessage(final String channel, final User user,
final String text, final String message, final int months,
final String emotes) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// Prepare, check ignore
String fullMessage;
if (StringUtil.isNullOrEmpty(message)) {
fullMessage = "[Notification] "+text;
} else {
fullMessage = String.format("[Notification] %s [%s]", text, message);
}
// Chat window
if (!checkInfoIgnore(fullMessage)) {
Emoticons.TagEmotes tagEmotes = Emoticons.parseEmotesTag(emotes);
SubscriberMessage m = new SubscriberMessage(user, text, message, months, tagEmotes, null);
channels.getChannel(channel).printMessage(m);
} else {
ignoredMessages.addInfoMessage(channel, fullMessage);
}
// Chatlog/User Info
client.chatLog.info(channel, fullMessage);
if (!user.getName().isEmpty()) {
user.addSub(message != null ? processMessage(message) : "", text);
updateUserInfoDialog(user);
}
}
});
}
/**
* Checks the dedicated user ignore list. The regular ignore list may still
* ignore the user.
*
* @param user
* @param whisper
* @return
*/
private boolean userIgnored(User user, boolean whisper) {
String setting = whisper ? "ignoredUsersWhisper" : "ignoredUsers";
return client.settings.listContains(setting, user.getName());
}
private String processMessage(String text) {
int mode = (int)client.settings.getLong("filterCombiningCharacters");
return Helper.filterCombiningCharacters(text, "****", mode);
}
private boolean checkHighlight(User user, String text, Highlighter hl, String setting, boolean isOwnMessage) {
if (client.settings.getBoolean(setting + "Enabled")) {
if (client.settings.getBoolean(setting + "OwnText") ||
!isOwnMessage) {
return hl.check(user, text);
}
}
return false;
}
private boolean checkInfoIgnore(String text) {
return checkHighlight(null, text, ignoreChecker, "ignore", false);
}
protected void ignoredMessagesCount(String channel, String message) {
if (client.settings.getLong("ignoreMode") == IgnoredMessages.MODE_COUNT
&& showIgnoredInfo()) {
if (channels.isChannel(channel)) {
channels.getChannel(channel).printLine(message);
}
}
}
private boolean showIgnoredInfo() {
return !client.settings.getBoolean("ignoreShowNotDialog") ||
!ignoredMessages.isVisible();
}
private boolean isOwnUsername(String name) {
String ownUsername = client.getUsername();
return ownUsername != null && ownUsername.equalsIgnoreCase(name);
}
public void userBanned(final User user, final long duration, final String reason, final String id) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.getChannel(user.getChannel()).userBanned(user, duration, reason, id);
user.addBan(duration, reason, id);
updateUserInfoDialog(user);
if (client.settings.listContains("streamChatChannels", user.getChannel())) {
streamChat.userBanned(user, duration, reason, id);
}
}
});
}
public void clearChat() {
clearChat(null);
}
public void clearChat(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel panel;
if (channel == null) {
panel = channels.getActiveChannel();
} else {
panel = channels.getChannel(channel);
if (client.settings.listContains("streamChatChannels", channel)) {
streamChat.clear();
}
}
if (panel != null) {
panel.clearChat();
}
}
});
}
public void testHotkey() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel panel = channels.getLastActiveChannel();
if (panel != null) {
panel.selectPreviousUser();
}
}
});
}
public void printLine(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel panel = channels.getLastActiveChannel();
if (panel != null) {
printInfo(panel, line);
client.chatLog.info(panel.getName(), line);
}
}
});
}
public void printSystem(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel panel = channels.getActiveChannel();
if (panel != null) {
printInfo(panel, line);
client.chatLog.system(panel.getName(), line);
}
}
});
}
public void printLine(final String channel, final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (channel == null) {
printLine(line);
} else {
printInfo(channels.getChannel(channel), line);
client.chatLog.info(channel, line);
}
}
});
}
public void printLineAll(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
//client.chatLog.info(null, line);
if (channels.getChannelCount() == 0) {
Channel panel = channels.getActiveChannel();
if (panel != null) {
printInfo(panel, line);
}
return;
}
for (Channel channel : channels.channels()) {
printInfo(channel, line);
client.chatLog.info(channel.getName(), line);
}
}
});
}
private void printInfo(Channel channel, String line) {
if (!checkInfoIgnore(line)) {
channel.printLine(line);
if (channel.getType() == Channel.Type.SPECIAL) {
channels.setChannelNewMessage(channel);
}
} else {
ignoredMessages.addInfoMessage(channel.getName(), line);
}
}
/**
* Calls the appropriate method from the given channel
*
* @param channel The channel this even has happened in.
* @param type The type of event.
* @param user The User object of who was the target of this event (mod/..).
*/
public void printCompact(final String channel, final String type, final User user) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.getChannel(channel).printCompact(type, user);
}
});
}
/**
* Perform search in the currently selected channel. Should only be called
* from the EDT.
*
* @param window
* @param searchText
* @return
*/
public boolean search(final Window window, final String searchText) {
Channel chan = channels.getChannelFromWindow(window);
if (chan == null) {
return false;
}
return chan.search(searchText);
}
public void resetSearch(final Window window) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Channel chan = channels.getChannelFromWindow(window);
if (chan != null) {
chan.resetSearch();
}
}
});
}
public void showMessage(final String message) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (connectionDialog.isVisible()) {
JOptionPane.showMessageDialog(connectionDialog, message);
}
else {
printLine(message);
}
}
});
}
/**
* Outputs a line to the debug window
*
* @param line
*/
public void printDebug(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
debugWindow.printLine(line);
}
});
}
public void printDebugFFZ(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
debugWindow.printLineFFZ(line);
}
});
}
public void printDebugPubSub(final String line) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
debugWindow.printLinePubSub(line);
}
});
}
public void printModerationAction(final ModeratorActionData data,
final boolean ownAction) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
moderationLog.add(data);
autoModDialog.addData(data);
String channel = Helper.toValidChannel(data.stream);
if (channels.isChannel(channel)) {
// Output directly to chat (if enabled)
if (data.type == ModeratorActionData.Type.AUTOMOD_REJECTED) {
// Automod
if (client.settings.getBoolean("showAutoMod")) {
channels.getChannel(channel).printLine(
String.format("[AutoMod] <%s> %s",
data.args.get(0),
StringUtil.join(data.args, " ", 1)));
}
} else if (!ownAction && client.settings.getBoolean("showModActions")) {
// Other Mod Actions
channels.getChannel(channel).printLine(String.format("[ModAction] %s: /%s %s",
data.created_by,
data.moderation_action,
StringUtil.join(data.args, " ")));
}
}
}
});
}
public void autoModRequestResult(final String result, final String msgId) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
autoModDialog.requestResult(result, msgId);
}
});
}
/**
* Outputs a line to the debug window
*
* @param line
*/
public void printDebugIrc(final String line) {
if (SwingUtilities.isEventDispatchThread()) {
debugWindow.printLineIrc(line);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
debugWindow.printLineIrc(line);
}
});
}
}
// User stuff
/**
* Adds a user to a channel, adding to the userlist
*
* @param channel
* @param user
*/
public void addUser(final String channel, final User user) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!shouldUpdateUser(user)) return;
Channel c = channels.getChannel(channel);
c.addUser(user);
if (channels.getActiveChannel() == c) {
state.update();
}
}
});
}
/**
* Removes a user from a channel, removing from the userlist
*
* @param channel
* @param user
*/
public void removeUser(final String channel, final User user) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!shouldUpdateUser(user)) return;
Channel c = channels.getChannel(channel);
c.removeUser(user);
if (channels.getActiveChannel() == c) {
state.update();
}
}
});
}
/**
* Updates a user.
*
* @param user
*/
public void updateUser(final User user) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!shouldUpdateUser(user)) return;
channels.getChannel(user.getChannel()).updateUser(user);
state.update();
}
});
}
private boolean shouldUpdateUser(User user) {
return !user.getChannel().equals(WhisperManager.WHISPER_CHANNEL)
|| channels.isChannel(WhisperManager.WHISPER_CHANNEL);
}
/**
* Resort users in the userlist of the given channel.
*
* @param channel
*/
public void resortUsers(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
channels.getChannel(channel).resortUserlist();
}
});
}
/**
* Clears the userlist on all channels.
*/
public void clearUsers(final String channel) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (channel != null) {
Channel c = channels.get(channel);
if (c != null) {
c.clearUsers();
}
} else {
for (Channel channel : channels.channels()) {
channel.clearUsers();
}
}
}
});
}
public void reconnect() {
client.commandReconnect();
}
public void setUpdateAvailable(final String newVersion) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
menu.setUpdateNotification(true);
updateMessage.setNewVersion(newVersion);
}
});
}
public void setAnnouncementAvailable(boolean enabled) {
menu.setAnnouncementNotification(enabled);
}
public void showSettings() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
getSettingsDialog().showSettings();
}
});
}
public void setColor(final String item) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
getSettingsDialog().showSettings("editUsercolorItem", item);
}
});
}
public void setCustomName(final String item) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
getSettingsDialog().showSettings("editCustomNameItem", item);
}
});
}
public void updateChannelInfo() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateChannelInfoDialog();
}
});
}
public void updateState() {
updateState(false);
}
public void updateState(final boolean forced) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
state.update(forced);
//client.testHotkey();
}
});
}
/**
* Manages updating the current state, mainly the titles and menus.
*/
private class StateUpdater {
/**
* Saves when the state was last setd, so the delay can be measured.
*/
private long stateLastUpdated = 0;
/**
* Update state no faster than this amount of milliseconds.
*/
private static final int UPDATE_STATE_DELAY = 500;
/**
* Update the title and other things based on the current state and
* stream/channel information. This is a convenience method that doesn't
* force the update.
*
* @see update(boolean)
*/
protected void update() {
update(false);
}
/**
* Update the title and other things based on the current state and
* stream/channel information.
*
* <p>The update is only performed once every {@literal UPDATE_STATE_DELAY}
* milliseconds, unless {@literal forced} is {@literal true}. This is meant
* to prevent flickering of the titlebar when a lot of updates would
* happen, for example when a lot of joins/parts happen at once.</p>
*
* <p>Of course this means that the info might not be always up-to-date:
* The chance is pretty high that the last update is skipped because it
* came to close to the previous. The UpdateTimer updates every 10s so
* it shouldn't take too long to be corrected. This also mainly affects
* the chatter count because it gets updated in many small steps when
* joins/parts happen (it also already isn't very up-to-date anyway from
* Twitch's side though).</p>
*
* @param forced If {@literal true} the update is performed with every call
*/
protected void update(boolean forced) {
if (!forced && System.currentTimeMillis() - stateLastUpdated < UPDATE_STATE_DELAY) {
return;
}
stateLastUpdated = System.currentTimeMillis();
int state = client.getState();
requestFollowedStreams();
updateMenuState(state);
updateTitles(state);
}
/**
* Disables/enables menu items based on the current state.
*
* @param state
*/
private void updateMenuState(int state) {
if (state > Irc.STATE_OFFLINE || state == Irc.STATE_RECONNECTING) {
menu.getMenuItem("connect").setEnabled(false);
} else {
menu.getMenuItem("connect").setEnabled(true);
}
if (state > Irc.STATE_CONNECTING || state == Irc.STATE_RECONNECTING) {
menu.getMenuItem("disconnect").setEnabled(true);
} else {
menu.getMenuItem("disconnect").setEnabled(false);
}
}
/**
* Updates the titles of both the main window and popout dialogs.
*
* @param state
*/
private void updateTitles(int state) {
// May be necessary to make the title either way, because it also
// requests stream info
String mainTitle = makeTitle(channels.getActiveTab(), state);
String trayTooltip = makeTitle(channels.getLastActiveChannel(), state);
trayIcon.setTooltipText(trayTooltip);
if (client.settings.getBoolean("simpleTitle")) {
setTitle("Chatty");
} else {
setTitle(mainTitle);
}
Map<Channel, JDialog> popoutChannels = channels.getPopoutChannels();
for (Channel channel : popoutChannels.keySet()) {
String title = makeTitle(channel, state);
popoutChannels.get(channel).setTitle(title);
}
}
/**
* Assembles the title of the window based on the current state and chat
* and stream info.
*
* @param channel The {@code Channel} object to create the title for
* @param state The current state
* @return The created title
*/
private String makeTitle(Channel channel, int state) {
String channelName = channel.getName();
// Current state
String stateText = "";
if (state == Irc.STATE_CONNECTING) {
stateText = "Connecting..";
} else if (state == Irc.STATE_CONNECTED) {
stateText = "Connecting...";
} else if (state == Irc.STATE_REGISTERED) {
if (channelName.isEmpty()) {
stateText = "Connected";
}
} else if (state == Irc.STATE_OFFLINE) {
stateText = "Not connected";
} else if (state == Irc.STATE_RECONNECTING) {
stateText = "Reconnecting..";
}
String title = stateText;
// Stream Info
if (!channelName.isEmpty()) {
boolean hideCounts = !client.settings.getBoolean("titleShowViewerCount");
String chanNameText = channelName;
if (client.isWhisperAvailable()) {
chanNameText += " [W]";
}
if (!title.isEmpty()) {
title += " - ";
}
String numUsers = Helper.formatViewerCount(channel.getNumUsers());
if (!client.isUserlistLoaded(channelName)) {
numUsers += "*";
}
if (hideCounts) {
numUsers = "";
}
String chanState = "";
if (client.settings.getBoolean("titleShowChannelState")) {
chanState = client.getChannelState(channelName).getInfo();
}
if (!chanState.isEmpty()) {
chanState = " "+chanState;
}
StreamInfo streamInfo = getStreamInfo(channel.getStreamName());
if (streamInfo.isValid()) {
if (streamInfo.getOnline()) {
String uptime = "";
if (client.settings.getBoolean("titleShowUptime")) {
if (client.settings.getBoolean("titleLongerUptime")) {
uptime = DateTime.agoUptimeCompact2(
streamInfo.getTimeStartedWithPicnic());
} else {
uptime = DateTime.agoUptimeCompact(
streamInfo.getTimeStartedWithPicnic());
}
}
String numViewers = "|"+Helper.formatViewerCount(streamInfo.getViewers());
if (!client.settings.getBoolean("titleShowViewerCount")) {
numViewers = "";
} else if (!uptime.isEmpty()) {
uptime = "|"+uptime;
}
title += chanNameText + " [" + numUsers + numViewers + uptime + "]";
} else {
title += chanNameText;
if (!hideCounts) {
title += " [" + numUsers + "]";
}
}
title += chanState+" - " + streamInfo.getFullStatus();
} else {
title += chanNameText;
if (!hideCounts) {
title += " [" + numUsers + "]";
}
title += chanState;
}
} else if (client.isWhisperAvailable()) {
title += " [W]";
}
title += " - Chatty";
String addition = client.settings.getString("titleAddition");
if (!addition.isEmpty()) {
title = addition+" "+title;
}
return title;
}
}
public void openConnectDialog(final String channelPreset) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
openConnectDialogInternal(channelPreset);
}
});
}
private void openConnectDialogInternal(String channelPreset) {
updateConnectionDialog(channelPreset);
connectionDialog.setLocationRelativeTo(this);
connectionDialog.setVisible(true);
}
private void openStreamChat() {
windowStateManager.setWindowPosition(streamChat);
streamChat.setVisible(true);
}
private void setStreamChatSize(int width, int height) {
streamChat.setSize(width, height);
}
public void updateEmotesDialog() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emotesDialog.updateEmotesets(client.getSpecialUser().getEmoteSet());
}
});
}
public void updateEmoticons(final EmoticonUpdate update) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emoticons.updateEmoticons(update);
emotesDialog.update();
}
});
}
public void addEmoticons(final Set<Emoticon> emotes) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emoticons.addEmoticons(emotes);
emotesDialog.update();
}
});
}
public void setCheerEmotes(final Set<CheerEmoticon> emotes) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emoticons.setCheerEmotes(emotes);
}
});
}
public void setEmotesets(final Map<Integer, String> emotesets) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emoticons.addEmotesetStreams(emotesets);
}
});
}
/* ###############
* Get token stuff
*/
public void webserverStarted() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (tokenGetDialog.isVisible()) {
tokenGetDialog.ready();
}
}
});
}
public void webserverError(final String error) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (tokenGetDialog.isVisible()) {
tokenGetDialog.error(error);
}
}
});
}
public void webserverTokenReceived(final String token) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
tokenReceived(token);
}
});
}
private void tokenGetDialogClosed() {
tokenGetDialog.setVisible(false);
client.stopWebserver();
}
/**
* Token received from the webserver.
*
* @param token
*/
private void tokenReceived(String token) {
client.settings.setString("token", token);
client.settings.setBoolean("foreignToken", false);
if (tokenGetDialog.isVisible()) {
tokenGetDialog.tokenReceived();
}
tokenDialog.update("",token);
updateConnectionDialog(null);
verifyToken(token);
}
/**
* Verify the given Token. This sends a request to the TwitchAPI.
*
* @param token
*/
private void verifyToken(String token) {
client.api.verifyToken(token);
tokenDialog.verifyingToken();
}
public void tokenVerified(final String token, final TokenInfo tokenInfo) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
tokenVerifiedInternal(token, tokenInfo);
}
});
}
private String manuallyChangedToken = null;
public void changeToken(final String token) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (token == null || token.isEmpty()) {
printSystem("You have to supply a token.");
} else if (manuallyChangedToken != null) {
printSystem("You already have changed the token, please wait..");
} else if (token.equals(client.settings.getString("token"))) {
printSystem("The token you entered is already set.");
} else {
printSystem("Setting new token. Please wait..");
client.settings.setString("username", null);
manuallyChangedToken = token;
tokenReceived(token);
}
}
});
}
/**
* This does the main work when a response for verifying the token is
* received from the Twitch API.
*
* A Token can be verified manually by pressing the button or automatically
* when a new Token was received by the webserver. So when this is called
* the original source can be both.
*
* The tokenGetDialog is closed if necessary.
*
* @param token The token that was verified
* @param username The usernamed that was received for this token. If this
* is null then an error occured, if it is empty then the token was
* invalid.
*/
private void tokenVerifiedInternal(String token, TokenInfo tokenInfo) {
// Stopping the webserver here, because it allows the /tokenreceived/
// page to be delievered, because of the delay of verifying the token.
// This should probably be solved better.
client.stopWebserver();
String result;
String currentUsername = client.settings.getString("username");
// Check if a new token was requested (the get token dialog should still
// be open at this point) If this is wrong, it just displays the wrong
// text, this shouldn't be used for something critical.
boolean getNewLogin = tokenGetDialog.isVisible();
boolean showInDialog = tokenDialog.isVisible();
boolean changedTokenResponse = token == null
? manuallyChangedToken == null : token.equals(manuallyChangedToken);
boolean valid = false;
if (tokenInfo == null) {
// An error occured when verifying the token
if (getNewLogin) {
result = "An error occured completing getting login data.";
}
else {
result = "An error occured verifying login data.";
}
}
else if (!tokenInfo.valid) {
// There was an answer when verifying the token, but it was invalid
if (getNewLogin) {
result = "Invalid token received when getting login data. Please "
+ "try again.";
client.settings.setString("token", "");
}
else if (changedTokenResponse) {
result = "Invalid token entered. Please try again.";
client.settings.setString("token", "");
}
else {
result = "Login data invalid. [help:login-invalid What does this mean?]";
}
if (!showInDialog && !changedTokenResponse) {
showTokenWarning();
}
}
else if (!tokenInfo.chat_access) {
result = "No chat access (required) with token.";
}
else {
// Everything is fine, so save username and token
valid = true;
String username = tokenInfo.name;
client.settings.setString("username", username);
client.settings.setString("userid", tokenInfo.userId);
client.settings.setString("token", token);
tokenDialog.update(username, token);
updateConnectionDialog(null);
if (!currentUsername.isEmpty() && !username.equals(currentUsername)) {
result = "Login verified and ready to connect (replaced '" +
currentUsername + "' with '" + username + "').";
}
else {
result = "Login verified and ready to connect.";
}
}
if (changedTokenResponse) {
printLine(result);
manuallyChangedToken = null;
}
setTokenScopes(tokenInfo);
// Always close the get token dialog, if it's not open, nevermind ;)
tokenGetDialog.setVisible(false);
// Show result in the token dialog
tokenDialog.tokenVerified(valid, result);
}
/**
* Sets the token scopes in the settings based on the given TokenInfo.
*
* @param info
*/
private void setTokenScopes(TokenInfo info) {
if (info == null) {
return;
}
if (info.valid) {
client.settings.setBoolean("token_chat", info.chat_access);
client.settings.setBoolean("token_editor", info.channel_editor);
client.settings.setBoolean("token_commercials", info.channel_commercials);
client.settings.setBoolean("token_user", info.user_read);
client.settings.setBoolean("token_subs", info.channel_subscriptions);
client.settings.setBoolean("token_follow", info.user_follows_edit);
}
else {
resetTokenScopes();
}
updateTokenScopes();
}
/**
* Updates the token scopes in the GUI based on the settings.
*/
private void updateTokenScopes() {
boolean chat = client.settings.getBoolean("token_chat");
boolean commercials = client.settings.getBoolean("token_commercials");
boolean editor = client.settings.getBoolean("token_editor");
boolean user = client.settings.getBoolean("token_user");
boolean subscriptions = client.settings.getBoolean("token_subs");
boolean follow = client.settings.getBoolean("token_follow");
tokenDialog.updateAccess(chat, editor, commercials, user, subscriptions, follow);
adminDialog.updateAccess(editor, commercials);
}
private void resetTokenScopes() {
client.settings.setBoolean("token_chat", false);
client.settings.setBoolean("token_commercials", false);
client.settings.setBoolean("token_editor", false);
client.settings.setBoolean("token_user", false);
client.settings.setBoolean("token_subs", false);
client.settings.setBoolean("token_follow", false);
}
public void showTokenWarning() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
String message = "<html><body style='width:400px;'>Login data was determined "
+ "invalid, which means you may have to request it again before "
+ "you can connect to chat or do authorized actions (like "
+ "getting notified about streams you follow, edit stream title..).";
String[] options = new String[]{"Close / Configure login","Just Close"};
int result = GuiUtil.showNonAutoFocusOptionPane(MainGui.this, "Error",
message, JOptionPane.ERROR_MESSAGE,
JOptionPane.DEFAULT_OPTION, options);
if (result == 0) {
openTokenDialog();
}
}
});
}
public void setSubscriberInfo(final FollowerInfo info) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
subscribersDialog.setFollowerInfo(info);
}
});
}
public void setFollowerInfo(final FollowerInfo info) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
followerDialog.setFollowerInfo(info);
}
});
}
public void setChannelInfo(final String channel, final ChannelInfo info, final RequestResultCode result) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
adminDialog.setChannelInfo(channel, info, result);
userInfoDialog.setChannelInfo(info);
}
});
}
public void putChannelInfo(ChannelInfo info) {
client.api.putChannelInfo(info);
}
public void getChannelInfo(String channel) {
client.api.getChannelInfo(channel);
}
public ChannelInfo getCachedChannelInfo(String channel, String id) {
return client.api.getCachedChannelInfo(channel, id);
}
public void getChatInfo(String stream) {
client.api.getChatInfo(stream);
}
/**
*
* @param info Can be null in an error occured
*/
public void setChatInfo(final ChatInfo info) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
chatRulesDialog.setChatInfo(info);
}
});
}
public String getActiveStream() {
return channels.getActiveChannel().getStreamName();
}
/**
* Saves the Set game favorites to the settings.
*
* @param favorites
*/
public void setGameFavorites(Set<String> favorites) {
client.settings.putList("gamesFavorites", new ArrayList(favorites));
}
public void setCommunityFavorites(Map<String, String> favorites) {
client.settings.putMap("communityFavorites", favorites);
}
/**
* Returns a Set of game favorites retrieved from the settings.
*
* @return
*/
public Set<String> getGameFavorites() {
return new HashSet<>(client.settings.getList("gamesFavorites"));
}
public Map<String, String> getCommunityFavorites() {
return client.settings.getMap("communityFavorites");
}
public void putChannelInfoResult(final RequestResultCode result) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
adminDialog.setPutResult(result);
}
});
}
public void saveCommercialDelaySettings(boolean enabled, long delay) {
client.settings.setBoolean("adDelay", enabled);
client.settings.setLong("adDelayLength", delay);
}
private void loadCommercialDelaySettings() {
boolean enabled = client.settings.getBoolean("adDelay");
long length = client.settings.getLong("adDelayLength");
adminDialog.updateCommercialDelaySettings(enabled, length);
}
public void runCommercial(String stream, int length) {
client.runCommercial(stream, length);
}
private void runCommercial(int length) {
if (adminDialog.isCommercialsTabVisible()) {
adminDialog.commercialHotkey(length);
} else {
runCommercial(getActiveStream(), length);
}
}
public void commercialResult(final String stream, final String text, final RequestResultCode result) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
adminDialog.commercialResult(stream, text, result);
}
});
}
/**
* Returns a list of open channels in the order the tabs are open (popouts
* at the end if present).
*
* Should probably only be used out of the EDT.
*
* @return
*/
public List<String> getOpenChannels() {
List<String> result = new ArrayList<>();
for (Channel chan : channels.getChannelsOfType(Channel.Type.CHANNEL)) {
result.add(chan.getName());
}
return result;
}
/**
* Get StreamInfo for the given stream, but also request it for all open
* channels.
*
* @param stream
* @return
*/
public StreamInfo getStreamInfo(String stream) {
Set<String> streams = new HashSet<>();
for (Channel chan : channels.getChannelsOfType(Channel.Type.CHANNEL)) {
streams.add(chan.getStreamName());
}
return client.api.getStreamInfo(stream, streams);
}
/**
* Outputs the full title if the StreamInfo for this channel is valid.
*
* @param channel
*/
public void printStreamInfo(final String channel) {
final String stream = channel.replace("#", "");
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (client.settings.getBoolean("printStreamStatus")) {
StreamInfo info = getStreamInfo(stream);
if (info.isValid()) {
printLine(channel, "~" + info.getFullStatus() + "~");
}
}
}
});
}
/**
* Possibly request followed streams from the API, if enabled and access
* was granted.
*/
private void requestFollowedStreams() {
if (client.settings.getBoolean("requestFollowedStreams") &&
client.settings.getBoolean("token_user")) {
client.api.getFollowedStreams(client.settings.getString("token"));
}
}
private class MySettingChangeListener implements SettingChangeListener {
/**
* Since this can also be called from other threads, run in EDT if
* necessary.
*
* @param setting
* @param type
* @param value
*/
@Override
public void settingChanged(final String setting, final int type, final Object value) {
if (SwingUtilities.isEventDispatchThread()) {
settingChangedInternal(setting, type, value);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
settingChangedInternal(setting, type, value);
}
});
}
}
private void settingChangedInternal(String setting, int type, Object value) {
if (type == Setting.BOOLEAN) {
boolean bool = (Boolean)value;
if (setting.equals("ontop")) {
setAlwaysOnTop((Boolean) value);
} else if (setting.equals("highlightUsername")) {
updateHighlightSetUsernameHighlighted((Boolean) value);
} else if (setting.equals("highlightNextMessages")) {
updateHighlightNextMessages();
} else if (setting.equals("popoutSaveAttributes") || setting.equals("popoutCloseLastChannel")) {
updatePopoutSettings();
} else if (setting.equals("livestreamer")) {
ContextMenuHelper.enableLivestreamer = (Boolean)value;
} else if (setting.equals("attachedWindows")) {
windowStateManager.setAttachedWindowsEnabled((Boolean)value);
} else if (setting.equals("globalHotkeysEnabled")) {
hotkeyManager.setGlobalHotkeysEnabled((Boolean)value);
} else if (setting.equals("imageCache")) {
ImageCache.setCachingEnabled(bool);
} else if (setting.equals("mainResizable")) {
setResizable(bool);
} else if (setting.equals("streamChatResizable")) {
streamChat.setResizable(bool);
} else if (setting.equals("closeEmoteDialogOnDoubleClick")) {
emotesDialog.setCloseOnDoubleClick(bool);
} else if (setting.equals("foreignToken")) {
tokenDialog.setForeignToken(bool);
}
if (setting.startsWith("title")) {
updateState(true);
}
loadMenuSetting(setting);
}
if (StyleManager.settingNames.contains(setting)) {
styleManager.refresh();
channels.refreshStyles();
highlightedMessages.refreshStyles();
ignoredMessages.refreshStyles();
streamChat.refreshStyles();
//menu.setForeground(styleManager.getColor("foreground"));
//menu.setBackground(styleManager.getColor("background"));
}
if (setting.equals("displayNamesModeUserlist")) {
channels.updateUserlistSettings();
}
if (type == Setting.STRING) {
if (setting.equals("timeoutButtons")) {
userInfoDialog.setUserDefinedButtonsDef((String) value);
} else if (setting.equals("token")) {
client.api.setToken((String)value);
} else if (setting.equals("laf")) {
GuiUtil.setLookAndFeel((String)value);
GuiUtil.updateLookAndFeel();
} else if (setting.equals("emoji")) {
emoticons.addEmoji((String)value);
} else if (setting.equals("cheersType")) {
emoticons.setCheerState((String)value);
} else if (setting.equals("backgroundColor")) {
emoticons.setCheerBackground(HtmlColors.decode((String)value));
} else if (setting.equals("soundDevice")) {
Sound.setDeviceName((String)value);
}
}
if (type == Setting.LIST) {
if (setting.equals("highlight")) {
updateHighlight();
} else if (setting.equals("ignore")) {
updateIgnore();
} else if (setting.equals("hotkeys")) {
hotkeyManager.loadFromSettings(client.settings);
}
}
if (type == Setting.LONG) {
if (setting.equals("dialogFontSize")) {
userInfoDialog.setFontSize((Long)value);
} else if (setting.equals("streamChatMessageTimeout")) {
streamChat.setMessageTimeout(((Long)value).intValue());
} else if (setting.equals("emoteScaleDialog")) {
emotesDialog.setEmoteScale(((Long)value).intValue());
}
}
if (setting.equals("channelFavorites") || setting.equals("channelHistory")) {
// TOCONSIDER: This means that it is updated twice in a row when an action
// requires both settings to be changed
updateFavoritesDialogWhenVisible();
}
if (setting.equals("liveStreamsSorting")) {
updateLiveStreamsDialog();
}
if (setting.equals("historyRange")) {
updateHistoryRange();
}
Set<String> notificationSettings = new HashSet<>(Arrays.asList(
"nScreen", "nPosition", "nDisplayTime", "nMaxDisplayTime",
"nMaxDisplayed", "nMaxQueueSize", "nActivity", "nActivityTime"));
if (notificationSettings.contains(setting)) {
updateNotificationSettings();
}
if (setting.equals("spamProtection")) {
client.setLinesPerSeconds((String)value);
}
if (setting.equals("urlPrompt")) {
UrlOpener.setPrompt((Boolean)value);
}
if (setting.equals("urlCommandEnabled")) {
UrlOpener.setCustomCommandEnabled((Boolean)value);
}
if (setting.equals("urlCommand")) {
UrlOpener.setCustomCommand((String)value);
}
if (setting.equals("abUniqueCats")) {
client.addressbook.setSomewhatUniqueCategories((String)value);
}
if (setting.equals("commands")) {
client.customCommands.loadFromSettings();
}
if (setting.equals("channelContextMenu")
|| setting.equals("userContextMenu")
|| setting.equals("livestreamerQualities")
|| setting.equals("streamsContextMenu")) {
updateCustomContextMenuEntries();
}
else if (setting.equals("chatScrollbarAlways") || setting.equals("userlistWidth")) {
updateChannelsSettings();
}
else if (setting.equals("ignoredEmotes")) {
emoticons.setIgnoredEmotes(client.settings.getList("ignoredEmotes"));
}
}
}
private class MySettingsListener implements SettingsListener {
@Override
public void aboutToSaveSettings(Settings settings) {
if (SwingUtilities.isEventDispatchThread()) {
System.out.println("Saving GUI settings.");
client.settings.setLong("favoritesSorting", favoritesDialog.getSorting());
emoticons.saveFavoritesToSettings(settings);
client.settings.setString("statusHistorySorting", adminDialog.getStatusHistorySorting());
}
}
}
public Settings getSettings() {
return client.settings;
}
public Collection<String> getSettingNames() {
return client.settings.getSettingNames();
}
public Collection<String> getEmoteNames() {
return emoticons.getEmoteNames();
}
public Collection<String> getEmoteNamesPerStream(String stream) {
return emoticons.getEmotesNamesByStream(stream);
}
public String getCustomCompletionItem(String key) {
return (String)client.settings.mapGet("customCompletion", key);
}
public void updateEmoteNames() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
emoticons.updateEmoteNames(client.getSpecialUser().getEmoteSet());
}
});
}
public WindowListener getWindowListener() {
return windowListener;
}
private class MyWindowListener extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
if (e.getSource() == tokenGetDialog) {
tokenGetDialogClosed();
}
}
}
private class MainWindowListener extends WindowAdapter {
@Override
public void windowStateChanged(WindowEvent e) {
if (e.getComponent() == MainGui.this) {
saveState(e.getComponent());
if (isMinimized()) {
if (client.settings.getBoolean("minimizeToTray")) {
minimizeToTray();
}
} else {
// Only cleanup from tray if not minimized, when minimized
// cleanup should never be done
cleanupAfterRestoredFromTray();
}
}
}
@Override
public void windowClosing(WindowEvent evt) {
if (evt.getComponent() == MainGui.this) {
if (client.settings.getBoolean("closeToTray")) {
minimizeToTray();
} else {
exit();
}
}
}
}
/**
* Checks if the main window is currently minimized.
*
* @return true if minimized, false otherwise
*/
private boolean isMinimized() {
return (getExtendedState() & ICONIFIED) == ICONIFIED;
}
/**
* Minimize window to tray.
*/
private void minimizeToTray() {
if (!isMinimized()) {
setExtendedState(getExtendedState() | ICONIFIED);
}
trayIcon.setIconVisible(true);
// Set visible to false, so it is removed from the taskbar
setVisible(false);
//trayIcon.displayInfo("Minimized to tray", "Double-click icon to show again..");
}
/**
* Remove tray icon if applicable.
*/
private void cleanupAfterRestoredFromTray() {
if (client.settings.getBoolean("useCustomNotifications")) {
trayIcon.setIconVisible(false);
}
}
/**
* Display an error dialog with the option to quit or continue the program
* and to report the error.
*
* @param error The error as a LogRecord
* @param previous Some previous debug messages as LogRecord, to provide
* context
*/
public void error(final LogRecord error, final LinkedList<LogRecord> previous) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
int result = errorMessage.show(error, previous);
if (result == ErrorMessage.QUIT) {
exit();
}
}
});
}
/**
* Exit the program.
*/
private void exit() {
client.exit();
}
public void cleanUp() {
if (SwingUtilities.isEventDispatchThread()) {
hotkeyManager.cleanUp();
setVisible(false);
dispose();
}
}
}