diff --git a/src/chatty/Chatty.java b/src/chatty/Chatty.java index 2beffb672..4923faa85 100644 --- a/src/chatty/Chatty.java +++ b/src/chatty/Chatty.java @@ -57,7 +57,7 @@ public class Chatty { * by points. May contain a single "b" for beta versions, which are counted * as older (so 0.8.7b4 is older than 0.8.7). */ - public static final String VERSION = "0.13.0.168"; + public static final String VERSION = "0.14.0.168"; /** * Enable Version Checker (if you compile and distribute this yourself, you diff --git a/src/chatty/SettingsManager.java b/src/chatty/SettingsManager.java index cb60d641b..52af7e58d 100644 --- a/src/chatty/SettingsManager.java +++ b/src/chatty/SettingsManager.java @@ -297,6 +297,7 @@ public void defineSettings() { settings.addBoolean("msgColorsEnabled", false); settings.addList("msgColors", new LinkedList(), Setting.STRING); settings.addBoolean("msgColorsPrefer", false); + settings.addBoolean("msgColorsLinks", true); // Usercolors settings.addBoolean("customUsercolors", false); @@ -346,6 +347,8 @@ public void defineSettings() { settings.addBoolean("reuseUserDialog", false); settings.addString("userDialogTimestamp", "[HH:mm:ss]"); settings.addLong("clearUserMessages", 12); + settings.addMap("userNotes", new HashMap(), Setting.STRING); + settings.addMap("userNotesChat", new HashMap(), Setting.STRING); // History / Favorites settings.addMap("channelHistory",new TreeMap(), Setting.LONG); @@ -416,6 +419,8 @@ public void defineSettings() { settings.addMap("windows", new HashMap<>(), Setting.STRING); settings.addLong("restoreMode", WindowStateManager.RESTORE_ON_START); settings.addBoolean("restoreOnlyIfOnScreen", true); + settings.addLong("highlightDock", 0); + settings.addLong("ignoreDock", 0); // Popouts settings.addBoolean("popoutSaveAttributes", true); @@ -437,6 +442,12 @@ public void defineSettings() { settings.addBoolean("tabsMwheelScrollingAnywhere", true); settings.addString("tabsPlacement", "top"); settings.addString("tabsLayout", "wrap"); + settings.addLong("tabsLive", 16); + settings.addLong("tabsMessage", 4); + settings.addLong("tabsHighlight", 8); + settings.addLong("tabsStatus", 32); + settings.addLong("tabsActive", 128); + settings.addLong("tabsPopoutDrag", 2); // Chat Window settings.addBoolean("chatScrollbarAlways", false); @@ -682,6 +693,9 @@ public void defineSettings() { settings.addList("autoUnhostStreams", new ArrayList(), Setting.STRING); settings.addMap("rewards", new HashMap(), Setting.STRING); + + settings.addBoolean("pronouns", false); + settings.addBoolean("pronounsChat", false); //=============== diff --git a/src/chatty/TwitchClient.java b/src/chatty/TwitchClient.java index 4eaed701f..88619a78a 100644 --- a/src/chatty/TwitchClient.java +++ b/src/chatty/TwitchClient.java @@ -1513,6 +1513,8 @@ else if (command.equals("bantest")) { } catch (NumberFormatException ex) { } StreamInfo info = api.getStreamInfo("tduva", null); info.set("Test 2", "Game", viewers, System.currentTimeMillis() - 1000, StreamType.LIVE); + } else if (command.equals("newstatus")) { + g.setChannelNewStatus(parameter, ""); } else if (command.equals("refreshstreams")) { api.manualRefreshStreams(); } else if (command.equals("usericonsinfo")) { @@ -2553,6 +2555,7 @@ private class MyStreamInfoListener implements StreamInfoListener { public void streamInfoUpdated(StreamInfo info) { g.updateState(true); g.updateChannelInfo(info); + g.updateStreamLive(info); g.addStreamInfo(info); String channel = "#"+info.getStream(); if (isChannelOpen(channel)) { @@ -2726,9 +2729,10 @@ public void usericonsReceived(List icons) { } @Override - public void botNamesReceived(Set botNames) { + public void botNamesReceived(String stream, Set botNames) { if (settings.getBoolean("botNamesFFZ")) { - botNameManager.addBotNames(null, botNames); + String channel = Helper.toValidChannel(stream); + botNameManager.addBotNames(channel, botNames); } } @@ -2899,7 +2903,7 @@ public void onChannelJoined(User user) { if (user.getRoom().hasTopic()) { g.printLine(user.getRoom(), user.getRoom().getTopicText()); } - g.loadRecentMessages(user.getChannel()); + g.loadRecentMessages(user); // Icons and FFZ/BTTV Emotes //api.requestChatIcons(Helper.toStream(channel), false); diff --git a/src/chatty/TwitchConnection.java b/src/chatty/TwitchConnection.java index cbaa3c910..c94d059f8 100644 --- a/src/chatty/TwitchConnection.java +++ b/src/chatty/TwitchConnection.java @@ -920,6 +920,9 @@ private void updateUserFromTags(User user, MsgTags tags) { Map badgeInfo = Helper.parseBadges(tags.get("badge-info")); String subMonths = badgeInfo.get("subscriber"); + if (subMonths == null) { + subMonths = badgeInfo.get("founder"); + } if (subMonths != null) { user.setSubMonths(Helper.parseShort(subMonths, (short)0)); } @@ -1151,7 +1154,7 @@ void onUsernotice(String channel, String message, MsgTags tags) { if (tags.isValueOf("msg-id", "resub", "sub", "subgift", "anonsubgift")) { text = text.trim(); if (giftMonths > 1 && !text.matches(".* gifted "+giftMonths+" .*")) { - text += " They gifted "+giftMonths+" months!"; + text += " It's a "+giftMonths+"-month gift!"; } // There are still some types of notifications that don't have // this info, and it might be useful diff --git a/src/chatty/gui/Channels.java b/src/chatty/gui/Channels.java index fdfcccc4b..502cc5be3 100644 --- a/src/chatty/gui/Channels.java +++ b/src/chatty/gui/Channels.java @@ -5,23 +5,26 @@ import chatty.Helper.IntegerPair; import chatty.Room; import chatty.gui.components.Channel; -import chatty.gui.components.ChannelDialog; -import chatty.gui.components.tabs.Tabs; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.menus.TabContextMenu; import chatty.util.Debugging; +import chatty.util.IconManager; +import chatty.util.dnd.DockContent; +import chatty.util.dnd.DockListener; +import chatty.util.dnd.DockManager; +import chatty.util.dnd.DockSetting; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Window; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; import java.util.*; -import javax.swing.JDialog; +import javax.swing.JPopupMenu; +import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; +import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import chatty.util.dnd.DockPopout; import chatty.util.ForkUtil; @@ -29,13 +32,16 @@ * Managing the Channel objects in the main window and popouts, providing a * default channel while no other is added. * + * TODO: + * - Switching tabs in right-hand split while the focus is not on the tab (e.g. + * input) will first switch focus to active tab in left-hand split + * * @author tduva */ public class Channels { private final MainGui gui; - private final WindowListener windowListener; private ChangeListener changeListener; /** @@ -44,20 +50,19 @@ public class Channels { private final HashMap channels = new HashMap<>(); /** - * Saves which channels are in a popout (and which dialog it is). + * Saves attributes of closed popout dialogs. */ - private final Map dialogs = new LinkedHashMap<>(); + private final List dialogsAttributes = new ArrayList<>(); + private final DockManager dock; /** - * Saves attributes of closed popout dialogs. + * The default channel does not represent an actual channel, but is just a + * placeholder for when the main window does not contain a channel. */ - private final List dialogsAttributes = new ArrayList<>(); - private final Tabs tabs; private Channel defaultChannel; private final StyleManager styleManager; private final ContextMenuListener contextMenuListener; private final MouseClickedListener mouseClickedListener = new MyMouseClickedListener(); - private Channel.OnceOffEditListener onceOffEditListener; /** * Default width of the userlist, given to Channel objects when created. @@ -66,59 +71,165 @@ public class Channels { private int minUserlistWidth = 0; private boolean defaultUserlistVisibleState = true; private boolean chatScrollbarAlaways; - private Channel lastActiveChannel = null; private boolean savePopoutAttributes; private boolean closeLastChannelPopout; /** - * Save channels whose state is new highlighted messages, so the color - * doesn't get overwritten by new messages. + * The DockManager tracks the last active content, however that might not be + * a Channel, so the last active Channel is tracked here. */ - private final Set highlighted = new HashSet<>(); + private Channel lastActiveChannel; + + /** + * Store which streams are currently live, since this is only updated when a + * stream changes status, which could be before the channel tab is actually + * added. This can be referred to when the tab is added to set the correct + * initial value. + */ + private final Set liveStreams = new HashSet<>(); public Channels(MainGui gui, StyleManager styleManager, ContextMenuListener contextMenuListener) { - windowListener = new MyWindowListener(); - tabs = new Tabs(); - tabs.setPopupMenu(new TabContextMenu(contextMenuListener)); + dock = new DockManager(new DockListener() { + @Override + public void activeContentChanged(DockPopout window, DockContent content, boolean focusChange) { + //System.out.println("changed: "+content+" "+Debugging.getStacktrace()); + channelChanged(); + if (content.getComponent() instanceof Channel) { + Channel c = (Channel) content.getComponent(); + lastActiveChannel = c; + if (!focusChange) { + // Changing focus due to a focus change is dodgy, so don't + // do that + setInitialFocus(c); + } + } + updateActiveContent(); + resetTab(content); + Debugging.println("dndp", "Path: %s", content.getPath()); + } + + @Override + public void popoutOpened(DockPopout popout, DockContent content) { + dockPopoutOpened(popout); + } + + @Override + public void popoutClosed(DockPopout popout, List contents) { + dockPopoutClosed(popout, contents); + } + + }); + // One-time settings + dock.setSetting(DockSetting.Type.POPOUT_ICONS, IconManager.getMainIcons()); + dock.setSetting(DockSetting.Type.POPOUT_PARENT, gui); this.styleManager = styleManager; this.contextMenuListener = contextMenuListener; this.gui = gui; - tabs.addChangeListener(new TabChangeListener()); - tabs.setMouseWheelScrollingEnabled(gui.getSettings().getBoolean("tabsMwheelScrolling")); - tabs.setMouseWheelScrollingAnywhereEnabled(gui.getSettings().getBoolean("tabsMwheelScrollingAnywhere")); - tabs.setTabPlacement(gui.getSettings().getString("tabsPlacement")); - tabs.setTabLayoutPolicy(gui.getSettings().getString("tabsLayout")); - gui.addWindowListener(windowListener); - //tabs.setOpaque(false); - //tabs.setBackground(new Color(0,0,0,0)); addDefaultChannel(); + updateSettings(); + gui.getSettings().addSettingChangeListener((setting, type, value) -> { + if (setting.startsWith("tab") || setting.equals("laf")) { + SwingUtilities.invokeLater(() -> { + updateSettings(); + }); + } + }); } - public void setOnceOffEditListener(Channel.OnceOffEditListener listener) { - this.onceOffEditListener = listener; - if (defaultChannel != null) { - defaultChannel.setOnceOffEditListener(listener); + public DockManager getDock() { + return dock; + } + + public Component getComponent() { + return dock.getBase(); + } + + private void channelTabClosed(Channel channel) { + gui.client.closeChannel(channel.getChannel()); + } + + //========================== + // Dock Settings + //========================== + private void updateSettings() { + for (Channel chan : channels.values()) { + updateSettings(chan); } + dock.setSetting(DockSetting.Type.TAB_LAYOUT, getTabLayoutPolicyValue(gui.getSettings().getString("tabsLayout"))); + dock.setSetting(DockSetting.Type.TAB_PLACEMENT, getTabPlacementValue(gui.getSettings().getString("tabsPlacement"))); + dock.setSetting(DockSetting.Type.TAB_SCROLL, gui.getSettings().getBoolean("tabsMwheelScrolling")); + dock.setSetting(DockSetting.Type.TAB_SCROLL_ANYWHERE, gui.getSettings().getBoolean("tabsMwheelScrollingAnywhere")); + dock.setSetting(DockSetting.Type.TAB_ORDER, getTabOrderValue(gui.getSettings().getString("tabOrder"))); + dock.setSetting(DockSetting.Type.FILL_COLOR, UIManager.getColor("TextField.selectionBackground")); + dock.setSetting(DockSetting.Type.LINE_COLOR, UIManager.getColor("TextField.selectionForeground")); + dock.setSetting(DockSetting.Type.POPOUT_TYPE_DRAG, getPopoutTypeValue((int)gui.getSettings().getLong("tabsPopoutDrag"))); + dock.setSetting(DockSetting.Type.DIVIDER_SIZE, 7); } - public void setChangeListener(ChangeListener listener) { - changeListener = listener; + private int getTabLayoutPolicyValue(String type) { + switch(type) { + case "wrap": return JTabbedPane.WRAP_TAB_LAYOUT; + case "scroll": return JTabbedPane.SCROLL_TAB_LAYOUT; + } + return JTabbedPane.WRAP_TAB_LAYOUT; } - private void channelChanged() { - if (changeListener != null) { - changeListener.stateChanged(new ChangeEvent(this)); + private int getTabPlacementValue(String location) { + switch(location) { + case "top": return JTabbedPane.TOP; + case "bottom": return JTabbedPane.BOTTOM; + case "left": return JTabbedPane.LEFT; + case "right": return JTabbedPane.RIGHT; } + return JTabbedPane.TOP; } + private DockSetting.TabOrder getTabOrderValue(String order) { + if (order.equals("alphabetical")) { + return DockSetting.TabOrder.ALPHABETIC; + } + return DockSetting.TabOrder.INSERTION; + } + + private DockSetting.PopoutType getPopoutTypeValue(int type) { + switch (type) { + case 1: return DockSetting.PopoutType.DIALOG; + case 2: return DockSetting.PopoutType.FRAME; + } + return DockSetting.PopoutType.NONE; + } + + private void updateSettings(Channel chan) { + chan.getDockContent().setSettings( + (int) gui.getSettings().getLong("tabsLive"), + (int) gui.getSettings().getLong("tabsMessage"), + (int) gui.getSettings().getLong("tabsHighlight"), + (int) gui.getSettings().getLong("tabsStatus"), + (int) gui.getSettings().getLong("tabsActive")); + } + + //========================== + // Change Channel state + //========================== + public void updateRoom(Room room) { Channel channel = channels.get(room.getChannel()); if (channel != null) { if (channel.setRoom(room)) { Debugging.printlnf("Update Room: %s", room); - updateChannelTabName(channel); + } + } + } + + private void updateActiveContent() { + for (Channel chan : channels.values()) { + if (getActiveChannel() == chan) { + chan.getDockContent().setActive(true); + } + else { + chan.getDockContent().setActive(false); } } } @@ -130,9 +241,9 @@ public void updateRoom(Room room) { * @param channel */ public void setChannelHighlighted(Channel channel) { - if (getActiveTab() != channel) { - tabs.setForegroundForComponent(channel, LaF.getTabForegroundHighlight()); - highlighted.add(channel); + if (!channel.getDockContent().hasNewHighlight() + && !dock.isContentVisible(channel.getDockContent())) { + channel.getDockContent().setNewHighlight(true); } } @@ -143,20 +254,38 @@ public void setChannelHighlighted(Channel channel) { * @param channel */ public void setChannelNewMessage(Channel channel) { - if (getActiveTab() != channel && !highlighted.contains(channel)) { - tabs.setForegroundForComponent(channel, LaF.getTabForegroundUnread()); + if (!channel.getDockContent().hasNewMessages() + && !channel.getDockContent().hasNewHighlight() + && !dock.isContentVisible(channel.getDockContent())) { + channel.getDockContent().setNewMessage(true); + } + } + + public void setStreamLive(String stream, boolean isLive) { + if (!Helper.isValidStream(stream)) { + return; + } + if (isLive) { + liveStreams.add(stream); + } + else { + liveStreams.remove(stream); + } + Channel chan = getExistingChannel(Helper.toChannel(stream)); + if (chan != null) { + chan.getDockContent().setLive(isLive); } } /** * Reset state (color, title suffixes) to default. * - * @param channel + * @param content */ - public void resetChannelTab(Channel channel) { - tabs.setForegroundForComponent(channel, null); - tabs.setTitleForComponent(channel, ForkUtil.removeSharpFromTitle(channel), channel.getToolTipText()); - highlighted.remove(channel); + public void resetTab(DockContent content) { + if (content instanceof DockStyledTabContainer) { + ((DockStyledTabContainer)content).resetNew(); + } } /** @@ -169,173 +298,69 @@ public void setChannelNewStatus(String ownerChannel) { Collection chans = getExistingChannelsByOwner(ownerChannel); // If any of the tabs for this channel is active, don't change for (Channel chan : chans) { - if (getActiveTab() == chan) { + if (dock.isContentVisible(chan.getDockContent())) { return; } } for (Channel chan : chans) { - tabs.setTitleForComponent(chan, ForkUtil.removeSharpFromTitle(chan) + "*", chan.getToolTipText()); + chan.getDockContent().setNewStatus(true); } } - // TODO: Retain new status and stuff, and maybe reset new status when clicked on one of the tabs belong to the stream - public void updateChannelTabName(Channel channel) { - tabs.setTitleForComponent(channel, ForkUtil.removeSharpFromTitle(channel), channel.getToolTipText()); - } + //========================== + // Add/remove + //========================== - /** - * This is the channel when no channel has been added yet. - */ - private void addDefaultChannel() { - defaultChannel = createChannel(Room.EMPTY, Channel.Type.NONE); - tabs.addTab(defaultChannel); - } - - /** - * - * @param room Must not be null - * @param type - * @return - */ - private Channel createChannel(Room room, Channel.Type type) { - Channel channel = new Channel(room,type,gui,styleManager, contextMenuListener); - channel.init(); - channel.setUserlistWidth(defaultUserlistWidth, minUserlistWidth); - channel.setMouseClickedListener(mouseClickedListener); - channel.setScrollbarAlways(chatScrollbarAlaways); - channel.setUserlistEnabled(defaultUserlistVisibleState); - channel.setOnceOffEditListener(onceOffEditListener); - if (type == Channel.Type.SPECIAL || type == Channel.Type.WHISPER) { - channel.setUserlistEnabled(false); - } - - if (!gui.getSettings().getBoolean("inputEnabled")) { - channel.toggleInput(); - } - - return channel; - } - - public Component getComponent() { - return tabs; - } - - public Collection channels() { - return channels.values(); - } - - /** - * Includes the defaultChannel that is there when no actual channel has been - * added yet. - * - * @return - */ - public Collection allChannels() { - if (channels.isEmpty() && defaultChannel != null) { - Collection result = new ArrayList<>(); - result.add(defaultChannel); - return result; - } - return channels.values(); - } - - public int getChannelCount() { - return channels.size(); - } - - /** - * Check if the given channel is added. - * - * @param channel - * @return - */ - public boolean isChannel(String channel) { - if (channel == null) { - return false; - } - return channels.get(channel) != null; + public Channel getChannel(Room room) { + String channelName = room.getChannel(); + return getChannel(room, getTypeFromChannelName(channelName)); } -// public Channel getChannel(String channel) { -// return getChannel(channel, getTypeFromChannelName(channel), null); -// } - public Channel.Type getTypeFromChannelName(String name) { if (name.startsWith("#")) { return Channel.Type.CHANNEL; - } else if (name.startsWith("$")) { + } + else if (name.startsWith("$")) { return Channel.Type.WHISPER; - } else if (name.startsWith("*")) { + } + else if (name.startsWith("*")) { return Channel.Type.SPECIAL; } return Channel.Type.NONE; } - public Channel getExistingChannel(String channel) { - return channels.get(channel); - } - - public Collection getExistingChannelsByOwner(String channel) { - List result = new ArrayList<>(); - for (Channel chan : channels.values()) { - if (Objects.equals(chan.getOwnerChannel(), channel)) { - result.add(chan); - } - } - return result; - } - - public Channel getChannel(Room room) { - String channel = room.getChannel(); - return getChannel(room, getTypeFromChannelName(channel)); - } - /** * Gets the Channel object for the given channel name. If none exists, the * channel is automatically added. - * + * * @param room Must not be null, but can be Room.EMPTY * @param type - * @return + * @return */ public Channel getChannel(Room room, Channel.Type type) { Channel panel = channels.get(room.getChannel()); if (panel == null) { panel = addChannel(room, type); - } else if (panel.setRoom(room)) { - Debugging.println("Updating Channel Name to "+panel.getName()); - updateChannelTabName(panel); } - return panel; - } - - public String getChannelNameFromPanel(Channel panel) { - for (String key : channels.keySet()) { - if (channels.get(key) == panel) { - return key; - } + else if (panel.setRoom(room)) { + Debugging.println("Updating Channel Name to " + panel.getName()); } - return null; + return panel; } - public Channel getChannelFromWindow(Object dialog) { - for (Channel channel : dialogs.keySet()) { - if (dialogs.get(channel) == dialog) { - return channel; - } - } - if (dialog == gui) { - return getActiveTab(); - } - return null; + /** + * This is the channel when no channel has been added yet. + */ + private void addDefaultChannel() { + defaultChannel = createChannel(Room.EMPTY, Channel.Type.NONE); + dock.addContent(defaultChannel.getDockContent()); } - /** * Adds a channel with the given name. If the default channel is still there * it is used for this channel and renamed. * - * @param channelName + * @param room * @param type * @return */ @@ -355,98 +380,120 @@ public Channel addChannel(Room room, Channel.Type type) { else { // No default channel, so create a new one panel = createChannel(room, type); - tabs.addTab(panel); + dock.addContent(panel.getDockContent()); if (type != Channel.Type.WHISPER) { - tabs.setSelectedComponent(panel); + dock.setActiveContent(panel.getDockContent()); } } channels.put(room.getChannel(), panel); + // Update after it has been added to "channels" + updateActiveContent(); return panel; } + /** + * Create and configure a Channel. + * + * @param room Must not be null + * @param type + * @return + */ + private Channel createChannel(Room room, Channel.Type type) { + Channel channel = new Channel(room,type,gui,styleManager, contextMenuListener); + channel.setDockContent(new DockChannelContainer(channel, dock, this, contextMenuListener)); + channel.init(); + channel.setUserlistWidth(defaultUserlistWidth, minUserlistWidth); + channel.setMouseClickedListener(mouseClickedListener); + channel.setScrollbarAlways(chatScrollbarAlaways); + channel.setUserlistEnabled(defaultUserlistVisibleState); + channel.getDockContent().setLive(liveStreams.contains(room.getStream())); + updateSettings(channel); + if (type == Channel.Type.SPECIAL || type == Channel.Type.WHISPER) { + channel.setUserlistEnabled(false); + } + if (!gui.getSettings().getBoolean("inputEnabled")) { + channel.toggleInput(); + } + return channel; + } + public void removeChannel(final String channelName) { Channel channel = channels.get(channelName); if (channel == null) { return; } channels.remove(channelName); - closePopout(channel); - tabs.removeTab(channel); + dock.removeContent(channel.getDockContent()); channel.cleanUp(); - if (tabs.getTabCount() == 0) { - if (dialogs.isEmpty() || !closeLastChannelPopout) { + if (dock.isMainEmpty() || channels.isEmpty()) { + if (!closeLastChannelPopout || !dock.closeWindow()) { addDefaultChannel(); - } else { - closePopout(dialogs.keySet().iterator().next()); } - lastActiveChannel = null; channelChanged(); gui.updateState(); } } + //========================== + // Popout + //========================== + /** - * Popout the given channel if it isn't already and if there is actually - * more than one tab. + * Popout the given content. * - * @param channel The {@code Channel} to popout + * @param content The {@code Channel} to popout + * @param window Open in window instead of dialog */ - public void popout(final Channel channel) { - if (channel == null) { - return; - } - if (dialogs.containsKey(channel)) { - return; - } - if (tabs.getTabCount() < 2) { - return; - } - tabs.removeTab(channel); - - // Create and configure new dialog for the popout - final JDialog newDialog = new ChannelDialog(gui, channel); - newDialog.setLocationRelativeTo(gui); - newDialog.addWindowListener(windowListener); - gui.popoutCreated(newDialog); - - // Restore attributes if available + public void popout(DockContent content, boolean window) { + /** + * Setting the location/size should only be done for popouts not created + * by drag, so not in dockPopoutOpened() + */ + Point location = null; + Dimension size = null; if (!dialogsAttributes.isEmpty()) { LocationAndSize attr = dialogsAttributes.remove(0); if (GuiUtil.isPointOnScreen(attr.location, 5, 5)) { - newDialog.setLocation(attr.location); + location = attr.location; } - newDialog.setSize(attr.size); + size = attr.size; } - dialogs.put(channel, newDialog); - - // Making it visible directly apparently makes it not properly detect - // it as active window - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - newDialog.setVisible(true); - } - }); - + dock.popout(content, window ? DockSetting.PopoutType.FRAME : DockSetting.PopoutType.DIALOG, location, size); gui.updateState(true); } - public void popoutActiveChannel() { - if (getActiveChannel() != null) { - popout(getActiveChannel()); - } + /** + * This is called by the DockManager when a popout is created. This also + * includes popouts created by the DockManager itself (e.g. by drag). + * + * @param popout + */ + private void dockPopoutOpened(DockPopout popout) { + // Register hotkeys if necessary + gui.popoutCreated(popout.getWindow()); } - public void setSavePopoutAttributes(boolean save) { - savePopoutAttributes = save; - if (!save) { - dialogsAttributes.clear(); + private void dockPopoutClosed(DockPopout popout, List contents) { + if (savePopoutAttributes) { + Window window = popout.getWindow(); + dialogsAttributes.add(0, new LocationAndSize( + window.getLocation(), window.getSize())); + } + if (!contents.isEmpty()) { + if (defaultChannel != null) { + dock.removeContent(defaultChannel.getDockContent()); + defaultChannel = null; + } + gui.updateState(true); } } + //-------------------------- + // Saving/loading attributes + //-------------------------- + /** * Returns a list of Strings that contain the location/size of open dialogs * and of closed dialogs that weren't reused. @@ -457,9 +504,10 @@ public void setSavePopoutAttributes(boolean save) { */ public List getPopoutAttributes() { List attributes = new ArrayList<>(); - for (JDialog dialog : dialogs.values()) { - attributes.add(dialog.getX()+","+dialog.getY() - +";"+dialog.getWidth()+","+dialog.getHeight()); + for (DockPopout popout : dock.getPopouts()) { + Window w = popout.getWindow(); + attributes.add(w.getX()+","+w.getY() + +";"+w.getWidth()+","+w.getHeight()); } for (LocationAndSize attr : dialogsAttributes) { attributes.add(attr.location.x + "," + attr.location.y @@ -495,46 +543,33 @@ public void setPopoutAttributes(List attributes) { } } + //-------------------------- + // Popout settings + //-------------------------- + public void setCloseLastChannelPopout(boolean close) { closeLastChannelPopout = close; } - /** - * Once the popout dialog was closed (either by the user or by the program) - * add the channel to the tabs again and update the GUI. - * - * @param channel - */ - private void popoutDisposed(Channel channel) { - if (channel == null) { - return; - } - dialogs.remove(channel); - if (defaultChannel != null) { - tabs.removeTab(defaultChannel); - defaultChannel = null; + public void setSavePopoutAttributes(boolean save) { + savePopoutAttributes = save; + if (!save) { + dialogsAttributes.clear(); } - tabs.addTab(channel); - tabs.setSelectedComponent(channel); - gui.updateState(true); } - /** - * Close the popout for the given channel (if it exists) and move the - * channel back to the main window. - * - * @param channel - */ - public void closePopout(Channel channel) { - if (channel == null) { - return; - } - if (!dialogs.containsKey(channel)) { - return; + //========================== + // Active content + //========================== + + public void setChangeListener(ChangeListener listener) { + changeListener = listener; + } + + private void channelChanged() { + if (changeListener != null) { + changeListener.stateChanged(new ChangeEvent(this)); } - JDialog dialog = dialogs.remove(channel); - dialog.dispose(); - popoutDisposed(channel); } /** @@ -547,62 +582,91 @@ public void closePopout(Channel channel) { * tab of the main window is returned. *

* - * @return The Channel object which is currently selected. + * @return The Channel object which is currently selected, could be null, + * but that would likely be a bug, since there should always be at least one + * channel */ public Channel getActiveChannel() { - for (Channel channel : dialogs.keySet()) { - if (dialogs.get(channel).isActive()) { - return channel; + // Prefer actual active + DockContent activeContent = dock.getActiveContent(); + if (activeContent instanceof DockChannelContainer) { + return ((DockChannelContainer)activeContent).getContent(); + } + if (channels.containsValue(lastActiveChannel) || lastActiveChannel == defaultChannel) { + return lastActiveChannel; + } + // If a Channel isn't active, try others + for (DockContent otherActive : dock.getAllActive().values()) { + if (otherActive instanceof DockChannelContainer) { + return ((DockChannelContainer)otherActive).getContent(); } } - return getActiveTab(); + return null; } /** - * Returns the Channel that was last active. If the focus is on a Window - * that contains a Channel, it should be the same as - * {@link getActiveChannel()}, otherwise it is the Channel that was active - * before a Window without a Channel was focused (e.g. an info dialog). - * - * @return + * This used to be slightly different, but is now just returning + * {@link getActiveChannel()}. + * + * @return */ public Channel getLastActiveChannel() { - if (lastActiveChannel == null) { - return getActiveTab(); - } - return lastActiveChannel; + return getActiveChannel(); } /** - * Returns channel of the active tab in the main window (as opposed to the - * active channel, which might also be in a popout). * - * @return + * + * @return The active content, may be null, although that could probably + * be considered a bug since there should always be an active content + */ + public DockContent getActiveContent() { + return dock.getActiveContent(); + } + + /** + * Returns channel of the active tab in the main window. Falls back to the + * overall active channel, but that normally shouldn't happen. + * + * @return Can return null */ - public Channel getActiveTab() { - Component c = tabs.getSelectedComponent(); - if (c instanceof Channel) { - return (Channel) c; + public Channel getMainActiveChannel() { + DockContent activeContent = getMainActiveContent(); + if (activeContent instanceof DockChannelContainer) { + return ((DockChannelContainer)activeContent).getContent(); } return null; } + public DockContent getMainActiveContent() { + return dock.getAllActive().get(null); + } + /** - * Returns a map of all channels and their respective dialog. + * Returns all popouts with their currently active content. * - * @return The {@literal Map} with {@literal Channel} objects as keys and - * {@literal JDialog} objects as values + * @return The popouts, without the main base (which would be null) */ - public Map getPopoutChannels() { - return new HashMap<>(dialogs); + public Map getActivePopoutContent() { + Map result = new HashMap<>(); + for (Map.Entry entry : dock.getAllActive().entrySet()) { + if (entry.getKey() != null) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; } + //========================== + // Get channels (other) + //========================== + /** * Return the channel from the given input box. - * + * * @param input The reference to the input box. - * @return The Channel object, or null if the given reference isn't an - * input box + * @return The Channel object, or null if the given reference isn't an input + * box */ public Channel getChannelFromInput(Object input) { if (defaultChannel != null && input == defaultChannel.getInput()) { @@ -616,96 +680,150 @@ public Channel getChannelFromInput(Object input) { } return null; } - - public List getChannels() { - return getChannelsOfType(null); - } /** - * A list of all channels, be it in the main window or in popouts. - * - * @param type - * @return The {@code List} of {@code Channel} objects + * Check if the given channel is added. + * + * @param channel + * @return */ - public List getChannelsOfType(Channel.Type type) { - List result = new ArrayList<>(getTabs(type)); - for (Channel c : channels.values()) { - // Add channels that aren't on tabs (popouts) - if ((type == null || c.getType() == type) && !result.contains(c)) { - result.add(c); - } + public boolean isChannel(String channel) { + if (channel == null) { + return false; } - return result; + return channels.get(channel) != null; } - public Collection getTabs() { - return getTabs(null); + public Channel getExistingChannel(String channel) { + return channels.get(channel); } - public Collection getTabs(Channel.Type type) { + public Collection getExistingChannelsByOwner(String channel) { List result = new ArrayList<>(); - for (Component comp : tabs.getAllComponents()) { - Channel chan = (Channel)comp; - if ((type == null || chan.getType() == type) - && channels.containsValue(chan) || chan == defaultChannel) { - result.add((Channel)comp); + for (Channel chan : channels.values()) { + if (Objects.equals(chan.getOwnerChannel(), channel)) { + result.add(chan); } } return result; } - public Collection getTabsRelativeToCurrent(int direction) { - return getTabsRelativeTo(getActiveTab(), direction); + public Collection channels() { + return channels.values(); + } + + /** + * Includes the defaultChannel that is there when no actual channel has been + * added yet. + * + * @return + */ + public Collection allChannels() { + if (channels.isEmpty() && defaultChannel != null) { + Collection result = new ArrayList<>(); + result.add(defaultChannel); + return result; + } + return channels.values(); + } + + public int getChannelCount() { + return channels.size(); + } + + public List getChannels() { + return getChannelsOfType(null); + } + + public List getChannelsOfType(Channel.Type type) { + List result = new ArrayList<>(); + for (DockContent content : dock.getContents()) { + if (content.getComponent() instanceof Channel) { + Channel chan = (Channel)content.getComponent(); + if ((type == null || chan.getType() == type) + && channels.containsValue(chan) || chan == defaultChannel) { + result.add(chan); + } + } + } + return result; } public Collection getTabsRelativeTo(Channel chan, int direction) { List result = new ArrayList<>(); - for (Component comp : tabs.getComponents(chan, direction)) { - if (channels.containsValue(comp)) { - result.add((Channel)comp); + for (DockContent c : dock.getContentsRelativeTo(chan.getDockContent(), direction)) { + if (c.getComponent() instanceof Channel) { + result.add((Channel)c.getComponent()); } } return result; } + //========================== + // Change active content + //========================== + + public void switchToChannel(String channel) { + Channel c = getExistingChannel(channel); + if (c != null) { + dock.setActiveContent(c.getDockContent()); + } + } + + public void switchToNextTab() { + DockContent c = dock.getContentTab(getActiveContent(), 1); + if (c != null) { + dock.setActiveContent(c); + } + } + + public void switchToPreviousTab() { + DockContent c = dock.getContentTab(getActiveContent(), -1); + if (c != null) { + dock.setActiveContent(c); + } + } + + + //========================== + // Focus + //========================== + public void setInitialFocus() { + setInitialFocus(getActiveChannel()); + } + + public void setInitialFocus(Channel channel) { if (gui.getSettings().getLong("inputFocus") != 2) { - getActiveChannel().requestFocusInWindow(); + if (channel == null) { + channel = getActiveChannel(); + } + channel.requestFocusInWindow(); } } + //========================== + // Settings + //========================== + public void refreshStyles() { for (Channel channel : getChannels()) { channel.refreshStyles(); } } - + public void updateUserlistSettings() { for (Channel channel : getChannels()) { channel.updateUserlistSettings(); } } - + public void setCompletionEnabled(boolean enabled) { for (Channel channel : getChannels()) { channel.setCompletionEnabled(enabled); } } - public void switchToChannel(String channel) { - if (isChannel(channel)) { - tabs.setSelectedComponent(getExistingChannel(channel)); - } - } - - public void switchToNextChannel() { - tabs.setSelectedNext(); - } - - public void switchToPreviousChannel() { - tabs.setSelectedPrevious(); - } - public void setDefaultUserlistWidth(int width, int minWidth) { defaultUserlistWidth = width; minUserlistWidth = minWidth; @@ -733,28 +851,43 @@ public void setChatScrollbarAlways(boolean always) { } } - public void setTabOrder(String order) { - Tabs.TabOrder setting = Tabs.TabOrder.INSERTION; - switch (order) { - case "alphabetical": setting = Tabs.TabOrder.ALPHABETIC; break; - } - tabs.setOrder(setting); - } - /** - * When the active tab is changed, keeps track of the lastActiveChannel and - * does some work necessary when tab is changed. + * Creates a map of channels for different closing options. + * + * @param channels + * @param channel + * @return */ - private class TabChangeListener implements ChangeListener { - - @Override - public void stateChanged(ChangeEvent e) { - lastActiveChannel = getActiveTab(); - - setInitialFocus(); - resetChannelTab(getActiveChannel()); - channelChanged(); + public static Map> getCloseTabsChans(Channels channels, Channel channel) { + Map> result = new HashMap<>(); + result.put("closeAllTabsButCurrent", channels.getTabsRelativeTo(channel, 0)); + result.put("closeAllTabsToLeft", channels.getTabsRelativeTo(channel, -1)); + result.put("closeAllTabsToRight", channels.getTabsRelativeTo(channel, 1)); + + Collection all = channels.getTabsRelativeTo(channel, 0); + all.add(channel); + result.put("closeAllTabs", all); + Collection allOffline = new ArrayList<>(); + for (Channel c : all) { + if (!c.getDockContent().isLive()) { + allOffline.add(c); + } + } + result.put("closeAllTabsOffline", allOffline); + + Collection all2 = channels.getChannels(); + all2.remove(channel); + result.put("closeAllTabs2ButCurrent", all2); + + Collection all2Offline = new ArrayList<>(); + result.put("closeAllTabs2", channels.getChannels()); + for (Channel c : channels.getChannels()) { + if (!c.getDockContent().isLive()) { + all2Offline.add(c); + } } + result.put("closeAllTabs2Offline", all2Offline); + return result; } /** @@ -763,49 +896,48 @@ public void stateChanged(ChangeEvent e) { private class MyMouseClickedListener implements MouseClickedListener { @Override - public void mouseClicked() { - setInitialFocus(); + public void mouseClicked(Channel chan) { + setInitialFocus(chan); + } + } + + private static class LocationAndSize { + public final Point location; + public final Dimension size; + + LocationAndSize(Point location, Dimension size) { + this.location = location; + this.size = size; } } /** - * Registered to popout dialogs and the main window, cleans up closed popout - * dialogs and keeps the lastActiveChannel up-to-date. + * The container used to add a Channel to the DockManager. */ - private class MyWindowListener extends WindowAdapter { + public static class DockChannelContainer extends DockStyledTabContainer { - @Override - public void windowClosed(WindowEvent e) { - if (e.getSource() == gui) { - return; - } - popoutDisposed(getChannelFromWindow(e.getSource())); - if (savePopoutAttributes) { - Window window = e.getWindow(); - dialogsAttributes.add(0, new LocationAndSize( - window.getLocation(), window.getSize())); - } + //-------------------------- + // References + //-------------------------- + private final ContextMenuListener listener; + private final Channels channels; + + public DockChannelContainer(Channel channel, DockManager m, Channels channels, ContextMenuListener listener) { + super(channel, channel.getName(), m); + this.listener = listener; + this.channels = channels; } @Override - public void windowActivated(WindowEvent e) { - Channel channel = getChannelFromWindow(e.getSource()); - if (channel != lastActiveChannel) { - lastActiveChannel = channel; - channelChanged(); - } + public JPopupMenu getContextMenu() { + return new TabContextMenu(listener, (Channel) getComponent(), Channels.getCloseTabsChans(channels, (Channel) getComponent())); } - } - - private static class LocationAndSize { - public final Point location; - public final Dimension size; - - LocationAndSize(Point location, Dimension size) { - this.location = location; - this.size = size; + @Override + public void remove() { + channels.channelTabClosed(getContent()); } + } } diff --git a/src/chatty/gui/DockStyledTabContainer.java b/src/chatty/gui/DockStyledTabContainer.java new file mode 100644 index 000000000..1e861b015 --- /dev/null +++ b/src/chatty/gui/DockStyledTabContainer.java @@ -0,0 +1,391 @@ + +package chatty.gui; + +import chatty.util.dnd.DockContentContainer; +import chatty.util.dnd.DockManager; +import chatty.util.dnd.DockTabComponent; +import chatty.util.ForkUtil; +import com.jtattoo.plaf.AbstractLookAndFeel; +import com.jtattoo.plaf.BaseTabbedPaneUI; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.util.Objects; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTabbedPane; +import javax.swing.border.Border; + +/** + * Dock content container that provides a styled tab component with various + * features. For examples used to indicate new messages or live stream status. + * + * @author tduva + * @param + */ +public class DockStyledTabContainer extends DockContentContainer { + + //-------------------------- + // Setting Constants + //-------------------------- + // Used to encode several booleans in one integer + public final static int BOLD = 1 << 0; + public final static int ITALIC = 1 << 1; + public final static int COLOR1 = 1 << 2; + public final static int COLOR2 = 1 << 3; + public final static int DOT1 = 1 << 4; + public final static int DOT2 = 1 << 5; + public final static int ASTERISK = 1 << 6; + public final static int LINE = 1 << 7; + + //-------------------------- + // Borders + //-------------------------- + private static final Border BORDER_INACTIVE = BorderFactory.createEmptyBorder(0, 3, 0, 3); + private static final Border BORDER_ACTIVE = BorderFactory.createEmptyBorder(0, 0, 0, 6); + + //-------------------------- + // References + //-------------------------- + private final TabComponent tab; + + //-------------------------- + // Status variables + //-------------------------- + private boolean isLive; + private int liveSetting; + private boolean hasMessages; + private int messageSetting; + private boolean hasHighlight; + private int highlightSetting; + private boolean hasStatus; + private int statusSetting; + private boolean isActive; + private int activeSetting; + + //-------------------------- + // Properties + //-------------------------- + private Color foreground; + private Color defaultForeground; + private Font defaultFont; + private boolean fontNoBold; + private boolean fontNoItalic; + private boolean fontBold; + private boolean fontItalic; + private Border border; + private String suffix; + + //-------------------------- + // Drawing variables + //-------------------------- + private boolean dot1; + private boolean dot2; + private boolean line; + + public DockStyledTabContainer(T content, String title, DockManager m) { + super(title, content, m); + tab = new TabComponent(); + defaultForeground = getForegroundColor(); + defaultFont = tab.getFont(); + } + + public void setActive(boolean isActive) { + if (isActive != this.isActive) { + this.isActive = isActive; + update(); + } + } + + public void setLive(boolean isLive) { + if (isLive != this.isLive) { + this.isLive = isLive; + update(); + } + } + + public boolean isLive() { + return isLive; + } + + public void setNewMessage(boolean hasMessage) { + if (hasMessage != this.hasMessages) { + this.hasMessages = hasMessage; + update(); + } + } + + public boolean hasNewMessages() { + return hasMessages; + } + + public void setNewHighlight(boolean hasHighlight) { + if (hasHighlight != this.hasHighlight) { + this.hasHighlight = hasHighlight; + update(); + } + } + + public boolean hasNewHighlight() { + return hasHighlight; + } + + public void setNewStatus(boolean newStatus) { + if (newStatus != this.hasStatus) { + this.hasStatus = newStatus; + update(); + } + } + + public void resetNew() { + if (hasMessages || hasHighlight || hasStatus) { + hasMessages = false; + hasHighlight = false; + hasStatus = false; + update(); + } + } + + public void setSettings(int liveSetting, int messageSetting, int highlightSetting, int statusSetting, int activeSetting) { + this.liveSetting = liveSetting; + this.messageSetting = messageSetting; + this.highlightSetting = highlightSetting; + this.statusSetting = statusSetting; + this.activeSetting = activeSetting; + update(); + } + + private void update() { + border = null; + dot1 = false; + dot2 = false; + line = false; + fontNoItalic = false; + fontNoBold = false; + fontItalic = false; + fontBold = false; + foreground = defaultForeground; + suffix = ""; + update(isLive, liveSetting); + update(hasMessages, messageSetting); + update(hasStatus, statusSetting); + update(hasHighlight, highlightSetting); + update(isActive, activeSetting); + int fontStyle = defaultFont.getStyle(); + if (fontNoItalic) { + fontStyle = fontStyle & ~Font.ITALIC; + } + if (fontNoBold) { + fontStyle = fontStyle & ~Font.BOLD; + } + if (fontItalic) { + fontStyle = fontStyle | Font.ITALIC; + } + if (fontBold) { + fontStyle = fontStyle | Font.BOLD; + } + tab.setBorder(border); + tab.setFont(defaultFont.deriveFont(fontStyle)); + // Color + if (!Objects.equals(foreground, tab.getForeground())) { + tab.setForeground(foreground); + } + // Title + String newText = getTitle()+suffix; + if (!newText.equals(tab.getText())) { + tab.setText(ForkUtil.removeSharpFromTitle(newText)); + } + } + + private void update(boolean enabled, int setting) { + if (enabled) { + if (isEnabled(setting, DOT1)) { + border = BORDER_ACTIVE; + dot1 = true; + } + if (isEnabled(setting, DOT2)) { + border = BORDER_ACTIVE; + dot2 = true; + } + if (isEnabled(setting, LINE)) { + line = true; + } + if (isEnabled(setting, ITALIC)) { + fontItalic = true; + } + if (isEnabled(setting, BOLD)) { + fontBold = true; + } + if (isEnabled(setting, COLOR1)) { + foreground = LaF.getTabForegroundUnread(); + } + if (isEnabled(setting, COLOR2)) { + foreground = LaF.getTabForegroundHighlight(); + } + if (isEnabled(setting, ASTERISK)) { + suffix = "*"; + } + } + else { + if (isEnabled(setting, DOT1) && border == null) { + border = BORDER_INACTIVE; + } + if (isEnabled(setting, DOT2) && border == null) { + border = BORDER_INACTIVE; + } + if (isEnabled(setting, ITALIC)) { + fontNoItalic = true; + } + if (isEnabled(setting, BOLD)) { + fontNoBold = true; + } + } + } + + /** + * Check if the given option is enabled in the setting value. + * + * @param settingValue + * @param optionConstant + * @return + */ + private boolean isEnabled(int settingValue, int optionConstant) { + return (settingValue & optionConstant) != 0; + } + + @Override + public void setTitle(String title) { + if (title.isEmpty()) { + title = "-"; + } + title = ForkUtil.removeSharpFromTitle(title); + super.setTitle(title); + tab.setText(title); + } + + @Override + public void setForegroundColor(Color color) { + tab.setForeground(color); + } + + /** + * Provides a customized component that draws the tab. + * + * @return + */ + @Override + public DockTabComponent getTabComponent() { + return tab; + } + + private class TabComponent extends JLabel implements DockTabComponent { + + @Override + public void paintComponent(Graphics g) { + int b = (int) (getHeight() * 0.2); + if (dot1) { + g.setColor(new Color(255, 100, 100)); + g.fillRect(getWidth() - 4, b, 4, 4); + } + if (dot2) { + g.setColor(new Color(100, 100, 255)); + g.fillRect(getWidth() - 4, b+6, 4, 4); + } +// if (line && getTitle().equals("#tduvatest")) { +//// if (getTitle().equals("#tduvatest")) { +// g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), 180)); +// int a = 10; +// g.drawLine(10, 0, getWidth() - a - 1, 0); +// for (int x = 0; x < a; x++) { +// int alpha1 = 40 + x * 14; +// int alpha2 = 180 - x * 14; +//// System.out.println(x+" "+alpha1+" "+alpha2); +// g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), alpha1)); +// g.drawLine(x, 0, x, 0); +// g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), alpha2)); +// g.drawLine(x + getWidth() - a, 0, x + getWidth() - a, 0); +// } +// } +// else { +// g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), 180)); +// g.drawLine(0, 0, getWidth(), 0); +// } +// } + if (line) { + paintLine(g, 0, 1, 180, 20); + paintLine(g, 1, 4, 100, 0); + } + super.paintComponent(g); + } + + private void paintLine(Graphics g, int y, int fadeLength, int baseAlpha, int lowestAlpha) { + fadeLength = Math.min(fadeLength, getWidth() / 3); + int alphaStep = (baseAlpha - lowestAlpha) / fadeLength; + g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), baseAlpha)); + // Draw most of the line + g.drawLine(fadeLength, y, getWidth() - fadeLength - 1, y); + for (int i = 0; i < fadeLength; i++) { + // Draw one pixel each left and right + int xLeft = i; + int xRight = getWidth() - 1 - i; + int pixelAlpha = lowestAlpha + i * alphaStep; + g.setColor(new Color(defaultForeground.getRed(), defaultForeground.getGreen(), defaultForeground.getBlue(), pixelAlpha)); + g.drawLine(xLeft, y, xLeft, y); + g.drawLine(xRight, y, xRight, y); +// System.out.println(xLeft + " " + xRight + " " + pixelAlpha); + } + } + + /** + * Called by the tab pane when a relevant state might have changed + * (e.g. tab changed). + * + * @param pane + * @param index + */ + @Override + public void update(JTabbedPane pane, int index) { + boolean changed = false; + + //-------------------------- + // Color + //-------------------------- + Color foreground; + if (pane.getUI() instanceof BaseTabbedPaneUI && pane.getSelectedIndex() == index) { + // Special for JTattoo LaF + foreground = AbstractLookAndFeel.getTheme().getTabSelectionForegroundColor(); + } + else { + foreground = pane.getForegroundAt(index); + } + if (!Objects.equals(foreground, defaultForeground)) { + defaultForeground = foreground; + changed = true; + } + + //-------------------------- + // Font + //-------------------------- + Font font = pane.getFont(); + if (!Objects.equals(font, defaultFont)) { + defaultFont = font; + changed = true; + } + + //-------------------------- + // Update + //-------------------------- + if (changed) { + DockStyledTabContainer.this.update(); + } + } + + @Override + public JComponent getComponent() { + return this; + } + + } + + } diff --git a/src/chatty/gui/GuiUtil.java b/src/chatty/gui/GuiUtil.java index de8ba9f38..ba69f7193 100644 --- a/src/chatty/gui/GuiUtil.java +++ b/src/chatty/gui/GuiUtil.java @@ -126,17 +126,7 @@ public static int showNonAutoFocusOptionPane(Component parent, String title, Str JOptionPane p = new JOptionPane(message, messageType, optionType); p.setOptions(options); final JDialog d = p.createDialog(parent, title); - d.setAutoRequestFocus(false); - d.setFocusableWindowState(false); - // Make focusable after showing the dialog, so that it can be focused - // by the user, but doesn't steal focus from the user when it opens. - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - d.setFocusableWindowState(true); - } - }); + setNonAutoFocus(d); d.setVisible(true); // Find index of result Object value = p.getValue(); @@ -148,6 +138,22 @@ public void run() { return -1; } + /** + * Configure the given window to not take focus immediately. It will be + * able to take focus afterwards. + * + * @param w + */ + public static void setNonAutoFocus(Window w) { + w.setAutoRequestFocus(false); + w.setFocusableWindowState(false); + // Make focusable after showing the dialog, so that it can be focused + // by the user, but doesn't steal focus from the user when it opens. + SwingUtilities.invokeLater(() -> { + w.setFocusableWindowState(true); + }); + } + public static void showNonModalMessage(Component parent, String title, String message, int type) { showNonModalMessage(parent, title, message, type, false); } diff --git a/src/chatty/gui/MainGui.java b/src/chatty/gui/MainGui.java index 460eaf940..023baf154 100644 --- a/src/chatty/gui/MainGui.java +++ b/src/chatty/gui/MainGui.java @@ -66,6 +66,7 @@ import chatty.gui.components.textpane.SubscriberMessage; import chatty.gui.components.textpane.UserNotice; import chatty.gui.components.userinfo.UserInfoManager; +import chatty.gui.components.userinfo.UserNotes; import chatty.gui.notifications.Notification; import chatty.gui.notifications.NotificationActionListener; import chatty.gui.notifications.NotificationManager; @@ -77,6 +78,7 @@ import chatty.util.api.pubsub.ModeratorActionData; import chatty.util.commands.CustomCommand; import chatty.util.commands.Parameters; +import chatty.util.dnd.DockContent; import chatty.util.hotkeys.HotkeyManager; import chatty.util.irc.MsgTags; import chatty.util.settings.FileManager; @@ -103,6 +105,7 @@ import javax.swing.event.ChangeListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; +import chatty.util.dnd.DockPopout; /** * The Main Hub for all GUI activity. @@ -254,18 +257,21 @@ private void createGui() { // Channels/Chat output styleManager = new StyleManager(client.settings); + channels = new Channels(this,styleManager, contextMenuListener); + channels.getComponent().setPreferredSize(new Dimension(600,300)); + add(channels.getComponent(), BorderLayout.CENTER); + channels.setChangeListener(new ChannelChangeListener()); + highlightedMessages = new HighlightedMessages(this, styleManager, Language.getString("highlightedDialog.title"), + Language.getString("menubar.dialog.highlightedMessages"), Language.getString("highlightedDialog.info"), - contextMenuListener); + contextMenuListener, channels, "highlightDock"); ignoredMessages = new HighlightedMessages(this, styleManager, Language.getString("ignoredDialog.title"), + Language.getString("menubar.dialog.ignoredMessages"), Language.getString("ignoredDialog.info"), - contextMenuListener); - channels = new Channels(this,styleManager, contextMenuListener); - channels.getComponent().setPreferredSize(new Dimension(600,300)); - add(channels.getComponent(), BorderLayout.CENTER); - channels.setChangeListener(new ChannelChangeListener()); + contextMenuListener, channels, "ignoreDock"); // Some newer stuff addressbookDialog = new AddressbookDialog(this, client.addressbook); @@ -343,7 +349,7 @@ public void setWindowAttached(Window window, boolean attached) { windowStateManager.setWindowAttached(window, attached); } - protected void popoutCreated(JDialog popout) { + protected void popoutCreated(Window popout) { hotkeyManager.registerPopout(popout); } @@ -431,7 +437,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { - channels.switchToNextChannel(); + channels.switchToNextTab(); } }); @@ -439,7 +445,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { - channels.switchToPreviousChannel(); + channels.switchToPreviousTab(); } }); @@ -447,7 +453,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { - client.closeChannel(channels.getActiveChannel().getChannel()); + channels.getActiveContent().remove(); } }); @@ -951,7 +957,6 @@ private void loadSettingsInternal() { 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")); @@ -972,6 +977,7 @@ private void loadSettingsInternal() { userInfoDialog.setTimestampFormat(styleManager.makeTimestampFormat("userDialogTimestamp", null)); userInfoDialog.setFontSize(client.settings.getLong("dialogFontSize")); + UserNotes.init(client.api, client.settings); hotkeyManager.setGlobalHotkeysEnabled(client.settings.getBoolean("globalHotkeysEnabled")); hotkeyManager.loadFromSettings(client.settings); @@ -986,6 +992,9 @@ private void loadSettingsInternal() { Sound.setDeviceName(client.settings.getString("soundDevice")); + highlightedMessages.loadSettings(); + ignoredMessages.loadSettings(); + updateTokenScopes(); //FORK SETTINGS @@ -1536,7 +1545,7 @@ else if (result == 1) { } else if (cmd.equals("srlRaces")) { openSrlRaces(); } else if (cmd.equals("srlRaceActive")) { - srl.searchRaceWithEntrant(channels.getActiveTab().getStreamName()); + srl.searchRaceWithEntrant(channels.getActiveChannel().getStreamName()); } else if (cmd.startsWith("srlRace4")) { String stream = cmd.substring(8); if (!stream.isEmpty()) { @@ -1564,12 +1573,17 @@ else if (result == 1) { public void menuSelected(MenuEvent e) { if (e.getSource() == menu.srlStreams) { ArrayList popoutStreams = new ArrayList<>(); - for (Channel channel : channels.getPopoutChannels().keySet()) { - if (channel.getStreamName() != null) { - popoutStreams.add(channel.getStreamName()); + String activeStream = channels.getActiveChannel().getStreamName(); + for (DockContent c : channels.getDock().getPopoutContents()) { + if (c instanceof Channels.DockChannelContainer) { + Channel channel = ((Channels.DockChannelContainer) c).getContent(); + if (channel.getStreamName() != null + && !channel.getStreamName().equals(activeStream)) { + popoutStreams.add(channel.getStreamName()); + } } } - menu.updateSrlStreams(channels.getActiveTab().getStreamName(), popoutStreams); + menu.updateSrlStreams(activeStream, popoutStreams); } else if (e.getSource() == menu.view) { menu.updateCount(highlightedMessages.getNewCount(), highlightedMessages.getDisplayedCount(), @@ -1640,6 +1654,9 @@ else if (cmd.equals("setcolor")) { else if (cmd.equals("setname")) { setCustomName(user.getName()); } + else if (cmd.equals("notes")) { + UserNotes.instance().showDialog(user, MainGui.this); + } else if (cmd.startsWith("command")) { Parameters parameters = Parameters.create(user.getRegularDisplayNick()); Helper.addUserParameters(user, msgId, autoModMsgId, parameters); @@ -1699,36 +1716,10 @@ else if (cmd.equals("join")) { @Override public void menuItemClicked(ActionEvent e) { Debugging.println("cmchan", "[cm] tab: %s chan: %s lastchan: %s", - channels.getActiveTab(), channels.getActiveChannel(), channels.getLastActiveChannel()); + channels.getMainActiveChannel(), channels.getActiveChannel(), channels.getLastActiveChannel()); String cmd = e.getActionCommand(); - if (cmd.equals("closeChannel")) { - // TabContextMenu - client.closeChannel(channels.getActiveChannel().getChannel()); - } - else if (cmd.startsWith("closeAllTabs")) { - // TabContextMenu - Collection 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.getChannel()); - } - } - } - else if (cmd.equals("popoutChannel")) { - // TabContextMenu - channels.popoutActiveChannel(); - } - else if (cmd.startsWith("historyRange")) { + if (cmd.startsWith("historyRange")) { int range = Integer.parseInt(cmd.substring("historyRange".length())); // Change here as well, because even if it's the same value, // update may be needed. This will make it update twice often. @@ -1750,7 +1741,7 @@ else if (cmd.startsWith("toggleVerticalZoom")) { @Override public void channelMenuItemClicked(ActionEvent e, Channel channel) { Debugging.println("cmchan", "[channelcm] tab: %s chan: %s lastchan: %s", - channels.getActiveTab(), channels.getActiveChannel(), channels.getLastActiveChannel()); + channels.getMainActiveChannel(), channels.getActiveChannel(), channels.getLastActiveChannel()); String cmd = e.getActionCommand(); if (cmd.equals("channelInfo")) { @@ -1763,6 +1754,23 @@ else if (cmd.equals("channelAdmin")) { else if (cmd.equals("closeChannel")) { client.closeChannel(channel.getChannel()); } + else if (cmd.equals("popoutChannel")) { + // TabContextMenu + channels.popout(channel.getDockContent(), false); + } + else if (cmd.equals("popoutChannelWindow")) { + // TabContextMenu + channels.popout(channel.getDockContent(), true); + } + else if (cmd.startsWith("closeAllTabs")) { + // TabContextMenu + Collection chans = Channels.getCloseTabsChans(channels, channel).get(cmd); + if (chans != null) { + for (Channel c : chans) { + client.closeChannel(c.getChannel()); + } + } + } else if (cmd.equals("joinHostedChannel")) { client.command(channel.getRoom(), "joinhosted"); } @@ -2473,7 +2481,10 @@ else if (!Helper.isValidStream(username)) { } }); client.commands.addEdt("popoutChannel", p -> { - channels.popoutActiveChannel(); + channels.popout(channels.getActiveContent(), false); + }); + client.commands.addEdt("popoutChannelWindow", p -> { + channels.popout(channels.getActiveContent(), true); }); } @@ -2871,7 +2882,7 @@ public void newFollowers(final FollowerInfo info) { public void setChannelNewStatus(final String ownerChannel, final String newStatus) { SwingUtilities.invokeLater(() -> { - channels.setChannelNewStatus(ownerChannel); + channels.setChannelNewStatus(ownerChannel); }); } @@ -3453,27 +3464,20 @@ public void run() { * Perform search in the currently selected channel. Should only be called * from the EDT. * - * @param window + * @param chan * @param searchText * @return */ - public boolean search(final Window window, final String searchText) { - Channel chan = channels.getChannelFromWindow(window); + public boolean search(Channel chan, final String searchText) { 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 resetSearch(Channel chan) { + SwingUtilities.invokeLater(() -> { + chan.resetSearch(); }); } @@ -3768,6 +3772,12 @@ public void run() { }); } + public void updateStreamLive(StreamInfo info) { + SwingUtilities.invokeLater(() -> { + channels.setStreamLive(info.stream, info.isValidEnough() && info.getOnline()); + }); + } + public void updateState() { updateState(false); } @@ -3879,18 +3889,17 @@ private void updateMenuState(int 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); + String mainTitle = makeTitle(channels.getMainActiveContent(), state); + String trayTooltip = makeTitle(channels.getLastActiveChannel().getDockContent(), state); trayIcon.setTooltipText(trayTooltip); if (client.settings.getBoolean("simpleTitle")) { setTitle("Chatty"); } else { setTitle(mainTitle); } - Map popoutChannels = channels.getPopoutChannels(); - for (Channel channel : popoutChannels.keySet()) { - String title = makeTitle(channel, state); - popoutChannels.get(channel).setTitle(title); + for (Map.Entry entry : channels.getActivePopoutContent().entrySet()) { + String title = makeTitle(entry.getValue(), state); + entry.getKey().setTitle(title); } } @@ -3902,7 +3911,14 @@ private void updateTitles(int state) { * @param state The current state * @return The created title */ - private String makeTitle(Channel channel, int state) { + private String makeTitle(DockContent content, int state) { + Channel channel = null; + if (content instanceof Channels.DockChannelContainer) { + channel = ((Channels.DockChannelContainer)content).getContent(); + } + if (channel == null) { + return appendToTitle(content.getTitle()); + } String channelName = channel.getName(); String chan = channel.getChannel(); @@ -4001,16 +4017,19 @@ private String makeTitle(Channel channel, int state) { } else { title += secondaryConnectionsStatus; } - + return appendToTitle(title); + } + + private String appendToTitle(String title) { title += " - Chatty"; String addition = client.settings.getString("titleAddition"); if (!addition.isEmpty()) { title = addition+" "+title; } - return title; } + } public void openConnectDialog(final String channelPreset) { @@ -4717,31 +4736,38 @@ private void updateForkSettings() { } } - public void loadRecentMessages(String channel) { + public void loadRecentMessages(User user) { + final String channel = user.getChannel(); + final Room room = user.getRoom(); List msgs = ForkUtil.getRecentMessages(channel); String[] msgArray = msgs.toArray(new String[0]); boolean firstMessagePrinted = false; for (String str : msgArray) { - if (printOneRecentMessage(channel, str, firstMessagePrinted)) { + if (printOneRecentMessage(room, channel, str, firstMessagePrinted)) { firstMessagePrinted = true; } } if (firstMessagePrinted) { // Dont show message if nothing was printed. - printLine(client.roomManager.getRoom(channel), "[End of recent messages.]"); + printLine(room, "[End of recent messages.]"); } } - private boolean printOneRecentMessage(String channel, String data, boolean firstMessagePrinted) { + private boolean printOneRecentMessage( + Room room, + String channel, + String data, + boolean firstMessagePrinted) { if (data == null) { return false; } MsgTags tags = MsgTags.EMPTY; if (data.startsWith("@")) { - int endOfTags = data.indexOf(" "); + // Get the second space. + int endOfTags = data.indexOf(" ", data.indexOf(" ") + 1); if (endOfTags == -1) { return false; } @@ -4755,53 +4781,50 @@ private boolean printOneRecentMessage(String channel, String data, boolean first User user = client.getUser(channel, tags.get("display-name")); ForkUtil.updateUserFromTags(user, tags); - String separator = "PRIVMSG " + channel + " :"; + final String separator = "PRIVMSG " + channel + " :"; - boolean action = false; - String textMsg = data.substring(data.indexOf(separator) + separator.length()); - if (textMsg.indexOf("\u0001ACTION ") > -1) { - action = true; - } + String textMsg = data.substring( + data.indexOf(separator) + separator.length()); + final boolean action = (textMsg.indexOf("\u0001ACTION ") > -1); textMsg = textMsg.replaceAll("\u0001ACTION ", ""); textMsg = textMsg.replaceAll("\u0001", ""); - String t = "tmi-sent-ts"; - String r = "rm-received-ts"; - String tmi; - if (tags.containsKey(t)) { - tmi = tags.get(t); - } else if (tags.containsKey(r)) { - tmi = tags.get(r); - } else { - tmi = System.currentTimeMillis() + ""; - } - long messageSentTimestamp = Long.parseLong(tmi); + final String t = "tmi-sent-ts"; + final String r = "rm-received-ts"; + final String tmi = tags.containsKey(t) + ? tags.get(t) + : tags.containsKey(r) + ? tags.get(r) + : (System.currentTimeMillis() + ""); + final long messageSentTimestamp = Long.parseLong(tmi); // Check for duplicates. List messagesOfUser = user.getMessages(); for (User.Message messageOfUser : messagesOfUser) { - if (messageOfUser instanceof User.TextMessage) { - User.TextMessage textMessage = (User.TextMessage)messageOfUser; + if (!(messageOfUser instanceof User.TextMessage)) { + continue; + } + User.TextMessage textMessage = (User.TextMessage)messageOfUser; - // If the timestamps are the same, no need to duplicate the message. - if (messageOfUser.getTime() == messageSentTimestamp) { + final long time = messageOfUser.getTime(); + // If the timestamps are the same, no need to duplicate the message. + if (time == messageSentTimestamp) { + return false; + /* But sent messages are a little different from the messages + that are stored on the server, so we have to take into account + the delay of let's say 2000 milliseconds and compare the text + before making sure the message already exists. + */ + } else if (Math.abs(time - messageSentTimestamp) < 2000) { + if (textMsg.equals(textMessage.text)) { return false; - /* But sent messages are a little different from the messages - that are stored on the server, so we have to take into account - the delay of let's say 2000 milliseconds and compare the text - before making sure the message already exists. - */ - } else if (Math.abs(messageOfUser.getTime() - messageSentTimestamp) < 2000) { - if (textMsg.equals(textMessage.text)) { - return false; - } } } } if (!firstMessagePrinted) { // Dont show message if nothing was printed. - printLine(client.roomManager.getRoom(channel), "[Begin of recent messages.]"); + printLine(room, "[Begin of recent messages.]"); } this.printMessage(user, textMsg, action, tags, tmi); diff --git a/src/chatty/gui/MouseClickedListener.java b/src/chatty/gui/MouseClickedListener.java index c17ab0f01..3dbe26698 100644 --- a/src/chatty/gui/MouseClickedListener.java +++ b/src/chatty/gui/MouseClickedListener.java @@ -1,10 +1,12 @@ package chatty.gui; +import chatty.gui.components.Channel; + /** * * @author tduva */ public interface MouseClickedListener { - public void mouseClicked(); + public void mouseClicked(Channel chan); } diff --git a/src/chatty/gui/StyleManager.java b/src/chatty/gui/StyleManager.java index b08e4915d..ab2478cd8 100644 --- a/src/chatty/gui/StyleManager.java +++ b/src/chatty/gui/StyleManager.java @@ -51,7 +51,7 @@ public class StyleManager implements StyleServer { "showImageTooltips", "showTooltipImages", "highlightMatches", "nickColorCorrection", "mentions", "mentionsInfo", "markHoveredUser", "highlightMatchesAll", - "nickColorBackground", "mentionMessages", + "nickColorBackground", "mentionMessages", "msgColorsLinks", "inputHistoryMultirowRequireCtrl" // Not delievered through this )); @@ -214,6 +214,7 @@ private void makeStyles() { addBooleanSetting(Setting.SHOW_TOOLTIPS, "showImageTooltips"); addBooleanSetting(Setting.SHOW_TOOLTIP_IMAGES, "showTooltipImages"); addBooleanSetting(Setting.HIGHLIGHT_MATCHES_ALL, "highlightMatchesAll"); + addBooleanSetting(Setting.LINKS_CUSTOM_COLOR, "msgColorsLinks"); addLongSetting(Setting.HIGHLIGHT_HOVERED_USER, "markHoveredUser"); addLongSetting(Setting.FILTER_COMBINING_CHARACTERS, "filterCombiningCharacters"); addBooleanSetting(Setting.PAUSE_ON_MOUSEMOVE, "pauseChatOnMouseMove"); diff --git a/src/chatty/gui/components/Channel.java b/src/chatty/gui/components/Channel.java index 698f9acf1..69c82547e 100644 --- a/src/chatty/gui/components/Channel.java +++ b/src/chatty/gui/components/Channel.java @@ -7,6 +7,7 @@ import chatty.gui.StyleServer; import chatty.gui.MainGui; import chatty.User; +import chatty.gui.Channels.DockChannelContainer; import chatty.gui.GuiUtil; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.menus.TextSelectionMenu; @@ -54,6 +55,8 @@ public enum Type { private final MainGui main; private Type type; + private DockChannelContainer content; + private boolean userlistEnabled = true; private int previousUserlistWidth; private int userlistMinWidth; @@ -117,30 +120,26 @@ public Channel(final Room room, Type type, MainGui main, StyleManager styleManag add(input, BorderLayout.SOUTH); } + public DockChannelContainer getDockContent() { + return content; + } + + public void setDockContent(DockChannelContainer content) { + this.content = content; + updateContentData(); + } + + private void updateContentData() { + if (content != null) { + content.setTitle(getName()); + } + } + public void init() { text.setChannel(this); input.requestFocusInWindow(); setStyles(); - - input.getDocument().addDocumentListener(new DocumentListener() { - - @Override - public void insertUpdate(DocumentEvent e) { - if (onceOffEditListener != null && room != Room.EMPTY) { - onceOffEditListener.edited(room.getChannel()); - onceOffEditListener = null; - } - } - - @Override - public void removeUpdate(DocumentEvent e) { - } - - @Override - public void changedUpdate(DocumentEvent e) { - } - }); } public boolean setRoom(Room room) { @@ -148,6 +147,7 @@ public boolean setRoom(Room room) { this.room = room; refreshBufferSize(); setName(room.getDisplayName()); + updateContentData(); return true; } return false; @@ -258,7 +258,6 @@ public boolean requestFocusInWindow() { input.requestFocusInWindow(); }); return input.requestFocusInWindow(); - } @@ -385,6 +384,16 @@ public void actionPerformed(ActionEvent e) { t.start(); } + /** + * Return 0 size, so resizing in a split pane works. + * + * @return + */ + @Override + public Dimension getMinimumSize() { + return new Dimension(0, 0); + } + /** * Toggle visibility for the text input box. */ @@ -453,20 +462,9 @@ public User getSelectedUser() { return text.getSelectedUser(); } - @Override public String toString() { return String.format("%s '%s'", type, room); } - private OnceOffEditListener onceOffEditListener; - - public void setOnceOffEditListener(OnceOffEditListener listener) { - onceOffEditListener = listener; - } - - public interface OnceOffEditListener { - public void edited(String channel); - } - } diff --git a/src/chatty/gui/components/HighlightedMessages.java b/src/chatty/gui/components/HighlightedMessages.java index db5d6795f..92f6c2200 100644 --- a/src/chatty/gui/components/HighlightedMessages.java +++ b/src/chatty/gui/components/HighlightedMessages.java @@ -3,21 +3,29 @@ import chatty.Room; import chatty.User; +import chatty.gui.Channels; +import chatty.gui.DockStyledTabContainer; +import chatty.gui.GuiUtil; import chatty.gui.Highlighter.Match; import chatty.gui.MainGui; import chatty.gui.components.textpane.UserMessage; import chatty.gui.StyleServer; +import chatty.gui.components.menus.ContextMenu; import chatty.gui.components.textpane.ChannelTextPane; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.menus.HighlightsContextMenu; import chatty.gui.components.textpane.InfoMessage; import chatty.gui.components.textpane.MyStyleConstants; +import chatty.util.MiscUtil; import chatty.util.api.Emoticon.EmoticonImage; import chatty.util.api.Emoticons.TagEmotes; import chatty.util.api.StreamInfo; import chatty.util.api.usericons.Usericon; import chatty.util.colors.ColorCorrector; +import chatty.util.dnd.DockContent; +import chatty.util.dnd.DockContentContainer; import chatty.util.irc.MsgTags; +import chatty.util.settings.Settings; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; @@ -28,8 +36,10 @@ import java.text.SimpleDateFormat; import java.util.Collection; import java.util.List; +import java.util.function.Supplier; import javax.swing.JDialog; import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; @@ -40,6 +50,9 @@ */ public class HighlightedMessages extends JDialog { + public final static int DOCKED = 1 << 0; + public final static int AUTO_OPEN = 1 << 1; + private final TextPane messages; private String currentChannel; private int currentChannelMessageCount = 0; @@ -56,6 +69,17 @@ public class HighlightedMessages extends JDialog { private final String label; private final ContextMenuListener contextMenuListener; + private final Channels channels; + private final Settings settings; + + // Dock + private final DockStyledTabContainer content; + private boolean isDocked; + + // Settings + private boolean autoOpen; + private String settingName; + /** * Creates a new dialog. @@ -68,11 +92,15 @@ public class HighlightedMessages extends JDialog { * @param contextMenuListener */ public HighlightedMessages(MainGui owner, StyleServer styleServer, - String title, String label, ContextMenuListener contextMenuListener) { + String title, String shortTitle, String label, ContextMenuListener contextMenuListener, + Channels channels, String settingName) { super(owner); this.title = title; this.label = label; + this.channels = channels; this.contextMenuListener = contextMenuListener; + this.settingName = settingName; + this.settings = owner.getSettings(); updateTitle(); this.addComponentListener(new MyVisibleListener()); @@ -121,7 +149,7 @@ public ColorCorrector getColorCorrector() { return styleServer.getColorCorrector(); } }; - messages = new TextPane(owner, modifiedStyleServer); + messages = new TextPane(owner, modifiedStyleServer, () -> new HighlightsContextMenu(isDocked, autoOpen)); messages.setContextMenuListener(new MyContextMenuListener()); //messages.setLineWrap(true); //messages.setWrapStyleWord(true); @@ -131,10 +159,75 @@ public ColorCorrector getColorCorrector() { messages.setScrollPane(scroll); add(scroll); + content = new DockStyledTabContainer(scroll, shortTitle, channels.getDock()); setPreferredSize(new Dimension(400,300)); pack(); + + updateTabSettings(owner.getSettings().getLong("tabsMessage")); + owner.getSettings().addSettingChangeListener((setting, type, value) -> { + if (setting.equals("tabsMessage")) { + SwingUtilities.invokeLater(() -> updateTabSettings((Long)value)); + } + }); + } + + private void updateTabSettings(long value) { + content.setSettings(0, (int)value, 0, 0, 0); + } + + private void saveSettings() { + int value = isDocked ? DOCKED : 0; + value = value | (autoOpen ? AUTO_OPEN : 0); + settings.setLong(settingName, value); + } + + public void loadSettings() { + int value = (int)settings.getLong(settingName); + setDocked(MiscUtil.isBitEnabled(value, DOCKED)); + autoOpen = MiscUtil.isBitEnabled(value, AUTO_OPEN); + } + + @Override + public void setVisible(boolean visible) { + setVisible(visible, true); + } + + private void setVisible(boolean visible, boolean switchTo) { + if (visible == isVisible()) { + return; + } + if (isDocked) { + if (visible) { + channels.getDock().addContent(content); + if (switchTo) { + channels.getDock().setActiveContent(content); + } + } + else { + channels.getDock().removeContent(content); + } + } + else { + if (!switchTo) { + GuiUtil.setNonAutoFocus(this); + } + super.setVisible(visible); + } + if (visible) { + newCount = 0; + } + } + + @Override + public boolean isVisible() { + if (isDocked) { + return channels.getDock().hasContent(content); + } + else { + return super.isVisible(); + } } public void addMessage(String channel, UserMessage message) { @@ -174,6 +267,12 @@ private void messageAdded(String channel) { if (!isVisible()) { newCount++; } + if (autoOpen) { + setVisible(true, false); + } + if (isDocked && !content.isContentVisible()) { + content.setNewMessage(true); + } } private void updateTitle() { @@ -217,14 +316,40 @@ public int getNewCount() { return newCount; } + private void setDocked(boolean docked) { + if (isDocked != docked && isVisible()) { + // Will make change visible as well, so only do if already visible + toggleDock(); + } + // Always update value, even if not currently visible + isDocked = docked; + } + + private void toggleDock() { + if (isDocked) { + channels.getDock().removeContent(content); + add(content.getComponent()); + isDocked = false; + super.setVisible(true); + } + else { + remove(content.getComponent()); + channels.getDock().addContent(content); + channels.getDock().setActiveContent(content); + isDocked = true; + super.setVisible(false); + } + saveSettings(); + } + /** * Normal channel text pane modified a bit to fit the needs for this. */ static class TextPane extends ChannelTextPane { - public TextPane(MainGui main, StyleServer styleServer) { + public TextPane(MainGui main, StyleServer styleServer, Supplier contextMenuCreator) { super(main, styleServer); - linkController.setContextMenuCreator(() -> new HighlightsContextMenu()); + linkController.setContextMenuCreator(contextMenuCreator); } public void clear() { @@ -237,8 +362,19 @@ private class MyContextMenuListener implements ContextMenuListener { @Override public void menuItemClicked(ActionEvent e) { - if (e.getActionCommand().equals("clearHighlights")) { - clear(); + switch (e.getActionCommand()) { + case "clearHighlights": + clear(); + break; + case "toggleDock": + toggleDock(); + break; + case "toggleAutoOpen": + autoOpen = !autoOpen; + saveSettings(); + break; + default: + break; } contextMenuListener.menuItemClicked(e); } diff --git a/src/chatty/gui/components/ImageDialog.java b/src/chatty/gui/components/ImageDialog.java index eb6b4549c..15ee6ffe1 100644 --- a/src/chatty/gui/components/ImageDialog.java +++ b/src/chatty/gui/components/ImageDialog.java @@ -3,6 +3,8 @@ import chatty.gui.GuiUtil; +import chatty.util.ForkUtil; + import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -72,7 +74,7 @@ public ImageDialog(final Window owner, String url, String type) { try { URL thisurl = new URL(url); URLConnection connection = thisurl.openConnection(); - connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36"); + connection.setRequestProperty("User-Agent", ForkUtil.USER_AGENT); Image image = null; image = ImageIO.read(connection.getInputStream()); if (image == null) { diff --git a/src/chatty/gui/components/SearchDialog.java b/src/chatty/gui/components/SearchDialog.java index 560afb19b..6ac523eeb 100644 --- a/src/chatty/gui/components/SearchDialog.java +++ b/src/chatty/gui/components/SearchDialog.java @@ -19,6 +19,7 @@ import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JTextField; +import javax.swing.SwingUtilities; import javax.swing.Timer; /** @@ -38,6 +39,8 @@ public class SearchDialog extends JDialog { private final JButton searchButton = new JButton(Language.getString("searchDialog.button.search")); //private final JCheckBox highlightAll = new JCheckBox("Highlight all occurences"); + private Channel chan; + private static final Map created = new HashMap<>(); public static void showSearchDialog(Channel channel, MainGui g, Window owner) { @@ -48,10 +51,11 @@ public static void showSearchDialog(Channel channel, MainGui g, Window owner) { GuiUtil.installEscapeCloseOperation(dialog); created.put(owner, dialog); } + dialog.setChannel(channel); dialog.setVisible(true); } - public SearchDialog(final MainGui g, final Window owner) { + private SearchDialog(final MainGui g, final Window owner) { super(owner); setTitle(Language.getString("searchDialog.title")); setResizable(false); @@ -82,7 +86,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { - if (!g.search(owner, searchText.getText())) { + if (!g.search(chan, searchText.getText())) { searchText.setBackground(COLOR_NO_RESULT); timer.restart(); } @@ -94,7 +98,7 @@ public void actionPerformed(ActionEvent e) { addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - g.resetSearch(owner); + g.resetSearch(chan); searchText.setText(null); searchText.setBackground(COLOR_NORMAL); } @@ -103,4 +107,8 @@ public void windowClosing(WindowEvent e) { pack(); } + public void setChannel(Channel chan) { + this.chan = chan; + } + } diff --git a/src/chatty/gui/components/TabComponent.java b/src/chatty/gui/components/TabComponent.java deleted file mode 100644 index e86c595c9..000000000 --- a/src/chatty/gui/components/TabComponent.java +++ /dev/null @@ -1,23 +0,0 @@ - -package chatty.gui.components; - -import chatty.gui.components.menus.ContextMenuListener; -import chatty.gui.components.menus.TabContextMenu; -import java.awt.FlowLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; - -/** - * - * @author tduva - */ -public class TabComponent extends JPanel { - - public TabComponent(String text, ContextMenuListener listener) { - super(new FlowLayout(FlowLayout.LEFT, 0, 0)); - JLabel label = new JLabel(text); - add(label); - setOpaque(false); - label.setComponentPopupMenu(new TabContextMenu(listener)); - } -} diff --git a/src/chatty/gui/components/TokenDialog.java b/src/chatty/gui/components/TokenDialog.java index ea6809784..608b26c3c 100644 --- a/src/chatty/gui/components/TokenDialog.java +++ b/src/chatty/gui/components/TokenDialog.java @@ -187,7 +187,10 @@ public void verifyingToken() { * @param result */ public void tokenVerified(boolean valid, String result) { - JOptionPane.showMessageDialog(this, result); + if (isVisible()) { + // Only show when verifying while the token dialog is open + JOptionPane.showMessageDialog(this, result); + } verifyToken.setEnabled(true); update(); } diff --git a/src/chatty/gui/components/help/help-releases.html b/src/chatty/gui/components/help/help-releases.html index e9c60911f..e9e7eb707 100644 --- a/src/chatty/gui/components/help/help-releases.html +++ b/src/chatty/gui/components/help/help-releases.html @@ -17,6 +17,7 @@

Release Information

+ 0.14 | 0.13.1 | 0.13 | 0.12 | @@ -62,7 +63,32 @@

Release Information

full list of changes.

- Version 0.13.1 (This one!) (2020-11-17) + Version 0.14 (This one!) (2020-12-??) + [back to top] +

+

This is a pre-release test version. No complete release yet.

+ +

This version features a more advanced drag&drop functionality:

+ +
    +
  • Tabs can be dragged to the edges of a chat to create a split view + with two tab panes side by side.
  • +
  • Tabs can be dragged from one tab pane to another.
  • +
  • Popouts also contain a tab pane now (if more than one tab is dragged + into it).
  • +
  • A popout can be opened through the tab context menu (as before) or + by dragging a tab outside of the window, either as a dialog or + separate window.
  • +
  • Tabs can be customized more, changing how which information is + displayed.
  • +
  • Relevant pages in the Settings are "Tabs" (Tab Info) and "Window" + (Popout).
  • +
  • The Highlighted and Ignored Messages dialogs can be docked as a tab + through their context menu.
  • +
+ +

+ Version 0.13.1 (2020-11-17) [back to top]

diff --git a/src/chatty/gui/components/help/help.html b/src/chatty/gui/components/help/help.html
index 90cf18c13..820f4d254 100644
--- a/src/chatty/gui/components/help/help.html
+++ b/src/chatty/gui/components/help/help.html
@@ -5,7 +5,7 @@
         
 
 
-    

Chatty (Version: 0.13.1)

+

Chatty (Version: 0.14-b1)

@@ -121,6 +121,8 @@

  • R9k for R9Kbeta Mode
  • EmoteOnly for Emote-Only Mode
  • A language code if Broadcaster Language Mode is enabled
  • +
  • [FM] indicates a websocket connection (F + for FrankerFaceZ, M for PubSub/Modlogs)
  • You can toggle showing some information under View - Options - Titlebar.

    diff --git a/src/chatty/gui/components/menus/HighlightsContextMenu.java b/src/chatty/gui/components/menus/HighlightsContextMenu.java index 4b032bd44..e6768db93 100644 --- a/src/chatty/gui/components/menus/HighlightsContextMenu.java +++ b/src/chatty/gui/components/menus/HighlightsContextMenu.java @@ -12,8 +12,11 @@ */ public class HighlightsContextMenu extends ContextMenu { - public HighlightsContextMenu() { + public HighlightsContextMenu(boolean isDocked, boolean autoOpen) { addItem("clearHighlights", Language.getString("highlightedDialog.cm.clear")); + addSeparator(); + addCheckboxItem("toggleDock", "Dock", isDocked); + addCheckboxItem("toggleAutoOpen", "Open on message", autoOpen); } @Override diff --git a/src/chatty/gui/components/menus/TabContextMenu.java b/src/chatty/gui/components/menus/TabContextMenu.java index 41a91df19..1b8230a13 100644 --- a/src/chatty/gui/components/menus/TabContextMenu.java +++ b/src/chatty/gui/components/menus/TabContextMenu.java @@ -1,7 +1,10 @@ package chatty.gui.components.menus; +import chatty.gui.components.Channel; import java.awt.event.ActionEvent; +import java.util.Collection; +import java.util.Map; /** * The Context Menu that appears on the tab bar. @@ -11,26 +14,50 @@ public class TabContextMenu extends ContextMenu { private final ContextMenuListener listener; + private final Channel chan; - public TabContextMenu(ContextMenuListener listener) { + public TabContextMenu(ContextMenuListener listener, Channel chan, Map> info) { this.listener = listener; + this.chan = chan; - String subMenu = "Close All"; - - addItem("popoutChannel", "Popout");addSeparator(); + addItem("popoutChannel", "Popout "+chan.getChannel()); + addItem("popoutChannelWindow", "Popout as Window"); + addSeparator(); addItem("closeChannel", "Close"); - addItem("closeAllTabsButCurrent", "Except current", subMenu); - addItem("closeAllTabsToLeft", "To left of current", subMenu); - addItem("closeAllTabsToRight", "To right of current", subMenu); - addSeparator(subMenu); - addItem("closeAllTabs", "All", subMenu); + String closeTabsMenu = "Close Tabs"; + addNumItem("closeAllTabsButCurrent", "Except current", closeTabsMenu, info); + addNumItem("closeAllTabsToLeft", "To left of current", closeTabsMenu, info); + addNumItem("closeAllTabsToRight", "To right of current", closeTabsMenu, info); + addSeparator(closeTabsMenu); + addNumItem("closeAllTabsOffline", "Offline", closeTabsMenu, info); + addSeparator(closeTabsMenu); + addNumItem("closeAllTabs", "All", closeTabsMenu, info); + + addSeparator(); + String closeAllMenu = "Close All"; + addNumItem("closeAllTabs2ButCurrent", "Except current", closeAllMenu, info); + addSeparator(closeAllMenu); + addNumItem("closeAllTabs2Offline", "Offline", closeAllMenu, info); + addSeparator(closeAllMenu); + addNumItem("closeAllTabs2", "All", closeAllMenu, info); + + CommandMenuItems.addCommands(CommandMenuItems.MenuType.CHANNEL, this); + } + + private void addNumItem(String cmd, String label, String submenu, Map> info) { + if (info.containsKey(cmd)) { + addItem(cmd, String.format("%s (%d)", label, info.get(cmd).size()), submenu); + } + else { + addItem(cmd, label, submenu); + } } @Override public void actionPerformed(ActionEvent e) { if (listener != null) { - listener.menuItemClicked(e); + listener.channelMenuItemClicked(e, chan); } } diff --git a/src/chatty/gui/components/menus/UserContextMenu.java b/src/chatty/gui/components/menus/UserContextMenu.java index f2f6133ca..ebdadd1ba 100644 --- a/src/chatty/gui/components/menus/UserContextMenu.java +++ b/src/chatty/gui/components/menus/UserContextMenu.java @@ -85,6 +85,8 @@ public UserContextMenu(User user, String msgId, String autoModMsgId, addSeparator(MISC_MENU); addItem("setcolor", Language.getString("userCm.setColor"), MISC_MENU); addItem("setname", Language.getString("userCm.setName"), MISC_MENU); + addSeparator(MISC_MENU); + addItem("notes", "Notes", MISC_MENU); // Get the preset categories from the addressbook, which may be empty // if not addressbook is set to this user diff --git a/src/chatty/gui/components/settings/MainSettings.java b/src/chatty/gui/components/settings/MainSettings.java index 89c0a139f..7d84aa5cd 100644 --- a/src/chatty/gui/components/settings/MainSettings.java +++ b/src/chatty/gui/components/settings/MainSettings.java @@ -160,6 +160,7 @@ public static Map getLanguageOptions() { Map languageOptions = new LinkedHashMap<>(); languageOptions.put("", Language.getString("settings.language.option.defaultLanguage")); languageOptions.put("zh_TW", "Chinese (traditional)"); + languageOptions.put("cs", "Czech / Čeština"); languageOptions.put("nl", "Dutch / Nederlands"); languageOptions.put("en_US", "English (US)"); languageOptions.put("en_GB", "English (UK)"); @@ -170,6 +171,7 @@ public static Map getLanguageOptions() { languageOptions.put("ja", "Japanese / 日本語"); languageOptions.put("ko", "Korean / 한국어"); languageOptions.put("pl", "Polish / Polski"); + languageOptions.put("pt_BR", "Portuguese (BR)"); languageOptions.put("ru", "Russian / Русский"); languageOptions.put("es", "Spanish / Español"); languageOptions.put("tr", "Turkish / Türk"); diff --git a/src/chatty/gui/components/settings/MsgColorSettings.java b/src/chatty/gui/components/settings/MsgColorSettings.java index 43fdb9db9..365c5b190 100644 --- a/src/chatty/gui/components/settings/MsgColorSettings.java +++ b/src/chatty/gui/components/settings/MsgColorSettings.java @@ -57,10 +57,13 @@ public MsgColorSettings(SettingsDialog d) { main.add(info, d.makeGbc(0, 2, 1, 1)); other.add(d.addSimpleBooleanSetting("msgColorsPrefer"), - d.makeGbc(0, 9, 2, 1, GridBagConstraints.WEST)); + d.makeGbc(0, 0, 1, 1, GridBagConstraints.WEST)); + + other.add(d.addSimpleBooleanSetting("msgColorsLinks"), + d.makeGbc(0, 1, 1, 1, GridBagConstraints.WEST)); other.add(d.addSimpleBooleanSetting("actionColored"), - d.makeGbc(0, 10, 2, 1, GridBagConstraints.WEST)); + d.makeGbc(0, 2, 1, 1, GridBagConstraints.WEST)); } diff --git a/src/chatty/gui/components/settings/OtherSettings.java b/src/chatty/gui/components/settings/OtherSettings.java index 2f027cde8..1e53ae88d 100644 --- a/src/chatty/gui/components/settings/OtherSettings.java +++ b/src/chatty/gui/components/settings/OtherSettings.java @@ -2,6 +2,8 @@ package chatty.gui.components.settings; import chatty.Chatty; +import chatty.gui.components.LinkLabel; +import java.awt.FlowLayout; import java.awt.GridBagConstraints; import static java.awt.GridBagConstraints.WEST; import javax.swing.JCheckBox; @@ -124,6 +126,25 @@ public OtherSettings(SettingsDialog d) { "Save Addressbook immediately after changing entries", "Save immediately after updating addressbook (including changes via commands)"), d.makeGbc(0, 9, 3, 1, GridBagConstraints.WEST)); + + JPanel pronouns = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + JCheckBox pronouns1 = d.addSimpleBooleanSetting( + "pronouns", + "Display pronouns in User Dialog", + "Click on a user in chat to open User Dialog, the pronoun (if available) will be shown in the title next to the username"); + pronouns.add(pronouns1); + pronouns.add(new LinkLabel("(based on [url:http://pronouns.alejo.io pronouns.alejo.io])", d.getLinkLabelListener())); + other.add(pronouns, + SettingsDialog.makeGbc(0, 10, 3, 1, GridBagConstraints.WEST)); + + JCheckBox pronouns2 = d.addSimpleBooleanSetting( + "pronounsChat", + "Display pronouns in chat (may not immediately show for all users)", + "Will work best in chats with a small amount of users. May not show up on the first message of a user."); + other.add(pronouns2, + SettingsDialog.makeGbcSub(0, 11, 3, 1, GridBagConstraints.WEST)); + + SettingsUtil.addSubsettings(pronouns1, pronouns2); } } diff --git a/src/chatty/gui/components/settings/SettingsDialog.java b/src/chatty/gui/components/settings/SettingsDialog.java index e217eb853..4ac816852 100644 --- a/src/chatty/gui/components/settings/SettingsDialog.java +++ b/src/chatty/gui/components/settings/SettingsDialog.java @@ -57,10 +57,10 @@ public class SettingsDialog extends JDialog implements ActionListener { "ffz", "nod3d", "noddraw", "userlistWidth", "userlistMinWidth", "userlistEnabled", "capitalizedNames", "correctlyCapitalizedNames", "ircv3CapitalizedNames", - "tabOrder", "tabsMwheelScrolling", "tabsMwheelScrollingAnywhere", "inputFont", + "inputFont", "bttvEmotes", "botNamesBTTV", "botNamesFFZ", "ffzEvent", "logPath", "logTimestamp", "logSplit", "logSubdirectories", - "tabsPlacement", "tabsLayout", "logLockFiles", "logMessageTemplate", + "logLockFiles", "logMessageTemplate", "laf", "lafTheme", "lafFontScale", "language", "timezone" )); diff --git a/src/chatty/gui/components/settings/TabSettings.java b/src/chatty/gui/components/settings/TabSettings.java index 531fcc380..88bcc29cf 100644 --- a/src/chatty/gui/components/settings/TabSettings.java +++ b/src/chatty/gui/components/settings/TabSettings.java @@ -1,13 +1,18 @@ package chatty.gui.components.settings; +import chatty.gui.Channels; import chatty.lang.Language; +import chatty.util.StringUtil; import java.awt.GridBagConstraints; +import static java.awt.GridBagConstraints.WEST; +import java.awt.GridBagLayout; import java.util.HashMap; import java.util.Map; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JTabbedPane; /** * @@ -18,6 +23,7 @@ public class TabSettings extends SettingsPanel { public TabSettings(final SettingsDialog d) { JPanel other = addTitledPanel(Language.getString("settings.section.tabs"), 0); + JPanel infoPanel = addTitledPanel("Tab Info", 1); //------------ // Tabs Order @@ -70,6 +76,80 @@ public TabSettings(final SettingsDialog d) { d.makeGbcSub(0, 6, 4, 1, GridBagConstraints.WEST)); SettingsUtil.addSubsettings(scroll, scroll2); + + //-------------------------- + // Tab Info + //-------------------------- + JTabbedPane infoPanelTabs = new JTabbedPane(); + infoPanelTabs.addTab("Live Stream", new TabInfoOptions("tabsLive", d)); + infoPanelTabs.addTab("New Stream Status", new TabInfoOptions("tabsStatus", d)); + infoPanelTabs.addTab("New Message", new TabInfoOptions("tabsMessage", d)); + infoPanelTabs.addTab("New Highlight", new TabInfoOptions("tabsHighlight", d)); + infoPanelTabs.addTab("Active Tab", new TabInfoOptions("tabsActive", d)); + + GridBagConstraints gbc = SettingsDialog.makeGbc(0, 0, 1, 1, GridBagConstraints.WEST); + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1; + infoPanel.add(infoPanelTabs, gbc); + } + + private static class TabInfoOptions extends JPanel implements LongSetting { + + private final Map options = new HashMap<>(); + + TabInfoOptions(String settingName, SettingsDialog settings) { + settings.addLongSetting(settingName, this); + setLayout(new GridBagLayout()); + add(makeOption(Channels.DockChannelContainer.BOLD, "bold"), + SettingsDialog.makeGbc(0, 1, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.ITALIC, "italic"), + SettingsDialog.makeGbc(1, 1, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.COLOR1, "color1"), + SettingsDialog.makeGbc(0, 2, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.COLOR2, "color2"), + SettingsDialog.makeGbc(1, 2, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.ASTERISK, "asterisk"), + SettingsDialog.makeGbc(1, 0, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.DOT1, "dot1"), + SettingsDialog.makeGbc(2, 1, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.DOT2, "dot2"), + SettingsDialog.makeGbc(2, 2, 1, 1, GridBagConstraints.WEST)); + add(makeOption(Channels.DockChannelContainer.LINE, "line"), + SettingsDialog.makeGbc(0, 0, 1, 1, GridBagConstraints.WEST)); + } + + private JCheckBox makeOption(int option, String labelKey) { + String text = Language.getString("settings.tabs."+labelKey); + String tip = Language.getString("settings.tabs."+labelKey + ".tip", false); + JCheckBox check = new JCheckBox(text); + check.setToolTipText(SettingsUtil.addTooltipLinebreaks(tip)); + options.put(option, check); + return check; + } + + @Override + public Long getSettingValue() { + long result = 0; + for (Map.Entry entry : options.entrySet()) { + if (entry.getValue().isSelected()) { + result = result | entry.getKey(); + } + } + return result; + } + + @Override + public Long getSettingValue(Long def) { + return getSettingValue(); + } + + @Override + public void setSettingValue(Long setting) { + for (Map.Entry entry : options.entrySet()) { + entry.getValue().setSelected((setting & entry.getKey()) != 0); + } + } + } } diff --git a/src/chatty/gui/components/settings/WindowSettings.java b/src/chatty/gui/components/settings/WindowSettings.java index f08b46674..c55572c71 100644 --- a/src/chatty/gui/components/settings/WindowSettings.java +++ b/src/chatty/gui/components/settings/WindowSettings.java @@ -100,6 +100,8 @@ public WindowSettings(final SettingsDialog d) { "Automatically close a popout if the last channel in the main window is closed"), d.makeGbc(1, 0, 1, 1)); + popout.add(SettingsUtil.createPanel("tabsPopoutDrag", d.addComboLongSetting("tabsPopoutDrag", new int[]{0,1,2})), + d.makeGbc(0, 1, 2, 1)); } } diff --git a/src/chatty/gui/components/textpane/ChannelTextPane.java b/src/chatty/gui/components/textpane/ChannelTextPane.java index 9ce468187..d957349bc 100644 --- a/src/chatty/gui/components/textpane/ChannelTextPane.java +++ b/src/chatty/gui/components/textpane/ChannelTextPane.java @@ -17,6 +17,7 @@ import chatty.gui.components.Channel; import chatty.util.api.usericons.Usericon; import chatty.gui.components.menus.ContextMenuListener; +import chatty.gui.components.userinfo.UserNotes; import chatty.gui.emoji.EmojiUtil; import chatty.util.ChattyMisc; import chatty.util.ChattyMisc.CombinedEmotesInfo; @@ -162,7 +163,7 @@ public enum Setting { DELETED_MESSAGES_MODE, BAN_DURATION_APPENDED, BAN_REASON_APPENDED, BAN_DURATION_MESSAGE, BAN_REASON_MESSAGE, - ACTION_COLORED, BUFFER_SIZE, AUTO_SCROLL_TIME, + ACTION_COLORED, LINKS_CUSTOM_COLOR, BUFFER_SIZE, AUTO_SCROLL_TIME, EMOTICON_MAX_HEIGHT, EMOTICON_SCALE_FACTOR, BOT_BADGE_ENABLED, FILTER_COMBINING_CHARACTERS, PAUSE_ON_MOUSEMOVE, PAUSE_ON_MOUSEMOVE_CTRL_REQUIRED, EMOTICONS_SHOW_ANIMATED, @@ -1900,9 +1901,20 @@ else if (styles.namesMode() != SettingsManager.DISPLAY_NAMES_MODE_CAPITALIZED } } + String addition = null; + // Add username in parentheses behind, if necessary if (!user.hasRegularDisplayNick() && !user.hasCustomNickSet() && styles.namesMode() == SettingsManager.DISPLAY_NAMES_MODE_BOTH) { + addition = user.getName(); + } + + String notes = UserNotes.instance().getChatNotes(user); + if (notes != null) { + addition = StringUtil.append(addition, ", ", notes); + } + + if (addition != null) { MutableAttributeSet style = styles.messageUser(user, msgId, background); StyleConstants.setBold(style, false); int fontSize = StyleConstants.getFontSize(style) - 2; @@ -1910,7 +1922,7 @@ else if (styles.namesMode() != SettingsManager.DISPLAY_NAMES_MODE_CAPITALIZED fontSize = StyleConstants.getFontSize(style); } StyleConstants.setFontSize(style, fontSize); - print(" ("+user.getName()+")", style); + print(" ("+addition+")", style); } // Finish up @@ -2210,7 +2222,8 @@ private void printSpecialsInfo(String text, AttributeSet style, TreeMap ranges = new TreeMap<>(); HashMap rangesStyle = new HashMap<>(); - findLinks(text, ranges, rangesStyle, style); + findLinks(text, ranges, rangesStyle, styles.isEnabled(Setting.LINKS_CUSTOM_COLOR) + ? style : styles.info()); if (styles.isEnabled(Setting.MENTIONS_INFO)) { findMentions(text, ranges, rangesStyle, style, Setting.MENTIONS_INFO); @@ -2244,7 +2257,8 @@ protected void printSpecialsNormal(String text, User user, MutableAttributeSet s applyReplacements(text, replacements, replacement, ranges, rangesStyle); if (!ignoreLinks) { - findLinks(text, ranges, rangesStyle, styles.standard()); + findLinks(text, ranges, rangesStyle, styles.isEnabled(Setting.LINKS_CUSTOM_COLOR) + ? style : styles.standard()); } if (styles.isEnabled(Setting.EMOTICONS_ENABLED)) { @@ -3611,6 +3625,7 @@ private void setSettings() { addSetting(Setting.EMOTICONS_SHOW_ANIMATED, false); addSetting(Setting.SHOW_TOOLTIPS, true); addSetting(Setting.SHOW_TOOLTIP_IMAGES, true); + addSetting(Setting.LINKS_CUSTOM_COLOR, true); addNumericSetting(Setting.MENTIONS, 0, 0, 200); addNumericSetting(Setting.MENTIONS_INFO, 0, 0, 200); addNumericSetting(Setting.MENTION_MESSAGES, 0, 0, 200); @@ -3977,7 +3992,8 @@ public MutableAttributeSet makeIconStyle(Usericon icon, User user) { StyleConstants.setIcon(style, addSpaceToIcon(icon.image)); style.addAttribute(Attribute.USERICON, icon); if (icon.type == Usericon.Type.TWITCH - && Usericon.typeFromBadgeId(icon.badgeType.id) == Usericon.Type.SUB + && (Usericon.typeFromBadgeId(icon.badgeType.id) == Usericon.Type.SUB + || Usericon.typeFromBadgeId(icon.badgeType.id) == Usericon.Type.FOUNDER) && user != null && user.getSubMonths() > 0) { style.addAttribute(Attribute.USERICON_INFO, DateTime.formatMonthsVerbose(user.getSubMonths())); diff --git a/src/chatty/gui/components/textpane/LinkController.java b/src/chatty/gui/components/textpane/LinkController.java index f9371b1be..9488979fb 100644 --- a/src/chatty/gui/components/textpane/LinkController.java +++ b/src/chatty/gui/components/textpane/LinkController.java @@ -249,7 +249,7 @@ public void mouseClicked(MouseEvent e) { && !e.isAltGraphDown()) { // Doing this on mousePressed would prevent selection of text, // because this is used to change the focus to the input - mouseClickedListener.mouseClicked(); + mouseClickedListener.mouseClicked(channel); } } diff --git a/src/chatty/gui/components/userinfo/UserInfo.java b/src/chatty/gui/components/userinfo/UserInfo.java index a80d3e45b..d4e7a6d43 100644 --- a/src/chatty/gui/components/userinfo/UserInfo.java +++ b/src/chatty/gui/components/userinfo/UserInfo.java @@ -10,6 +10,7 @@ import static chatty.gui.components.userinfo.Util.makeGbc; import chatty.lang.Language; import chatty.util.MiscUtil; +import chatty.util.Pronouns; import chatty.util.api.ChannelInfo; import chatty.util.api.Follower; import chatty.util.api.TwitchApi; @@ -43,6 +44,7 @@ public enum Action { private final JButton closeButton = new JButton(Language.getString("dialog.button.close")); private final JCheckBox pinnedDialog = new JCheckBox(Language.getString("userDialog.setting.pin")); + private final JButton notesButton = new JButton("Notes"); private final JCheckBox singleMessage = new JCheckBox(SINGLE_MESSAGE_CHECK); private final BanReasons banReasons; private final Buttons buttons; @@ -68,12 +70,15 @@ public enum Action { private final UserInfoRequester requester; + private final Settings settings; + public UserInfo(final Window parent, UserInfoListener listener, UserInfoRequester requester, Settings settings, final ContextMenuListener contextMenuListener) { super(parent); this.requester = requester; + this.settings = settings; GuiUtil.installEscapeCloseOperation(this); banReasons = new BanReasons(this, settings); @@ -114,7 +119,7 @@ public void actionPerformed(ActionEvent e) { //========================== JPanel topPanel = new JPanel(new GridBagLayout()); - gbc = makeGbc(0,0,3,1); + gbc = makeGbc(0,0,4,1); gbc.insets = new Insets(2, 2, 0, 2); topPanel.add(buttons.getPrimary(), gbc); @@ -138,6 +143,15 @@ public void actionPerformed(ActionEvent e) { topPanel.add(banReasons, gbc); gbc = makeGbc(2, 1, 1, 1); + gbc.weightx = 1; + gbc.anchor = GridBagConstraints.EAST; + notesButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS); + notesButton.addActionListener(e -> { + UserNotes.instance().showDialog(currentUser, this); + }); + topPanel.add(notesButton, gbc); + + gbc = makeGbc(3, 1, 1, 1); gbc.insets = new Insets(2, 8, 2, 8); gbc.anchor = GridBagConstraints.EAST; pinnedDialog.setToolTipText(Language.getString("userDialog.setting.pin.tip")); @@ -368,7 +382,24 @@ private void setUser(User user, String msgId, String autoModMsgId, String localU currentAutoModMsgId = autoModMsgId; } currentLocalUsername = localUsername; - + + updateTitle(user, null); + if (settings.getBoolean("pronouns")) { + Pronouns.instance().getUser((username, pronoun) -> { + if (currentUser.getName().equals(username)) { + updateTitle(user, pronoun); + } + }, user.getName()); + } + + updateMessages(); + infoPanel.update(user); + singleMessage.setEnabled(currentMsgId != null); + updateButtons(); + finishDialog(); + } + + private void updateTitle(User user, String pronoun) { String categoriesString = ""; Set categories = user.getCategories(); if (categories != null && !categories.isEmpty()) { @@ -379,13 +410,9 @@ private void setUser(User user, String msgId, String autoModMsgId, String localU +(user.hasCustomNickSet() ? " ("+user.getDisplayNick()+")" : "") +(!user.hasRegularDisplayNick() ? " ("+user.getName()+")" : "") +displayNickInfo + +(pronoun != null ? " ("+pronoun+")" : "") +" / "+user.getRoom().getDisplayName() +" "+categoriesString); - updateMessages(); - infoPanel.update(user); - singleMessage.setEnabled(currentMsgId != null); - updateButtons(); - finishDialog(); } public void show(Component owner, User user, String msgId, String autoModMsgId, String localUsername) { diff --git a/src/chatty/gui/components/userinfo/UserNotes.java b/src/chatty/gui/components/userinfo/UserNotes.java new file mode 100644 index 000000000..9163eeec1 --- /dev/null +++ b/src/chatty/gui/components/userinfo/UserNotes.java @@ -0,0 +1,191 @@ + +package chatty.gui.components.userinfo; + +import chatty.Room; +import chatty.User; +import chatty.gui.GuiUtil; +import chatty.gui.components.menus.TextSelectionMenu; +import chatty.lang.Language; +import chatty.util.Pronouns; +import chatty.util.StringUtil; +import chatty.util.api.TwitchApi; +import chatty.util.settings.Settings; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Window; +import java.awt.event.ActionListener; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +/** + * Provides methods to get user notes and a dialog to edit them. + * + * @author tduva + */ +public class UserNotes { + + private static final String SETTING_NOTES = "userNotes"; + private static final String SETTING_CHAT_NOTES = "userNotesChat"; + + private static UserNotes instance; + + /** + * Must only be called once. + * + * @param api + * @param settings + */ + public static void init(TwitchApi api, Settings settings) { + instance = new UserNotes(api, settings); + } + + public static UserNotes instance() { + return instance; + } + + private final TwitchApi api; + private final Settings settings; + + private UserNotes(TwitchApi api, Settings settings) { + this.api = api; + this.settings = settings; + } + + public void showDialog(User user, Window parent) { + if (user.getId() == null) { + JOptionPane.showMessageDialog(parent, "User ID not found."); + } + else { + UserNotesDialog d = new UserNotesDialog(user, parent, get(SETTING_CHAT_NOTES, user), get(SETTING_NOTES, user)); + d.showDialog(e -> { + set(SETTING_NOTES, user, d.getNotes()); + set(SETTING_CHAT_NOTES, user, d.getChatNotes()); + }); + } + } + + public String getNotes(User user) { + return get(SETTING_NOTES, user); + } + + public String getChatNotes(User user) { + String notes = get(SETTING_CHAT_NOTES, user); + String pronouns = null; + if (settings.getBoolean("pronounsChat")) { + pronouns = Pronouns.instance().getUser2(user.getName()); + } + return StringUtil.append(notes, ", ", pronouns); + } + + public String get(String setting, User user) { + if (user.getId() == null) { + return null; + } + String result = (String) settings.mapGet(setting, user.getId()); + if (StringUtil.isNullOrEmpty(result)) { + return null; + } + return result; + } + + public void set(String setting, User user, String newNotes) { + if (user.getId() != null) { + if (StringUtil.isNullOrEmpty(newNotes)) { + settings.mapRemove(setting, user.getId()); + } + else { + settings.mapPut(setting, user.getId(), newNotes); + } + } + } + + private static class UserNotesDialog extends JDialog { + + private final JTextField chatNotesTextField = new JTextField(20); + private final JTextArea notesTextArea = new JTextArea(); + + private final JButton saveButton = new JButton(Language.getString("dialog.button.save")); + private final JButton cancelButton = new JButton(Language.getString("dialog.button.cancel")); + + public UserNotesDialog(User user, Window parent, String chatNotes, String notes) { + super(parent); + setTitle("User Notes: "+user.toString()); + setResizable(false); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + setLayout(new GridBagLayout()); + + notesTextArea.setRows(6); + notesTextArea.setColumns(40); + TextSelectionMenu.install(chatNotesTextField); + TextSelectionMenu.install(notesTextArea); + + add(new JLabel("Shown in chat messages after the username:"), + GuiUtil.makeGbc(0, 1, 2, 1, GridBagConstraints.WEST)); + + add(chatNotesTextField, + GuiUtil.makeGbc(0, 2, 2, 1, GridBagConstraints.WEST)); + + add(new JLabel("Notes (just shown here):"), + GuiUtil.makeGbc(0, 3, 2, 1, GridBagConstraints.WEST)); + + add(new JScrollPane(notesTextArea), + GuiUtil.makeGbc(0, 4, 2, 1, GridBagConstraints.WEST)); + + add(new JLabel("All notes are stored locally only."), + GuiUtil.makeGbc(0, 5, 2, 1, GridBagConstraints.WEST)); + + GridBagConstraints gbc = GuiUtil.makeGbc(0, 6, 1, 1, GridBagConstraints.WEST); + gbc.weightx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + add(saveButton, + gbc); + add(cancelButton, + GuiUtil.makeGbc(1, 6, 1, 1, GridBagConstraints.WEST)); + + cancelButton.addActionListener(e -> { + setVisible(false); + dispose(); + }); + + notesTextArea.setText(notes); + chatNotesTextField.setText(chatNotes); + + pack(); + setLocationRelativeTo(parent); + } + + public void showDialog(ActionListener listener) { + saveButton.addActionListener(e -> { + setVisible(false); + dispose(); + listener.actionPerformed(e); + }); + setVisible(true); + } + + public String getChatNotes() { + return chatNotesTextField.getText(); + } + + public String getNotes() { + return notesTextArea.getText(); + } + + } + + public static void main(String[] args) { + User user = new User("abc", Room.EMPTY); + + UserNotesDialog d = new UserNotesDialog(user, null, "chat notes", "regular notes"); + d.showDialog(e -> { + System.out.println("Save"); + }); + } + +} diff --git a/src/chatty/lang/Strings.properties b/src/chatty/lang/Strings.properties index 2bdea380c..46188903a 100644 --- a/src/chatty/lang/Strings.properties +++ b/src/chatty/lang/Strings.properties @@ -761,6 +761,7 @@ settings.boolean.msgColorsEnabled.tip = If enabled, entries in the table below c settings.section.msgColorsOther = Other Settings settings.boolean.msgColorsPrefer = Prefer Custom Message Colors over Highlight Colors +settings.boolean.msgColorsLinks = Apply Custom/Highlight Colors to links settings.boolean.actionColored = Color action messages (/me) with Usercolor !-- Usercolors --! @@ -1105,6 +1106,18 @@ settings.tabs.option.wrap = Wrap (Multiple Rows) settings.tabs.option.scroll = Scroll (Single Row) settings.boolean.tabsMwheelScrolling = Scroll over tabs with Mousewheel to switch channels settings.boolean.tabsMwheelScrollingAnywhere = Scroll over inputbox to switch channels as well +settings.tabs.bold = Bold +settings.tabs.italic = Italic +settings.tabs.asterisk = Asterisk +settings.tabs.dot1 = Dot Top +settings.tabs.dot2 = Dot Bottom +settings.tabs.line = Line Above +settings.tabs.color1 = New Message Color +settings.tabs.color2 = Highlight Color +settings.label.tabsPopoutDrag = Drag tab outside window: +settings.long.tabsPopoutDrag.option.0 = Do nothing +settings.long.tabsPopoutDrag.option.1 = Popout +settings.long.tabsPopoutDrag.option.2 = Popout as window !-- Notification Settings --! settings.notifications.tab.events = Events @@ -1127,9 +1140,9 @@ settings.notifications.textMatch.tip = Trigger only when notification text (or o settings.notifications.soundFile = Sound file: settings.notifications.soundVolume = Volume: settings.notifications.soundCooldown = Cooldown: -settings.notifications.soundCooldown.tip = Number of seconds that must have passed since the last sound to play again +settings.notifications.soundCooldown.tip = Only play the sound if enough time has passed since this sound was last played. settings.notifications.soundPassiveCooldown = Passive Cooldown: -settings.notifications.soundPassiveCooldown.tip = Number of seconds that must have passed since the sound last would have been played to play again +settings.notifications.soundPassiveCooldown.tip = Only play the sound if enough time has passed since this event was last matched. In other words it will reset the Cooldown every time the sound would have been played, even if it actually wasn't because another sound took precedence or a Cooldown didn't allow it. notification.type.stream_status.tip = When a stream changes status (going live, going offline, changing title/game). notification.type.info.tip = Most non-chat messages, such as command responses, Subscribers and more (may cause duplicates in combination with Events like Subscriber Notification or AutoMod Message). diff --git a/src/chatty/lang/Strings_cs.properties b/src/chatty/lang/Strings_cs.properties new file mode 100644 index 000000000..74ea4c616 --- /dev/null +++ b/src/chatty/lang/Strings_cs.properties @@ -0,0 +1,1140 @@ +## +## Contributors: MrJaroslavik +## +## This file is UTF-8 encoded. +## + +!==================! +!== Main Menubar ==! +!==================! + +!-- Main Menu --! +menubar.menu.main = Hlavní Menu +menubar.dialog.connect = Připojit se +menubar.action.disconnect = Odpojit se +menubar.dialog.settings = Nastavení +menubar.dialog.login = Účet +menubar.dialog.save = Uložit.. +menubar.application.exit = Ukončit + +!-- View Menu --! +menubar.menu.view = Zobrazení +menubar.setting.ontop = Vždy navrchu +menubar.menu.options = Možnosti +menubar.menu.titlebar = Lišta Názvu +menubar.setting.titleShowUptime = Délka Streamu +menubar.setting.titleLongerUptime = Více Detailní Délka Streamu +menubar.setting.titleLongerUptime.tip = Délka Streamu jako Hodiny a Minuty +menubar.setting.titleShowChannelState = Stav Kanálu +menubar.setting.titleShowChannelState.tip = Zobrazit informace kanálu, jako je pomalý režim, režim pouze pro předplatitele, a podobně +menubar.setting.titleShowViewerCount = Počet Diváků/Uživatelů v chatu +menubar.setting.titleConnections = Ostatní Stav Připojení +menubar.setting.titleConnections.tip = Zobrazit, pokud je připojeno\: [M] PubSub (Protokol Moderování), [F] FFZ Websocket +menubar.setting.simpleTitle = Jednoduchý Název +menubar.setting.simpleTitle.tip = Změnit Název Pouze na "Chatty" +menubar.setting.showJoinsParts = Zobrazit připojení/odpojení se +menubar.setting.showModMessages = Zobrazit přidání/odebrání moderátora +menubar.setting.attachedWindows = Přiložené dialogy +menubar.setting.mainResizable = Velikost okna lze měnit +menubar.dialog.channelInfo = Informace o Kanálu +menubar.dialog.channelAdmin = Správa Kanálu +menubar.dialog.highlightedMessages = Zvýrazněné +menubar.dialog.ignoredMessages = Ignorované +menubar.dialog.search = Najít text.. +menubar.dialog.eventLog = Protokol Událostí + +!-- Channels Menu --! +menubar.menu.channels = Kanály +menubar.dialog.favorites = Oblíbené / Historie +menubar.dialog.streams = Živé Kanály +menubar.dialog.addressbook = Adresář +menubar.dialog.joinChannel = Připojit se ke Kanálu +menubar.rooms.none = Místnosti nenalezeny +menubar.rooms.reload = Znovu načíst místnosti + +!-- Extra Menu --! +menubar.menu.extra = Další +menubar.dialog.toggleEmotes = Emotikony +menubar.dialog.followers = Sledující +menubar.dialog.subscribers = Předplatitelé +menubar.dialog.moderationLog = Protokol Moderace +# This is in context of the "StreamChat" submenu +menubar.dialog.streamchat = Otevřít Dialog +menubar.stream.addhighlight = Přidat Značku Streamu + +!-- Help Menu --! +menubar.menu.help = Pomoc +menubar.action.openWebsite = Webová Stránka +menubar.dialog.updates = Zkontrolovat aktualizace +menubar.about = O nás / Pomoc + +!-- Other --! +menubar.notification.chattyInfo = Prohlédnout důležité zprávy +menubar.notification.update = Je dostupná nová verze Chatty + +!========================! +!== Main Context Menus ==! +!========================! + +!-- Channel/Streams Context Menu Entries --! +channelCm.menu.misc = Různé +channelCm.hostChannel = Hostovat Kanál +channelCm.joinHosted = Připojit se k Hostovanému Kanálu +channelCm.copyStreamname = Zkopírovat Jméno Kanálu +channelCm.follow = Sledovat Kanál +channelCm.unfollow = Zrušit Sledování +channelCm.favorite = Přidat Kanál k Oblíbeným +channelCm.unfavorite = Odebrat Kanál z Oblíbených +channelCm.favoriteGame = Přidat Hru k Oblíbeným +channelCm.unfavoriteGame = Odebrat Hru z Oblíbených +channelCm.speedruncom = Otevřít Speedrun.com +channelCm.closeChannel = Zavřít Kanál +# Uses plural form and shows number when joining more than one channel +channelCm.join = Připojit se k {0,choice,1#kanálu|1<{0} kanálům} + +!-- Other Context Menu Entries --! +textCm.copy = Zkopírovat +textCm.cut = Vyjmout +textCm.paste = Vložit + +!-- User Context Menu Entries --! +# {0} = User Display Name +userCm.user = Uživatel\: {0} +# {0} = Username +userCm.join = Připojit se k \#{0} +userCm.menu.misc = Různé +userCm.menu.openIn = Otevřít v.. +userCm.copyName = Zkopírovat Jméno +userCm.copyDisplayName = Zkopírovat Zobrazované Jméno +userCm.follow = Sledovat +userCm.unfollow = Zrušit Sledování +userCm.ignoreChat = Ignorovat (v chatu) +userCm.ignoreWhisper = Ignorovat (v šeptu) +userCm.setColor = Nastavit barvu +userCm.setName = Nastavit jméno + +!-- Emote Context Menu Entries --! +# {0} = Creator +emoteCm.emoteBy = Emotikon od\: {0} +emoteCm.subEmote = Předplatitelské Emotikony +emoteCm.showDetails = Zobrazit Podrobnosti +# Add emote to ignored emotes +emoteCm.ignore = Ignorovat Emotikon +# Add emote to favorites +emoteCm.favorite = Přidat Emotikon k Oblíbeným +# Remove emote from favorites +emoteCm.unfavorite = Odebrat Emotikon z Oblíbených +emoteCm.showEmotes = Zobrazit Emotikony + +!-- Tray Icon Context Menu Entries --! +trayCm.show = Zobrazit +trayCm.exit = Odejít + +!==================! +!== Live Streams ==! +!==================! +# {0} = Number of Live Streams, {1} = Current sorting +streams.title = Živé Streamy ({0}) [Řazení\: {1}] +streams.sorting.recent = Změněno (Nejnovější) +streams.sorting.recent.tip = Nejnovější stream nebo změněný název/hra +streams.sorting.uptime = Délka Streamu (Nejnovější) +streams.sorting.uptime.tip = Nejnovější streamy +streams.sorting.name = Jméno +streams.sorting.name.tip = Jméno kanálu (abecedně) +streams.sorting.game = Hra +streams.sorting.game.tip = Jméno hry (abecedně) +streams.sorting.viewers = Diváci +streams.sorting.viewers.tip = Nejvyšší počet diváků +streams.sortingOption.fav = Oblíbené první +streams.sortingOption.fav.tip = Oblíené kanály z "Kanály - Oblíbené / Historie" a oblíbené Hry (Kategorie) jsou nahoře +streams.cm.menu.sortBy = Řadit dle.. +streams.cm.removedStreams = Odebrané Streamy.. +streams.cm.refresh = Znovu načíst +streams.removed.title = Offline/Left Streamy +streams.removed.button.back = Zpět na Živé Streamy + +!===================! +!== Chat Messages ==! +!===================! +chat.connecting = Pokouším se připojit k {0} +chat.secured = zabezpečeno +chat.joining = Připojování se k {0}.. +chat.joined = Připojili jste se k {0} +chat.left = Odpojili jste se z {0} +chat.joinError.notConnected = Nemůžete se připojit k ''{0}'' (nepřipojeno) +chat.joinError.alreadyJoined = Nemůžete se připojit k ''{0}'' (už jste připojen) +chat.joinError.invalid = Nemůžete se připojit k ''{0}'' (neplatné uživatelské jméno) +chat.joinError.rooms = Místnosti byly z Twitche odstraněny ({0}) +chat.disconnected = Odpojeno +chat.error.unknownHost = Neznámý host +chat.error.connectionTimeout = Spojení vypršelo +chat.error.loginFailed = Nepodařilo se dokončit přihlášení +# {0} = Channel name +chat.error.joinFailed = Nepodařilo se k {0}\: Prosím, zkontrolujte, jestli je uživatelské jméno správně. Pokud máte stejný problém ve všch kanálech (a možná jste nedávno změnili své uživatelské jméno), zkuste a restartujte Chatty. +chat.topic = Téma +# {0} = The name of the entered command +chat.unknownCommand = Neznámý příkaz\: ''/{0}'' (Tip\: Pošlete Příkazy Twitch Chatu s tečkou na začátku, např. ".mods", k obejití příkazového systému Chatty a použití Příkazů Twitch Chatu, o kterých Chatty ještě neví.) + +!====================! +!== General Dialog ==! +!====================! +# For closing a dialog without any further action +dialog.button.close = Zavřít +# For closing a dialog without any action, where the alternative is an action +dialog.button.cancel = Zrušit +# For closing a dialog, where some sort of changes are saved +dialog.button.save = Uložit +# Some sort of changes are saved, while prompting the user for a name of sorts +dialog.button.saveAs = Uložit jako.. +# For closing a dialog, but a bit more general than "Save" +dialog.button.ok = OK +# Perform a test on the current input, e.g. simulating what it would do if actually used like this +dialog.button.test = Test +# Open a help of sorts +dialog.button.help = Pomoc +# Edit an entry or setting +dialog.button.edit = Upravit +# Add an entry of sorts +dialog.button.add = Přidat +# Remove an entry of sorts +dialog.button.remove = Odebrat +# Indiviualize something +dialog.button.customize = Přispůsobit +# Change some setting +dialog.button.change = Změnit + +!====================! +!== General Status ==! +!====================! +status.loading = Načítání.. +status.default = Výchozí + +!====================! +!== Connect Dialog ==! +!====================! +connect.title = Připojit se +connect.button.connect = Připojit se +connect.button.connect.tip = Připojit se k chatu a připojit se k uvedeným kanálům +connect.button.rejoin = Znovu se připojit k otevřeným kanálům +connect.button.configureLogin = Konfigurovat Účet +connect.button.favoritesHistory = Oblíbené / Historie +connect.account = Účet\: +connect.accountEmpty.tip = Žádný Twitch účet není připojen. Pokud chcete používat Chatty, musíte připojit účet na "Konfigurovat Účet". +connect.account.tip = Twitch účet, který máte propojen s Chatty. +connect.channel = Kanál\: +connect.channel.tip = Jeden nebo několik kanálů Twitch chatu, ke kterým se můžete připojit, když se Chatty připojí. +connect.error.noLogin = Nemůžete se připojit\: Pokud chcete používat Chatty, musíte propojit váš Twitch účet. +connect.error.noChannel = Nemůžete se připojit\: Musíte uvést kanál, ke kterému se chcete připojit. + +!==================! +!== Login Dialog ==! +!==================! +login.title = Připojený Twitch Účet +login.accountName = Jméno účtu\: +login.access = Přístup\: +login.button.removeLogin = Odebrat přihlášení +login.button.removeLogin.tip = Odebrat přihlášení pro připojení jiného Twitch účtu, nebo změnu oprávnění. +login.button.verifyLogin = Ověřit přihlášení +login.verifyingLogin = Ověřování přihlášení.. +login.createLogin = +login.button.requestLogin = Připojit Twitch Účet +login.tokenPermissions = Oprávnění přihlášení +login.getTokenInfo = Chcete-li používat Chatty, musíte otevřít níže uvedený odkaz v prohlížeči, přihlásit se na Twitch a udělit přístup, který umožní Chatty posílat chat zprávy vaším jménem, a také další oprávnění vybraná níže. +login.tokenUrlInfo = Zobrazuje URL pro žádost od přihlašovací data.Jedním z níže uvedených tlačítek jej otevřete v prohlížeči. +login.access.chat = Oprávnění chatu +login.access.chat.tip = Používáno pro připojení k chatu a čtení/odesílaní zpráv. +login.access.user = Číst informace o uživateli +login.access.user.tip = Používáno pro požádání o živé streamy, které sledujete. +login.access.editor = Přístup editora +login.access.editor.tip = Pro editaci stavu vašeho streamu pomocí Správy Vysílání. +login.access.broadcast = Upravovat vysílání +login.access.broadcast.tip = Používáno k vytváření Stream Značek/nastavení štítků. +# Commercials as in Advertisements +login.access.commercials = Pouštět reklamy +login.access.commercials.tip = K pouštění reklam na vašem streamu +login.access.subscribers = zobrazit předplatitele +login.access.subscribers.tip = Používáno k zobrazení seznamu vašich předplatitelů +login.access.follow = Sledovat kanály +login.access.follow.tip = Používáno ke sledování kanálů, pomocí kontextového menu kanálu nebo příkazu /follow +login.access.subscriptions = Vaše Předplatná +login.access.subscriptions.tip = Zjistit informace o Vašich vlastních předplatných (používáno k "Moje Emotikony") +login.access.chanMod = Moderovat Kanál +login.access.chanMod.tip = Vyžadováno pro přijímání akcí moderátora v kanálech, kde jste moderátorem +login.access.points = Věrnostní Odměny +login.access.points.tip = Zobrazit více detailní informace o odměnách za věrnostní body ve vašem kanále +login.removeLogin.title = Odebrat přihlášení +login.removeLogin = Toto odebere Přihlašovací Token z Chatty. +login.removeLogin.revoke = Odvolat Token\: Odebere včechny přístupy tohoto Tokenu (obvykle doporučeno) +login.removeLogin.remove = Odebrat Token\: Odebere Token z Chatty, ale stále platný +login.removeLogin.note = Poznámka\: K odstranění přístupu pro všechny tokeny spojené s Chatty běžte na twitch.tv/settings/connections a odpojte aplikaci. +login.removeLogin.button.revoke = Odvolat Token +login.removeLogin.button.remove = Odebrat Token + +!===================! +!== Save Settings ==! +!===================! +saveSettings.title = Uložit Nastavení do Souboru +saveSettings.text = Uložit Nastavení do složky nyní? (Rovněž se automaticky ukládají, když je Chatty spuštěné.) +saveSettings.textBackup = Můžete také vytvořit volitelné manuální zálohu. Zálohy, můžete zobrazit v nastavení. +saveSettings.saveAndBackup = Uložení & Záloha + +!=================! +!== Join Dialog ==! +!=================! +join.title = Připojit se k Kanálu +# Uses plural form when joining more than one channel +join.button.join = Připojit se k {0,choice,1#kanálu|1F, Del pro smazání, nebo klikněte pravým tlačítkem myši k otevření kontextového menu. + +!-- Select Game --! +admin.game.title = Vybrat hru +# HTML +admin.game.info =

    Zadejte část názvu hry a stiskněte Enter nebo klikněte na 'Hledat', poté z výsledků vyberte hru (použitím Twitch názvů zajistí, že se zobrazíte ve správném adresáři).

    +admin.game.button.search = Hledat +admin.game.button.clearSearch = Vyčistit hledání +admin.game.button.favorite = Přidat do oblíbených +admin.game.button.unfavorite = Odebrat z oblíbených +admin.game.searching = Hledání.. +# {0} = Number of search results, {1} = Number of favorites +admin.game.listInfo = Hledání\: {0} / Oblíbené\: {1} + +!-- Select Tags --! +admin.tags.title = Vyerte až {0} štítků +admin.tags.listInfo = Štítky\: {0}/{1} - Oblíbené\: {2}/{3} +admin.tags.info = Dvojklikem v seznamu přidáte/odeberete štítky. Deaktivujte "Zobrazit Aktivní / Pouze oblíbené" k prohlížení všech štítků. Zadejte text k filtrování současnému seznamu. +admin.tags.button.showAll = Zobrazit pouze Aktivní / Oblíbené +admin.tags.button.clearFilter = Vyčistit filtrování +# Favorite as an action, to add to favorites +admin.tags.button.favorite = Přidat k oblíbeným +# Unfavorite as an action, to remove from favorites +admin.tags.button.unfavorite = Odebrat z oblíbených +admin.tags.button.addSelected = Přidat vybrané +admin.tags.button.remove.tip = Odebrat ''{0}'' +admin.tags.cm.replaceAll = Nahradit Vše +admin.tags.cm.replace = Nahradit ''{0}'' + +!=========================! +!== Channel Info Dialog ==! +!=========================! +channelInfo.title = Kanál\: {0} +channelInfo.title.followed = sledováno +channelInfo.status = Stav +channelInfo.history = Stav (Historie) +channelInfo.playing = Hraje +channelInfo.viewers = Diváků +channelInfo.streamOffline = Stream neběží +channelInfo.noInfo = [Bez Informací o Streamu] +channelInfo.cm.copyAllCommunities = Zkopírovat Vše +channelInfo.offline = Offline +channelInfo.offline.tip = Délka posledního vysílání (pravděpodobně cca.)\: {0} +# "With PICNIC" refers to the uptime that disregards small stream downtimes (so it's longer) +channelInfo.offline.tip.picnic = Délka posledního vysílání (pravděpodoně cca.)\: {0} (S PICNIC\: {1}) +channelInfo.uptime = Živě\: {0} +channelInfo.uptime.tip = Stream začal\: {0} +channelInfo.uptime.picnic = Živě\: {0} ({1}) +# "With PICNIC" refers to the uptime that disregards small stream downtimes (so it's longer) +channelInfo.uptime.tip.picnic = Stream začal\: {0} (S PICNIC\: {1}) +channelInfo.viewers.latest = poslední\: {0} +channelInfo.viewers.now = nyní\: {0} +channelInfo.viewers.hover = Diváci\: {0} +channelInfo.viewers.min = min\: {0} +channelInfo.viewers.max = max\: {0} +channelInfo.viewers.noHistory = Ještě bez historie diváků +channelInfo.viewers.cm.timeRange = Časový Úsek +channelInfo.viewers.cm.timeRange.option = {0} {0,choice,1#Hodina|1Poznámka\: Obecnou velikost písma můžete pro nějaké Vzhledy & Pocity upravit v nastavení 'Vzhled'. +settings.otherFonts.restrictedInfo = Výběr písem je omezen, aby nedocházelo k chybě, která narušuje záložní mechanismus písem. + +!-- Choose font --! +settings.chooseFont.title = Vybrat písmo +settings.chooseFont.selectFont = Vybrat rodinu písem a velikost +settings.chooseFont.preview = Náhled +settings.chooseFont.enterText = Vložte další text o otestování +settings.chooseFont.bold = Tučně +settings.chooseFont.italic = Kurzíva + +!-- Startup --! +settings.section.startup = Spuštění +settings.boolean.splash = Zobrazit úvodní obrazovku +settings.boolean.splash.tip = Zobrazí se malé okno, které označuje, že se Chatty spouští +settings.startup.onStart = Na začátku\: +settings.startup.channels = Kanály\: +settings.startup.channels.tip = Jeden nebo více kanálů, kterým se připojíte, pokud je odpovídajícím způsobem nastaveno předchozí nastavení +settings.startup.option.doNothing = Nedělat nic +settings.startup.option.openConnect = Otevřít připojovací dialog +settings.startup.option.connectJoinSpecified = Připojit se a připojit se k uvedeným kanálům +settings.startup.option.connectJoinPrevious = Připojit se a připojit se k minule otevřeným kanálům +settings.startup.option.connectJoinFavorites = Připojit se a připojit se k oblíbeným kanálům + +!-- Look & Feel --! +settings.section.lookandfeel = Vzhled & Pocit +settings.laf.lookandfeel = Vzhled & Pocit\: +settings.laf.font = Písmo\: +settings.laf.option.defaultFont = Výchozí +settings.laf.option.smallFont = Malé Písmo +settings.laf.option.largeFont = Velké Písmo +settings.laf.option.giantFont = Obří Písmo +settings.boolean.lafNativeWindow = Použít nativní okno +settings.boolean.lafNativeWindow.tip = Použít nestylované okno, které může nabídnout více funkcí, například přichycení okna +settings.laf.restartRequired = (Všechny změny se projeví až po restartu Chatty.) +# HTML +settings.laf.info = Tato stránka mění celkový vzhled programu. Uvedené barvy, písma a další styly chatu lze nastavit na dalších stránkách nastavení. +settings.label.lafGradient = Sklon\: +settings.long.lafGradient.option.0 = Žádné +settings.label.lafScroll = Posuvník\: +settings.label.lafStyle = Styl\: +settings.laf.colors = Barvy\: +settings.label.lafVariant = Varianta\: +settings.long.lafVariant.option.0 = Normální +settings.section.preview = Náhled +settings.laf.previewInfo = Náhled aktuálního nastavení na této stránce. Aby se některé věci po změně nastavení správně zobrazily, je nutné restartovat Chatty. + +!-- Language --! +settings.section.language = Jazyk +settings.language.language = Jazyk\: +settings.language.option.defaultLanguage = Výchozí +settings.language.info = Všimněte si, že nápověda a některé další části programu nejsou přeloženy, protože by bylo příliš mnoho práce na aktualizaci. +settings.label.timezone = Časová zóna\: + +!-- Setting Management --! +settings.section.settings = Správa Nastavení +# {0} = Details on how settings dir was set +settings.directory.info = Adresář nastavení ({0})\: +settings.directory.default = výchozí +# {0} = The commandline argument, e.g. "-d" +settings.directory.argument = nastaveno argumentem {0} +settings.directory.invalid = Neplatná sada složek s argumentem -d\: +settings.backup.button.open = Zobrazit Zálohy +settings.backup.title = Zálohy + +!-- Chat Colors --! +settings.section.colors = Přizpůsobit Barvy Chatu +settings.colors.general.backgroundColor = Barva Pozadí +settings.colors.general.foregroundColor = Barva Popředí +settings.colors.heading.misc = Různé Barvy +settings.colors.heading.highlights = Zvýrazněné Zprávy +settings.colors.heading.searchResult = Výsledek Hledání +settings.colors.background = Pozadí +settings.boolean.alternateBackground = Použít střídavé pozadí +settings.colors.background2 = Pozadí 2 +settings.colors.button.switchBackgrounds = Prohodit Pozadí +settings.colors.button.switchBackgrounds.tip = Prohodit barvy Pozadí a Pozadí 2, např. otestování čitelnosti barev popředí. +settings.boolean.messageSeparator = Zobrazit řádky oddělující zprávy +settings.colors.messageSeparator = Oddělovač Zpráv +settings.colors.foreground = Zpráva chatu +settings.colors.info = Informační zpráva +settings.colors.compact = Kompaktní (např. MOD/UNMOD) +settings.colors.highlight = Text (Zvýrazněná zpráva) +settings.boolean.highlightBackground = Použít Pozadí Zvýrazněné Zprávy +settings.colors.highlightBackground = Pozadí (Zvýrazněná zpráva) +settings.colors.inputBackground = Pozadí Vstupu +settings.colors.inputForeground = Text Vstupu +settings.boolean.timestampColorEnabled = Použít vlastní barvu časových značek +settings.label.timestampColorInherit = Zdědit nestandardní barvu\: +settings.label.timestampColorInherit.tip = Pokud je povoleno, časová značka dědí barvu z např. informační nebo zvýrazněné zprávy (namísto použití vlastní barvu časové značky). Při nižších procentech se jas zděděné barvy upraví tak, aby více odpovídal vlastní barvě časové značky. +settings.colors.timestamp = Časová značka +settings.colors.searchResult = Výsledek Hledání +settings.colors.searchResult2 = Zvýraznění Výsledek Hledání +settings.colors.lookandfeel = Poznámka\: Celkový vzhled programu ovlivňuje Vzhled & Dojem na stránce nastavení „Vzhled“. +settings.colors.button.choose = Vybrat +settings.colorChooser.title = Změnit Barvu\: {0} +settings.colorChooser.button.useSelected = Použít vybranou barvu +settings.colorPresets.colorPresets = Předvolby Barev +settings.colorPresets.option.default = Výchozí +settings.colorPresets.option.dark = Tmavý +settings.colorPresets.option.dark2 = Tmavý 2 +settings.colorPresets.button.save = Uložit +settings.colorPresets.button.saveAs = Uložit jako.. +settings.colorPresets.button.delete = Smazat +settings.colorPresets.info = (Předvolby se ukládají přímo do nastavení) + +!-- Message Colors --! +settings.section.msgColors = Vlastní Barva Zpráv +settings.boolean.msgColorsEnabled = Povolit vlastní barvu zprávy +settings.boolean.msgColorsEnabled.tip = Pokud je povoleno, položky v tabulce níže mohou ovlivnit barvy zpráv chatu +settings.section.msgColorsOther = Ostatní Nastavení +settings.boolean.msgColorsPrefer = Upřednostňuje Vlastní Barvy Zpráv před Barvou Zvýraznění +settings.boolean.actionColored = Barevné akční zprávy (/me) s Barvou Uživatele + +!-- Usercolors --! +settings.section.usercolorsOther = Ostatní Nastavení +settings.string.nickColorCorrection = Oprava Čitelnosti Barvy Uživatele\: +settings.string.nickColorCorrection.option.off = Vypnuto +settings.string.nickColorCorrection.option.normal = Normalní +settings.string.nickColorCorrection.option.strong = Silný +settings.string.nickColorCorrection.option.old = Starý +settings.string.nickColorCorrection.option.gray = Stupně šedi +settings.label.nickColorBackground = Změnit pozadí, ke zlepšení čitelnosti\: +settings.label.nickColorBackground.tip = Pokud je uživatelské jméno na vlastní řádce pozadí špatně čitelné, použijte místo toho hlavní barvu pozadí (protože uživatelské barvy budou pro hlavní barvu pozadí opraveny) +settings.long.nickColorBackground.option.0 = Vypnuto +settings.long.nickColorBackground.option.1 = Normální +settings.long.nickColorBackground.option.2 = Silný + +!-- Usericons --! +settings.section.usericons = Nastavení Odznaků +settings.boolean.usericonsEnabled = Zobrazit Odznaky +settings.boolean.botBadgeEnabled = Zobrazit Odznak Bota +settings.section.customUsericons = Vlastní Odznaky +settings.boolean.customUsericonsEnabled = Povolit Vlastní Odznaky +settings.customUsericons.info = Tip\: Chcete-li skrýt odznaky, přidejte vlastní odznak bez obrázku. + +!-- Emoticons --! +settings.section.emoticons = Obecné Nastavení Emotikonů +settings.boolean.emoticonsEnabled = Zobraazit emotikony +settings.emoticons.button.editIgnored = Upravit Ignorované Emotikony +settings.emoticons.chatScale = Měřítko (v Chatu)\: +settings.emoticons.dialogScale = Dialog Emotikonů\: +settings.emoticons.maxHeight = Maximální Výška\: +settings.emoticons.maxHeightPixels = pixelů +settings.boolean.closeEmoteDialogOnDoubleClick = Dvojklikem na emotikon zavřete Dialog Emotikonů +settings.emoticons.cheers = Fandění (Bity)\: +settings.emoticons.cheers.option.text = Pouze Text +settings.emoticons.cheers.option.static = Statické Obrázky +settings.emoticons.cheers.option.animated = Animované + +!-- Third-Party Emoticons --! +settings.section.3rdPartyEmotes = Emotikony Třetích Stran +settings.boolean.bttvEmotes = Povolit BetterTTV Emotikony +settings.boolean.showAnimatedEmotes = Zobrazit animované emotikony +settings.boolean.showAnimatedEmotes.tip = Momentálně má GIF emotikony pouze BTTV +settings.boolean.ffz = Povolit FrankerFaceZ (FFZ) +settings.boolean.ffzModIcon = Povolit Moderátorskou Ikonu FFZ +settings.boolean.ffzModIcon.tip = Zobrazit vlastní moderátorskou emotikonu na nějakých kanálech +settings.boolean.ffzEvent = Povolit Vybrané Emotikony FFZ +settings.boolean.ffzEvent.tip = Vybrané Emotikony jsou dostupné na nějakých událostních kanálech (jako Speedrunning Marathons) + +!-- Emoji --! +settings.section.emoji = Emoji +settings.emoji.set = Sada\: +settings.emoji.option.none = Žádné +settings.boolean.emojiReplace = V zadaném textu proměnit text na Emoji +settings.boolean.emojiReplace.tip = Kódy jako \:joy\: zadané do vstupního pole se změní na odpovídající Emoji (Tip\: Použijte Dokončování TABem) + +!-- Ignored Emotes --! +settings.ignoredEmotes.title = Ignorované Emotikony +settings.ignoredEmotes.info1 = Ignorované emotikony se zobrazují pouze jako kód emotikonu a nezmění se na obrázek. +settings.ignoredEmotes.info2 = Můžete použít kontextovou nabídku emotikonů (klepněte pravým tlačítkem myši na emotikon v chatu) k přidání emotikonů do tohoto seznamu. + +!-- Chat --! +settings.boolean.showImageTooltips = Zobrazit Emotikony / Popisky odznaku +settings.boolean.showTooltipImages = V popisech zobrazit obrázek +settings.label.bufferSize = Výchozí velikost vyrovnávací paměti chatu\: +settings.boolean.mentionReplyRestricted = Nabídnout odpověď, pouze když zpráva začíná @@ +settings.boolean.mentionReplyRestricted.tip = Pokud je deaktivováno, jednoduchá @-zmínka na začátku zprávy bude nabízet odeslání zprávy jako odpovědi + +!-- Messages --! +settings.section.deletedMessages = Smazané Zprávy (Dočasná Zablokování/Trvalá Zablokování) +settings.option.deletedMessagesMode.delete = Smazat Zprávu +settings.option.deletedMessagesMode.strikeThrough = Přeěkrtnutá +settings.option.deletedMessagesMode.strikeThroughShorten = Přeškrtnutá, kratší +settings.deletedMessages.max = max. +settings.deletedMessages.characters = znaků +settings.boolean.banDurationAppended = Zobrazit délku zablokování +settings.boolean.banDurationAppended.tip = Zobrazuje dobu v sekundách pro dočasná zablokování za poslední odstraněnou zprávou +settings.boolean.banReasonAppended = Zobrazit důvody zablokování (pouze moderátoři) +settings.boolean.banReasonAppended.tip = Zobrazuje důvod zablokování za poslední smazanou zprávou (pouze moderátoři, kromě vašich vlastních zablokování) +settings.boolean.showBanMessages = Zobrazit samostatné zprávy o zablokování, s následujícími možnostmi\: +settings.boolean.banDurationMessage = Zobrazit důvod zablokování +settings.boolean.banDurationMessags.tip = Zobrazuje dobu v sekundách pro dočasná zablokování v samostatných zprávách zablokování +settings.boolean.banReasonMessage = Zobrazit důvod zablokování (pouze moderátoři) +settings.boolean.banReasonMessage.tip = Zorazuje důvod zablokování v samostatných zprávách o zablokování (pouze moderátoři, s výjimkou vlastních zablokování) +settings.boolean.combineBanMessages = Zkombinovat zprávy o zablokování +settings.boolean.combineBanMessages.tip = Kombinuje podobné zprávy o zablokování do jedné a přidává počet zablokování +settings.boolean.clearChatOnChannelCleared = Vyčistit chat, když je vyčištěn moderátorem +settings.section.otherMessageSettings = Ostatní +settings.otherMessageSettings.timestamp = Časová značka\: +settings.otherMessageSettings.customizeTimestamp = Přizpůsobit časové razítko +settings.otherMessageSettings.customizeTimestamp.info = Časové razítko lze dále přizpůsobit v části „Barvy Chatu“ a „Písma“. +settings.boolean.showModMessages = Zobrazit mod/unmod (nespolehlivé) +settings.boolean.showModMessages.tip = Zobrazit přidání/odebrání moderátora, když byl někdo přidán/odebrán jako moderátor nebo se připojil/odpojil z kanálu. +settings.boolean.showJoinsParts = Zobrazit připojení/odpojení se (nespolehlivé) +settings.boolean.printStreamStatus = Zobrazit stav streamu (např. název/hra) v chatu +settings.boolean.printStreamStatus.tip = Zobrazit na vstupu a když se změní + +!-- Moderation --! +settings.section.modInfos = Informace Pouze pro Moderátory +settings.boolean.showModActions = Zobrazit Moderátorské Akce v chatu (podobné jako ) +settings.boolean.showModActions.tip = Zobrazit, jaké příkazy moderátoři provádějí, kromě vašich vlastních (také můžete otevřít v Další - Protokol Moderace). +settings.boolean.showModActionsRestrict = Skrýt, pokud je již přidružená akce v chatu viditelná (např. Zablokování) +settings.boolean.showModActionsRestrict.tip = Pokud se tato akce režimu zobrazí připojením @Mod k existující informační zprávě v chatu, nezobrazí samostatnou zprávu akce režimu. +settings.boolean.showActionBy = Připojit, který @Mod způsobil akci (např. Zablokování) +settings.boolean.showActionBy.tip = Pokud několik moderátorů provede stejnou akci v krátkém čase, může být toto zobrazení nepřesné. +settings.section.userDialog = Dialog Uživatele +settings.boolean.closeUserDialogOnAction = Zavřít Dialog Uživatele, když provedete akci (Tlačítko) +settings.boolean.closeUserDialogOnAction.tip = Po kliknutí na tlačítko se dialogové okno Informace o uživateli automaticky zavře. +settings.boolean.openUserDialogByMouse = Vždy otevřít Dialog Uživatele poblíž ukazatele myši +settings.boolean.reuseUserDialog = Znovu použít již otevřený Dialog Uživatele stejného uživatele +settings.boolean.reuseUserDialog.tip = Při otevírání dialogového okna uživatele u uživatele, který již má jeden otevřený, neotvírejte / nepřepínejte jiný. +settings.long.clearUserMessages.option.-1 = Nikdy +settings.long.clearUserMessages.option.3 = 3 Hodiny +settings.long.clearUserMessages.option.6 = 6 Hodin +settings.long.clearUserMessages.option.12 = 12 Hodin +settings.long.clearUserMessages.option.24 = 24 Hodin +settings.long.clearUserMessages.label = Vymazat historii zpráv neaktivních uživatelů po době\: +settings.label.banReasonsHotkey = Zástupce k otevření seznamu důvodů zákazu\: +settings.label.banReasonsInfo = Důvody zákazu lze upravit přímo v Dialogu Uživatele + +!-- Names --! +settings.label.displayNamesMode = Jména v Chatu\: +settings.label.displayNamesMode.tip = Lokalizovaná jména mohou obsahovat ne-anglické znaky. +settings.label.displayNamesModeUserlist = Jména v Seznamu Uživatelů\: +settings.label.displayNamesModeUserlist.tip = Lokalizovaná jména mohou obsahovat ne-anglické znaky. +settings.label.mentions = Klikatelná Označení\: +settings.label.mentions.tip = Uživatelská jména uživatelů, kteří nedávno chatovali, jsou klikatelná a zdůrazňují se v chatových zprávách. +settings.label.mentionsInfo = Klikatelná Označení (Informace)\: +settings.label.mentionsInfo.tip = Stejné jako výše, kromě Informačních zpráv (např. odpovědi na příkazy, AutoMod) +settings.label.mentionsBold = Tučně +settings.label.mentionsUnderline = Podtrže +settings.label.mentionsColored = Barevné +settings.label.mentionsColored.tip = Klikatelná označení v barvě uživatele +settings.long.markHoveredUser.option.0 = Vypnuto +settings.long.markHoveredUser.option.1 = Vše / Vždy +settings.long.markHoveredUser.option.2 = Pouze Označení +settings.long.markHoveredUser.option.3 = Pouze Označení, vše při držení Ctrl +settings.long.markHoveredUser.option.4 = Pouze při držení Ctrl +settings.label.markHoveredUser = Označit uživatelské jméno, na které jste najeli myší +settings.label.markHoveredUser.tip = Zdůraznit další výskyty uživatelského jména, na které najedete myší v chatu. +settings.label.mentionMessages = Zobrazit zprávy při najetí myší\: +settings.long.mentionMessages.option.0 = Vypnuto + +!-- Highlight --! +settings.section.highlightMessages = Zvýraznit Zprávy +settings.boolean.highlightEnabled = Povolit Zvýraznění +settings.boolean.highlightEnabled.tip = Zprávy odpovídající kritériím se zobrazí v jiné barvě, přidají se do okna Zvýrazněné Zprávy a spustí se upozornění (ve výchozím nastavení). +settings.boolean.highlightUsername = Zvýraznit vlastní jméno +settings.boolean.highlightUsername.tip = Zvýrazní zprávy obsahující vaše aktuální uživatelské jméno, i když jste je nepřidali do seznamu. +settings.boolean.highlightNextMessages = Zvýraznit odpověď +settings.boolean.highlightNextMessages.tip = Zvýraznit zprávy od stejného uživatele, které jsou napsány krátce po posledním zvýrazněném. +settings.boolean.highlightOwnText = Zkontrolovat zvýraznění vlastního textu +settings.boolean.highlightOwnText.tip = Umožňuje zvýraznění vlastních zpráv, jinak nebudou vaše vlastní zprávy nikdy zvýrazněny. Dobré pro testování. +settings.boolean.highlightIgnored = Kontrolovat ignorované zprávy +settings.boolean.highlightIgnored.tip = Umožňuje zvýraznění ignorovaných zpráv, jinak nebudou ignorované zprávy nikdy zvýrazněny. +settings.boolean.highlightMatches = Označit shodující se Zvýraznění/Ignorování +settings.boolean.highlightMatches.tip = Obklopuje části textu, které způsobily zvýraznění obdélníkem (a v oknech Zvýrazněné/Ignorované zprávy). +settings.boolean.highlightMatchesAll = Označte všechny výskyty +settings.boolean.highlightMatchesAll.tip = Také označit shodující se zvýrazněné, např. odkazy a zmínky +settings.boolean.highlightByPoints = Vybarvit zprávy zvýrazněné body kanálu +settings.boolean.highlightByPoints.tip = U zpráv, které jsou „zvýrazněny“ pomocí bodů kanálu, použijte výchozí barvy zvýraznění. Chcete-li mít aktuální zvýraznění, přidejte do seznamu „config\: hl“. Chcete-li upravit barvu, přidejte v nastavení „Barvy Zpráv“ „config\: hl“. + +!-- Ignore --! +settings.section.ignoreMessages = Ignorovat Zprávy +settings.boolean.ignoreEnabled = Povolit Ignoraci + +!-- Filter --! +settings.section.filterMessages = Filtrování Částí Zpráv +settings.boolean.filterEnabled = Povolit Filtry + +!-- Log to file --! +settings.log.section.channels = Kanály k protokolování do souboru +settings.log.loggingMode = Režim Protokolování\: +settings.option.logMode.always = Vždy +settings.option.logMode.blacklist = Černá Listina +settings.option.logMode.whitelist = Bílá Listina +settings.option.logMode.off = Vypnuto +settings.log.noList = +settings.log.alwaysInfo = Jsou protokolovány všechny kanály. +settings.log.blacklistInfo = Jsou protokolovány všechny kanály, kromě těchto. +settings.log.whitelistInfo = Jsou protokolovány pouze kanály na seznamu. +settings.log.offInfo = Nic není protokolováno. +settings.boolean.logMessage = Zprávy Chatu +settings.boolean.logMessage.tip = Protokolovat normální zprávy chatu +settings.boolean.logMessage.template = Šablona Zpráv Chatu +settings.boolean.logInfo = Informace Chatu +settings.boolean.logInfo.tip = Protokolovat informace jako název streamu, zprávy z Twitche, připojování, odpojování +settings.boolean.logBan = Trvalá/Dočasná zablokování +settings.boolean.logBan.tip = Protokolovat Trvalá/Dočasná zablokování jako BAN zprávy +settings.boolean.logDeleted = Smazané Zprávy +settings.boolean.logDeleted.tip = Protokolovat Smazané Zprávy jako DELETED zprávy +settings.boolean.logMod = Přidání/Odebrání moderátora +# MOD/UNMOD should not be translated since it refers to how it appears in the log +settings.boolean.logMod.tip = Protokolovat MOD/UNMOD zprávy (když je uživatel přidán/odebrán jako moderátor nebo se připojí/odpojí, velmi nespolehlivé) +settings.boolean.logJoinPart = Připojení/Odpojení se +# JOIN/PART should not be translated since it refers to how it appears in the log +settings.boolean.logJoinPart.tip = Protokolovat JOIN/PART zprávy (když se uživatelé připojí/odpojí z chatu, velmi nespolehlivé) +settings.boolean.logSystem = Systémové Informace +settings.boolean.logSystem.tip = Protokolovat systémové zprávy Chatty, jako kontrola verze, odpovědí na nastavení příkazů, a tak dále.. +settings.boolean.logViewerstats = Statistiky Diváků +settings.boolean.logViewerstats.tip = Protokolovat statistiky diváků (jakési shrnutí počtu diváků) v polopravidelném intervalu +settings.boolean.logViewercount = Počet Diváků +settings.boolean.logViewercount.tip = Protokolovat počet díváků, vždy, když jsou přijata nová data +settings.boolean.logModAction = Akce Moderátorů +settings.boolean.logModAction.tip = Protokolovat kdo provedl jaký příkaz (pouez moderátoři) +# Bits as in the Twitch donation currency (use the term that Twitch uses for your language) +settings.boolean.logBits = Bity +# Bits as in the Twitch donation currency (use the term that Twitch uses for your language) +settings.boolean.logBits.tip = Protokolovat množství darovaných bitů, odděleně před zprávou +settings.boolean.logIgnored = Ignorované Zprávy +settings.boolean.logIgnored.tip = Protokolovat zprávy, které jsou ignorovány také seznamem ignorovaných +settings.boolean.logIgnored2 = Ignorované Zprávy +settings.boolean.logIgnored2.tip = Protokolovat Ignorované Zprávy do samostatného souboru protokolu „ignored“ (není ovlivněno nastavením „Režim Protokolování“) +settings.boolean.logHighlighted2 = Zvýrazněné Zprávy +settings.boolean.logHighlighted2.tip = Protokolovat Zvýrazněné Zprávy do samostatného souboru protokolu „highlighted“ (není ovlivněno nastavením „Režim Protokolování“) +settings.log.section.other = Ostatní Nastavení +settings.log.folder = Složka\: +settings.log.splitLogs = Zničit Protokoly\: +settings.option.logSplit.never = Nikdy +settings.option.logSplit.daily = Denně +settings.option.logSplit.weekly = Týdně +settings.option.logSplit.monthly = Měsičně +settings.boolean.logSubdirectories = PodAdresáře Kanálu +settings.boolean.logSubdirectories.tip = Uspořádá protokoly do podadresářů kanálu. +settings.boolean.logLockFiles = Zamknout Soubory +settings.boolean.logLockFiles.tip = Získá exkluzivní přístup k souborům protokolů, aby zajistil, že do něj nezapíše žádný jiný program. Může také někdy zabránit čtení. +settings.log.timestamp = Časová Značka\: +settings.option.logTimestamp.off = Vypnuto + +!-- Other --! +settings.label.titleAddition = Předřadit před název okna\: + +!-- Commands --! +settings.label.channelContextMenu = Kontextové Menu Kanálu\: +settings.label.channelContextMenu.tip = Otevřete pravým klikem, kdekoli v kanálu +settings.label.streamsContextMenu = Kontextové Menu Streamu\: +settings.label.streamsContextMenu.tip = Otevřete pravým klikem na jeden nebo více streamů v seznamu Živých Přenosů, Oblíbených nebo Sledujících +settings.label.userContextMenu = Kontextové Menu Uživatele\: +settings.label.userContextMenu.tip = Otevřete pravým klikem na uživatele v chatu, seznamu uživatelů nebo dialogu Sledujících +settings.label.timeoutButtons = Dialogová Tlačítka Uživatele\: +settings.label.timeoutButtons.tip = Otevřete klikem na uživatele v chatu nebo dvojklikem na jméno v seznamu uživatelů nebo dialogu sledujících +settings.label.textContextMenu = Vybraná Textová Kontextová Menu\: +settings.label.textContextMenu.tip = Otevřete pravým klikem na vybraný text v chatu a nějakých ostatních místech +settings.label.adminContextMenu = Správcovské Kontextové Menu\: +settings.label.adminContextMenu.tip = Otevřete pravým klikem na část Správcovského Dialogu (Zobrazení - Správa Kanálu) + +!-- TAB Completion --! +settings.section.completion = Dokončování TABem (Jména, Emotikony, Příkazy) +settings.boolean.completionEnabled = Povolit Dokončování TABem +settings.boolean.completionEnabled.tip = Držení TABu nebo Shift-TAB v zadávacím boxu chatu může dokončit, mnoho věcí, ale ale zabrání tomu, aby byl TAB použit pro procházení fokusem, pokud není textové pole prázdné +settings.completion.option.names = Jména +settings.completion.option.emotes = Emotikony +settings.completion.option.namesEmotes = Jména, pak Emotikony +settings.completion.option.emotesNames = Emotikony, pak Jména +settings.completion.option.custom = Vlastní Dokončování +settings.completion.info = Tip\: Předpona s @ vždy získá jména, \: pro Emoji a co je nastaveno níže pro Emotikony. (Příklad\: \:think + TAB > \:thinking\:) +settings.label.completionEmotePrefix = Předpona Twitch Emotikonů\: +settings.label.completionEmotePrefix.tip = Použít tuto předponu vždy k dokončování Emotikonů (Emoji jsou vždy '\:'). +settings.completionEmotePrefix.option.none = Žádné +settings.long.completionMixed.option.0 = Nejlepší shoda +settings.long.completionMixed.option.1 = Emoji první +settings.long.completionMixed.option.2 = Emotes první +settings.long.completionMixed.tip = Jak řadit, když jsou Emotikony a Emoji promíchány +settings.string.completionSearch = Hledat v Emotikonech / Příkazech\: +# Example: Emote "joshWithIt" would match only when entering the beginning of "joshWithIt" +settings.string.completionSearch.option.start = Na začátku jména +# Example: Emote "joshWithIt" would match when entering the beginning of "joshWithIt", "With" or "It" +settings.string.completionSearch.option.words = Na začátku a kapitalizovaná slova +# Example: Emote "joshWithIt" would match when entering any part of the name (even just "t") +settings.string.completionSearch.option.anywhere = Kdekoli ve jméně +settings.section.completionNames = Lokalizovaná Jména +settings.boolean.completionPreferUsernames = U příkazů založených na uživatelských jménech, preferovat běžné jméno +settings.boolean.completionAllNameTypes = Ve výsledku zahrnout všechny typy jmen (Normální/Lokalizovaná/Vlastní) +settings.boolean.completionAllNameTypesRestriction = Pouze, když nejsou více, než dvě shody +settings.section.completionAppearance = Vzhled / Chování +settings.boolean.completionShowPopup = Zobrazit vyskakovací okno s +settings.boolean.completionShowPopup.tip = Zobrazit aktuální výsledky hledání dokončení ve vyskakovacím okně +settings.label.searchResults = výsledky hledání +settings.boolean.completionAuto = U některých typů automaticky zobrazit (jako Emoji) +settings.boolean.completionCommonPrefix = Vyplnit běžnou předponu (pokud je více, než jedna shoda) +settings.completion.nameSorting = Řazení Jmen\: +settings.completion.option.predictive = Předvídatelné +settings.completion.option.alphabetical = Abecední +settings.completion.option.userlist = Stejné jako Seznam Uživatelů +settings.boolean.completionSpace = Připojit mezeru + +!-- Dialogs Settings --! +settings.section.dialogs = Umístění/Velikost dialogů +settings.dialogs.restore = Obnovit dialogy\: +settings.dialogs.option.openDefault = Vždy otevřít ve výchozím umístění +settings.dialogs.option.keepSession = Udržovat polohu během relace +settings.dialogs.option.restore = Zachovat umístění/velikost z poslední relace +settings.dialogs.option.reopen = Znovuotevřít z poslední relace +settings.boolean.restoreOnlyIfOnScreen = Obnovit polohu, pokud je na obrazovce +settings.boolean.attachedWindows = Přesunout dialogy při přesunu hlavního okna + +!-- Minimizing Settings --! +settings.section.minimizing = Minimalizace / Zásobník +settings.boolean.minimizeToTray = Minimalizovat do zásobníku +settings.boolean.closeToTray = Zavřít do zásobníku +settings.boolean.trayIconAlways = Vždy zobrazit ikonu zásobníku +settings.boolean.hideStreamsOnMinimize = Skrýt okno Živých Streamů při minimalizaci +settings.boolean.hideStreamsOnMinimize.tip = Automaticky skryje okno Živých Streamů, když minimalizujete Hlavní okno +settings.boolean.singleClickTrayOpen = Jedním kliknutím na ikonu zásobníku se zobrazíte/skryjete +settings.boolean.singleClickTrayOpen.tip = Pokud toto políčko nezaškrtnete, může být nutné dvojité kliknutí (závisí na platformě) + +!-- Other Window Settings --! +settings.section.otherWindow = Ostatní +# Enable the confirmation dialog when opening an URL out of Chatty +settings.boolean.urlPrompt = "Otevřít URL" Prompt +settings.boolean.chatScrollbarAlways = Vždy zobrazit posuvník chatu +settings.window.defaultUserlistWidth = Výchozí Šířka Seznamu Uživatelů\: +# In context of Userlist Width, so don't need to repeat "Userlist" +settings.window.minUserlistWidth = Min. Šířka\: +settings.boolean.userlistEnabled = Ve výchozím nastavení povolit seznam uživatelů +settings.boolean.userlistEnabled.tip = Ve výchozím nastavení můžete k přepnutí seznamu uživatelů použít Shift + F10 +settings.boolean.inputEnabled = Ve výchozím nastavení povolit vstupní pole +settings.boolean.inputEnabled.tip = Ve výchozím nastavení můžete k přepínání vstupního pole použít kombinaci kláves Ctrl + F10 +settings.label.inputFocus = Zaměření vstupního pole chatu\: +settings.long.inputFocus.option.0 = Výchozí fokus vstupu +settings.long.inputFocus.option.1 = Zaostřit vstup při návratu do okna +settings.long.inputFocus.option.2 = Automaticky neměnit zaměření + +!-- Tab Settings --! +settings.section.tabs = Nastavení Záložej +settings.tabs.order = Řazení Záložek\: +settings.tabs.option.normal = Normální (jak je otevřeno) +settings.tabs.option.alphabetical = Abecedně +settings.tabs.placement = Umístění Záložky\: +settings.tabs.option.top = Nahoře +settings.tabs.option.left = Vlevo +settings.tabs.option.right = Vpravo +settings.tabs.option.bottom = Dole +settings.tabs.layout = Rozložení Záložky\: +settings.tabs.option.wrap = Zalomení (Více Řádků) +settings.tabs.option.scroll = Posun (Jeden Řádek) +settings.boolean.tabsMwheelScrolling = Přepínáním karet přes kolečko myši přepínáte kanály +settings.boolean.tabsMwheelScrollingAnywhere = Posunutím vstupního pole můžete také přepínat kanály + +!-- Notification Settings --! +settings.notifications.tab.events = Události +settings.notifications.tab.events.tip = Přidáním položky do tabulky spustíte oznámení nebo zvuk na ploše u konkrétní události +settings.notifications.tab.notificationSettings = Nastavení Oznámení na Ploše +settings.notifications.tab.soundSettings = Nastavení Zvuku Oznámení +# When notifications are turned off +settings.notifications.tab.notificationSettingsOff = Nastavení Zvuku Oznámení (Vypnuto) +# When sounds are turned off +settings.notifications.tab.soundSettingsOff = Nastavení Zvuku Oznámení (Ztišeno) +settings.notifications.column.event = Událost +settings.notifications.column.notification = Oznámení Plochy +settings.notifications.column.sound = Zvuk +settings.boolean.nHideOnStart = Zabránit tomu na začátku +settings.boolean.nHideOnStart.tip = Během prvních 120 sekund po spuštění Chatty nebudou spuštěna žádná oznámení +settings.notifications.event = Událost\: +settings.notifications.channel = Kanál\: +settings.notifications.textMatch = Shoda\: +settings.notifications.textMatch.tip = Spustit, pouze když text oznámení (nebo jiné vlastnosti) odpovídá tomuto nastavení, pomocí formátu zvýraznění shody +settings.notifications.soundFile = Zvukový soubor\: +settings.notifications.soundVolume = Hlasitost\: +settings.notifications.soundCooldown = Cooldown\: +settings.notifications.soundCooldown.tip = Počet sekund, které musí uplynout od posledního zvuku, aby se znovu přehrál +settings.notifications.soundPassiveCooldown = Pasivní Cooldown\: +settings.notifications.soundPassiveCooldown.tip = Počet sekund, které musí uplynout od posledního zvuku, aby se znovu přehrál +notification.type.stream_status.tip = Když se změní stav streamu (začne vysílat, přestane vysílat, změní název/hra). +notification.type.info.tip = Většina ne-chatových zpráv, například odpovědi na příkazy, předplatitelé a další (může způsobit duplicity v kombinaci s událostmi, jako je upozornění předplatitele nebo zprávy AutoModa). +notification.type.message.tip = Jakákoli normální zpráva chatu, kromě Zvýrazněných. +notification.type.new_followers.tip = Noví sledující, jak je uvedeno na "Další - Sledující" (funguje pouze s otevřeným seznamem sledujících). +notification.type.subscriber.tip = Na základě oznámení odběratelů (předplatné, obnovení předplatného) v chatu. +notification.type.highlight.tip = Zprávy zvýrazněné seznamem "Zvýrazněné". +notification.type.join.tip = Uživatel, který se připojil ke kanálu (velmi nespolehlivé). +notification.type.part.tip = Uživatel, který se odpojil z kanálu (velmi nespolehlivé). +notification.type.automod.tip = Zprávy odmítnuté AutoModem (pouze moderátoři). +notification.type.whisper.tip = Šeptání (Je možné, že bude třeba povolit šeptání pod "Pokročilé - Šeptání"). +notification.typeOption.fav = Pouze oblíbené kanály +notification.typeOption.fav.tip = Kanály, které byly přidány k oblíbeným pod "Kanály > Oblíbené / Historie" +notification.typeOption.favGame = Pouze oblíbené hry +notification.typeOption.favGame.tip = Hry, které byly přidány jako oblíbené v kontextové nabídce Živé Přenosy v části „Řadit dle ..“ +notification.typeOption.bits = Pouze zprávy obsahující bity +notification.typeOption.own = Také vlastní zprávy +notification.typeOption.noOffline = Only live streams (ne 'Stream není Živě') +notification.typeOption.hideOnStart = Nespouštět ihned po spuštění Chatty +notification.typeOption.hideOnStart.tip = Během prvních 120 sekund po spuštění Chatty se oznámení nespustí +notification.typeOption.justLive = Pouze streamy, které nedávno začaly (délka < 15m) +notification.typeOption.nowLive = Pouze streamy, které nebyly nedávno živě (> 15m) +notification.typeOption.nowLive.tip = Streamy, které nebyly živě, nebo které Chatty nevidělo jako živé za posledních 15 minut (na délce streamu nezáleží) +notification.typeOption.noUptime = Skrýt délku streamu + +!-- Stream Settings --! +settings.section.streamHighlights = Značky Streamu +settings.section.streamHighlightsCommand = Příkazy Chatu +settings.streamHighlights.info = Značky Streamu (zapisuje délku streamu/poznámku do souboru) mohou být přidány pomocí příkazu /addStreamHighlight, klávesovou zkratkou (Nastavení Klávesových Zkratek) nebo příkaz chatu (který můžete konfigurovat zde). Pro více informací, vizte nápovědu. +settings.streamHighlights.matchInfo = Důrazně doporučujeme omezit příkaz chatu na důvěryhodné uživatele,jako jsou Moderátoři. +settings.streamHighlights.channel = Kanál Příkazu\: +settings.streamHighlights.command = Jméno Příkazu\: +settings.streamHighlights.match = Přístup k Příkazu\: +settings.boolean.streamHighlightChannelRespond = Na příkaz odpovědět zprávou chatu +settings.boolean.streamHighlightChannelRespond.tip = Poslat zprávu do chatu, aby moderátoři viděli, že příkaz byl úspěšný +settings.streamHighlights.responseMsg = Zpráva Odpovědi na Příkaz +settings.boolean.streamHighlightMarker = Vytvořit Značku Streamu, když přidáváte Značku Streamu +settings.boolean.streamHighlightMarker.tip = Kromě zápisu Značky Streamu do souboru vytvořit Značku Streamu + +!-- Hotkey Settings --! +settings.hotkeys.key.button.set = Nastavit kombinaci kláves +settings.hotkeys.key.button.clear = Vyčistit kombinaci kláves +settings.hotkeys.key.set.info = Stiskněte klávesu, kombinaci kláves, nebo ESC ke zrušení +settings.hotkeys.key.set.title = Nastavit kombinaci kláves +# Should be kept short +settings.hotkeys.key.empty = Klávesa nenastavena. +settings.hotkeys.action = Akce\: +settings.hotkeys.delay = Zpoždění\: +settings.hotkeys.delay.tip = Čas (desetiny sekundy), který musí uplynout, aby se akce klávesové zkratky znovu spustila + +!===================! +!== Emotes Dialog ==! +!===================! +# {0} = Name of the channel (or "-" if no channel) +emotesDialog.title = Emotikony (Globální/Předplatitelské/{0}) +emotesDialog.tab.favorites = Oblíbené +emotesDialog.tab.myEmotes = Moje Emotikony +emotesDialog.tab.channel = Kanál +emotesDialog.tab.other = Ostatní +emotesDialog.refresh = Znovu načíst emotikony v aktuální záložce +emotesDialog.refreshInactive = Znovu-načtení není k dispozici +emotesDialog.noFavorites = Nepřidali jste žádné oblíbené emotikony +emotesDialog.noFavorites.hint = (Pravým tlačítkem klikněte na emotikonu a vyberte 'Přidat do oblíbených') +emotesDialog.subscriptionRequired = Chcete-li používat tyto emotikony, musíte si tento kanál předplatit\: +emotesDialog.notFoundFavorites = Oblíbené položky zatím nebyly nalezeny (metadata nebyly načteny)\: +emotesDialog.favoriteCmInfo = (Kliknutím pravým tlačítkem zobrazíte informace a v případě potřeby je uložíte do oblíbených.) +emotesDialog.subEmotesAccess = Přidejte přístup "Moje Předplatná" přes "Hlavní Menu - Účet" pro přesnější zobrazení speciálních emotikonů. +emotesDialog.noSubemotes = Zdá se, že nemáte žádné předplatitelské nebo turbo emoty +emotesDialog.subEmotesJoinChannel = (Musíte být připojen ke kanálu, aby byly rozpoznány.) +emotesDialog.otherSubemotes = Ostatní +emotesDialog.noChannel = Bez kanálu. +emotesDialog.noChannelEmotes = Nebyly nalezeny emotikony pro \#{0} +emotesDialog.noChannelEmotes2 = Nebyly nalezeny FFZ nebo BTTV emotikony. +emotesDialog.channelEmotes = Emoce specifické pro \#{0} +# {0} = Channel name (without leading #) +emotesDialog.backToChannel = Zpět na \#{0} +emotesDialog.subemotes = Předplatitelské Emotikony {0} +emotesDialog.subscriptionRequired2 = (Chcete-li používat tyto emotikony, musíte si předplatit tento kanál.) +emotesDialog.subscriptionRequired3 = (Chcete-li používat tyto emotikony, musíte si předplatit vyšší úroveň.) +emotesDialog.globalTwitch = Globální Twitch Emotikony +emotesDialog.globalFFZ = Globální FFZ Emotikony +emotesDialog.globalBTTV = Globální BTTV Emotikony +# {0} = Emote Code +emotesDialog.details.title = Podrobnosti Emotikonu\: {0} +emotesDialog.details.code = Kód\: +emotesDialog.details.type = Typ\: +emotesDialog.details.id = ID Emotikonu\: +emotesDialog.details.channel = Kanál\: +# Details where the emote can be used (channel/globally) +emotesDialog.details.usableIn = Použitelnost\: +emotesDialog.details.usableInChannel = Kanál +emotesDialog.details.usableEverywhere = Všude +# Details who can use the emote (restricted/everyone) +emotesDialog.details.access = Přístup\: +emotesDialog.details.everyone = Všichni +emotesDialog.details.restricted = Omezeno +emotesDialog.details.accessAvailable = (Máte přístup) +emotesDialog.details.size = Normální Velikost\: +emotesDialog.details.by = Emotikon od\: +emotesDialog.details.info = Klepnutím pravým tlačítkem myši na emotikony zde, nebo v chatu otevřete kontextovou nabídku s informacemi / možnostmi. diff --git a/src/chatty/lang/Strings_de.properties b/src/chatty/lang/Strings_de.properties index bbcf72761..9ef20ea8c 100644 --- a/src/chatty/lang/Strings_de.properties +++ b/src/chatty/lang/Strings_de.properties @@ -90,7 +90,7 @@ channelCm.unfavoriteGame = Spiel entfavorisieren channelCm.speedruncom = Speedrun.com öffnen channelCm.closeChannel = Kanal schließen # Uses plural form and shows number when joining more than one channel -channelCm.join = {0,choice,1\#Kanal|1<{0} Kanälen} beitreten +channelCm.join = {0,choice,1#Kanal|1<{0} Kanälen} beitreten !-- Other Context Menu Entries --! textCm.copy = Kopieren @@ -283,7 +283,7 @@ saveSettings.saveAndBackup = Speichern & Sicherheitskopie !=================! join.title = Kanal beitreten # Uses plural form when joining more than one channel -join.button.join = {0,choice,1\#Kanal|1 puis redémarrer. +chat.error.joinFailed = Impossible de rejoindre {0}\: Veuillez vérifier que le nom de la chaîne soit correct. Si vous rencontrez le même problème sur toutes les chaînes (et que peut-être vous avez changé récemment de nom d''utilisateur), essayez puis redémarrer. chat.topic = Topic # {0} = The name of the entered command -chat.unknownCommand = Commande inconnue\: ''/{0}'' (Astuce\: Utilisez les commandes du chat Twitch avec un point à la place du slash, ex ".mods", afin d'outrepasser les commandes système de Chatty afin de pouvoir utiliser la commande du chat Twitch que Chatty ne reconnait pas encore.) +chat.unknownCommand = Commande inconnue\: ''/{0}'' (Astuce\: Utilisez les commandes du chat Twitch avec un point à la place du slash, ex ".mods", afin d''outrepasser les commandes système de Chatty afin de pouvoir utiliser la commande du chat Twitch que Chatty ne reconnait pas encore.) !====================! !== General Dialog ==! @@ -259,7 +259,7 @@ saveSettings.text = Sauvegarder les paramètres dans un fichier maintenant? (Ils !=================! join.title = Rejoindre la chaîne # Uses plural form when joining more than one channel -join.button.join = Rejoindre {0,choice,1\#la chaîne|1 e riavvia Chatty. -chat.topic = Tema +chat.error.joinFailed = Accesso fallito per {0}\: Per favore controlla che il nome del canale sia corretto. Inoltre, se hai lo stesso problema per tutti i canali (forse di recente hai cambiato il tuo nome utente), prova a seguire questa procedura e riavvia Chatty. +chat.topic = Argomento # {0} = The name of the entered command -chat.unknownCommand = Comando sconosciuto\: ''/{0}'' (Suggerimento\: Invia i comandi per le Chat Twitch con un punto davanti, ad esempio ".mods", per aggirare il sistema di comando di Chatty e usare i comandi per le Chat Twitch di cui Chatty non è ancora a conoscenza.) +chat.unknownCommand = Comando sconosciuto\: ''/{0}'' (Consiglio\: Invia i comandi della chat di Twitch con un punto davanti, ad esempio ".mods", per aggirare il sistema di comandi di Chatty e usare i comandi della chat di Twitch di cui Chatty non è ancora a conoscenza.) !====================! !== General Dialog ==! @@ -188,6 +194,10 @@ dialog.button.test = Test dialog.button.help = Aiuto # Edit an entry or setting dialog.button.edit = Modifica +# Add an entry of sorts +dialog.button.add = Agg. +# Remove an entry of sorts +dialog.button.remove = Rimuovi # Indiviualize something dialog.button.customize = Personalizza # Change some setting @@ -196,67 +206,85 @@ dialog.button.change = Cambia !====================! !== General Status ==! !====================! -status.loading = Caricamento.. +status.loading = Caricamento... !====================! !== Connect Dialog ==! !====================! connect.title = Connetti connect.button.connect = Connetti -connect.button.rejoin = Rientra nel canali aperti -connect.button.configureLogin = Configura login.. -connect.button.favoritesHistory = Preferiti / Cronologia +connect.button.connect.tip = Si connette alla chat e si unisce al canale specificato. +connect.button.rejoin = Rientra nei canali aperti +connect.button.configureLogin = Configura account +connect.button.favoritesHistory = Preferiti / cronologia connect.account = Account\: +connect.accountEmpty.tip = Nessun account di Twitch connesso. Devi connetterlo via "Configura Account" per usare Chatty. +connect.account.tip = L'account Twitch che hai connesso a Chatty. connect.channel = Canale\: +connect.channel.tip = Uno o più chat di Twitch da aprire quando Chatty si connette. +connect.error.noLogin = Connessione fallita\: devi connetterti con il tuo account di Twitch per usare Chatty. +connect.error.noChannel = Connessione fallita\: devi specificare un canale in cui entrare. !==================! !== Login Dialog ==! !==================! -login.title = Configurazione del Login.. +login.title = Account di Twitch connesso login.accountName = Nome Account\: login.access = Accesso\: login.button.removeLogin = Rimuovi Login +login.button.removeLogin.tip = Rimuovi l'accesso se vuoi connetterti con un altro account di Twitch o se vuoi cambiare i permessi. login.button.verifyLogin = Verifica Login login.verifyingLogin = Verifica Login in corso.. login.createLogin = -login.button.requestLogin = Richiesta dati di Login +login.button.requestLogin = Connetti account di Twitch +login.tokenPermissions = Permessi d'accesso +login.getTokenInfo = Per usare Chatty devi aprire il link qui sotto in un browser, accedere a Twitch e dare il permesso a Chatty di inviare messaggi in chat per conto tuo e altri permessi aggiuntivi selezionati qui sotto. +login.tokenUrlInfo = Mostra l'URL per richiedere i dati d'accesso. Usa uno dei bottoni qui sotto per aprire il link in un browser. login.access.chat = Accesso alle chat -login.access.chat.tip = Usato per connettersi alla chat e leggere/inviare messaggi +login.access.chat.tip = Serve a connettersi alla chat e leggere/inviare messaggi login.access.user = Leggere informazioni utenti -login.access.user.tip = Usato per richiedere streaming live che segui +login.access.user.tip = Serve per richiedere gli stream live che segui login.access.editor = Accesso all'editor -login.access.editor.tip = Per modificare lo stato del tuo stream nella finestra di dialogo Amministratore -login.access.broadcast = Modificare trasmissione (broadcast) -login.access.broadcast.tip = Utilizzato per creare indicatori dello stream/impostare i tag dello stream +login.access.editor.tip = Serve per modificare lo stato del tuo stream nella finestra Amministratore Canale +login.access.broadcast = Modifica la trasmissione +login.access.broadcast.tip = Serve per creare indicatori (marker) ed impostare le categorie dello stream. # Commercials as in Advertisements login.access.commercials = Avviare spot pubblicitari -login.access.commercials.tip = Per avviare annunci pubblicitari sul tuo stream -login.access.subscribers = Mostrare Iscritti/Abbonati -login.access.subscribers.tip = Utilizzato per mostrare l'elenco dei tuoi iscritti -login.access.follow = Seguire i canali -login.access.follow.tip = Usato per seguire i canali attraverso il menù contestuale del canale o il comando /follow -login.access.subscriptions = I tuoi Abbonamenti -login.access.subscriptions.tip = Ottenere informazioni sui tuoi abbonamenti (serve per "Mie Emotes") +login.access.commercials.tip = Serve per far partire pubblicità durante il tuo stream +login.access.subscribers = Mostrare abbonati +login.access.subscribers.tip = Serve per mostrare l'elenco dei tuoi abbonati +login.access.follow = Seguire canali +login.access.follow.tip = Serve per seguire i canali attraverso il menù contestuale del canale o il comando /follow +login.access.subscriptions = I tuoi abbonamenti +login.access.subscriptions.tip = Serve per ottenere informazioni sui tuoi abbonamenti (usate per "Le mie Emoticon"). login.access.chanMod = Moderare canali login.access.chanMod.tip = Richiesto per le azioni di moderazione nei canali dove sei moderatore login.access.points = Redenzioni Punti Canale login.access.points.tip = Vedere le redenzioni di ricompense dei Punti Canale sul tuo canale login.removeLogin.title = Rimuovi login -login.removeLogin = Ciò rimuove il token di accesso da Chatty. -login.removeLogin.revoke = Revoca del Token\: rimuove tutti gli accessi per questo token (di solito consigliato) -login.removeLogin.remove = Rimuovi token\: rimuove il Token da Chatty, ma rimane valido +login.removeLogin = Rimuove il token di accesso da Chatty. +login.removeLogin.revoke = Revoca del token\: rimuove tutti gli accessi per questo token (di solito consigliato) +login.removeLogin.remove = Rimuovi token\: rimuove il token da Chatty, ma rimane valido login.removeLogin.note = Nota\: per rimuovere l'accesso per tutti i token associati a Chatty, vai a twitch.tv/settings/connections e disconnetti l'app. -login.removeLogin.button.revoke = Revoca il Token -login.removeLogin.button.remove = Rimuovi Token +login.removeLogin.button.revoke = Revoca il token +login.removeLogin.button.remove = Rimuovi token + +!===================! +!== Save Settings ==! +!===================! +saveSettings.title = Salva file impostazioni +saveSettings.text = Salvare le impostazioni su file ora? (Vengono anche salvate automaticamente mentre Chatty è in esecuzione.) +saveSettings.textBackup = Puoi anche creare dei backup manuali facoltativi. I backup possono essere visualizzati nelle Impostazioni. +saveSettings.saveAndBackup = Salva e fai un backup !=================! !== Join Dialog ==! !=================! -join.title = Entra nei canali +join.title = Entra in un canale # Uses plural form when joining more than one channel -join.button.join = Entra {0,choice,1\#canale|1F per Aggiungere o Rimuovere dai Preferiti, Del per Eliminare, o click destro per aprire il menù contestuale. +admin.presets.info = Seleziona e premi F per aggiungere o rimuovere dai preferiti, Del per eliminare, o clic destro per aprire il menù contestuale. !-- Select Game --! -admin.game.title = Seleziona Gioco +admin.game.title = Seleziona gioco # HTML -admin.game.info =

    Digita parte del nome del gioco e premi Invio o clicca 'Cerca', quindi seleziona il gioco dai risultati ottenuti (usando i nomi di Twitch).

    +admin.game.info =

    Digita parte del nome del gioco e premi Invio o clicca 'Cerca', poi seleziona il gioco dai risultati ottenuti (usare i nomi suggeriti da Twitch ti permette di apparire nella sezione giusta).

    admin.game.button.search = Cerca -admin.game.button.clearSearch = Vuota la ricerca -admin.game.button.favorite = Aggiungi ai Preferiti -admin.game.button.unfavorite = Rimuovi dai Preferiti +admin.game.button.clearSearch = Cancella ricerca +admin.game.button.favorite = Aggiungi ai preferiti +admin.game.button.unfavorite = Rimuovi dai preferiti admin.game.searching = Ricerca in corso.. # {0} = Number of search results, {1} = Number of favorites admin.game.listInfo = Trovati\: {0} / Preferiti\: {1} !-- Select Tags --! -admin.tags.title = Scegli fino {0} tags -admin.tags.listInfo = Tags\: {0}/{1} - Preferiti\: {2}/{3} -admin.tags.info = Fai doppio click sui Tag in lista per aggiungerli o rimuoverli. Disattiva "Mostra solo Attivi/Preferiti" per vedere tutti i Tag. Inserisci del testo per filtrare l'elenco corrente. -admin.tags.button.showAll = Mostra solo Attivi/Preferiti -admin.tags.button.clearFilter = Azzera filtro +admin.tags.title = Scegli fino a {0} categorie +admin.tags.listInfo = Categorie\: {0}/{1} - Preferiti\: {2}/{3} +admin.tags.info = Fai doppio clic sulle categorie in lista per aggiungerle o rimuoverle. Disattiva "Mostra solo attive/preferite" per vedere tutte le categorie. Scrivi qui sotto per filtrare l'elenco corrente. +admin.tags.button.showAll = Mostra solo attive/preferite +admin.tags.button.clearFilter = Cancella filtro # Favorite as an action, to add to favorites -admin.tags.button.favorite = Preferiti +admin.tags.button.favorite = Agg. preferite # Unfavorite as an action, to remove from favorites -admin.tags.button.unfavorite = Rimuovi Preferiti -admin.tags.button.addSelected = Aggiungi selezionati +admin.tags.button.unfavorite = Rimuovi preferite +admin.tags.button.addSelected = Aggiungi selezionate admin.tags.button.remove.tip = Rimuovi ''{0}'' admin.tags.cm.replaceAll = Sostituisci tutto admin.tags.cm.replace = Sostituisci ''{0}'' @@ -377,7 +417,7 @@ channelInfo.history = Stato (Cronologia) channelInfo.playing = Gioca a channelInfo.viewers = Spettatori channelInfo.streamOffline = Stream offline -channelInfo.noInfo = [Nessuna Informazione dello Stream] +channelInfo.noInfo = [Nessuna informazione sullo Stream] channelInfo.cm.copyAllCommunities = Copia tutto channelInfo.offline = Offline channelInfo.offline.tip = Durata dell''ultima trasmissione (probabilmente approssimativa)\: {0} @@ -395,7 +435,7 @@ channelInfo.viewers.min = minimo\: {0} channelInfo.viewers.max = massimo {0}; channelInfo.viewers.noHistory = Ancora nessuna cronologia degli spettatori channelInfo.viewers.cm.timeRange = Intervallo di tempo -channelInfo.viewers.cm.timeRange.option = {0} {0,choice,1\#Ora|1Nota\: La dimensione generale del carattere di Chatty può essere regolata dalla pagina delle impostazioni 'Aspetto'. +settings.otherFonts.info = Nota\: La dimensione generale del carattere del programma può essere regolata per alcuni temi nella pagina 'Aspetto'. settings.otherFonts.restrictedInfo = La scelta dei font è limitata per evitare bug dei font di fallback !-- Choose font --! settings.chooseFont.title = Scegli carattere -settings.chooseFont.selectFont = Seleziona la famiglia e la dimensione del carattere +settings.chooseFont.selectFont = Seleziona carattere e dimensione del testo settings.chooseFont.preview = Anteprima settings.chooseFont.enterText = Inserisci del testo addizionale per fare un test settings.chooseFont.bold = Grassetto @@ -537,26 +576,29 @@ settings.chooseFont.italic = Corsivo !-- Startup --! settings.section.startup = All'avvio di Chatty settings.boolean.splash = Mostra schermata iniziale -settings.boolean.splash.tip = Quando attivata viene mostrata una piccola finestra per indicare che Chatty si sta avviando +settings.boolean.splash.tip = Viene mostrata una piccola finestra per indicare che Chatty si sta avviando settings.startup.onStart = All'avvio\: settings.startup.channels = Canali\: +settings.startup.channels.tip = Uno o più canali che verranno aperti se l'impostazione precedente è attivata. settings.startup.option.doNothing = Non fare niente settings.startup.option.openConnect = Apri finestra di connessione settings.startup.option.connectJoinSpecified = Connettiti ed entra nei canali specificati settings.startup.option.connectJoinPrevious = Connetti ed entra nei canali aperti precedentemente -settings.startup.option.connectJoinFavorites = Connetti ed entra nei canali Preferiti +settings.startup.option.connectJoinFavorites = Connetti ed entra nei canali preferiti !-- Look & Feel --! -settings.section.lookandfeel = Look & Feel -settings.laf.lookandfeel = Look & Feel +settings.section.lookandfeel = Temi +settings.laf.lookandfeel = Tema\: settings.laf.font = Carattere\: settings.laf.option.defaultFont = Predefinito settings.laf.option.smallFont = Piccolo settings.laf.option.largeFont = Grande settings.laf.option.giantFont = Gigante +settings.boolean.lafNativeWindow = Usa finestra nativa +settings.boolean.lafNativeWindow.tip = Usa una finestra minimale, ma che offre più funzioni, come l'ancoraggio finestre. settings.laf.restartRequired = (Si rende necessario il riavvio di Chatty per rendere effettive queste modifiche) # HTML -settings.laf.info = L'aspetto del "Sistema" dipende dal tuo sistema operativo. Rispetto a "Predefinito" o "Sistema" le altre voci consentono di regolare ulteriormente la dimensione del carattere globale (ma potrebbero bloccare alcune funzionalità come lo snap di Windows). +settings.laf.info = Questa pagina cambia l'aspetto generale del programma. Il colore e i caratteri della chat vengono impostati nelle relative pagine. settings.label.lafGradient = Gradiente\: settings.long.lafGradient.option.0 = Nessuno settings.label.lafScroll = Barra scorrimento\: @@ -571,45 +613,49 @@ settings.laf.previewInfo = Anteprima delle impostazioni attuali in questa pagina settings.section.language = Lingua settings.language.language = Lingua\: settings.language.option.defaultLanguage = Predefinito -settings.language.info = AVVISO\: Mantenere aggiornato Chatty in ogni lingua è veramente un duro lavoro. Per questo motivo la guida ed altre parti del programma non sono state tradotte. +settings.language.info = Nota\: la guida e altre parti del programma non sono state tradotte, in quanto mantenerle tradotte e aggiornate richiederebbe troppo lavoro. +settings.label.timezone = Fuso orario\: !-- Setting Management --! +settings.section.settings = Gestione impostazioni # {0} = Details on how settings dir was set -settings.directory.info = posizione impostazioni {0}\: +settings.directory.info = Cartella impostazioni {0}\: settings.directory.default = predefinito # {0} = The commandline argument, e.g. "-d" settings.directory.argument = Imposta per argomento {0} settings.directory.invalid = Cartella impostata con argomento -d non valida\: +settings.backup.button.open = Visualizza backup +settings.backup.title = Backup !-- Chat Colors --! -settings.section.colors = Personalizza colori delle chat +settings.section.colors = Personalizza i colori della chat settings.colors.general.backgroundColor = Colore di sfondo settings.colors.general.foregroundColor = Colore in primo piano settings.colors.heading.misc = Colori vari -settings.colors.heading.highlights = Messaggi in evidenza (Highlighted) +settings.colors.heading.highlights = Messaggi evidenziati settings.colors.heading.searchResult = Risultati di ricerca settings.colors.background = Sfondo settings.boolean.alternateBackground = Usa sfondi alternati settings.colors.background2 = Sfondo 2 -settings.colors.button.switchBackgrounds = Inverti con lo sfondo -settings.colors.button.switchBackgrounds.tip = Inverti i colori di sfondo con sfondo 2, per testare la leggibilità dei colori in primo piano. -settings.boolean.messageSeparator = Mostra linee di separazione messaggi +settings.colors.button.switchBackgrounds = Inverti sfondi +settings.colors.button.switchBackgrounds.tip = Inverte i due colori di sfondo per permettere di testare la leggibilità dei colori in primo piano. +settings.boolean.messageSeparator = Mostra linee di separazione tra i messaggi settings.colors.messageSeparator = Separatore dei messaggi -settings.colors.foreground = Messaggi della chat -settings.colors.info = Messaggi di Informazione +settings.colors.foreground = Messaggio della chat +settings.colors.info = Messaggio informativo settings.colors.compact = Compatto (es. MOD/UNMOD) -settings.colors.highlight = Testo (messaggio In Evidenza) -settings.boolean.highlightBackground = Usa lo sfondo del messaggio evidenziato -settings.colors.highlightBackground = Sfondo (messaggio In Evidenza) -settings.colors.inputBackground = Sfondo di Input -settings.colors.inputForeground = Testo di Input -settings.boolean.timestampColorEnabled = Usa colore personalizzato del timestamp +settings.colors.highlight = Testo (messaggio evidenziato) +settings.boolean.highlightBackground = Usa sfondo personalizzato per i messaggi evidenziati +settings.colors.highlightBackground = Sfondo (messaggi evidenziati) +settings.colors.inputBackground = Sfondo del campo di testo +settings.colors.inputForeground = Testo campo di testo +settings.boolean.timestampColorEnabled = Usa colore personalizzato per la data e l'ora settings.label.timestampColorInherit = Eredita colore non-standard\: -settings.label.timestampColorInherit.tip = Se abilitato, il timestamp eredita il colore da (ad. es.) messaggi di Informazione o In Evidenza, invece di usare il colore personalizzato. Più bassa è la percentuale e più la luminosità del colore ereditato sarà fatta corrispondere a quella del colore personalizzato. -settings.colors.timestamp = Timestamp -settings.colors.searchResult = Risultati trovati -settings.colors.searchResult2 = Risultati trovati evidenziati -settings.colors.lookandfeel = Note\: L'estetica generale del programma è influenzata anche dalle scelte effettuate nelle impostazioni relative all'Aspetto +settings.label.timestampColorInherit.tip = Se abilitato, data e ora ereditano il colore dei messaggi (ad es.) informativi o evidenziati, invece di usare il colore personalizzato per la data e l'ora. Più bassa è la percentuale e più la luminosità del colore ereditato sarà fatta corrispondere a quella del colore personalizzato. +settings.colors.timestamp = Data e ora +settings.colors.searchResult = Risultato ricerca +settings.colors.searchResult2 = Risultato ricerca evidenziato +settings.colors.lookandfeel = Nota\: L'aspetto generale del programma è dovuto al tema selezionato nella pagina "Aspetto" delle impostazioni. settings.colors.button.choose = Scegli settings.colorChooser.title = Cambia colore\: {0} settings.colorChooser.button.useSelected = Usa il colore selezionato @@ -623,12 +669,12 @@ settings.colorPresets.button.delete = Elimina settings.colorPresets.info = (I preset vengono salvati direttamente nelle impostazioni) !-- Message Colors --! -settings.section.msgColors = Colore dei messaggi personalizzati () -settings.boolean.msgColorsEnabled = Abilita i colori dei messaggi personalizzati +settings.section.msgColors = Personalizzazione colore dei messaggi +settings.boolean.msgColorsEnabled = Abilita colori personalizzati per i messaggi settings.boolean.msgColorsEnabled.tip = Se abilitato, le voci nella tabella sottostante possono influire sui colori dei messaggi di chat settings.section.msgColorsOther = Altre impostazioni -settings.boolean.msgColorsPrefer = Sostituisci i colori dei messaggi In Evidenza con i colori dei messaggi personalizzati -settings.boolean.actionColored = Colora i messaggi-azione (/me) con il colore dell'utente +settings.boolean.msgColorsPrefer = Dai priorità ai colori personalizzati rispetto ai colori dei messaggi evidenziati +settings.boolean.actionColored = Usa il colore dell'utente per i messaggi-azione (/me) !-- Usercolors --! settings.section.usercolorsOther = Altre impostazioni @@ -645,97 +691,100 @@ settings.long.nickColorBackground.option.1 = Normale settings.long.nickColorBackground.option.2 = Forte !-- Usericons --! -settings.section.usericons = Impostazioni icona utente -settings.boolean.usericonsEnabled = Mostra le icone dell'utente -settings.boolean.botBadgeEnabled = Mostra il badge del Bot -settings.section.customUsericons = Badge personalizzati -settings.boolean.customUsericonsEnabled = Abilita badge personalizzati -settings.customUsericons.info = Consiglio\: aggiungi un badge personalizzato senza immagine per nascondere i badge di quel tipo. +settings.section.usericons = Impostazioni stemmi +settings.boolean.usericonsEnabled = Mostra stemmi utenti +settings.boolean.botBadgeEnabled = Mostra lo stemma del bot +settings.section.customUsericons = Stemmi personalizzati +settings.boolean.customUsericonsEnabled = Abilita stemmi personalizzati +settings.customUsericons.info = Consiglio\: aggiungi uno stemma personalizzato senza immagine per nascondere gli stemmi di quel tipo. !-- Emoticons --! settings.section.emoticons = Impostazioni generali delle Emoticon -settings.boolean.emoticonsEnabled = Mostra emoticons -settings.emoticons.button.editIgnored = Modifica Emotes ignorate +settings.boolean.emoticonsEnabled = Mostra Emoticon +settings.emoticons.button.editIgnored = Modifica Emoticon ignorate settings.emoticons.chatScale = Scala (Chat)\: -settings.emoticons.dialogScale = Finestra Emotes\: +settings.emoticons.dialogScale = Finestra delle Emoticon\: settings.emoticons.maxHeight = Altezza massima\: -settings.emoticons.maxHeightPixels = pixels -settings.boolean.closeEmoteDialogOnDoubleClick = Doppio clic sulla emote chiude la finestra delle Emote -settings.emoticons.cheers = Saluti (Bits)\: +settings.emoticons.maxHeightPixels = pixel +settings.boolean.closeEmoteDialogOnDoubleClick = Fai doppio clic su un'Emoticon per chiudere la finestra delle Emoticon +settings.emoticons.cheers = Emotifon (Bit)\: settings.emoticons.cheers.option.text = Solo testo settings.emoticons.cheers.option.static = Immagini statiche settings.emoticons.cheers.option.animated = Animate !-- Third-Party Emoticons --! -settings.section.3rdPartyEmotes = Emoticons di terze parti -settings.boolean.bttvEmotes = Abilita le emote di BTTV -settings.boolean.showAnimatedEmotes = Mostra le emotes animate -settings.boolean.showAnimatedEmotes.tip = Attualmente solo BTTV ha GIF emotes -settings.boolean.ffz = Abilita le emote di FrankerFaceZ (FFZ) -settings.boolean.ffzModIcon = Abilita le icone moderatori di FFZ -settings.boolean.ffzModIcon.tip = Mostra le icone moderatori personalizzate in alcuni canali -settings.boolean.ffzEvent = Abilita le FFZ emotes in primo piano -settings.boolean.ffzEvent.tip = Le emotes in primo piano sono disponibili tra gli eventi di alcuni canali (come le maratone di speedrunning) +settings.section.3rdPartyEmotes = Emoticon di terze parti +settings.boolean.bttvEmotes = Abilita le Emoticon di BTTV +settings.boolean.showAnimatedEmotes = Mostra le Emoticon animate +settings.boolean.showAnimatedEmotes.tip = Per ora solo BTTV ha Emoticon in formato GIF. +settings.boolean.ffz = Abilita le Emoticon di FrankerFaceZ (FFZ) +settings.boolean.ffzModIcon = Abilita gli Stemmi FFZ dei moderatori +settings.boolean.ffzModIcon.tip = Mostra gli stemmi personalizzati dei moderatori per alcuni canali. +settings.boolean.ffzEvent = Abilita le Emoticon in primo piano di FFZ +settings.boolean.ffzEvent.tip = Le Emoticon in primo piano sono disponibili per i canali di alcuni eventi (come le maratone di speedrun). !-- Emoji --! settings.section.emoji = Emoji settings.emoji.set = Set di Emoji\: settings.emoji.option.none = Nessuna -settings.boolean.emojiReplace = Trasforma il codice in emoji nel testo inserito -settings.boolean.emojiReplace.tip = Codici come \:joy\: inseriti nella casella di input vengono trasformati nelle Emoji corrispondenti (Suggerimento\: utilizzare il completamento del TAB) +settings.boolean.emojiReplace = Trasforma un codice in emoji nel testo che scrivi. +settings.boolean.emojiReplace.tip = Codici come \:joy\: inseriti nel campo di testo vengono trasformati nelle emoji corrispondenti (Consiglio\: usa l'autocompletamento con il tasto TAB). !-- Ignored Emotes --! -settings.ignoredEmotes.title = Emotes ignorate -settings.ignoredEmotes.info1 = Le emotes ignorate vengono visualizzate solo col codice dell'emote, non vengono trasformate in un'immagine. -settings.ignoredEmotes.info2 = Puoi usare il menù contestuale (click destro su una emote in chat) per aggiungerla a questa lista. +settings.ignoredEmotes.title = Emoticon ignorate +settings.ignoredEmotes.info1 = Le Emoticon ignorate vengono visualizzate solo come codice e non vengono trasformate in immagine. +settings.ignoredEmotes.info2 = Puoi usare il menù contestuale delle Emoticon (fai clic con il tasto destro su un'Emoticon in chat) per aggiungerla a questa lista. !-- Chat --! -settings.boolean.showImageTooltips = Mostra tooltip Emoticon / Badge -settings.boolean.showTooltipImages = Mostra immagine nelle tooltip +settings.boolean.showImageTooltips = Mostra descrizione Emoticon / stemma +settings.boolean.showTooltipImages = Mostra immagine nella descrizione +settings.label.bufferSize = Dimensioni predefinite del buffer della chat\: +settings.boolean.mentionReplyRestricted = Mostra l'opzione "Rispondi" solo quando il messaggio inizia per @@ +settings.boolean.mentionReplyRestricted.tip = Se disattivato, l'opzione "Rispondi" verrà mostrata quando il messaggio inizia con una menzione semplice (@). !-- Messages --! -settings.section.deletedMessages = Elimina messaggi (Timeouts/Bans) -settings.option.deletedMessagesMode.delete = Messaggio eliminato +settings.section.deletedMessages = Messaggi eliminati (timeout/ban) +settings.option.deletedMessagesMode.delete = Elimina messaggio settings.option.deletedMessagesMode.strikeThrough = Barra messaggio settings.option.deletedMessagesMode.strikeThroughShorten = Barra e accorcia messaggio -settings.deletedMessages.max = massimo. +settings.deletedMessages.max = massimo settings.deletedMessages.characters = caratteri settings.boolean.banDurationAppended = Mostra la durata del ban settings.boolean.banDurationAppended.tip = Mostra la durata in secondi del timeout in coda all'ultimo messaggio eliminato settings.boolean.banReasonAppended = Mostra il motivo del ban (solo moderatori) settings.boolean.banReasonAppended.tip = Mostra il motivo di un ban dietro l'ultimo messaggio eliminato (solo moderatori, ad eccezione dei tuoi ban) -settings.boolean.showBanMessages = Mostra i messaggi dei ban separati, con le seguenti opzioni\: +settings.boolean.showBanMessages = Mostra i messaggi di ban separati con le seguenti opzioni\: settings.boolean.banDurationMessage = Mostra la durata del ban settings.boolean.banDurationMessags.tip = Mostra la durata in secondi del timeout in messaggi di ban separati settings.boolean.banReasonMessage = Mostra il motivo del ban (solo moderatori) settings.boolean.banReasonMessage.tip = Mostra il motivo di un ban in messaggi di ban separati (solo moderatori, ad eccezione dei tuoi ban) settings.boolean.combineBanMessages = Combina i messaggi di ban -settings.boolean.combineBanMessages.tip = Combina i messaggi di ban simili in uno, aggiungendo il numero di ban -settings.boolean.clearChatOnChannelCleared = Cancella chat quando cancellata da un moderatore +settings.boolean.combineBanMessages.tip = Combina i messaggi di ban simili in uno solo, aggiungendo il numero di ban alla fine. +settings.boolean.clearChatOnChannelCleared = Cancella chat quando viene cancellata da un moderatore settings.section.otherMessageSettings = Altro settings.otherMessageSettings.timestamp = Data e ora\: -settings.otherMessageSettings.customizeTimestamp = Personalizza il timestamp -settings.otherMessageSettings.customizeTimestamp.info = Il timestamp può essere personalizzato ulteriormente sotto "Colori delle chat" e "Caratteri". +settings.otherMessageSettings.customizeTimestamp = Personalizza data e ora +settings.otherMessageSettings.customizeTimestamp.info = Data e ora possono essere personalizzate ulteriormente sotto "Colori della chat" e "Caratteri". settings.boolean.showModMessages = Mostra mod/unmod (inaffidabile) settings.boolean.showModMessages.tip = Mostra MOD/UNMOD quando viene promosso o rimosso un moderatore, oppure un moderatore si è unito o ha lasciato il canale. -settings.boolean.showJoinsParts = Mostra chi ENTRA e chi ESCE (inaffidabile) -settings.boolean.printStreamStatus = Mostra lo stato dello Stream (es. Titolo/Gioco) in chat -settings.boolean.printStreamStatus.tip = Mostra quando entri e quando cambia +settings.boolean.showJoinsParts = Mostra chi entra/esce dal canale (inaffidabile) +settings.boolean.printStreamStatus = Mostra lo stato dello stream (es. titolo/gioco) in chat +settings.boolean.printStreamStatus.tip = Mostra lo stato quando entri nel canale e quando viene cambiato. !-- Moderation --! settings.section.modInfos = Informazioni solo per moderatori -settings.boolean.showModActions = Mostra le azioni di moderazione nella chat (simile a ) +settings.boolean.showModActions = Mostra le azioni di moderazione nella chat (simile a ) settings.boolean.showModActions.tip = Mostra quali comandi eseguono i moderatori, eccetto i tuoi (puoi anche aprire il menù principale Extra - Registro di moderazione). settings.boolean.showModActionsRestrict = Nascondi se l'azione associata è già visibile nella chat (es.\: ban) settings.boolean.showModActionsRestrict.tip = Se questa azione di moderazione viene mostrata aggiungendo @Mod a un messaggio informativo esistente in chat, non mostrerà un messaggio Mod Action separato. -settings.boolean.showActionBy = Aggiungi quale @Mod ha causato un'azione (esempio\: Ban) -settings.boolean.showActionBy.tip = Se diversi moderatori eseguono la stessa azione in un breve lasso di tempo, questa visualizzazione potrebbe essere inaccurata. +settings.boolean.showActionBy = Aggiungi alla fine del messaggio quale @Mod ha effettuato un'azione (es\: ban) +settings.boolean.showActionBy.tip = Se diversi moderatori eseguono la stessa azione in un breve lasso di tempo, questa visualizzazione potrebbe non essere accurata. settings.section.userDialog = Finestra di dialogo utente -settings.boolean.closeUserDialogOnAction = Chiudi la Finestra di dialogo utente quando esegui un'azione (Pulsante) -settings.boolean.closeUserDialogOnAction.tip = Dopo aver cliccato un pulsante, la Finestra di dialogo informazioni utente verrà chiusa automaticamente. -settings.boolean.openUserDialogByMouse = Apri la Finestra di dialogo utente sempre vicino al puntatore del mouse -settings.boolean.reuseUserDialog = Riutilizzare la dialog utente già aperta dello stesso utente -settings.boolean.reuseUserDialog.tip = Quando si apre la finestra di dialogo utente di un utente che ne ha già una aperta, non aprirne un'altra. +settings.boolean.closeUserDialogOnAction = Chiudi la finestra di dialogo utente quando esegui un'azione (Pulsante) +settings.boolean.closeUserDialogOnAction.tip = Dopo aver cliccato un pulsante, la finestra di dialogo informazioni utente verrà chiusa automaticamente. +settings.boolean.openUserDialogByMouse = Apri la finestra di dialogo utente sempre vicino al puntatore del mouse +settings.boolean.reuseUserDialog = Riutilizza la finestra utente già aperta di una stesso utente +settings.boolean.reuseUserDialog.tip = Quando si apre la finestra di dialogo utente di un utente di cui se ne ha già una aperta, non ne apre un'altra. settings.long.clearUserMessages.option.-1 = Mai settings.long.clearUserMessages.option.3 = 3 ore settings.long.clearUserMessages.option.6 = 6 ore @@ -743,224 +792,261 @@ settings.long.clearUserMessages.option.12 = 12 ore settings.long.clearUserMessages.option.24 = 24 ore settings.long.clearUserMessages.label = Cancella la cronologia dei messaggi degli utenti inattivi per\: settings.label.banReasonsHotkey = Scorciatoia per lista motivazioni ban\: -settings.label.banReasonsInfo = Le motivazioni del ban possono essere modificate direttamente nella Finestra Utente +settings.label.banReasonsInfo = Le motivazioni del ban possono essere modificate direttamente dalla finestra utente !-- Names --! -settings.label.mentions = Menzioni cliccabili -settings.label.mentions.tip = I nomi utente degli utenti che hanno chattato di recente sono resi cliccabili e enfatizzati nei messaggi di chat -settings.label.mentionsInfo = Menzioni cliccabili (Info)\: -settings.label.mentionsInfo.tip = Come sopra, eccetto per i messaggi di Informazione (es. risposte ai comandi, AutoMod) -settings.label.mentionsBold = Grassetto -settings.label.mentionsUnderline = Sottolineato -settings.label.mentionsColored = Colorato -settings.label.mentionsColored.tip = Menzioni cliccabili nel colore dell'utente +settings.label.displayNamesMode = Nomi in chat\: +settings.label.displayNamesMode.tip = I nomi localizzati possono contenere caratteri non occidentali. +settings.label.displayNamesModeUserlist = Nomi nella lista degli utenti\: +settings.label.displayNamesModeUserlist.tip = I nomi localizzati possono contenere caratteri non occidentali. +settings.label.mentions = Menzioni cliccabili\: +settings.label.mentions.tip = I nomi utente degli utenti che hanno chattato di recente sono cliccabili e messi in evidenza nei messaggi della chat. +settings.label.mentionsInfo = Menzioni cliccabili (mess. informativi)\: +settings.label.mentionsInfo.tip = Come sopra, eccetto per i messaggi informativi (es. risposte ai comandi, AutoMod). +settings.label.mentionsBold = In grassetto +settings.label.mentionsUnderline = Sottolineate +settings.label.mentionsColored = Colorate +settings.label.mentionsColored.tip = Le menzioni cliccabili sono dello stesso colore dell'utente. settings.long.markHoveredUser.option.0 = Off settings.long.markHoveredUser.option.1 = Tutto / Sempre settings.long.markHoveredUser.option.2 = Solo menzioni settings.long.markHoveredUser.option.3 = Solo menzioni, tutto quando si tiene premuto CTRL settings.long.markHoveredUser.option.4 = Solo tenendo premuto CTRL settings.label.markHoveredUser = Contrassegna il nome utente con il mouse\: -settings.label.markHoveredUser.tip = Enfatizza altre occorrenze di un nome utente se ci passi sopra con il mouse in chat. +settings.label.markHoveredUser.tip = Enfatizza altre occorrenze in chat di un nome utente passandoci sopra con il mouse. +settings.label.mentionMessages = Mostra messaggi passando con il mouse sopra una menzione\: +settings.long.mentionMessages.option.0 = No !-- Highlight --! -settings.section.highlightMessages = Messaggi in evidenza (Highlight) -settings.boolean.highlightEnabled = Abilita i messaggi In Evidenza -settings.boolean.highlightEnabled.tip = I messaggi che corrispondono ai criteri verranno visualizzati in un colore diverso, verranno aggiunti alla finestra Messaggi In Evidenza e attiveranno di default una notifica. +settings.section.highlightMessages = Testo (messaggi evidenziati) +settings.boolean.highlightEnabled = Abilita messaggi evidenziati +settings.boolean.highlightEnabled.tip = I messaggi che corrispondono ai criteri verranno visualizzati in un colore diverso, verranno aggiunti alla finestra Messaggi Evidenziati e attiveranno di default una notifica. settings.boolean.highlightUsername = Evidenzia il mio nome -settings.boolean.highlightUsername.tip = Metti in evidenza i messaggi contenenti il mio nome utente corrente, anche se non l'hai aggiunti all'elenco. -settings.boolean.highlightNextMessages = Evidenzia il seguito -settings.boolean.highlightNextMessages.tip = Mette in evidenza i messaggi dello stesso utente che vengono scritti poco dopo l'ultimo evidenziato. -settings.boolean.highlightOwnText = Controlla il tuo testo per i punti in evidenza -settings.boolean.highlightOwnText.tip = Consente di evidenziare i propri messaggi, altrimenti i propri messaggi non saranno mai evidenziati. Buono per i test. +settings.boolean.highlightUsername.tip = Metti in evidenza i messaggi contenenti il mio nome utente corrente, anche se non l'hai aggiunto all'elenco. +settings.boolean.highlightNextMessages = Evidenzia anche il mess. successivo +settings.boolean.highlightNextMessages.tip = Evidenzia i messaggi di uno stesso utente che vengono scritti poco dopo l'ultimo evidenziato. +settings.boolean.highlightOwnText = Controlla il tuo testo per le corrispondenze +settings.boolean.highlightOwnText.tip = Consente di evidenziare i propri messaggi, altrimenti i propri messaggi non vengono mai evidenziati. Va bene per fare dei test. settings.boolean.highlightIgnored = Controlla i messaggi ignorati settings.boolean.highlightIgnored.tip = Consente di evidenziare i messaggi ignorati, altrimenti i messaggi ignorati non vengono mai evidenziati. settings.boolean.highlightMatches = Contrassegna le corrispondenze evidenziate/ignorate -settings.boolean.highlightMatches.tip = Circonda con un rettangolo le sezioni di testo che hanno causato l'evidenziazione (e nelle finestre dei messaggi evidenziati/ignorati). +settings.boolean.highlightMatches.tip = Disegna un rettangolo attorno alle sezioni di testo che hanno causato l'evidenziazione (anche nelle finestre dei messaggi evidenziati/ignorati). settings.boolean.highlightMatchesAll = Segna tutte le occorrenze -settings.boolean.highlightMatchesAll.tip = Contrassegna anche le corrispondenze in evidenza nei collegamenti e menzioni +settings.boolean.highlightMatchesAll.tip = Contrassegna anche le corrispondenze evidenziate anche in collegamenti e menzioni. settings.boolean.highlightByPoints = Colora i messaggi evidenziati con i Punti Canale -settings.boolean.highlightByPoints.tip = Usa il colore dei messaggi in evidenza per i messaggi che vengono "messi in evidenza" usando i punti del canale. Se vuoi impostare un'evidenziazione, aggiungi "config\:hl" alla lista. Se vuoi cambiarne il colore, aggiungi "config\:hl" in "Colori dei messaggi". +settings.boolean.highlightByPoints.tip = Usa il colore dei messaggi evidenziati per i messaggi che vengono "messi in evidenza" usando i punti del canale. Se vuoi impostare un'evidenziazione, aggiungi "config\:hl" alla lista. Se vuoi cambiarne il colore, aggiungi "config\:hl" in "Colori dei messaggi". !-- Ignore --! settings.section.ignoreMessages = Ignora messaggi settings.boolean.ignoreEnabled = Abilita Ignora !-- Filter --! -settings.section.filterMessages = Filtra le parti dei messaggi +settings.section.filterMessages = Filtra parti dei messaggi settings.boolean.filterEnabled = Abilita filtro !-- Log to file --! -settings.log.section.channels = Canali da loggare al file -settings.log.loggingMode = Modalità del Logging\: +settings.log.section.channels = Canali per i quali creare file di log +settings.log.loggingMode = Modalità di log settings.option.logMode.always = Sempre -settings.option.logMode.blacklist = Lista nera -settings.option.logMode.whitelist = Lista bianca +settings.option.logMode.blacklist = Solo lista nera +settings.option.logMode.whitelist = Solo lista bianca settings.option.logMode.off = Mai settings.log.noList = -settings.log.alwaysInfo = Tutti i canali sono loggati -settings.log.blacklistInfo = Tutti i canali, ma quelli nella lista sono loggati. -settings.log.whitelistInfo = Solo i canali nella lista sono loggati. -settings.log.offInfo = Niente è loggato. -settings.boolean.logMessage = Messaggi di chat -settings.boolean.logMessage.tip = Logga i messaggi di chat regolari -settings.boolean.logInfo = Informazioni Chat -settings.boolean.logInfo.tip = Logga informazioni come titolo dello stream, messaggi da Twitch, connessione, disconnessione -settings.boolean.logBan = Ban e Timeout -settings.boolean.logBan.tip = Logga Bans/Timeouts come messaggi di BAN -settings.boolean.logDeleted = Messaggio cancellati. -settings.boolean.logDeleted.tip = Logga messaggi eliminati come messaggi ELIMINATI -settings.boolean.logMod = Mod/Unmod +settings.log.alwaysInfo = Viene fatto il log di tutti i canali. +settings.log.blacklistInfo = Viene fatto il log di tutti i canali tranne quelli nella lista. +settings.log.whitelistInfo = Viene fatto il log solo dei canali nella lista. +settings.log.offInfo = Non vengono fatti log dei canali. +settings.boolean.logMessage = Messaggi della chat +settings.boolean.logMessage.tip = Scrivi su file i messaggi normali della chat. +settings.boolean.logMessage.template = Template del messaggio in chat +settings.boolean.logInfo = Informazioni chat +settings.boolean.logInfo.tip = Scrive sul file informazioni come il titolo dello stream, i messaggi da Twitch, connessione, disconnessione. +settings.boolean.logBan = Ban e timeout +settings.boolean.logBan.tip = Scrive sul file i ban/timeout come messaggi di BAN. +settings.boolean.logDeleted = Messaggi eliminati +settings.boolean.logDeleted.tip = Scrive su file i messaggi eliminati come messaggi ELIMINATI +settings.boolean.logMod = MOD/UNMOD # MOD/UNMOD should not be translated since it refers to how it appears in the log -settings.boolean.logMod.tip = Logga messaggi di MOD/UNMOD -settings.boolean.logJoinPart = Entrate/Uscite +settings.boolean.logMod.tip = Scrive sul file i messaggi di MOD/UNMOD, ossia quando utenti ricevono o perdono i privilegi di moderatore o i moderatori entrano o escono dalla chat (molto inaffidabile). +settings.boolean.logJoinPart = JOIN/PART # JOIN/PART should not be translated since it refers to how it appears in the log -settings.boolean.logJoinPart.tip = Logga i messaggi di ENTRATA/USCITA +settings.boolean.logJoinPart.tip = Scrive sul file i messaggi di JOIN/PART, ossia quando gli utenti entrano o escono dalla chat (molto inaffidabile). settings.boolean.logSystem = Informazioni sistema -settings.boolean.logSystem.tip = Logga i messaggi del sistema Chatty come il controllo della versione, la risposta ai comandi di impostazione e così via.. +settings.boolean.logSystem.tip = Scrive su file i messaggi di sistema di Chatty, come il controllo della versione, la risposta ai comandi di impostazione e così via... settings.boolean.logViewerstats = Statistiche spettatori -settings.boolean.logViewerstats.tip = Logga le statistiche spettatori (una sorta di riepilogo del numero di spettatori) a intervalli semi-regolari +settings.boolean.logViewerstats.tip = Scrive su file le statistiche spettatori (una sorta di riepilogo del numero di spettatori) a intervalli semi-regolari. settings.boolean.logViewercount = Conteggio spettatori -settings.boolean.logViewercount.tip = Logga il conteggio di spettatori ogni volta che riceve nuovi dati +settings.boolean.logViewercount.tip = Scrive su file il conteggio di spettatori ogni volta che riceve nuovi dati settings.boolean.logModAction = Azioni di moderazione -settings.boolean.logModAction.tip = Logga chi e quale comando ha eseguito (solo moderatori) -settings.boolean.logIgnored = Messaggi ignorati. -settings.boolean.logIgnored.tip = Logga i messaggi che vengono ignorati anche dall'elenco Ignora +settings.boolean.logModAction.tip = Scrive su file quale utente ha eseguito quale comando (solo moderatori). +# Bits as in the Twitch donation currency (use the term that Twitch uses for your language) +settings.boolean.logBits = Bit +# Bits as in the Twitch donation currency (use the term that Twitch uses for your language) +settings.boolean.logBits.tip = Scrivi su file il numero di Bit donati prima del messaggio. +settings.boolean.logIgnored = Messaggi ignorati +settings.boolean.logIgnored.tip = Scrive su file anche i messaggi che vengono ignorati dall'elenco Ignora +settings.boolean.logIgnored2 = Messaggi ignorati +settings.boolean.logIgnored2.tip = Scrive su un file log separato i messaggi ignorati (non è affetto dall'impostazione "modalità del Logging"). +settings.boolean.logHighlighted2 = Messaggi evidenziati +settings.boolean.logHighlighted2.tip = Scrive su un file log separato i messaggi evidenziati (non è affetto dall'impostazione "modalità del Logging"). settings.log.section.other = Altre impostazioni settings.log.folder = Cartella\: -settings.log.splitLogs = Dividi Logs per\: +settings.log.splitLogs = Dividi Log per\: settings.option.logSplit.never = Mai -settings.option.logSplit.daily = Giornalmente -settings.option.logSplit.weekly = Settimanalmente -settings.option.logSplit.monthly = Mensilmente +settings.option.logSplit.daily = Giorno +settings.option.logSplit.weekly = Settimana +settings.option.logSplit.monthly = Mese settings.boolean.logSubdirectories = Sottocartelle dei canali -settings.boolean.logSubdirectories.tip = Organizza Logs nelle sottocartelle dei canali +settings.boolean.logSubdirectories.tip = Organizza i log in sottocartelle dei canali. settings.boolean.logLockFiles = Bloccare i files -settings.boolean.logLockFiles.tip = Ottiene l'accesso esclusivo ai file di registro per garantire che nessun altro programma scriva su di esso. Può anche a volte impedire la lettura. +settings.boolean.logLockFiles.tip = Ottiene l'accesso esclusivo ai file di log per impedire la scrittura da parte di altri programmi. Può a volte anche impedire la lettura. settings.log.timestamp = Data e ora\: settings.option.logTimestamp.off = Disattivato +!-- Other --! +settings.label.titleAddition = Prefisso della finestra del titolo\: + !-- Commands --! settings.label.channelContextMenu = Menù contestuale del canale\: settings.label.channelContextMenu.tip = Si apre cliccando con il tasto destro nel canale. settings.label.streamsContextMenu = Menù contestuale dello stream\: -settings.label.streamsContextMenu.tip = Si apre cliccando con il tasto destro su uno o più stream nelle liste degli stream live, dei Preferiti o dei Follower. +settings.label.streamsContextMenu.tip = Si apre cliccando con il tasto destro su uno o più stream nelle liste degli stream live, dei preferiti o dei seguaci. settings.label.userContextMenu = Menù contestuale dell'utente\: -settings.label.userContextMenu.tip = Si apre cliccando con il tasto destro su un utente in chat, nella lista utenti o nella finestra Followers +settings.label.userContextMenu.tip = Si apre cliccando con il tasto destro su un utente in chat, nella lista utenti o nella finestra dei seguaci. settings.label.timeoutButtons = Bottoni della finestra Utente\: -settings.label.timeoutButtons.tip = Si apre cliccando su un utente in chat o cliccando due volte su un utente nella lista utenti o nella finestra Followers +settings.label.timeoutButtons.tip = Si apre cliccando su un utente in chat o cliccando due volte su un utente nella lista utenti o nella finestra dei seguaci. +settings.label.textContextMenu = Menù contestuale del testo selezionato +settings.label.textContextMenu.tip = Viene aperto cliccando con il tasto destro sul testo selezionato nella chat e in altri posti. +settings.label.adminContextMenu = Menù contestuale dell'Amministratore +settings.label.adminContextMenu.tip = Si apre cliccando con il tasto destro sulla finestra dell'Amministratore (Visualizza - Amministratore del canale) !-- TAB Completion --! -settings.section.completion = Completamento del TAB (Nomi, Emotes, Comandi) +settings.section.completion = Autocompletamento con il tasto TAB (nomi, Emoticon, comandi) +settings.boolean.completionEnabled = Permette il completamento con TAB +settings.boolean.completionEnabled.tip = Attiva l'autocompletamento di nomi di utenti ed emote premendo TAB o Shift+TAB nel campo di testo della chat, ma impedisce l'uso di TAB per focalizzare elementi se il campo di testo non è vuoto. settings.completion.option.names = Nomi -settings.completion.option.emotes = Emotes -settings.completion.option.namesEmotes = Nomi, poi Emotes -settings.completion.option.emotesNames = Emotes, poi Nomi +settings.completion.option.emotes = Emoticon +settings.completion.option.namesEmotes = Prima i nomi, poi le Emoticon +settings.completion.option.emotesNames = Prima le Emoticon, poi i nomi settings.completion.option.custom = Completamento personalizzato -settings.completion.info = Suggerimento\: Indipendentemente da queste impostazioni, puoi aggiungere un prefisso con @ sempre per il TAB-complete ai nomi, con . (punto) per usare il completamento personalizzato e con \: per completare le Emoji.

    Esempio\: \:think + TAB > \:thinking\: -settings.label.completionEmotePrefix = Prefisso Emote di Twitch\: -settings.label.completionEmotePrefix.tip = Usa questo prefisso per autocompletare sempre le Emoticon (le Emoji usano sempre '\:'). +settings.completion.info = Consiglio\: Digita prima @ per completare nomi, \: per emoji e quello che imposti qua sotto per le Emoticon.\n(Es\: \:think + TAB > \:thinking\:) +settings.label.completionEmotePrefix = Prefisso Emoticon di Twitch\: +settings.label.completionEmotePrefix.tip = Usa questo prefisso per autocompletare sempre le Emoticon (le emoji usano sempre '\:'). settings.completionEmotePrefix.option.none = Nessuno -settings.long.completionMixed.option.0 = Miglior Corrispondenza -settings.long.completionMixed.option.1 = Prima le Emoji -settings.long.completionMixed.option.2 = Prima le Emotes -settings.long.completionMixed.tip = Come riordinare i risultati quando Emote e Emoji sono miste -settings.string.completionSearch = Cerca in Emotes / Comandi\: +settings.long.completionMixed.option.0 = Miglior corrispondenza +settings.long.completionMixed.option.1 = Prima le emoji +settings.long.completionMixed.option.2 = Prima le Emoticon +settings.long.completionMixed.tip = Come riordinare i risultati quando Emoticon e emoji sono miste. +settings.string.completionSearch = Cerca in Emoticon / comandi a partire da\: # Example: Emote "joshWithIt" would match only when entering the beginning of "joshWithIt" -settings.string.completionSearch.option.start = All'inizio del nome +settings.string.completionSearch.option.start = Inizio parole # Example: Emote "joshWithIt" would match when entering the beginning of "joshWithIt", "With" or "It" -settings.string.completionSearch.option.words = All'inizio, e parole con iniziale maiuscola +settings.string.completionSearch.option.words = Inizio parole e lettere maiuscole (per il camelCase) # Example: Emote "joshWithIt" would match when entering any part of the name (even just "t") -settings.string.completionSearch.option.anywhere = Ovunque nel nome +settings.string.completionSearch.option.anywhere = Qualsiasi punto nelle parole settings.section.completionNames = Nomi localizzati -settings.boolean.completionPreferUsernames = Preferisci il nome Normale per i comandi basati su nomi utente -settings.boolean.completionAllNameTypes = Includi tutti i tipi di nome nei risultati (Normali/Localizzati/Personalizzati) +settings.boolean.completionPreferUsernames = Preferisci il nome normale per i comandi basati sul nome utente +settings.boolean.completionAllNameTypes = Includi tutti i tipi di nome nei risultati (normali/localizzati/personalizzati) settings.boolean.completionAllNameTypesRestriction = Solo quando non ci sono più di due corrispondenze settings.section.completionAppearance = Aspetto / Comportamento -settings.boolean.completionShowPopup = Mostra informazioni popup +settings.boolean.completionShowPopup = Mostra popup con settings.boolean.completionShowPopup.tip = Mostra risultati di autocompletamento correnti in una finestra popup. settings.label.searchResults = risultati di ricerca -settings.boolean.completionAuto = Mostra automaticamente per alcune categorie (come le Emoji) -settings.boolean.completionCommonPrefix = Completa al prefisso comune (se più di una corrispondenza) +settings.boolean.completionAuto = Mostra automaticamente per alcuni elementi (come le emoji) +settings.boolean.completionCommonPrefix = Completa fino al prefisso comune (se c'è più di una corrispondenza) settings.completion.nameSorting = Ordine dei nomi\: -settings.completion.option.predictive = Predittiva -settings.completion.option.alphabetical = Alfabetica -settings.completion.option.userlist = Uguale alla lista utenti -settings.boolean.completionSpace = Spazio alla fine +settings.completion.option.predictive = Predittivo +settings.completion.option.alphabetical = Alfabetico +settings.completion.option.userlist = Come la lista utenti +settings.boolean.completionSpace = Agg. spazio alla fine !-- Dialogs Settings --! -settings.section.dialogs = Posizione / Dimensione della finestra di dialogo -settings.dialogs.restore = Ripristina finestre dialogo\: +settings.section.dialogs = Posizione / dimensioni finestra +settings.dialogs.restore = Ripristina finestre di dialogo\: settings.dialogs.option.openDefault = Apri sempre nella posizione predefinita -settings.dialogs.option.keepSession = Mantenere la posizione durante la sessione +settings.dialogs.option.keepSession = Mantieni la posizione durante la sessione settings.dialogs.option.restore = Mantieni la posizione / dimensione dell'ultima sessione settings.dialogs.option.reopen = Riapri dall'ultima sessione settings.boolean.restoreOnlyIfOnScreen = Ripristina la posizione solo se sullo schermo -settings.boolean.attachedWindows = Sposta anche le finestre di dialog quando sposti la finestra principale +settings.boolean.attachedWindows = Sposta anche le finestre di dialogo assieme alla finestra principale !-- Minimizing Settings --! -settings.section.minimizing = Minimizza / Tray (orologio) -settings.boolean.minimizeToTray = Minimizza nella tray (vicino all'orologio) -settings.boolean.closeToTray = Vicino alla tray -settings.boolean.trayIconAlways = Mostra sempre l'icona nella Tray (orologio) -settings.boolean.hideStreamsOnMinimize = Nascondi finestra Live Stream quando minimizzato -settings.boolean.hideStreamsOnMinimize.tip = Nasconde automaticamente la finestra di Live Stream quando viene minimizzata la finestra principale -settings.boolean.singleClickTrayOpen = Clic singolo sull'icona nella barra degli strumenti per mostrare/nascondere +settings.section.minimizing = Icona nell'area notifiche +settings.boolean.minimizeToTray = Se Chatty viene ridotto a icona +settings.boolean.closeToTray = Se Chatty viene chiuso +settings.boolean.trayIconAlways = Mostra sempre l'icona nell'area notifiche +settings.boolean.hideStreamsOnMinimize = Nascondi finestra degli stream live quando ridotto a icona +settings.boolean.hideStreamsOnMinimize.tip = Nasconde automaticamente la finestra degli stream live quando la finestra principale viene ridotta a icona. +settings.boolean.singleClickTrayOpen = Clic singolo sull'icona nell'area notifiche per mostrare/nascondere settings.boolean.singleClickTrayOpen.tip = Se questo è deselezionato, può essere richiesto un doppio clic (dipende dalla piattaforma) !-- Other Window Settings --! settings.section.otherWindow = Altro # Enable the confirmation dialog when opening an URL out of Chatty -settings.boolean.urlPrompt = Richiesta di "Aprire URL" -settings.boolean.chatScrollbarAlways = Mostra sempre la barra di scorrimento nelle chat +settings.boolean.urlPrompt = Mostra avviso di apertura URL +settings.boolean.chatScrollbarAlways = Mostra sempre la barra di scorrimento della chat settings.window.defaultUserlistWidth = Larghezza lista utenti predefinita # In context of Userlist Width, so don't need to repeat "Userlist" settings.window.minUserlistWidth = Larghezza minima\: settings.boolean.userlistEnabled = Abilita di default la lista utenti settings.boolean.userlistEnabled.tip = Di default puoi usare Shift+F10 per nascondere/visualizzare la lista utenti -settings.boolean.inputEnabled = Abilita di default il campo di inserimento +settings.boolean.inputEnabled = Abilita di default il campo di inserimento testo settings.boolean.inputEnabled.tip = Di default puoi usare Ctrl+F10 per nascondere/visualizzare il campo di inserimento +settings.label.inputFocus = Focus del campo di testo della chat\: +settings.long.inputFocus.option.0 = Focus sull'input predefinito +settings.long.inputFocus.option.1 = Focus sull'input quando si ritorna alla finestra +settings.long.inputFocus.option.2 = Non cambiare automaticamente focus !-- Tab Settings --! -settings.section.tabs = Impostazioni Schede +settings.section.tabs = Impostazioni schede settings.tabs.order = Ordine delle schede\: -settings.tabs.option.normal = Normale (come aperto) +settings.tabs.option.normal = Normale (come le hai aperte) settings.tabs.option.alphabetical = Alfabetico settings.tabs.placement = Posizionamento delle schede -settings.tabs.option.top = Alto -settings.tabs.option.left = Sinistra -settings.tabs.option.right = Destra -settings.tabs.option.bottom = Basso +settings.tabs.option.top = In alto +settings.tabs.option.left = A sinistra +settings.tabs.option.right = A destra +settings.tabs.option.bottom = In basso settings.tabs.layout = Disposizione schede\: settings.tabs.option.wrap = Su più righe settings.tabs.option.scroll = A scorrimento (riga singola) settings.boolean.tabsMwheelScrolling = Scorri le schede con la rotellina del mouse per cambiare canale -settings.boolean.tabsMwheelScrollingAnywhere = Scorri la casella di input per cambiare canale +settings.boolean.tabsMwheelScrollingAnywhere = Scorri con la rotella del mouse sulla casella di input per cambiare canale !-- Notification Settings --! settings.notifications.tab.events = Eventi -settings.notifications.tab.notificationSettings = Impostazioni notifiche +settings.notifications.tab.events.tip = Aggiunge un elemento alla tabella per attivare ad un evento specifico una notifica sul desktop o un suono. +settings.notifications.tab.notificationSettings = Impostazioni notifiche del desktop settings.notifications.tab.soundSettings = Impostazioni suoni # When notifications are turned off -settings.notifications.tab.notificationSettingsOff = Impostazioni Notifiche (Off) +settings.notifications.tab.notificationSettingsOff = Impostazioni notifiche (Off) # When sounds are turned off -settings.notifications.tab.soundSettingsOff = Impostazioni Suoni (Silenzioso) +settings.notifications.tab.soundSettingsOff = Impostazioni suoni (Silenzioso) settings.notifications.column.event = Eventi -settings.notifications.column.notification = Notifiche +settings.notifications.column.notification = Notifiche del desktop settings.notifications.column.sound = Suoni settings.boolean.nHideOnStart = No notifiche all'avvio settings.boolean.nHideOnStart.tip = Non verranno mostrate notifiche nei primi 120 secondi dall'avvio di Chatty. +settings.notifications.event = Evento\: +settings.notifications.channel = Canale\: +settings.notifications.textMatch = Corrispondenza\: +settings.notifications.textMatch.tip = Si attiva solo quando il testo della notifica (o di altre proprietà) corrisponde a quello di questa impostazione. Usa il formato delle corrispondenze evidenziate. +settings.notifications.soundFile = File audio\: +settings.notifications.soundCooldown.tip = Numero di secondi che devono passare prima di poter riprodurre nuovamente l'ultimo suono. notification.type.stream_status.tip = Quando cambia lo status di uno stream (inizio, fine, cambio titolo/gioco). -notification.type.info.tip = I messaggi che non sono della chat, come le risposte a comandi, Abbonamenti e altro (può causare duplicati in combinazione con Eventi quali Notifiche Abbonamenti e Messaggi dell'AutoMod). -notification.type.message.tip = Tutti i messaggi in chat, tranne quelli "In Evidenza". -notification.type.new_followers.tip = I nuovi Follower sono elencati in "Extra - Followers" (funziona solo con la lista Follower aperta). +notification.type.info.tip = I messaggi che non sono della chat, come le risposte a comandi, abbonamenti e altro (può causare duplicati in combinazione con eventi quali notifiche abbonamenti e messaggi dell'AutoMod). +notification.type.message.tip = Tutti i messaggi in chat, tranne quelli evidenziati. +notification.type.new_followers.tip = I nuovi seguaci sono elencati in "Extra - Seguaci" (funziona solo con la lista seguaci aperta). notification.type.subscriber.tip = Si basa sulle notifiche degli abbonamenti nella chat. -notification.type.highlight.tip = Messaggi messi in evidenza dalla lista "In Evidenza". +notification.type.highlight.tip = Messaggi evidenziati. notification.type.join.tip = Un utente è entrato nel canale (molto inaffidabile). notification.type.part.tip = Un utente è uscito dal canale (molto inaffidabile). notification.type.automod.tip = Messaggi rifiutati da AutoMod (solo moderatori). -notification.type.whisper.tip = Messaggi Privati (devono essere abilitati in "Avanzate - Messaggi Privati"). +notification.type.whisper.tip = Messaggi privati (devono essere abilitati in "Avanzate - Messaggi Privati"). notification.typeOption.fav = Solo canali preferiti -notification.typeOption.fav.tip = Canali che sono stati inseriti nei Preferiti in "Canali > Preferiti / Cronologia" +notification.typeOption.fav.tip = Canali che sono stati inseriti nei preferiti in "Canali > Preferiti / Cronologia" +notification.typeOption.favGame = Solo giochi preferiti +notification.typeOption.favGame.tip = Giochi aggiunti ai preferiti nel menù contestuale dei canali live, sotto "Ordina per..." notification.typeOption.bits = Solo i messaggi che contengono Bit. notification.typeOption.own = Anche i propri messaggi. notification.typeOption.noOffline = Solo gli stream live (non 'stream offline') @@ -973,54 +1059,66 @@ notification.typeOption.noUptime = Nascondi uptime dello stream !-- Stream Settings --! settings.section.streamHighlights = Highlight dello stream -settings.section.streamHighlightsCommand = Comandi della chat -settings.streamHighlights.info = Gli Highlight dello Stream (scrive l'uptime/nota su file) possono essere aggiunti tramite il comando /addStreamHighlight, hotkey (impostazioni Hotkeys) o comando chat (che puoi configurare qui). Vedere l'aiuto per ulteriori informazioni. -settings.streamHighlights.matchInfo = Si consiglia vivamente di limitare i comandi della chat ad utenti fidati, come i Moderatori. -settings.streamHighlights.channel = Comando del canale\: +settings.section.streamHighlightsCommand = Comando della chat +settings.streamHighlights.info = Gli Highlight dello stream (scrive l'uptime e note su un file) possono essere aggiunti tramite il comando /addStreamHighlight, hotkey (impostazioni Hotkeys) o un comando chat (che puoi configurare qui). Leggi la guida per ulteriori informazioni. +settings.streamHighlights.matchInfo = Si consiglia vivamente di limitare il comando della chat ad utenti fidati, come i moderatori. +settings.streamHighlights.channel = Canale del comando\: settings.streamHighlights.command = Nome del comando\: settings.streamHighlights.match = Accesso al comando\: -settings.boolean.streamHighlightChannelRespond = Rispondi al comando con un messaggio di chat -settings.boolean.streamHighlightChannelRespond.tip = Invia un messaggio alla chat, in modo che i moderatori possano vedere che il comando ha avuto successo -settings.boolean.streamHighlightMarker = Crea un indicatore dello Stream quando aggiungi un Highlight dello Stream -settings.boolean.streamHighlightMarker.tip = Crea un indicatore dello Stream oltre a scrivere l'Highlight dello Stream sul file +settings.boolean.streamHighlightChannelRespond = Rispondi al comando con un messaggio in chat +settings.boolean.streamHighlightChannelRespond.tip = Invia un messaggio alla chat, in modo che i moderatori possano vedere che il comando ha avuto successo. +settings.streamHighlights.responseMsg = Messaggio di risposta al comando. +settings.boolean.streamHighlightMarker = Crea un indicatore dello stream quando aggiungi un Highlight +settings.boolean.streamHighlightMarker.tip = Crea un indicatore dello stream oltre a scrivere l'Highlight su file. + +!-- Hotkey Settings --! +settings.hotkeys.key.button.set = Aggiungi combinazione di tasti +settings.hotkeys.key.button.clear = Rimuovi combinazione di tasti +settings.hotkeys.key.set.info = Premi un tasto, una combinazione di tasti o ESC per annullare. +settings.hotkeys.key.set.title = Aggiungi combinazione di tasti +# Should be kept short +settings.hotkeys.key.empty = Nessun tasto. +settings.hotkeys.action = Azione\: +settings.hotkeys.delay = Ritardo\: +settings.hotkeys.delay.tip = Tempo (in decimi di secondo) che deve passare per poter attivare nuovamente l'azione. !===================! !== Emotes Dialog ==! !===================! # {0} = Name of the channel (or "-" if no channel) -emotesDialog.title = Emoticons (Globali/Abbonati/{0}) -emotesDialog.tab.favorites = Preferiti -emotesDialog.tab.myEmotes = Mie Emotes -emotesDialog.tab.channel = Canale -emotesDialog.tab.other = Altro -emotesDialog.refresh = Ricarica emote nella finestra corrente +emotesDialog.title = Emoticon (globali/abbonati/{0}) +emotesDialog.tab.favorites = Preferite +emotesDialog.tab.myEmotes = Le mie Emoticon +emotesDialog.tab.channel = Emot. canale +emotesDialog.tab.other = Altre +emotesDialog.refresh = Ricarica emoticon nella finestra corrente emotesDialog.refreshInactive = Impossibile ricaricare -emotesDialog.noFavorites = Non hai aggiunto nessuna emotes preferita -emotesDialog.noFavorites.hint = (Click destro sulla emote e scegli 'Preferiti') -emotesDialog.subscriptionRequired = Devi essere Abbonato per usare queste emotes\: +emotesDialog.noFavorites = Non hai Emoticon preferite +emotesDialog.noFavorites.hint = (Fai clic destro su un'Emoticon e scegli 'Aggiungi ai preferiti') +emotesDialog.subscriptionRequired = Devi essere un abbonato per usare queste Emoticon\: emotesDialog.notFoundFavorites = Preferiti non ancora trovati (metadati non caricati)\: emotesDialog.favoriteCmInfo = (Clic destro per visualizzare le informazioni e rimuovere dai Preferiti, se necessario.) -emotesDialog.subEmotesAccess = Aggiungi l'accesso a "I miei abbonamenti" attraverso "Menù - Login.." per mostrare le emote speciali. -emotesDialog.noSubemotes = Non sembri avere emote sub o turbo +emotesDialog.subEmotesAccess = Aggiungi l'accesso a "I miei abbonamenti" attraverso "Menù - Login.." per mostrare le emoticon speciali. +emotesDialog.noSubemotes = Non sembri avere Emoticon Turbo o per abbonati. emotesDialog.subEmotesJoinChannel = (Devi entrare in un canale perché vengano riconosciute.) emotesDialog.otherSubemotes = Altro emotesDialog.noChannel = Nessun canale. -emotesDialog.noChannelEmotes = Nessuna emotes trovata per \#{0} -emotesDialog.noChannelEmotes2 = Nessuna emote FFZ o BTTV trovata. -emotesDialog.channelEmotes = Emotes specifiche per \#{0} +emotesDialog.noChannelEmotes = Nessuna Emoticon trovata in \#{0} +emotesDialog.noChannelEmotes2 = Nessuna emoticon di FFZ o BTTV trovata. +emotesDialog.channelEmotes = Emoticon del canale \#{0} # {0} = Channel name (without leading #) emotesDialog.backToChannel = Torna in \#{0} -emotesDialog.subemotes = Emote dagli abbonamenti {0} +emotesDialog.subemotes = Emot. per abbonati di {0} emotesDialog.subscriptionRequired2 = (È necessario essere abbonati per utilizzare queste.) emotesDialog.subscriptionRequired3 = (Abbonamento di fascia più alta necessario per utilizzare queste emote.) -emotesDialog.globalTwitch = Emotes globali di Twitch -emotesDialog.globalFFZ = Emotes globali di FFZ -emotesDialog.globalBTTV = Emotes globali di BTTV +emotesDialog.globalTwitch = Emoticon globali di Twitch +emotesDialog.globalFFZ = Emoticon globali di FFZ +emotesDialog.globalBTTV = Emoticon globali di BTTV # {0} = Emote Code -emotesDialog.details.title = Dettagli Emotes\: {0} +emotesDialog.details.title = Dettagli Emoticon\: {0} emotesDialog.details.code = Codice\: emotesDialog.details.type = Tipo\: -emotesDialog.details.id = Emote ID\: +emotesDialog.details.id = ID dell'Emoticon\: emotesDialog.details.channel = Canale\: # Details where the emote can be used (channel/globally) emotesDialog.details.usableIn = Usabilità\: @@ -1032,5 +1130,5 @@ emotesDialog.details.everyone = Chiunque emotesDialog.details.restricted = Limitato emotesDialog.details.accessAvailable = (Hai accesso) emotesDialog.details.size = Dimensione regolare\: -emotesDialog.details.by = Emote da\: -emotesDialog.details.info = Click destro sulla emotes qui o in chat per aprire il menù contestuale con Info/Opzioni +emotesDialog.details.by = Creata da\: +emotesDialog.details.info = Fai clic con il tasto destro su un'Emoticon qui o in chat per aprire il menù contestuale con informazioni e opzioni diff --git a/src/chatty/lang/Strings_ko.properties b/src/chatty/lang/Strings_ko.properties index 303f6fe83..319ec0d91 100644 --- a/src/chatty/lang/Strings_ko.properties +++ b/src/chatty/lang/Strings_ko.properties @@ -78,7 +78,7 @@ channelCm.unfavorite = 즐겨찾기 삭제 channelCm.speedruncom = Speedrun.com 열기 channelCm.closeChannel = 채널 닫기 # Uses plural form and shows number when joining more than one channel -channelCm.join = {0,choice,1\#|1<{0}개 }채널 입장 +channelCm.join = {0,choice,1#|1<{0}개 }채널 입장 !-- User Context Menu Entries --! # {0} = User Display Name @@ -230,7 +230,7 @@ login.removeLogin.button.remove = 토큰 제거 !=================! join.title = 채널 입장 # Uses plural form when joining more than one channel -join.button.join = {0,choice,1\#|1<{0}개 }채널 입장 +join.button.join = {0,choice,1#|1<{0}개 }채널 입장 join.button.favoritesHistory = 즐겨찾기/기록 join.channel = 채널\: @@ -245,7 +245,7 @@ favorites.button.addToFavorites = 즐겨찾기 추가 favorites.button.removeFromFavorites = 선택된 것 즐겨찾기 삭제 favorites.button.remove = 선택된 것 삭제 # Uses plural form and shows number when joining more than one channel -favorites.button.joinChannels = {0,choice,1\#|1<{0}개 }채널 입장 +favorites.button.joinChannels = {0,choice,1#|1<{0}개 }채널 입장 !================================! !== Highlighted/Ignored Dialog ==! @@ -398,7 +398,7 @@ searchDialog.button.search = 검색 !=====================! openUrl.title = 기본 브라우저에서 열까요? # {0} = Number of URLs to open, decide between singular and plural -openUrl.button.open = {0,choice,1\#|1<{0}개 }URL 열기 +openUrl.button.open = {0,choice,1#|1<{0}개 }URL 열기 openUrl.button.copy = URL 복사 !=====================! diff --git a/src/chatty/lang/Strings_pl.properties b/src/chatty/lang/Strings_pl.properties index c8330e9bc..d9227fdf4 100644 --- a/src/chatty/lang/Strings_pl.properties +++ b/src/chatty/lang/Strings_pl.properties @@ -90,7 +90,7 @@ channelCm.unfavoriteGame = Usuń grę z ulubionych channelCm.speedruncom = Otwórz Speedrun.com channelCm.closeChannel = Zamknij kanał # Uses plural form and shows number when joining more than one channel -channelCm.join = Dołącz do {0,choice,1\#kanału|1<{0} kanałów} +channelCm.join = Dołącz do {0,choice,1#kanału|1<{0} kanałów} !-- Other Context Menu Entries --! textCm.copy = Kopiuj @@ -283,7 +283,7 @@ saveSettings.saveAndBackup = Zapisz i zrób kopię !=================! join.title = Dołącz do kanału # Uses plural form when joining more than one channel -join.button.join = Dołącz do {0,choice,1\#kanału|1 +login.button.requestLogin = Conectar conta Twitch +login.access.chat = Acessar Chat +login.access.user = Ler info. Usuário +login.access.editor = Permissão de Editor +# Commercials as in Advertisements +login.access.commercials = Rodar Propagandas +login.access.subscribers = Mostrar Inscritos +login.access.follow = Seguir Canais + +!=================! +!== Join Dialog ==! +!=================! +join.title = Conectar Canal +# Uses plural form when joining more than one channel +join.button.join = Entrar no(s) {0,choice,1#canal|1F para Fav/Desfavoritar, Del para apagar ou clique com botão direito para abrir o menu de opções. + +!-- Select Game --! +admin.game.title = Selecionar Jogo +# HTML +admin.game.info =

    Digite parte do jogo e pressione Enter ou clique em 'Procurar', depois selecione o jogo pelo resultado (Usar os nomes que a Twitch retorna garante que que você apareça na categoria correta).

    +admin.game.button.search = Procurar +admin.game.button.clearSearch = Limpar Pesquisa +admin.game.button.favorite = Add aos Favoritos +admin.game.button.unfavorite = Remover dos Favoritos +admin.game.searching = Procurando.. +# {0} = Number of search results, {1} = Number of favorites +admin.game.listInfo = Busca\: {0} / Favoritos\: {1} + +!=========================! +!== Channel Info Dialog ==! +!=========================! +channelInfo.title = Canal\: {0} +channelInfo.title.followed = seguido +channelInfo.history = Status (Histórico) +channelInfo.playing = Jogando +channelInfo.noInfo = [Sem informações da Stream] +channelInfo.cm.copyAllCommunities = Copiar tudo +channelInfo.offline.tip = Duração Última Transmissão (aprox.)\: {0} +# "With PICNIC" refers to the uptime that disregards small stream downtimes (so it's longer) +channelInfo.offline.tip.picnic = Duração Última Transmissão (aprox.)\: {0} (Com PICNIC\: {1}) +channelInfo.uptime.tip = Stream Iniciada\: {0} +# "With PICNIC" refers to the uptime that disregards small stream downtimes (so it's longer) +channelInfo.uptime.tip.picnic = Stream Iniciada\: {0} (Com PICNIC\: {1}) +channelInfo.viewers.latest = Recente\: {0} +channelInfo.viewers.now = Agora\: {0} +channelInfo.viewers.noHistory = Sem Histórico de Viwers +channelInfo.viewers.cm.timeRange = Intervalo de Tempo +channelInfo.viewers.cm.timeRange.option = {0} {0,choice,1#Hora|1 rotasını izle ve programı yeniden başlat. chat.topic = Başlık # {0} = The name of the entered command -chat.unknownCommand = Bilinmeyen komut\: "/{0}" (İpucu\: Twitch'in kendi sohbet komutlarını önünde bir nokta ile gönderin, örn. ".mods", böylece Chatty'nin komutları ile karışmaz) +chat.unknownCommand = Bilinmeyen komut\: "/{0}" (İpucu\: Twitch''in kendi sohbet komutlarını önünde bir nokta ile gönderin, örn. ".mods", böylece Chatty''nin komutları ile karışmaz) !====================! !== General Dialog ==! @@ -213,7 +213,7 @@ login.removeLogin.button.remove = Tokeni Kaldır !=================! join.title = Kanala katıl # Uses plural form when joining more than one channel -join.button.join = {0,choice,1\#Kanala|1 completionLangs = new HashMap<>(); static { // "Ж" and "ж" are swaped because "search" text is in lower case. @@ -66,10 +78,15 @@ public static String replaceWrongLanguage(String text, String fromLang, String t } public static String removeSharpFromTitle(Channel channel) { - if (channel.getType() == Channel.Type.CHANNEL && REMOVE_SHARP) { - return channel.getName().substring(1); - } - return channel.getName(); + return (channel.getType() == Channel.Type.CHANNEL) + ? removeSharpFromTitle(channel.getName()) + : channel.getName(); + } + + public static String removeSharpFromTitle(String title) { + return (REMOVE_SHARP && title.startsWith("#")) + ? title.substring(1) + : title; } public static boolean isItYoutubeUrl(String url) { @@ -86,7 +103,7 @@ public static String getTooltip(String url) { String taks = "https://api.betterttv.net/2/link_resolver/" + URLEncoder.encode(url, "ISO-8859-1"); taks = taks.replaceAll("\\+", ""); JSONParser parser = new JSONParser(); - JSONObject json = (JSONObject) parser.parse(readUrl(taks)); + JSONObject json = (JSONObject) parser.parse(getUrl(taks)); String tooltip = (String)json.get("tooltip"); tooltip = tooltip.replaceAll("\n", "
    "); tooltip = chatty.Helper.htmlspecialchars_decode(tooltip); @@ -102,11 +119,10 @@ public static String getIdChannel(String channel) { String urlId = "https://api.twitch.tv/kraken/channels/" + channel.substring(1) + "?client_id=" + Chatty.CLIENT_ID; System.out.println(urlId); JSONParser parser = new JSONParser(); - JSONObject json = (JSONObject) parser.parse(readUrl(urlId)); + JSONObject json = (JSONObject) parser.parse(getUrl(urlId)); String id = (Long)json.get("_id") + ""; System.out.println(id); return id; - } catch (org.json.simple.parser.ParseException | java.io.UnsupportedEncodingException ee) { } catch (Exception e) { } return ""; @@ -114,14 +130,17 @@ public static String getIdChannel(String channel) { public static List getRecentMessages(String channel) { try { - String channelId = getIdChannel(channel); + // String channelId = getIdChannel(channel); // Old API. // String urlId = "https://tmi.twitch.tv/api/rooms/" + channelId + "/recent_messages?client_id=" + Chatty.CLIENT_ID; // New custom API from RAnders00. - String urlId = "https://recent-messages.robotty.de/api/v2/recent-messages/" + channel.substring(1) + "?clearchatToNotice=true"; - System.out.println(urlId); + String urlId = "https://recent-messages.robotty.de/" + + "api/v2/recent-messages/" + + channel.substring(1) + + "?clearchatToNotice=true"; + // System.out.println(urlId); JSONParser parser = new JSONParser(); - JSONObject json = (JSONObject) parser.parse(readUrl(urlId)); + JSONObject json = (JSONObject) parser.parse(getUrl(urlId)); JSONArray msg = (JSONArray) json.get("messages"); List messages = new ArrayList(); @@ -134,68 +153,108 @@ public static List getRecentMessages(String channel) { //System.out.println(id); return messages; - } catch (org.json.simple.parser.ParseException | java.io.UnsupportedEncodingException ee) { } catch (Exception e) { } return new ArrayList(); } public static void updateUserFromTags(User user, MsgTags tags) { - //From TwitchConnection + // From TwitchConnection. if (tags.isEmpty()) { return; } + /** + * Any and all tag values may be null, so account for that when + * checking against them. + */ + // Whether anything in the user changed to warrant an update boolean changed = false; - - Map badges = chatty.Helper.parseBadges(tags.get("badges")); + + Map badges = Helper.parseBadges(tags.get("badges")); if (user.setTwitchBadges(badges)) { changed = true; - } + } + + Map badgeInfo = Helper.parseBadges(tags.get("badge-info")); + String subMonths = badgeInfo.get("subscriber"); + if (subMonths == null) { + subMonths = badgeInfo.get("founder"); + } + if (subMonths != null) { + user.setSubMonths(Helper.parseShort(subMonths, (short)0)); + } + + // if (settings.getBoolean("ircv3CapitalizedNames")) { + if (user.setDisplayNick(StringUtil.trim(tags.get("display-name")))) { + changed = true; + } + // } + // Update color String color = tags.get("color"); if (color != null && !color.isEmpty()) { user.setColor(color); - } + } + // Update user status boolean turbo = tags.isTrue("turbo") || badges.containsKey("turbo") || badges.containsKey("premium"); if (user.setTurbo(turbo)) { changed = true; } - if (user.setSubscriber(tags.isTrue("subscriber"))) { + boolean subscriber = badges.containsKey("subscriber") || badges.containsKey("founder"); + if (user.setSubscriber(subscriber)) { + changed = true; + } + if (user.setVip(badges.containsKey("vip"))) { changed = true; } - - // Temporarily check both for containing a value as Twitch is - // changing it - String userType = tags.get("user-type"); - if (user.setModerator("mod".equals(userType))) { + if (user.setModerator(badges.containsKey("moderator"))) { changed = true; } - if (user.setStaff("staff".equals(userType))) { + if (user.setAdmin(badges.containsKey("admin"))) { changed = true; } - if (user.setAdmin("admin".equals(userType))) { + if (user.setStaff(badges.containsKey("staff"))) { changed = true; } - + user.setId(tags.get("user-id")); } - private static String readUrl(String urlString) throws Exception { - BufferedReader reader = null; + private static String getUrl(String targetUrl) { + Charset charset = Charset.forName("UTF-8"); + URL url; + HttpURLConnection connection = null; + try { - URL url = new URL(urlString); - reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8")); - StringBuffer buffer = new StringBuffer(); - int read; - char[] chars = new char[1024]; - while ((read = reader.read(chars)) != -1) - buffer.append(chars, 0, read); - - return buffer.toString(); + url = new URL(targetUrl); + connection = (HttpURLConnection)url.openConnection(); + connection.setRequestProperty("User-Agent", USER_AGENT); + + // Read response + InputStream input = connection.getInputStream(); + + StringBuilder response; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(input, charset))) { + String line; + response = new StringBuilder(); + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + + return response.toString(); + } catch (SocketTimeoutException ex) { + System.out.println(ex.toString()); + return null; + } catch (IOException ex) { + System.out.println(ex.toString()); + return null; } finally { - if (reader != null) - reader.close(); + if (connection != null) { + connection.disconnect(); + } } } @@ -204,5 +263,5 @@ public static String safeSubstring(final String str, final int start, final int Math.max(0, start), Math.min(end, str.length())); } - + } diff --git a/src/chatty/util/MiscUtil.java b/src/chatty/util/MiscUtil.java index 89b3c24c1..ebfb13898 100644 --- a/src/chatty/util/MiscUtil.java +++ b/src/chatty/util/MiscUtil.java @@ -288,4 +288,8 @@ public static void addLimited(Set source, Set target, int limit) { } } + public static boolean isBitEnabled(int value, int bit) { + return (value & bit) != 0; + } + } diff --git a/src/chatty/util/Pronouns.java b/src/chatty/util/Pronouns.java new file mode 100644 index 000000000..805067b00 --- /dev/null +++ b/src/chatty/util/Pronouns.java @@ -0,0 +1,165 @@ + +package chatty.util; + +import chatty.Helper; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.logging.Logger; +import javax.swing.Timer; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +/** + * + * @author tduva + */ +public class Pronouns { + + private static final Logger LOGGER = Logger.getLogger(Pronouns.class.getName()); + + private static final Object LOCK = new Object(); + + // Should be handled as immutable + private volatile Map pronouns = new HashMap<>(); + private final CachedBulkManager data; + private volatile static Pronouns instance; + + public static Pronouns instance() { + synchronized(LOCK) { + if (instance == null) { + instance = new Pronouns(); + } + return instance; + } + } + + private static final String NOT_FOUND = "__EMPTY_RESULT__"; + + public Pronouns() { + data = new CachedBulkManager<>(new CachedBulkManager.Requester() { + @Override + public void request(CachedBulkManager manager, Set asap, Set normal, Set backlog) { + String username = manager.makeAndSetRequested(asap, normal, backlog, 1).iterator().next(); + UrlRequest request = new UrlRequest("https://pronouns.alejo.io/api/users/" + username); + request.async((result, responseCode) -> { + if (responseCode == 404) { + manager.setNotFound(username); + } else if (result == null) { + manager.setError(username); + } else { + String pronoun_id = parseUser(result); + if (pronoun_id == null) { + /** + * Empty result means it won't overwrite an already + * requested one in getUser2(), but it will not find + * a pronoun for it. + */ + manager.setResult(username, NOT_FOUND); + } + else { + manager.setResult(username, pronoun_id); + } + } + }); + } + }, CachedBulkManager.DAEMON | CachedBulkManager.UNIQUE); + requestPronouns(); + } + + private final Object UNIQUE = new Object(); + + /** + * Previous request is always overwritten (UNIQUE). If it should be used + * from other stuff than just the User Dialog, this may need to change. + * + * @param listener + * @param username + */ + public void getUser(BiConsumer listener, String username) { + if (!Helper.isValidStream(username)) { + return; + } + data.query(UNIQUE, (CachedBulkManager.Result result) -> { + if (pronouns.isEmpty()) { + // If other request isn't finished yet, wait a bit + Timer timer = new Timer(1000, e -> sendResult(listener, username, result)); + timer.setRepeats(false); + timer.start(); + } + else { + sendResult(listener, username, result); + } + }, CachedBulkManager.ASAP, username); + } + + private long userCounter = 0; + + public String getUser2(String username) { + if (!Helper.isValidStream(username)) { + return null; + } + /** + * Keep only the most recent requests. This should ensure that it + * doesn't keep requesting for ages, even after e.g. leaving a busy + * channel. + */ + userCounter++; + String unique = "user" + (userCounter % 5); + return pronouns.get(data.getOrQuerySingle(unique, null, CachedBulkManager.NONE, username)); + } + + private void sendResult(BiConsumer listener, String username, CachedBulkManager.Result result) { + String r = pronouns.get(result.get(username)); + if (r != null) { + listener.accept(username, r); + } + } + + private void requestPronouns() { + UrlRequest request = new UrlRequest("https://pronouns.alejo.io/api/pronouns"); + request.async((result, responseCode) -> { + if (result != null) { + Map parsed = parsePronouns(result); + if (!parsed.isEmpty()) { + pronouns = parsed; + } + } + }); + } + + private Map parsePronouns(String json) { + Map result = new HashMap<>(); + try { + JSONParser parser = new JSONParser(); + JSONArray root = (JSONArray)parser.parse(json); + for (Object o : root) { + JSONObject entry = (JSONObject)o; + String name = JSONUtil.getString(entry, "name"); + String display = JSONUtil.getString(entry, "display"); + if (name != null && display != null) { + result.put(name, display); + } + } + } catch (Exception ex) { + LOGGER.warning("Error parsing pronouns: "+ex); + } + return result; + } + + private String parseUser(String json) { + try { + JSONParser parser = new JSONParser(); + JSONArray root = (JSONArray)parser.parse(json); + if (!root.isEmpty()) { + return JSONUtil.getString((JSONObject) root.get(0), "pronoun_id"); + } + } catch (Exception ex) { + LOGGER.warning("Error parsing pronouns: "+ex); + } + return null; + } + +} diff --git a/src/chatty/util/RawMessageTest.java b/src/chatty/util/RawMessageTest.java index f87e8998b..a9fa76282 100644 --- a/src/chatty/util/RawMessageTest.java +++ b/src/chatty/util/RawMessageTest.java @@ -27,7 +27,7 @@ public static String simulateIRC(String channel, String parameters, String local return "@badges=subscriber/1;color=;display-name=USERNAME;emotes=;id=123;login=username;mod=0;msg-id=resub;msg-param-months=4;subscriber=1;system-msg=USERNAME\\ssubscribed\\sfor\\s4\\smonths\\sin\\sa\\srow!;tmi-sent-ts=1475037717295;turbo=0;user-id=123;user-type= :tmi.twitch.tv USERNOTICE "+channel; } if (type.equals("sub2")) { - return "@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=Tduvatest;emotes=;mod=0;msg-id=sub;msg-param-months=6;room-id=1337;subscriber=1;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Channel\\sSubscription\\s(display_name);system-msg=Tduvatest\\shas\\ssubscribed\\sfor\\s6\\smonths!;login=tduvatest;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE "+channel+" :Great stream -- keep it up!"; + return "@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=Tduvatest;emotes=;mod=0;msg-id=sub;msg-param-months=6;room-id=1337;subscriber=1;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Channel\\sSubscription\\s(display_name);system-msg=Tduvatest\\shas\\ssubscribed\\sfor\\s6\\smonths!;login=tduvatest;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE "+channel+" :"+(options != null ? options : "Great stream -- keep it up!"); } if (type.equals("sub3")) { return "@badges=subscriber/0,bits/100;color=#B22222;display-name=TWITCH_UserName;emotes=;id=123;login=twitch_username;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=USER2;msg-param-recipient-id=123;msg-param-recipient-user-name=user2;msg-param-sub-plan-name=Abc;msg-param-sub-plan=1000;room-id=123;subscriber=1;system-msg=TWITCH_UserName\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sUSER2!;tmi-sent-ts=1520532381349;turbo=0;user-id=123;user-type= :tmi.twitch.tv USERNOTICE "+channel; @@ -49,6 +49,9 @@ public static String simulateIRC(String channel, String parameters, String local if (type.equals("anonsubgift3m")) { return "@badge-info=;badges=;color=;display-name=AnAnonymousGifter;emotes=;flags=;id=1234;login=ananonymousgifter;mod=0;msg-id=subgift;msg-param-fun-string=FunStringTwo;msg-param-gift-months=3;msg-param-months=22;msg-param-origin-id=da\\s39\\sa3\\see\\s5e\\s6b\\s4b\\s0d\\s32\\s55\\sbf\\sef\\s95\\s60\\s18\\s90\\saf\\sd8\\s07\\s09;msg-param-recipient-display-name=USERNAME;msg-param-recipient-id=1234;msg-param-recipient-user-name=username;msg-param-sub-plan-name=StreamName\\sSub;msg-param-sub-plan=1000;room-id=1234;subscriber=0;system-msg=An\\sanonymous\\suser\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sUSERNAME!\\s;tmi-sent-ts=1234;user-id=1234;user-type= :tmi.twitch.tv USERNOTICE "+channel; } + if (type.equals("subgift2")) { + return "@badge-info=subscriber/3;badges=moderator/1,subscriber/3;color=;display-name=USERNAME;emotes=;flags=;id=1234;login=username;mod=1;msg-id=resub;msg-param-anon-gift=false;msg-param-cumulative-months=3;msg-param-gift-month-being-redeemed=3;msg-param-gift-months=3;msg-param-gifter-id=12345;msg-param-gifter-login=gifterusername;msg-param-gifter-name=gifterUSERNAME;msg-param-months=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(channel);msg-param-sub-plan=1000;msg-param-was-gifted=true;room-id=123;subscriber=1;system-msg=USERNAME\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s3\\smonths!;tmi-sent-ts=12345;user-id=1234;user-type=mod :tmi.twitch.tv USERNOTICE "+channel+" :Thanks to @gifterUSERNAME for my sub gift!"; + } if (type.equals("primesub")) { return "@badge-info=subscriber/14;badges=subscriber/12;color=#00FF7F;display-name=USERNAME;emotes=;flags=;id=1234;login=username;mod=0;msg-id=resub;msg-param-cumulative-months=14;msg-param-months=0;msg-param-multimonth-duration=0;msg-param-multimonth-tenure=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=StreamName\\sSub;msg-param-sub-plan=Prime;msg-param-was-gifted=false;room-id=1234;subscriber=1;system-msg=USERNAME\\ssubscribed\\swith\\sTwitch\\sPrime.\\sThey've\\ssubscribed\\sfor\\s14\\smonths!;tmi-sent-ts=1234;user-id=1234;user-type= :tmi.twitch.tv USERNOTICE "+channel+" :F1 subscription fee"; } @@ -151,6 +154,9 @@ public static String simulateIRC(String channel, String parameters, String local if (type.equals("reply")) { return "@badge-info=subscriber/40;badges=broadcaster/1,subscriber/3024,partner/1;client-nonce=abc;color=#FF526F;display-name=TestUser;emotes=;flags=;id=abc;mod=0;reply-parent-display-name=OtherUser;reply-parent-msg-body=Test:\\sAbc;reply-parent-msg-id=abcd;reply-parent-user-id=123;reply-parent-user-login=otheruser;room-id=123;subscriber=1;tmi-sent-ts=123;turbo=0;user-type= :testuser!testuser@testuser.tmi.twitch.tv PRIVMSG "+channel+" :@OtherUser This is a reply!"; } + if (type.equals("founder")) { + return "@badge-info=founder/29;badges=moderator/1,founder/0;client-nonce=1234;color=#0000FF;display-name=USERNAME;emotes=;flags=;id=1234;mod=1;room-id=1234;subscriber=0;tmi-sent-ts=1234;turbo=0;user-id=1234;user-type=mod :username!username@username.tmi.twitch.tv PRIVMSG " + channel + " :Ja bitte?"; + } return null; } diff --git a/src/chatty/util/dnd/DockBase.java b/src/chatty/util/dnd/DockBase.java new file mode 100644 index 000000000..cb156714c --- /dev/null +++ b/src/chatty/util/dnd/DockBase.java @@ -0,0 +1,251 @@ + +package chatty.util.dnd; + +import chatty.util.dnd.DockDropInfo.DropType; +import java.awt.BorderLayout; +import java.util.List; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; + +/** + * Holds content or components that hold content (tabs). + * + * @author tduva + */ +public class DockBase extends JPanel implements DockChild { + + private final DockOverlay overlay; + private final DockManager manager; + + private DockChild child; + + public DockBase(DockManager m) { + setLayout(new BorderLayout()); + child = new DockTabsContainer(); + child.setBase(this); + child.setDockParent(this); + add(child.getComponent(), BorderLayout.CENTER); + overlay = new DockOverlay(this); + this.manager = m; + } + + public DockDropInfo findDrop(DockImportInfo info) { + DockDropInfo childDrop = child.findDrop(info); + if (childDrop != null && childDrop.location == DropType.INVALID) { + return null; + } + if (childDrop != null) { + return childDrop; + } + else { + DropType location = DockDropInfo.determineLocation(this, info.getLocation(this), 20, 80, 0); + if (location != null && location != DropType.CENTER) { + return new DockDropInfo(this, location, DockDropInfo.makeRect(this, location, 20, 0, 80), -1); + } + } + return childDrop; + } + + @Override + public void addContent(DockContent content) { + child.addContent(content); + } + + public void requestDrag() { + manager.requestDrag(); + } + + public void startDrag() { + getRootPane().setGlassPane(overlay); + overlay.setVisible(true); + overlay.setOpaque(false); + } + + /** + * Informs the manager that the drag has stopped. The manager will then + * inform all bases. + * + * @param t + */ + public void requestStopDrag(DockTransferable t) { + manager.requestStopDrag(t); + } + + /** + * Remove the drag overlay. + */ + public void stopDrag() { + overlay.setVisible(false); + repaint(); + } + + @Override + public JComponent getComponent() { + return this; + } + + @Override + public void split(DockDropInfo info, DockContent content) { + if (child.isEmpty()) { + child.addContent(content); + return; + } + DockTabsContainer newCompTabs = new DockTabsContainer(); + DockSplit newChildSplit = DockUtil.createSplit(info, child, newCompTabs); + if (newChildSplit != null) { + // Configure new child + applySettings(newChildSplit); + newChildSplit.setBase(this); + newChildSplit.setDockParent(this); + // Configure tabs (basicially caused the split) + applySettings(newCompTabs); + newCompTabs.setBase(this); + newCompTabs.setDockParent(newChildSplit); + newCompTabs.addContent(content); + // Configure old child (now component in new split) + child.setDockParent(newChildSplit); + + // Exchange child + remove(child.getComponent()); + child = newChildSplit; + add(child.getComponent(), BorderLayout.CENTER); + // Divider + DockSplit split = newChildSplit; + split.setDividerLocation(0.5); + split.setResizeWeight(0.5); + SwingUtilities.invokeLater(() -> { + split.setDividerLocation(0.5); + split.setResizeWeight(0.5); + }); + } + } + + @Override + public void setBase(DockBase base) { + // Not applicable, this is the base + } + + @Override + public void setDockParent(DockChild parent) { + // Not applicable, this is the top component + } + + @Override + public DockChild getDockParent() { + return null; + } + + @Override + public boolean isEmpty() { + return child.isEmpty(); + } + + @Override + public void removeContent(DockContent content) { + child.removeContent(content); + validate(); + } + + @Override + public void drop(DockTransferInfo info) { + // Once a drop was initiated by the user, should reset target path + info.importInfo.content.setTargetPath(null); + + info.importInfo.source.removeContent(info.importInfo.content); + split(info.dropInfo, info.importInfo.content); + } + + @Override + public void replace(DockChild old, DockChild replacement) { + if (replacement == null) { +// System.out.println("BASE EMPTY"); + manager.baseEmpty(this); + return; + } + if (old == child && old != replacement) { + // System.out.println("BASE REPLACE "+old+"\nWITH "+replacement); + remove(child.getComponent()); + replacement.setBase(this); + replacement.setDockParent(this); + child = replacement; + add(child.getComponent(), BorderLayout.CENTER); + } + } + + @Override + public List getContents() { + return child.getContents(); + } + + /** + * When a new tab is active. Also when a split gets removed. + * + * @param tabs The tab pane the tab was changed in, may be null + * @param content + */ + public void tabChanged(DockTabs tabs, DockContent content) { + manager.changedActiveContent(content, false); + } + + @Override + public void setActiveContent(DockContent content) { + child.setActiveContent(content); + } + + @Override + public boolean isContentVisible(DockContent content) { + return child.isContentVisible(content); + } + + @Override + public List getContentsRelativeTo(DockContent content, int direction) { + return child.getContentsRelativeTo(content, direction); + } + + @Override + public void setSetting(DockSetting.Type setting, Object value) { + switch (setting) { + case FILL_COLOR: + overlay.setFillColor(DockSetting.getColor(value)); + break; + case LINE_COLOR: + overlay.setLineColor(DockSetting.getColor(value)); + break; + } + child.setSetting(setting, value); + } + + /** + * Apply all setting values stored in the manager to the given child. + * + * @param child + */ + public void applySettings(DockChild child) { + manager.applySettings(child); + } + + @Override + public DockPath getPath() { + return buildPath(new DockPath(), null); + } + + @Override + public DockPath buildPath(DockPath path, DockChild child) { + String popoutId = null; + for (DockPopout popout : manager.getPopouts()) { + if (popout.getBase() == this) { + popoutId = popout.getId(); + } + } + path.addParent(DockPathEntry.createPopout(popoutId)); + return path; + } + + @Override + public String toString() { + return "DockBase"; + } + +} diff --git a/src/chatty/util/dnd/DockChild.java b/src/chatty/util/dnd/DockChild.java new file mode 100644 index 000000000..e3ef6136c --- /dev/null +++ b/src/chatty/util/dnd/DockChild.java @@ -0,0 +1,161 @@ + +package chatty.util.dnd; + +import java.util.List; +import javax.swing.JComponent; + +/** + * Any kind of component that is part of the hierarchy that holds content. + * + * @author tduva + */ +public interface DockChild { + + /** + * The actual component that should be part of the layout. + * + * @return + */ + public JComponent getComponent(); + + /** + * Split up into two components, whereas the new one will contain the + * given content. + * + * @param info Contains info on where the new component should be added + * @param content The content to add + */ + public void split(DockDropInfo info, DockContent content); + + /** + * Replace the given child with another one. This is commonly used to change + * what a split pane contains. + * + * @param old What to replace (required) + * @param replacement What to replace it with (can be null to replace it + * with nothing) + */ + public void replace(DockChild old, DockChild replacement); + + /** + * Add content to the component. If the component itself does not hold + * content, it can be referred to a child component. + * + * @param content The content to add + */ + public void addContent(DockContent content); + + /** + * Remove content from the component. If the component itself does not hold + * content, it can be referred to a child component. + * + * @param content The content to remove + */ + public void removeContent(DockContent content); + + public void setActiveContent(DockContent content); + + public boolean isContentVisible(DockContent content); + + /** + * Change the current base of this component. + * + * @param base + */ + public void setBase(DockBase base); + + /** + * Determine whether a drop can occur at the given location. If this + * component does not provide drop points, it should be referred any child + * components that may be able to receive drops. + * + * This will be called a lot as the mouse is being moved around during a + * drag and drop movement. + * + * @param info Information on the potential drop + * @return A DockDropInfo if this component has an opinion on what drop + * should occur, null otherwise + */ + public DockDropInfo findDrop(DockImportInfo info); + + /** + * A drop has occured on this component and should be acted on accordingly. + * The info contains things like what content was dropped, where it comes + * from and how it should be added. + * + * When adding the content, it should first be removed from where it comes + * from. + * + * @param info + */ + public void drop(DockTransferInfo info); + + /** + * Whether this component contains any content. + * + * @return true if the component contains no content, false otherwise + */ + public boolean isEmpty(); + + /** + * Change the current parent of this component. A change can e.g. happen + * because the component was moved to or from a split pane. + * + * @param parent + */ + public void setDockParent(DockChild parent); + + /** + * The component above in the docking structure. + * + * @return The DockChild, can be null + */ + public DockChild getDockParent(); + + /** + * Get the path to this component (not including the component itself). + * + * @return + */ + public DockPath getPath(); + + /** + * Add the current component to the path (if applicable) and continue with + * the parent, so that the path is build up from the bottom up. + * + * @param path The path so far + * @param child The child component + * @return + */ + public DockPath buildPath(DockPath path, DockChild child); + + /** + * All content contained in this component (should be ordered the way it is + * displayed). + * + * @return The content, possibly and empty collection when there is none + * (never null) + */ + public List getContents(); + + /** + * Get all contents from the same tab pane relative to the given content. + * + * @param content The content that defines the starting position + * @param direction -1 for left, 1 for right, 0 for both directions + * @return The content, possibly and empty collection when there is none + * (never null) + */ + public List getContentsRelativeTo(DockContent content, int direction); + + /** + * Set a setting. The setting value should not be modified, since the + * unmodified values are stored in DockManager to be applied later directly + * to a child, so it might not work correctly. + * + * @param setting + * @param value + */ + public void setSetting(DockSetting.Type setting, Object value); + +} diff --git a/src/chatty/util/dnd/DockContent.java b/src/chatty/util/dnd/DockContent.java new file mode 100644 index 000000000..f7bde6062 --- /dev/null +++ b/src/chatty/util/dnd/DockContent.java @@ -0,0 +1,70 @@ + +package chatty.util.dnd; + +import java.awt.Color; +import javax.swing.JComponent; +import javax.swing.JPopupMenu; + +/** + * This holds the component that is the actually visible content and provides + * various meta information and methods related to the content. + * + * @author tduva + */ +public interface DockContent { + + /** + * The component that will be added to the layout. + * + * @return + */ + public JComponent getComponent(); + + /** + * The title (used e.g. for tab names). Should be rather short and usually + * not change (but it can). + * + * @return + */ + public String getTitle(); + + public DockPath getPath(); + + public void setTargetPath(DockPath path); + + public DockPath getTargetPath(); + + public void setDockParent(DockChild parent); + + /** + * The context menu for the tab. + * + * @return The menu, can be null to show no menu + */ + public JPopupMenu getContextMenu(); + + /** + * Provides a custom tab component. + * + * @return The tab component, can be null to use the default + */ + public DockTabComponent getTabComponent(); + + /** + * This can be called to remove the content. Was exactly is performed may + * depend on the component, but commonly this should call the DockManager to + * remove the content. + */ + public void remove(); + public void addListener(DockContentPropertyListener listener); + public void removeListener(DockContentPropertyListener listener); + public Color getForegroundColor(); + + public interface DockContentPropertyListener { + + public void titleChanged(DockContent content); + public void foregroundColorChanged(DockContent content); + + } + +} diff --git a/src/chatty/util/dnd/DockContentContainer.java b/src/chatty/util/dnd/DockContentContainer.java new file mode 100644 index 000000000..c84ba860f --- /dev/null +++ b/src/chatty/util/dnd/DockContentContainer.java @@ -0,0 +1,147 @@ + +package chatty.util.dnd; + +import java.awt.Color; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +/** + * + * @author tduva + */ +public class DockContentContainer implements DockContent { + + private final T component; + private String title; + private final DockManager m; + private final Set listeners; + private Color foregroundColor; + private DockChild parent; + private DockPath targetPath; + + public DockContentContainer(String title, T component, DockManager m) { + this.component = component; + this.title = title; + this.m = m; + listeners = new HashSet<>(); + } + + @Override + public JComponent getComponent() { + return component; + } + + public T getContent() { + return component; + } + + public boolean isContentVisible() { + return m.isContentVisible(this); + } + + @Override + public String getTitle() { + return title; + } + + public void setTitle(String newTitle) { + if (newTitle == null) { + newTitle = ""; + } + if (!newTitle.equals(title)) { + title = newTitle; + listeners.forEach(l -> l.titleChanged(this)); + } + } + + @Override + public String toString() { + return getTitle()+" ["+getComponent()+"]"; + } + + @Override + public JPopupMenu getContextMenu() { + JPopupMenu menu = new JPopupMenu(); + JMenuItem item = new JMenuItem("Popout "+title); + item.addActionListener(e -> { + m.popout(this, DockSetting.PopoutType.DIALOG, null, null); + }); + menu.add(item); + JMenuItem item2 = new JMenuItem("Popout as window"); + item2.addActionListener(e -> { + m.popout(this, DockSetting.PopoutType.FRAME, null, null); + }); + menu.add(item2); + JMenuItem closeItem = new JMenuItem("Close"); + closeItem.addActionListener(e -> { + m.removeContent(this); + remove(); + }); + menu.add(closeItem); + return menu; + } + + @Override + public void addListener(DockContentPropertyListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public void removeListener(DockContentPropertyListener listener) { + if (listener != null) { + listeners.remove(listener); + } + } + + @Override + public Color getForegroundColor() { + return foregroundColor; + } + + public void setForegroundColor(Color color) { + if (!Objects.equals(color, foregroundColor)) { + this.foregroundColor = color; + listeners.forEach(l -> l.foregroundColorChanged(this)); + } + } + + @Override + public DockTabComponent getTabComponent() { + return null; + } + + @Override + public void remove() { + // No action + } + + @Override + public DockPath getPath() { + if (parent != null) { + return parent.buildPath(new DockPath(this), null); + } + return null; + } + + @Override + public void setTargetPath(DockPath path) { + this.targetPath = path; + } + + @Override + public DockPath getTargetPath() { + return targetPath; + } + + @Override + public void setDockParent(DockChild parent) { + this.parent = parent; + } + +} diff --git a/src/chatty/util/dnd/DockDropInfo.java b/src/chatty/util/dnd/DockDropInfo.java new file mode 100644 index 000000000..b34b2a4c1 --- /dev/null +++ b/src/chatty/util/dnd/DockDropInfo.java @@ -0,0 +1,95 @@ + +package chatty.util.dnd; + +import java.awt.Point; +import java.awt.Rectangle; +import javax.swing.JComponent; + +/** + * Info provided by a component in reaction to being asked whether a drop is + * possible at a specific location. + * + * @author tduva + */ +public class DockDropInfo { + + /** + * What a drop should do. TOP/LEFT/BOTTOM/RIGHT creates a split in that + * location. TAB inserts a tab into a tab pane (possibly just moving). + * INVALID marks the location explicitly as an invalid drop, which prevents + * additional looking for a drop for that location. + */ + public enum DropType { + TOP, LEFT, BOTTOM, RIGHT, CENTER, TAB, INVALID + } + + public final DockChild dropComponent; + public final DropType location; + public final Rectangle rect; + public final int index; + + public DockDropInfo(DockChild dropComponent, DropType location, Rectangle rect, int index) { + this.dropComponent = dropComponent; + this.rect = rect; + this.location = location; + this.index = index; + } + + public static Rectangle makeRect(JComponent component, DropType location, int size, int max) { + return makeRect(component, location, size, 0, max); + } + + public static Rectangle makeRect(JComponent component, DropType location, int size, int otherSize, int max) { + int width = component.getWidth(); + int height = component.getHeight(); + int w = Math.min(width * size / 100, max); + int h = Math.min(height * size / 100, max); + int w2 = width * otherSize / 100; + int h2 = height * otherSize / 100; + switch (location) { + case TOP: + return new Rectangle(w2, 0, width - w2*2, h); + case LEFT: + return new Rectangle(0, h2, w, height - h2*2); + case RIGHT: + return new Rectangle(width - w - 1, h2, w, height - h2*2 - 1); + case BOTTOM: + return new Rectangle(w2, height - h - 1, width - w2*2, h); + } + return new Rectangle((width - w)/2, (height - h)/2, w, h); + } + + public static DropType determineLocation(JComponent component, Point point, int size, int max, int size2) { + int width = component.getWidth(); + int height = component.getHeight(); + int w = Math.min(width * size / 100, max); + int h = Math.min(height * size / 100, max); + int w2 = width * size2 / 100; + int h2 = height * size2 / 100; + +// System.out.println(String.format("%dx%d %d/%d %d,%d", width, height, w, h, point.x, point.y)); + + if (point.x < w && point.y > h2 && point.y < height - h2 + // Diagonal + && (point.y > w || point.y > point.x) + && (point.y < height - w || point.y < height - point.x)) { + return DropType.LEFT; + } + if (point.x > width - w && point.y > h2 && point.y < height - h2 + && (point.y > w || point.y > -point.x + width) + && (point.y < height - w || point.y < height -(width - point.x))) { + return DropType.RIGHT; + } + if (point.y < h && point.y >= 0 && point.x > w2 && point.x < width - w2) { + return DropType.TOP; + } + if (point.y > height - h && point.y < height && point.x > w2 && point.x < width - w2) { + return DropType.BOTTOM; + } + if (point.x > w && point.y > h && point.x < width - w && point.y < height - h) { + return DropType.CENTER; + } + return null; + } + +} diff --git a/src/chatty/util/dnd/DockExportHandler.java b/src/chatty/util/dnd/DockExportHandler.java new file mode 100644 index 000000000..43abd42b2 --- /dev/null +++ b/src/chatty/util/dnd/DockExportHandler.java @@ -0,0 +1,58 @@ + +package chatty.util.dnd; + +import java.awt.datatransfer.Transferable; +import java.awt.event.MouseEvent; +import javax.swing.JComponent; +import javax.swing.TransferHandler; +import static javax.swing.TransferHandler.MOVE; + +/** + * + * @author tduva + */ +public class DockExportHandler extends TransferHandler { + + private final DockTabs tabs; + private Transferable transferable; + + public DockExportHandler(DockTabs tabs) { + this.tabs = tabs; + } + + @Override + public int getSourceActions(JComponent c) { + return MOVE; + } + + @Override + public Transferable createTransferable(JComponent c) + { + return transferable; + } + + public void drag(int index, MouseEvent e) { + transferable = new DockTransferable(tabs.getContent(index), tabs, index, tabs.createScreenshot(index)); + exportAsDrag(tabs.getComponent(), e, TransferHandler.MOVE); + } + + @Override + public void exportDone(JComponent c, Transferable t, int action) + { +// System.out.println("EXPORT DONE:"+action+" "+MouseInfo.getPointerInfo().getLocation()); + tabs.requestStopDrag(DockUtil.getTransferable(t)); + } +// +// @Override +// public boolean canImport(TransferHandler.TransferSupport info) { +// System.out.println("Tabs:"+info.getDropLocation()); +// return true; +// } +// +// @Override +// public boolean importData(TransferHandler.TransferSupport info) { +// System.out.println("Tabs:"+info.getComponent()); +// return true; +// } + +} diff --git a/src/chatty/util/dnd/DockImportInfo.java b/src/chatty/util/dnd/DockImportInfo.java new file mode 100644 index 000000000..b9ac0ba63 --- /dev/null +++ b/src/chatty/util/dnd/DockImportInfo.java @@ -0,0 +1,42 @@ + +package chatty.util.dnd; + +import java.awt.Component; +import java.awt.Point; +import javax.swing.SwingUtilities; +import javax.swing.TransferHandler; + +/** + * Information for a potential import, which helps decide a component if a drop + * can occur. It combines the transferable (containing info from the component + * where the drag movement started) and the support info (which is provided by + * the drag&drop system and contains info like the potential drop coordinates). + * + * @author tduva + */ +public class DockImportInfo { + + public final TransferHandler.TransferSupport info; + public final DockTransferable tf; + + public DockImportInfo(TransferHandler.TransferSupport info, DockTransferable tf) { + this.tf = tf; + this.info = info; + } + + /** + * Gets the drop location relative to the given component. + * + * @param comp + * @return + */ + public Point getLocation(Component comp) { + return SwingUtilities.convertPoint(info.getComponent(), info.getDropLocation().getDropPoint(), comp); + } + + public String toString() { + return String.format("DIF(%s,%s)", + info, tf); + } + +} diff --git a/src/chatty/util/dnd/DockListener.java b/src/chatty/util/dnd/DockListener.java new file mode 100644 index 000000000..c71a4846c --- /dev/null +++ b/src/chatty/util/dnd/DockListener.java @@ -0,0 +1,23 @@ + +package chatty.util.dnd; + +import java.util.List; + +/** + * + * + * @author tduva + */ +public interface DockListener { + + /** + * The active content changed, probably due to a focus change. + * + * @param popout The popout, or null if in main DockBase + * @param content The content (never null) + */ + public void activeContentChanged(DockPopout popout, DockContent content, boolean focusChange); + public void popoutOpened(DockPopout popout, DockContent content); + public void popoutClosed(DockPopout popout, List content); + +} diff --git a/src/chatty/util/dnd/DockManager.java b/src/chatty/util/dnd/DockManager.java new file mode 100644 index 000000000..5f952aea1 --- /dev/null +++ b/src/chatty/util/dnd/DockManager.java @@ -0,0 +1,508 @@ + +package chatty.util.dnd; + +import chatty.util.Debugging; +import chatty.util.dnd.DockSetting.PopoutType; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Image; +import java.awt.KeyboardFocusManager; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Window; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; + +/** + * The main class acting as the interface for external classes to add content, + * change settings and retrieve information. Holds the main DockBase, which is + * a JPanel that the calling program must add to it's GUI and that holds the + * content. Also handles popouts, which provide additional DockBase instances + * that can also hold content. + * + *
    DockManager
    + * DockBase (main)
    + * - DockChild (several can be nested)
    + * -- DockContent
    + * DockBase (popout)
    + * - DockBase (several can be nested)
    + * -- DockChild
    + * 
    + * @author tduva
    + */
    +public class DockManager {
    +    
    +    private final DockBase main;
    +    private final DockListener listener;
    +    
    +    //--------------------------
    +    // Popouts
    +    //--------------------------
    +    private final Set popouts = new HashSet<>();
    +    private final LinkedList unusedFrames = new LinkedList<>();
    +    private final LinkedList unusedDialogs = new LinkedList<>();
    +    
    +    //--------------------------
    +    // Active content
    +    //--------------------------
    +    private DockContent currentlyActive;
    +    private final Map active = new HashMap<>();
    +    
    +    //--------------------------
    +    // Settings
    +    //--------------------------
    +    private final Map settings = new HashMap<>();
    +    
    +    private DockSetting.PopoutType popoutType = DockSetting.PopoutType.DIALOG;
    +    private DockSetting.PopoutType popoutTypeDrag = DockSetting.PopoutType.DIALOG;
    +    private List popoutIcons;
    +    private Frame popoutParent;
    +    private final PropertyChangeListener popoutParentPropertyListener;
    +    
    +    public DockManager(DockListener listener) {
    +        main = new DockBase(this);
    +        this.listener = listener;
    +        
    +        // Track focus
    +        KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusOwner", e -> {
    +            Object o = e.getNewValue();
    +            if (o != null && o instanceof Component) {
    +                checkFocus((Component)o);
    +            }
    +        });
    +        
    +        popoutParentPropertyListener = (PropertyChangeEvent evt) -> {
    +            if (evt.getPropertyName().equals("alwaysOnTop")) {
    +                updateWindowsAlwaysOnTop(popoutParent.isAlwaysOnTop());
    +            }
    +        };
    +    }
    +    
    +    /**
    +     * Used to track focus. Finds the first Dock related class to find the
    +     * content the focus changed to.
    +     * 
    +     * @param c 
    +     */
    +    private void checkFocus(Component c) {
    +//        System.out.println("Focus gain: "+c+" "+c.getClass());
    +        if (c instanceof DockContent) {
    +            changedActiveContent((DockContent)c, true);
    +        }
    +        else if (c instanceof DockTabs) {
    +            DockTabs t = (DockTabs)c;
    +            DockContent content = t.getCurrentContent();
    +            if (content != null) {
    +                changedActiveContent(content, true);
    +            }
    +            else if (t.getDockParent() instanceof DockTabsContainer) {
    +                changedActiveContent(((DockTabsContainer) t.getDockParent()).getCurrentContent(), true);
    +            }
    +        }
    +        else if (c instanceof DockTabsContainer) {
    +            changedActiveContent(((DockTabsContainer) c).getCurrentContent(), true);
    +        }
    +        else if (c instanceof DockPopout) {
    +            List contents = ((DockPopout)c).getBase().getContents();
    +            if (contents.size() == 1) {
    +                changedActiveContent(contents.get(0), true);
    +            }
    +        }
    +        else {
    +            if (c.getParent() != null) {
    +                checkFocus(c.getParent());
    +            }
    +        }
    +    }
    +    
    +    protected void changedActiveContent(DockContent content, boolean focusChange) {
    +        Debugging.println("dnda", "Changed active (Focus: %s): %s", focusChange, content);
    +        if (content == null) {
    +            return;
    +        }
    +        if (content == currentlyActive) {
    +            return;
    +        }
    +        currentlyActive = content;
    +        DockPopout popout = getDockWindowFromContent(content);
    +        active.put(popout, content);
    +        listener.activeContentChanged(popout, content, focusChange);
    +    }
    +    
    +    private void removeActive(DockContent content) {
    +        Debugging.println("dnda", "Remove active: %s", content);
    +        if (currentlyActive == content) {
    +            currentlyActive = null;
    +        }
    +        Iterator> it = active.entrySet().iterator();
    +        while (it.hasNext()) {
    +            if (it.next().getValue() == content) {
    +                it.remove();
    +            }
    +        }
    +    }
    +    
    +    private DockPopout getDockWindowFromContent(DockContent content) {
    +        Component c = content.getComponent();
    +        do {
    +            c = c.getParent();
    +            if (c instanceof DockBase) {
    +                for (DockPopout window : popouts) {
    +                    if (window.getBase() == c) {
    +                        return window;
    +                    }
    +                }
    +                return null;
    +            }
    +        } while (c != null);
    +        return null;
    +    }
    +    
    +    public DockContent getActiveContent() {
    +        return currentlyActive;
    +    }
    +    
    +    /**
    +     * Get all active contents. The main DockBase is included with the null
    +     * key.
    +     * 
    +     * @return 
    +     */
    +    public Map getAllActive() {
    +        return active;
    +    }
    +    
    +    /**
    +     * Get the main base that holds content. This component should be added
    +     * to the layout so that content is visible.
    +     * 
    +     * @return 
    +     */
    +    public JComponent getBase() {
    +        return main;
    +    }
    +    
    +    public Collection getPopouts() {
    +        return popouts;
    +    }
    +    
    +    public List getContents() {
    +        List result = new ArrayList<>();
    +        result.addAll(main.getContents());
    +        popouts.forEach(w -> result.addAll(w.getBase().getContents()));
    +        return result;
    +    }
    +    
    +    public Collection getPopoutContents() {
    +        List result = new ArrayList<>();
    +        popouts.forEach(w -> result.addAll(w.getBase().getContents()));
    +        return result;
    +    }
    +    
    +    /**
    +     * 
    +     * @param content
    +     * @param direction -1 for tabs to the left, 1 for tabs to the right and 0
    +     * for tabs in both directions
    +     * @return List of components in the given direction, except the given
    +     * center one, or empty if no components are found
    +     */
    +    public List getContentsRelativeTo(DockContent content, int direction) {
    +        List result = new ArrayList<>();
    +        result.addAll(main.getContentsRelativeTo(content, direction));
    +        popouts.forEach(w -> result.addAll(w.getBase().getContentsRelativeTo(content, direction)));
    +        return result;
    +    }
    +    
    +    public DockContent getContentTab(DockContent content, int direction) {
    +        List c = getContentsRelativeTo(content, direction);
    +        if (!c.isEmpty()) {
    +            return c.get(0);
    +        }
    +        c = getContentsRelativeTo(content, -direction);
    +        if (!c.isEmpty()) {
    +            return c.get(c.size() - 1);
    +        }
    +        return null;
    +    }
    +    
    +    public void addContent(DockContent content) {
    +        if (currentlyActive == null) {
    +            changedActiveContent(content, false);
    +        }
    +        else {
    +            setTargetPath(content, currentlyActive);
    +        }
    +        
    +        // Add content
    +        DockPath target = content.getTargetPath();
    +        if (target == null || target.getPopoutId() == null) {
    +            main.addContent(content);
    +        }
    +        else {
    +            for (DockPopout p : popouts) {
    +                if (p.getId().equals(target.getPopoutId())) {
    +                    p.getBase().addContent(content);
    +                }
    +            }
    +        }
    +    }
    +    
    +    private void setTargetPath(DockContent content, DockContent target) {
    +        if (target == null || content.getTargetPath() != null) {
    +            return;
    +        }
    +        DockPath path = target.getPath();
    +        if (path != null) {
    +            content.setTargetPath(path);
    +        }
    +    }
    +    
    +    public void removeContent(DockContent content) {
    +        removeActive(content);
    +        main.removeContent(content);
    +        for (DockPopout w : popouts) {
    +            w.getBase().removeContent(content);
    +        }
    +    }
    +    
    +    public boolean hasContent(DockContent content) {
    +        return getContents().contains(content);
    +    }
    +    
    +    public void setActiveContent(DockContent content) {
    +        main.setActiveContent(content);
    +        popouts.forEach(w -> w.getBase().setActiveContent(content));
    +    }
    +    
    +    protected void baseEmpty(DockBase base) {
    +        for (DockPopout w : popouts) {
    +            if (w.getBase() == base) {
    +                w.getWindow().setVisible(false);
    +            }
    +        }
    +    }
    +    
    +    public boolean isMainEmpty() {
    +        return main.isEmpty();
    +    }
    +    
    +    public boolean closeWindow() {
    +        if (popouts.isEmpty()) {
    +            return false;
    +        }
    +        popouts.iterator().next().getWindow().setVisible(false);
    +        return true;
    +    }
    +    
    +    protected void requestDrag() {
    +        main.startDrag();
    +        for (DockPopout window : popouts) {
    +            window.getBase().startDrag();
    +        }
    +    }
    +    
    +    protected void requestStopDrag(DockTransferable t) {
    +        if (!DockUtil.isMouseOverWindow()
    +                && popoutTypeDrag != PopoutType.NONE) {
    +            // Manually changed location, so reset
    +            t.content.setTargetPath(null);
    +            // Popout from dragging outside window
    +            Point location = MouseInfo.getPointerInfo().getLocation();
    +            DockContent c = t != null ? t.content : currentlyActive;
    +            popout(c, popoutTypeDrag, new Point(location.x - 80, location.y - 10), null);
    +        }
    +        main.stopDrag();
    +        for (DockPopout window : popouts) {
    +            window.getBase().stopDrag();
    +        }
    +    }
    +    
    +    private void closePopout(DockPopout popout) {
    +        // Add content to main (if present)
    +        List contents = popout.getBase().getContents();
    +        DockContent activeInPopout = active.get(popout);
    +        DockContent activeInMain = active.get(null);
    +        for (DockContent c : contents) {
    +            popout.getBase().removeContent(c);
    +            setTargetPath(c, activeInMain);
    +            main.addContent(c);
    +        }
    +        if (activeInPopout != null) {
    +            main.setActiveContent(activeInPopout);
    +        }
    +        
    +        // Remove popout from stuff
    +        popouts.remove(popout);
    +        active.remove(popout);
    +        popout.getWindow().setVisible(false);
    +        
    +        // Store unused popouts
    +        if (popout instanceof DockPoputDialog) {
    +            if (!unusedDialogs.contains((DockPoputDialog) popout)) {
    +                unusedDialogs.add((DockPoputDialog)popout);
    +            }
    +        }
    +        else if (popout instanceof DockPopoutFrame) {
    +            if (!unusedFrames.contains((DockPopoutFrame) popout)) {
    +                unusedFrames.add((DockPopoutFrame)popout);
    +            }
    +        }
    +        
    +        // Inform listeners
    +        listener.popoutClosed(popout, contents);
    +    }
    +    
    +    public DockPopout popout(DockContent content) {
    +        return popout(content, popoutType, null, null);
    +    }
    +    
    +    /**
    +     * Popout the given content into an extra window.
    +     * 
    +     * @param content The first content to add to the popout
    +     * @param type The type of popout
    +     * @param location The location to open the popout in (may be null to open
    +     * in default location)
    +     * @param size The size of the popout (may be null to use default size)
    +     * @return  
    +     */
    +    public DockPopout popout(DockContent content, PopoutType type, Point location, Dimension size) {
    +        if (type == PopoutType.NONE) {
    +            return null;
    +        }
    +        
    +        // Remove content from previous location
    +        main.removeContent(content);
    +        popouts.forEach(w -> w.getBase().removeContent(content));
    +        
    +        // Get existing or create new popout
    +        DockPopout popout = getUnusedPopout(type);
    +        if (popout == null) {
    +            // Need to create a new one
    +            if (type == PopoutType.DIALOG) {
    +                popout = new DockPoputDialog(this, popoutParent);
    +            }
    +            else {
    +                popout = new DockPopoutFrame(this);
    +            }
    +            // Configure new popout
    +            applySettings(popout.getBase());
    +            if (popoutIcons != null) {
    +                popout.getWindow().setIconImages(popoutIcons);
    +            }
    +            DockPopout popout2 = popout;
    +            popout.getWindow().addComponentListener(new ComponentAdapter() {
    +                @Override
    +                public void componentHidden(ComponentEvent e) {
    +                    closePopout(popout2);
    +                }
    +            });
    +        }
    +        popout.getBase().addContent(content);
    +        
    +        // Configure window
    +        Window window = popout.getWindow();
    +        if (size != null) {
    +            window.setSize(size);
    +        } else {
    +            window.setSize(600, 400);
    +        }
    +        if (location != null) {
    +            window.setLocation(location);
    +        }
    +        else {
    +            window.setLocationByPlatform(true);
    +        }
    +        if (popoutParent != null) {
    +            window.setAlwaysOnTop(popoutParent.isAlwaysOnTop());
    +        }
    +        window.setVisible(true);
    +        SwingUtilities.invokeLater(() -> window.toFront());
    +        
    +        // Finish up
    +        popouts.add(popout);
    +        changedActiveContent(content, false);
    +        listener.popoutOpened(popout, content);
    +        
    +        return popout;
    +    }
    +    
    +    private DockPopout getUnusedPopout(PopoutType type) {
    +        if (type == PopoutType.DIALOG) {
    +            return unusedDialogs.poll();
    +        }
    +        else if (type == PopoutType.FRAME) {
    +            return unusedFrames.poll();
    +        }
    +        return null;
    +    }
    +    
    +    private void updateWindowsAlwaysOnTop(boolean onTop) {
    +        for (DockPopout p : popouts) {
    +            p.getWindow().setAlwaysOnTop(onTop);
    +        }
    +    }
    +    
    +    public boolean isContentVisible(DockContent content) {
    +        if (main.isContentVisible(content)) {
    +            return true;
    +        }
    +        for (DockPopout w : popouts) {
    +            if (w.getBase().isContentVisible(content)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
    +    
    +    public void setSetting(DockSetting.Type setting, Object value) {
    +        if (setting == DockSetting.Type.POPOUT_TYPE) {
    +            popoutType = (DockSetting.PopoutType) value;
    +        }
    +        else if (setting == DockSetting.Type.POPOUT_TYPE_DRAG) {
    +            popoutTypeDrag = (DockSetting.PopoutType) value;
    +        }
    +        else if (setting == DockSetting.Type.POPOUT_ICONS) {
    +            popoutIcons = (List) value;
    +        }
    +        else if (setting == DockSetting.Type.POPOUT_PARENT) {
    +            if (popoutParent != value) {
    +                if (popoutParent != null) {
    +                    popoutParent.removePropertyChangeListener("alwaysOnTop", popoutParentPropertyListener);
    +                }
    +                popoutParent = (Frame) value;
    +                popoutParent.addPropertyChangeListener("alwaysOnTop", popoutParentPropertyListener);
    +            }
    +        }
    +        else {
    +            settings.put(setting, value);
    +            main.setSetting(setting, value);
    +            popouts.forEach(w -> w.getBase().setSetting(setting, value));
    +        }
    +    }
    +    
    +    /**
    +     * Apply all stored setting values to the given child.
    +     * 
    +     * @param child 
    +     */
    +    public void applySettings(DockChild child) {
    +        settings.forEach((type, value) -> child.setSetting(type, value));
    +    }
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockOverlay.java b/src/chatty/util/dnd/DockOverlay.java
    new file mode 100644
    index 000000000..3b42d4304
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockOverlay.java
    @@ -0,0 +1,187 @@
    +
    +package chatty.util.dnd;
    +
    +import java.awt.AlphaComposite;
    +import java.awt.Color;
    +import java.awt.Composite;
    +import java.awt.Graphics;
    +import java.awt.Graphics2D;
    +import java.awt.Point;
    +import java.awt.Rectangle;
    +import javax.swing.JPanel;
    +import javax.swing.SwingUtilities;
    +import javax.swing.Timer;
    +import javax.swing.TransferHandler;
    +
    +/**
    + * Each base has it's own overlay, which handles updating various things when a
    + * drop occurs as well as drawing information about the current drop location.
    + * 
    + * @author tduva
    + */
    +public class DockOverlay extends JPanel {
    +    
    +    private final DockBase base;
    +    
    +    /**
    +     * The current drop rectangle to paint, may be null.
    +     */
    +    private Rectangle paintRect;
    +    
    +    /**
    +     * The current drop info, updated continuously when a drag occurs. May be
    +     * null.
    +     */
    +    private DockDropInfo dropInfo;
    +    
    +    /**
    +     * The current import info, updated continuously when a drag occurs.
    +     */
    +    private DockImportInfo importInfo;
    +    
    +    private Color fillColor = new Color(64, 64, 64, 64);
    +    private Color lineColor = Color.DARK_GRAY;
    +    
    +    public DockOverlay(DockBase base) {
    +        this.base = base;
    +        
    +        // Stop drawing rectangle when mouse moves outside of area
    +        Timer timer = new Timer(100, e -> {
    +            if (paintRect != null && getMousePosition() == null) {
    +                paintRect = null;
    +                repaint();
    +            }
    +        });
    +        timer.start();
    +        
    +        /**
    +         * Handles all imports, but asks the base and thus child components
    +         * what kind of drop should occur. Handling all imports in a central
    +         * place allows easy updating and drawing on this overlay.
    +         */
    +        setTransferHandler(new TransferHandler() {
    +
    +            @Override
    +            public boolean canImport(TransferHandler.TransferSupport info) {
    +                return updateImport(info);
    +            }
    +
    +            @Override
    +            public boolean importData(TransferHandler.TransferSupport info) {
    +                // Seems redundant since canImport() already checks this
    +//                DockTransferable dtf = getTransferable(info);
    +//                if (dtf == null) {
    +//                    return false;
    +//                }
    +                if (canImport(info)) {
    +                    // At this point, dropInfo should never be null
    +                    DockTransferable dtf = DockUtil.getTransferable(info);
    +                    paintRect = null;
    +                    repaint();
    +                    base.requestStopDrag(dtf);
    +//                    System.out.println("IMPORT!" + dtf.content);
    +                    try {
    +                        dropInfo.dropComponent.drop(new DockTransferInfo(dropInfo, dtf));
    +                    } catch (Exception ex) {
    +                        ex.printStackTrace();
    +                    }
    +                    return true;
    +                }
    +                return false;
    +            }
    +
    +        });
    +    }
    +    
    +    public void setFillColor(Color color) {
    +        if (color != null) {
    +            this.fillColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), 90);
    +        }
    +    }
    +    
    +    public void setLineColor(Color color) {
    +        if (color != null) {
    +            this.lineColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), 180);
    +        }
    +    }
    +    
    +    /**
    +     * Updates the drop info variables based on the current info provided by the
    +     * drag&drop system, by asking components whether a drop can occur.
    +     * 
    +     * @param info
    +     * @return 
    +     */
    +    private boolean updateImport(TransferHandler.TransferSupport info) {
    +//        System.out.println("UPDATE_IMPORT "+info);
    +        DockTransferable dtf = DockUtil.getTransferable(info);
    +        if (dtf == null) {
    +            return false;
    +        }
    +//        System.out.println("!!!!!!!!!!!!!!!!!"+dtf.content);
    +
    +        // Ask other components which drop can occur at the current location
    +        DockImportInfo importInfoUpdated = new DockImportInfo(info, dtf);
    +        DockDropInfo dropInfoUpdated = base.findDrop(importInfoUpdated);
    +        
    +        // Update some stuff based on the info received
    +        if (dropInfoUpdated != null) {
    +            Rectangle rect = dropInfoUpdated.rect;
    +            paintRect = SwingUtilities.convertRectangle(dropInfoUpdated.dropComponent.getComponent(), rect, DockOverlay.this);
    +        }
    +        else {
    +            paintRect = null;
    +        }
    +        this.dropInfo = dropInfoUpdated;
    +        this.importInfo = importInfoUpdated;
    +        repaint();
    +        return dropInfoUpdated != null;
    +    }
    +    
    +    @Override
    +    public void paintComponent(Graphics g) {
    +        if (paintRect != null) {
    +            g.setColor(fillColor);
    +            g.fillRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
    +            g.setColor(lineColor);
    +            g.drawRect(paintRect.x, paintRect.y, paintRect.width, paintRect.height);
    +        }
    +        if (importInfo != null) {
    +            Graphics2D g2d = (Graphics2D)g;
    +//            String title = "[Moving "+importInfo.tf.content.getTitle()+"]";
    +//            if (dropInfo != null) {
    +//                if (dropInfo.location == Location.TAB) {
    +//                    title += " Add as Tab";
    +//                }
    +//                else {
    +//                    title += " Add into new split";
    +//                }
    +//            }
    +//            if (paintRect == null) {
    +//                title += " Abort";
    +//            }
    +//            
    +//            int width = g.getFontMetrics().stringWidth(title);
    +//            int height = g.getFontMetrics().getHeight();
    +//            int x = getWidth() / 2 - width / 2;
    +//            int y = getHeight() / 2;
    +//            g.setColor(FILL_COLOR);
    +//            g.fillRect(x - 2, y - 2, width + 4, height + 4);
    +//            g.setColor(Color.BLUE);
    +//            g.drawRect(x - 2, y - 2, width + 4, height + 4);
    +//            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    +//            g.drawString(title, x, y + (int)(height / 1.5));
    +            
    +            if (importInfo.tf.image != null && getMousePosition() != null) {
    +                Point p = importInfo.info.getDropLocation().getDropPoint();
    +//                int imgWidth = importInfo.tf.image.getWidth(null);
    +                int imgHeight = importInfo.tf.image.getHeight(null);
    +                Composite origComp = g2d.getComposite();
    +                g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
    +                g.drawImage(importInfo.tf.image, p.x + 14, p.y - imgHeight / 3, this);
    +                g2d.setComposite(origComp);
    +            }
    +        }
    +    }
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockPath.java b/src/chatty/util/dnd/DockPath.java
    new file mode 100644
    index 000000000..42b4aa8b8
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockPath.java
    @@ -0,0 +1,62 @@
    +
    +package chatty.util.dnd;
    +
    +import chatty.util.dnd.DockPathEntry.Type;
    +import java.util.LinkedList;
    +
    +/**
    + * Stores the path to a certain child or content. Each entry may contain data
    + * required to recreate the path (for example split orientation, split size, tab
    + * index), although most of it isn't used yet. Currently the path is only used
    + * to add a tab to a certain tab pane that already exists.
    + * 
    + * @author tduva
    + */
    +public class DockPath {
    +    
    +    private final LinkedList list = new LinkedList<>();
    +    private final DockContent content;
    +    
    +    public DockPath(DockContent content) {
    +        this.content = content;
    +    }
    +    
    +    public DockPath() {
    +        this.content = null;
    +    }
    +    
    +    public void addParent(DockPathEntry parent) {
    +        list.addFirst(parent);
    +    }
    +    
    +    public String getPopoutId() {
    +        if (list.isEmpty() || list.getFirst().type != Type.POPOUT) {
    +            return null;
    +        }
    +        return list.getFirst().id;
    +    }
    +    
    +    /**
    +     * Get the entry of this path that would come after the given path, based on
    +     * length alone.
    +     * 
    +     * @param path
    +     * @return 
    +     */
    +    public DockPathEntry getNext(DockPath path) {
    +        if (path.list.size() >= list.size()) {
    +            return null;
    +        }
    +        return list.get(path.list.size());
    +    }
    +    
    +    public DockContent getContent() {
    +        return content;
    +    }
    +    
    +    @Override
    +    public String toString() {
    +        return String.format("%s (%s)", list, content);
    +    }
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockPathEntry.java b/src/chatty/util/dnd/DockPathEntry.java
    new file mode 100644
    index 000000000..e3a670e40
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockPathEntry.java
    @@ -0,0 +1,64 @@
    +
    +package chatty.util.dnd;
    +
    +import chatty.util.dnd.DockDropInfo.DropType;
    +
    +/**
    + *
    + * @author tduva
    + */
    +public class DockPathEntry {
    +
    +    public enum Type {
    +        SPLIT, TAB, POPOUT
    +    }
    +    
    +    public final Type type;
    +    public final DropType location;
    +    public final int index;
    +    public final String id;
    +    
    +    private DockPathEntry(DropType location) {
    +        this.type = Type.SPLIT;
    +        this.location = location;
    +        this.index = -1;
    +        this.id = null;
    +    }
    +    
    +    private DockPathEntry(int index) {
    +        this.type = Type.TAB;
    +        this.location = null;
    +        this.index = index;
    +        this.id = null;
    +    }
    +    
    +    private DockPathEntry(String id) {
    +        this.type = Type.POPOUT;
    +        this.location = null;
    +        this.index = -1;
    +        this.id = id;
    +    }
    +    
    +    public static DockPathEntry createSplit(DropType location) {
    +        return new DockPathEntry(location);
    +    }
    +    
    +    public static DockPathEntry createTab(int index) {
    +        return new DockPathEntry(index);
    +    }
    +    
    +    public static DockPathEntry createPopout(String popoutId) {
    +        return new DockPathEntry(popoutId);
    +    }
    +    
    +    @Override
    +    public String toString() {
    +        switch (type) {
    +            case POPOUT: return String.format("%s (%s)", type, id);
    +            case SPLIT: return String.format("%s (%s)", type, location);
    +            case TAB: return String.format("%s (%s)", type, index);
    +        }
    +        return "?";
    +    }
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockPopout.java b/src/chatty/util/dnd/DockPopout.java
    new file mode 100644
    index 000000000..d59f01b85
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockPopout.java
    @@ -0,0 +1,22 @@
    +
    +package chatty.util.dnd;
    +
    +import java.awt.Window;
    +
    +/**
    + *
    + * @author tduva
    + */
    +public interface DockPopout {
    +
    +    public Window getWindow();
    +    
    +    public DockBase getBase();
    +    
    +    public void setTitle(String title);
    +    
    +    public String getId();
    +    
    +    public void setId();
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockPopoutFrame.java b/src/chatty/util/dnd/DockPopoutFrame.java
    new file mode 100644
    index 000000000..f9b30a864
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockPopoutFrame.java
    @@ -0,0 +1,47 @@
    +
    +package chatty.util.dnd;
    +
    +import java.awt.BorderLayout;
    +import java.awt.Window;
    +import javax.swing.JFrame;
    +import javax.swing.WindowConstants;
    +
    +/**
    + *
    + * @author tduva
    + */
    +public class DockPopoutFrame extends JFrame implements DockPopout {
    +
    +    private static int counter = 0;
    +    
    +    private final DockBase base;
    +    private String id;
    +
    +    public DockPopoutFrame(DockManager m) {
    +        id = "frame"+(counter++);
    +        base = new DockBase(m);
    +        add(base, BorderLayout.CENTER);
    +        setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
    +    }
    +
    +    @Override
    +    public DockBase getBase() {
    +        return base;
    +    }
    +
    +    @Override
    +    public Window getWindow() {
    +        return this;
    +    }
    +
    +    @Override
    +    public String getId() {
    +        return id;
    +    }
    +
    +    @Override
    +    public void setId() {
    +        this.id = id;
    +    }
    +
    +}
    diff --git a/src/chatty/util/dnd/DockPoputDialog.java b/src/chatty/util/dnd/DockPoputDialog.java
    new file mode 100644
    index 000000000..53995f055
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockPoputDialog.java
    @@ -0,0 +1,48 @@
    +
    +package chatty.util.dnd;
    +
    +import java.awt.BorderLayout;
    +import java.awt.Frame;
    +import java.awt.Window;
    +import javax.swing.JDialog;
    +
    +/**
    + *
    + * @author tduva
    + */
    +public class DockPoputDialog extends JDialog implements DockPopout {
    +
    +    private static int counter = 0;
    +    
    +    private final DockBase base;
    +    private String id;
    +    
    +    public DockPoputDialog(DockManager m, Frame parent) {
    +        super(parent);
    +        id = "frame"+(counter++);
    +        setLayout(new BorderLayout());
    +        base = new DockBase(m);
    +        add(base, BorderLayout.CENTER);
    +    }
    +
    +    @Override
    +    public DockBase getBase() {
    +        return base;
    +    }
    +
    +    @Override
    +    public Window getWindow() {
    +        return this;
    +    }
    +
    +    @Override
    +    public String getId() {
    +        return id;
    +    }
    +
    +    @Override
    +    public void setId() {
    +        this.id = id;
    +    }
    +    
    +}
    diff --git a/src/chatty/util/dnd/DockSetting.java b/src/chatty/util/dnd/DockSetting.java
    new file mode 100644
    index 000000000..496637309
    --- /dev/null
    +++ b/src/chatty/util/dnd/DockSetting.java
    @@ -0,0 +1,120 @@
    +
    +package chatty.util.dnd;
    +
    +import java.awt.Color;
    +
    +/**
    + * Provides constants and functions for use with
    + * {@link DockChild#setSetting(DockSetting.Type, Object) DockChild#setSetting()}.
    + *
    + * @author tduva
    + */
    +public class DockSetting {
    +    
    +    /**
    +     * The setting type defines what values are accepted and what the setting
    +     * changes.
    +     */
    +    public enum Type {
    +        /**
    +         * Sets {@link javax.swing.JTabbedPane#setTabPlacement(int)}.
    +         */
    +        TAB_PLACEMENT,
    +        
    +        /**
    +         * Sets {@link javax.swing.JTabbedPane#setTabLayoutPolicy(int)}.
    +         */
    +        TAB_LAYOUT,
    +        
    +        /**
    +         * Sets where added tabs are inserted, accepts {@link TabOrder} values.
    +         */
    +        TAB_ORDER,
    +        
    +        /**
    +         * Enables changing tabs by using the mousewheel while the mouse is
    +         * over it. Accepts booleans.
    +         */
    +        TAB_SCROLL,
    +        
    +        /**
    +         * Extends upon {@link TAB_SCROLL} to allow mousewheel tab scrolling
    +         * anywhere the scroll event is accepted. Accepts booleans.
    +         */
    +        TAB_SCROLL_ANYWHERE,
    +        
    +        /**
    +         * The fill {@link Color} of the box that shows where a drag&drop
    +         * movement accepts a drop.
    +         */
    +        FILL_COLOR,
    +        
    +        /**
    +         * The line {@link Color} of the box that shows where a drag&drop
    +         * movement accepts a drop.
    +         */
    +        LINE_COLOR,
    +        
    +        DIVIDER_SIZE,
    +        
    +        POPOUT_TYPE,
    +        
    +        POPOUT_TYPE_DRAG,
    +        
    +        POPOUT_ICONS,
    +        
    +        POPOUT_PARENT,
    +        
    +        DEBUG
    +    }
    +    
    +    public enum TabOrder {
    +        /**
    +         * Inserts added tabs at the end.
    +         */
    +        INSERTION,
    +        
    +        /**
    +         * Inserts new tabs in alphabetic order based on the name of the added
    +         * Component. If tabs have been reordered manually, then tabs are
    +         * inserted before the first tab whose name would be greater than the
    +         * new one.
    +         * 
    +         * 

    Ordering is done ignoring case.

    + */ + ALPHABETIC + } + + public enum PopoutType { + FRAME, DIALOG, NONE + } + + public static boolean getBoolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } + return false; + } + + public static String getString(Object value) { + if (value instanceof String) { + return (String) value; + } + return ""; + } + + public static Color getColor(Object value) { + if (value instanceof Color) { + return (Color) value; + } + return null; + } + + public static Integer getInteger(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + return -1; + } + +} diff --git a/src/chatty/util/dnd/DockSplit.java b/src/chatty/util/dnd/DockSplit.java new file mode 100644 index 000000000..84fe49482 --- /dev/null +++ b/src/chatty/util/dnd/DockSplit.java @@ -0,0 +1,288 @@ + +package chatty.util.dnd; + +import chatty.util.Debugging; +import chatty.util.dnd.DockDropInfo.DropType; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; + +/** + * Shows two components side by side (horizontal or vertical). + * + * @author tduva + */ +public class DockSplit extends JSplitPane implements DockChild { + + private DockBase base; + private DockChild parent; + + private DockChild left; + private DockChild right; + + public DockSplit(int orientation, DockChild left, DockChild right) { + super(orientation, left.getComponent(), right.getComponent()); +// if (left instanceof JSplitPane) { +// ((JSplitPane)left).setBorder(null); +// } +// if (right instanceof JSplitPane) { +// ((JSplitPane)right).setBorder(null); +// } + this.left = left; + this.right = right; + } + + @Override + public JComponent getComponent() { + return this; + } + + public DockChild getLeftChild() { + return left; + } + + public DockChild getRightChild() { + return right; + } + + @Override + public void split(DockDropInfo info, DockContent content) { + // System.out.println("LEFT: "+left+" DROP: "+info.dropComponent+" RIGHT: "+right); + DockChild presentComp = null; + if (checkComponent(left, info)) { + presentComp = left; + } + else if (checkComponent(right, info)) { + presentComp = right; + } + if (presentComp == null) { + return; + } + DockTabsContainer newCompTabs = new DockTabsContainer(); + DockSplit newChildSplit = DockUtil.createSplit(info, presentComp, newCompTabs); + if (newChildSplit != null) { + // Configure child first + base.applySettings(newChildSplit); + newChildSplit.setBase(base); + newChildSplit.setDockParent(this); + // Configure tabs (new left or right component) + base.applySettings(newCompTabs); + newCompTabs.setBase(base); + newCompTabs.setDockParent(newChildSplit); + newCompTabs.addContent(content); + // Configure present comp + presentComp.setDockParent(newChildSplit); + + // Exchange left or right component + if (checkComponent(left, info)) { + setLeftComponent(newChildSplit); + left = newChildSplit; + } + else if (checkComponent(right, info)) { + setRightComponent(newChildSplit); + right = newChildSplit; + } + // Divider + DockSplit split = newChildSplit; + split.setDividerLocation(0.5); + split.setResizeWeight(0.5); + SwingUtilities.invokeLater(() -> { + split.setDividerLocation(0.5); + split.setResizeWeight(0.5); + }); + DockUtil.preserveDividerLocation(this); + } + } + + private boolean checkComponent(DockChild parent, DockDropInfo info) { + if (parent == info.dropComponent) { + return true; + } + return parent == info.dropComponent.getComponent().getParent(); + } + + @Override + public void addContent(DockContent content) { + DockPathEntry next = DockUtil.getNext(content, this); + if (next != null) { + Debugging.println("dndp", "%s -> %s", this, next); + if (next.type == DockPathEntry.Type.SPLIT + && (next.location == DropType.RIGHT || next.location == DropType.BOTTOM)) { + right.addContent(content); + } + else { + left.addContent(content); + } + } + else { + left.addContent(content); + } + } + + @Override + public void removeContent(DockContent content) { + left.removeContent(content); + right.removeContent(content); + } + + @Override + public void setBase(DockBase base) { + this.base = base; + } + + @Override + public DockDropInfo findDrop(DockImportInfo info) { +// System.out.println(info.getLocation(this)+" "+getLeftComponent().getBounds()+" "+getRightComponent().getBounds()); + Rectangle leftBounds = getLeftComponent().getBounds(); + Point p = info.getLocation(this); + if (leftBounds.contains(p)) { + return left.findDrop(info); + } + else if (getRightComponent().getBounds().contains(p)) { + return right.findDrop(info); + } + return null; + } + + @Override + public boolean isEmpty() { + // If the split exists, it should probably always contain content, but + // not entirely sure + return false; + } + + @Override + public void drop(DockTransferInfo info) { + // This component does not receive drops + } + + @Override + public void setDockParent(DockChild parent) { + this.parent = parent; + } + + @Override + public DockChild getDockParent() { + return parent; + } + + @Override + public String toString() { + if (getOrientation() == JSplitPane.HORIZONTAL_SPLIT) { + return String.format("%s || %s", left, right); + } + else { + return String.format("%s -- %s", left, right); + } + } + + @Override + public void replace(DockChild old, DockChild replacement) { + if (old == left) { + if (replacement == null) { + // Remove "left" side (close this split, replace with "right") + parent.replace(this, right); + DockUtil.activeTabAfterJoin(right, base); + } + else { + // Exchange "left" side (keep this split open) + setLeftComponent(replacement.getComponent()); + left = replacement; + replacement.setDockParent(this); + DockUtil.preserveDividerLocation(this); + } + } + else if (old == right) { + if (replacement == null) { + // Remove "right" side (close this split, replace with "left") + parent.replace(this, left); + DockUtil.activeTabAfterJoin(left, base); + } + else { + // Exchange "right" side (keep this split open) + setRightComponent(replacement.getComponent()); + right = replacement; + replacement.setDockParent(this); + DockUtil.preserveDividerLocation(this); + } + } + } + + @Override + public List getContents() { + List result = new ArrayList<>(); + result.addAll(left.getContents()); + result.addAll(right.getContents()); + return result; + } + + @Override + public void setActiveContent(DockContent content) { + left.setActiveContent(content); + right.setActiveContent(content); + } + + @Override + public boolean isContentVisible(DockContent content) { + return left.isContentVisible(content) || right.isContentVisible(content); + } + + @Override + public List getContentsRelativeTo(DockContent content, int direction) { + if (left.getContents().contains(content)) { + return left.getContentsRelativeTo(content, direction); + } + else { + return right.getContentsRelativeTo(content, direction); + } + } + + @Override + public void setSetting(DockSetting.Type setting, Object value) { + left.setSetting(setting, value); + right.setSetting(setting, value); + + if (setting == DockSetting.Type.DEBUG) { + setBorder(value == Boolean.TRUE ? BorderFactory.createLineBorder(Color.BLUE, 4) : null); + } + if (setting == DockSetting.Type.DIVIDER_SIZE) { + setDividerSize(((Number)value).intValue()); + } + } + + @Override + public DockPath getPath() { + return parent.buildPath(new DockPath(), this); + } + + @Override + public DockPath buildPath(DockPath path, DockChild child) { + DropType type = null; + if (child == left) { + if (getOrientation() == HORIZONTAL_SPLIT) { + type = DropType.LEFT; + } + else { + type = DropType.TOP; + } + } + else if (child == right) { + if (getOrientation() == HORIZONTAL_SPLIT) { + type = DropType.RIGHT; + } + else { + type = DropType.BOTTOM; + } + } + path.addParent(DockPathEntry.createSplit(type)); + return parent.buildPath(path, this); + } + +} diff --git a/src/chatty/util/dnd/DockTabComponent.java b/src/chatty/util/dnd/DockTabComponent.java new file mode 100644 index 000000000..c4e24c101 --- /dev/null +++ b/src/chatty/util/dnd/DockTabComponent.java @@ -0,0 +1,34 @@ + +package chatty.util.dnd; + +import javax.swing.JComponent; +import javax.swing.JTabbedPane; + +/** + * Allows a tab component to receive an update notification to update it's + * styling when something relevant in the tab pane changed. + * + * @author tduva + */ +public interface DockTabComponent { + + /** + * Called when the tab pane first installs a tab component and when + * something relevant in the tab pane changes (e.g. selected tab changes, + * which causes a tab to have different colors) so that the tab component + * can update accordingly. + * + * @param pane The tab pane + * @param index The tab index associated with this tab component + */ + public void update(JTabbedPane pane, int index); + + /** + * The actual tab component to install on the tab pane. + * + * @return The component (may be null, in which case no custom tab component + * is used) + */ + public JComponent getComponent(); + +} diff --git a/src/chatty/util/dnd/DockTabs.java b/src/chatty/util/dnd/DockTabs.java new file mode 100644 index 000000000..e9cf288fa --- /dev/null +++ b/src/chatty/util/dnd/DockTabs.java @@ -0,0 +1,714 @@ + +package chatty.util.dnd; + +import chatty.util.dnd.DockDropInfo.DropType; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JPopupMenu; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Tab pane that handles dragging tabs. + * + * @author tduva + */ +public class DockTabs extends JTabbedPane implements DockChild { + + private final DockExportHandler transferHandler; + private final Map assoc = new HashMap<>(); + private final DockContent.DockContentPropertyListener dockContentListener; + + private DockChild parent; + private DockBase base; + + private boolean canStartDrag; + private long dragStarted; + private int dragIndex; + + private boolean mouseWheelScrolling = true; + private boolean mouseWheelScrollingAnywhere = true; + private DockSetting.TabOrder order = DockSetting.TabOrder.INSERTION; + + public DockTabs() { + transferHandler = new DockExportHandler(this); + setTransferHandler(transferHandler); + + addMouseMotionListener(new MouseMotionAdapter() { + + @Override + public void mouseDragged(MouseEvent e) { + if (!canStartDrag) { + return; + } +// System.out.println(System.currentTimeMillis() - dragStarted); + if (dragStarted == 0) { + dragStarted = System.currentTimeMillis(); + dragIndex = getIndexForPoint(e.getPoint()); + } + // Start actual drag only after a short delay of dragging + if (dragIndex >= 0 && System.currentTimeMillis() - dragStarted > 120) { + transferHandler.drag(dragIndex, e); + base.requestDrag(); + canStartDrag = false; + } + repaint(); + } + }); + + addMouseListener(new MouseAdapter() { + + @Override + public void mousePressed(MouseEvent e) { + dragStarted = 0; + /** + * Only allow a drag to start using the left mouse button. Can't + * use isPopupTrigger() to prevent it from dragging when opening + * a context menu, because the popup trigger isn't necessarily + * available in mousePressed(). + */ + canStartDrag = SwingUtilities.isLeftMouseButton(e); + openPopupMenu(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + dragStarted = 0; + repaint(); + base.requestStopDrag(null); + openPopupMenu(e); + } + + @Override + public void mouseClicked(MouseEvent e) { + openPopupMenu(e); + } + + }); + + addMouseWheelListener(new MouseWheelListener() { + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (mouseWheelScrolling) { + // Only scroll if actually on tabs area + int index = indexAtLocation(e.getX(), e.getY()); + if (mouseWheelScrollingAnywhere || index != -1 + || isNearLastTab(e.getPoint())) { + if (e.getWheelRotation() < 0) { + setSelectedPrevious(); + } else if (e.getWheelRotation() > 0) { + setSelectedNext(); + } + } + } + } + }); + + addChangeListener(new ChangeListener() { + + @Override + public void stateChanged(ChangeEvent e) { + dragStarted = 0; + repaint(); + if (base != null) { + base.tabChanged(DockTabs.this, getCurrentContent()); + } + updateTabComponents(); + } + }); + + dockContentListener = new DockContent.DockContentPropertyListener() { + @Override + public void titleChanged(DockContent content) { + int index = getIndexByContent(content); + if (index != -1) { + setTitleAt(index, content.getTitle()); + } + } + + @Override + public void foregroundColorChanged(DockContent content) { + System.out.println("foregroundColorChanged"+content); + int index = getIndexByContent(content); + if (index != -1) { + setForegroundAt(index, content.getForegroundColor()); + } + } + }; + } + + @Override + public void setBase(DockBase base) { + this.base = base; + } + + public void requestStopDrag(DockTransferable t) { + base.requestStopDrag(t); + } + + @Override + public void setDockParent(DockChild parent) { + this.parent = parent; + } + + @Override + public DockChild getDockParent() { + return parent; + } + + @Override + public void split(DockDropInfo info, DockContent content) { + parent.split(info, content); + } + + @Override + public JComponent getComponent() { + return this; + } + + /** + * Open context menu manually instead of relying on the JTabbedPane, so we + * can check if it's the currently selected tab (since the context menu will + * trigger actions based on the currently selected tab). + * + * @param e + */ + private void openPopupMenu(MouseEvent e) { + if (!e.isPopupTrigger()) { + return; + } + final int index = indexAtLocation(e.getX(), e.getY()); + if (index != -1) { + DockContent content = getContent(index); + if (content != null) { + JPopupMenu menu = content.getContextMenu(); + if (menu != null) { + menu.show(this, e.getX(), e.getY()); + } + } + } + } + + //========================== + // Tab component + //========================== + /** + * Install tab component (if necessary) for a specific content and update + * it's tab component settings. + * + * @param content + */ + private void updateTabComponent(DockContent content) { + int index = getIndexByContent(content); + if (index != -1) { + DockTabComponent tabComp = content.getTabComponent(); + JComponent comp = tabComp != null ? tabComp.getComponent() : null; + if (getTabComponentAt(index) != comp) { + setTabComponentAt(index, comp); + } + if (tabComp != null) { + // Update settings + tabComp.update(this, index); + } + } + } + + /** + * Install tab components if necessary and update tab component settings. + */ + private void updateTabComponents() { + assoc.entrySet().forEach(e -> updateTabComponent(e.getValue())); + } + + /** + * When Look&Feel is changed, tab component settings have to be updated as + * well, otherwise the colors and stuff might not be correct. + */ + @Override + public void updateUI() { + super.updateUI(); + // Prevent updating before this instance is properly created + if (assoc != null) { + updateTabComponents(); + } + } + + //========================== + // Add/remove content + //========================== + /** + * Add content in the default tab location. A drop (see further below) is + * another way to add content. + * + * @param content + */ + @Override + public void addContent(DockContent content) { + if (assoc.containsValue(content)) { + return; + } + assoc.put(content.getComponent(), content); + insertTab(content.getTitle(), null, content.getComponent(), null, + findInsertPosition(content.getTitle())); + updateTabComponent(content); + content.addListener(dockContentListener); + } + + /** + * Remove content. + * + * @param content + */ + @Override + public void removeContent(DockContent content) { + assoc.remove(content.getComponent()); + remove(content.getComponent()); + content.removeListener(dockContentListener); + if (getTabCount() == 0) { + parent.replace(this, null); + } + else if (getTabCount() == 1) { + ((DockTabsContainer)parent).updateSingleAllowed(); + } + } + + //========================== + // Current content + //========================== + public boolean containsContent(DockContent content) { + return assoc.containsValue(content); + } + + @Override + public List getContents() { + List result = new ArrayList<>(); + for (int i=0;i 0) { + setSelectedIndex(0); + } + } + + public void setSelectedPrevious() { + int index = getSelectedIndex(); + int count = getTabCount(); + if (count > 0) { + if (index - 1 >= 0) { + setSelectedIndex(index - 1); + } + else { + setSelectedIndex(count - 1); + } + } + } + + @Override + public boolean isContentVisible(DockContent content) { + return getSelectedComponent() == content.getComponent(); + } + + @Override + public List getContentsRelativeTo(DockContent content, int direction) { + List result = new ArrayList<>(); + int index = indexOfComponent(content.getComponent()); + if (index != -1) { + if (direction == 1 || direction == 0) { + for (int i = index+1; i < getTabCount(); i++) { + result.add(getContent(i)); + } + } + if (direction == -1 || direction == 0) { + for (int i = index-1; i >= 0; i--) { + result.add(getContent(i)); + } + } + } + return result; + } + + @SuppressWarnings("element-type-mismatch") + public DockContent getContent(int index) { + return assoc.get(getComponentAt(index)); + } + + public JComponent getComponentByContent(DockContent content) { + for (Map.Entry entry : assoc.entrySet()) { + if (entry.getValue() == content) { + return entry.getKey(); + } + } + return null; + } + + private int getIndexByContent(DockContent content) { + for (int i=0;i= 0) { + if (!validDropIndex(info, index)) { + // No drop possible + return new DockDropInfo(this, DockDropInfo.DropType.INVALID, null, index); + } + else { + // Move tab within same tab pane or from other tab pane + return new DockDropInfo(this, DockDropInfo.DropType.TAB, getDropRectangle(index), index); + } + } + if (getTabCount() == 1 && info.tf.source == this) { + // Can't move a tab within the same tab pane if there is only one + return null; + } + if (DockDropInfo.determineLocation(this, info.getLocation(this), 30, 1400, 20) == DropType.CENTER) { + if (info.tf.source != this) { + // Append new tab from other tab pane + return new DockDropInfo(this, DockDropInfo.DropType.TAB, DockDropInfo.makeRect(this, DropType.CENTER, 40, 100000), -1); + } + } + DockDropInfo.DropType location = DockDropInfo.determineLocation(this, info.getLocation(this), 20, 80, 20); + if (location != null && location != DropType.CENTER) { + // Split + return new DockDropInfo(this, location, DockDropInfo.makeRect(this, location, 20, 20, 80), -1); + } + return null; + } + + /** + * If moving within the same tab pane, make sure it's not dropped in the + * location it's already in. + * + * @param info + * @param index The target index + * @return true if it's a valid drop location, false otherwise + */ + private boolean validDropIndex(DockImportInfo info, int index) { + return info.tf.source != this + || !(info.tf.sourceIndex == index || info.tf.sourceIndex + 1 == index); + } + + /** + * Make a rectangle for inserting tab at a specific index. + * + * @param index + * @return + */ + private Rectangle getDropRectangle(int index) { + boolean drawBeforeTab = true; + if (index >= getTabCount()) { + // If after last tab, then draw behind the last tab + index = getTabCount() - 1; + drawBeforeTab = false; + } + Rectangle bounds = getUI().getTabBounds(this, index); + if (drawBeforeTab) { + if (tabsAreHorizontal()) { + return new Rectangle(bounds.x, bounds.y, 3, bounds.height); + } + return new Rectangle(bounds.x, bounds.y, bounds.width, 3); + } + if (tabsAreHorizontal()) { + return new Rectangle(bounds.x + bounds.width - 3, bounds.y, 3, bounds.height); + } + return new Rectangle(bounds.x, bounds.y + bounds.height - 3, bounds.width, 3); + } + + /** + * Get the drop index for the given location. The drop index is the location + * between the tabs to insert the dragged tab into. Basicially it's the + * index of the tab to insert the dragged tab in front of, or the last tab + * index + 1 if it should be inserted after the last tab. + * + * @param p The location to find the drop index for + * @return The drop index (> 0 and <= tab count), or -1 if none could be + * found + */ + private int getDropIndexForPoint(Point p) { + int index = getIndexForPoint(p); + if (index >= 0) { + Rectangle bounds = getBoundsAt(index); + boolean isCurrentTab; + if (tabsAreHorizontal()) { + isCurrentTab = p.x < bounds.x + bounds.width / 2; + } + else { + isCurrentTab = p.y < bounds.y + bounds.height / 2; + } + if (isCurrentTab) { + return index; + } else { + return index+1; + } + } else if (getTabCount() > 0) { // TODO: Drop on empty? + /** + * Basicially making the last tab wider to have more leeway with + * dropping it after the last tab. + */ + Rectangle bounds = getBoundsAt(getTabCount() - 1); + if (tabsAreHorizontal()) { + bounds.width += 300; + } + else { + bounds.height += 300; + } + if (bounds.contains(p)) { + return getTabCount(); + } + } + return -1; + } + + @Override + public void drop(DockTransferInfo info) { + // Once a drop was initiated by the user, should reset target path + info.importInfo.content.setTargetPath(null); + + if (info.dropInfo.location != DockDropInfo.DropType.TAB) { + //-------------------------- + // Splitting + //-------------------------- +// System.out.println("ABORT: Can only receive TAB insert"); + info.importInfo.source.removeContent(info.importInfo.content); + split(info.dropInfo, info.importInfo.content); + return; + } + //-------------------------- + // Moving TAB + //-------------------------- + int targetIndex = info.dropInfo.index; + if (targetIndex == -1) { + targetIndex = findInsertPosition(info.importInfo.content.getTitle()); + } + if (info.importInfo.source == this) { + // Move within tab pane + moveTab(info.importInfo.sourceIndex, targetIndex); + } + else { + DockContent content = info.importInfo.content; + DockTabsContainer container = (DockTabsContainer)parent; + container.setSingleAllowedLocked(); + container.switchToTabs(); + info.importInfo.source.removeContent(content); + assoc.put(content.getComponent(), content); + insertTab(content.getTitle(), null, content.getComponent(), null, targetIndex); + updateTabComponent(content); + content.addListener(dockContentListener); + setSelectedIndex(targetIndex); + container.resetSingleAllowed(); + } + } + + /** + * Move a tab from a given index to another. + * + * @param from The index of the tab to move + * @param to The index to move the tab to + * @throws IndexOutOfBoundsException if from or to are not in the range of + * tab indices + */ + private void moveTab(int from, int to) { + if (from == to) { + // Nothing to do here + return; + } + Component comp = getComponentAt(from); + String title = getTitleAt(from); + String toolTip = getToolTipTextAt(from); + Icon icon = getIconAt(from); + Component tabComp = getTabComponentAt(from); + removeTabAt(from); + + /** + * If the source index (the tab that is moved) is in front of the target + * index, then removing the source index will have shifted the target + * index back by one, so adjust for that. + */ + if (from < to) { + to--; + } + insertTab(title, icon, comp, toolTip, to); + setTabComponentAt(to, tabComp); + setSelectedComponent(comp); + } + + //========================== + // Other / Helper + //========================== + + @Override + public String toString() { + if (getTabCount() == 0) { + return "EmptyTabs"; + } + StringBuilder b = new StringBuilder(); + for (int i=0;i getContents() { + if (singleContent != null) { + return Arrays.asList(new DockContent[]{singleContent}); + } + return tabs.getContents(); + } + + @Override + public void setActiveContent(DockContent content) { + tabs.setActiveContent(content); + } + + @Override + public boolean isContentVisible(DockContent content) { + return singleContent == content || tabs.isContentVisible(content); + } + + @Override + public List getContentsRelativeTo(DockContent content, int direction) { + // If singleContent is active, the result would be empty anyway + return tabs.getContentsRelativeTo(content, direction); + } + + @Override + public void setSetting(DockSetting.Type setting, Object value) { + tabs.setSetting(setting, value); + + if (setting == DockSetting.Type.DEBUG) { + setBorder(value == Boolean.TRUE ? BorderFactory.createLineBorder(Color.RED, 4) : null); + } + } + + @Override + public String toString() { + if (singleContent != null) { + return "-["+singleContent.getTitle()+"]-"; + } + return "-["+tabs.toString()+"]-"; + } + + @Override + public DockPath getPath() { + return parent.buildPath(new DockPath(), this); + } + + @Override + public DockPath buildPath(DockPath path, DockChild child) { + DockContent content = path.getContent(); + if (content != null) { + if (singleContent == content) { + path.addParent(DockPathEntry.createTab(0)); + } + else { + path.addParent(DockPathEntry.createTab(tabs.indexOfComponent(content.getComponent()))); + } + } + return parent.buildPath(path, this); + } + +} diff --git a/src/chatty/util/dnd/DockTransferInfo.java b/src/chatty/util/dnd/DockTransferInfo.java new file mode 100644 index 000000000..3fee93873 --- /dev/null +++ b/src/chatty/util/dnd/DockTransferInfo.java @@ -0,0 +1,26 @@ + +package chatty.util.dnd; + +/** + * Info provided to the component that receives a drop. Some of that info comes + * from the export, other info may have been created by the same component that + * receives it. + * + * @author tduva + */ +public class DockTransferInfo { + + public final DockDropInfo dropInfo; + public final DockTransferable importInfo; + + public DockTransferInfo(DockDropInfo dropInfo, DockTransferable importInfo) { + this.dropInfo = dropInfo; + this.importInfo = importInfo; + } + + public String toString() { + return String.format("(%s,%s)", + dropInfo, importInfo); + } + +} diff --git a/src/chatty/util/dnd/DockTransferable.java b/src/chatty/util/dnd/DockTransferable.java new file mode 100644 index 000000000..52f3a7615 --- /dev/null +++ b/src/chatty/util/dnd/DockTransferable.java @@ -0,0 +1,58 @@ + +package chatty.util.dnd; + +import java.awt.Image; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; + +/** + * Contains information provided by the source component of a drag operation, + * used mainly for drawing the interface and the potential drop. + * + * @author tduva + */ +public class DockTransferable implements Transferable { + + public static final DataFlavor FLAVOR = + new DataFlavor(DockTransferable.class, "DockTransferable"); + + public final DockContent content; + public final DockChild source; + public final Image image; + public final int sourceIndex; + + public DockTransferable(DockContent content, DockChild source, int index, Image image) { + this.content = content; + this.source = source; + this.sourceIndex = index; + this.image = image; + } + + @Override + public DataFlavor[] getTransferDataFlavors() + { + return new DataFlavor[]{FLAVOR}; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) + { + if (FLAVOR.equals(flavor)) + { + return true; + } + return false; + } + + @Override + public Object getTransferData(DataFlavor flavor) { + return this; + } + + @Override + public String toString() { + return String.format("TF(%d,%s,%s)", + sourceIndex, content, source); + } + +} diff --git a/src/chatty/util/dnd/DockUtil.java b/src/chatty/util/dnd/DockUtil.java new file mode 100644 index 000000000..359753bfc --- /dev/null +++ b/src/chatty/util/dnd/DockUtil.java @@ -0,0 +1,109 @@ + +package chatty.util.dnd; + +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Window; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; +import javax.swing.TransferHandler; + +/** + * + * @author tduva + */ +public class DockUtil { + + public static DockSplit createSplit(DockDropInfo info, DockChild prev, DockChild added) { + DockSplit result = null; + switch (info.location) { + case LEFT: + result = new DockSplit(JSplitPane.HORIZONTAL_SPLIT, added, prev); + break; + case RIGHT: + result = new DockSplit(JSplitPane.HORIZONTAL_SPLIT, prev, added); + break; + case BOTTOM: + result = new DockSplit(JSplitPane.VERTICAL_SPLIT, prev, added); + break; + case TOP: + result = new DockSplit(JSplitPane.VERTICAL_SPLIT, added, prev); + break; + } + return result; + } + + public static boolean isMouseOverWindow() { + Point p = MouseInfo.getPointerInfo().getLocation(); + for (Window w : Window.getWindows()) { + if (w.isVisible() && w.getBounds().contains(p)) { + return true; + } + } + return false; + } + + public static DockTransferable getTransferable(TransferHandler.TransferSupport info) { + try { + return (DockTransferable) info.getTransferable().getTransferData(DockTransferable.FLAVOR); + } + catch (IOException | UnsupportedFlavorException ex) { + System.out.println(ex); + return null; + } + } + + public static DockTransferable getTransferable(Transferable t) { + try { + return (DockTransferable) t.getTransferData(DockTransferable.FLAVOR); + } + catch (IOException | UnsupportedFlavorException ex) { + System.out.println(ex); + return null; + } + } + + /** + * When a split is removed, choose a tab from the remaining child as active + * (child may be another split or tabs directly). + * + * @param child The remaining child from the split + * @param base The base to inform + */ + public static void activeTabAfterJoin(DockChild child, DockBase base) { + if (base == null) { + return; + } + if (child instanceof DockTabsContainer) { + DockTabsContainer tabs = (DockTabsContainer)child; + base.tabChanged(null, tabs.getCurrentContent()); + } + else if (child instanceof DockSplit) { + // Keep looking for tabs in the left child of the split + activeTabAfterJoin(((DockSplit)child).getLeftChild(), base); + } + } + + public static DockPathEntry getNext(DockContent content, DockChild current) { + DockPath target = content.getTargetPath(); + if (target != null) { + return target.getNext(current.getPath()); + } + return null; + } + + /** + * Exchanging a component can reset the divider location (seems affected by + * resize weight), so set previous location again. + * + * @param split + */ + public static void preserveDividerLocation(JSplitPane split) { + int location = split.getDividerLocation(); + SwingUtilities.invokeLater(() -> split.setDividerLocation(location)); + } + +} diff --git a/src/chatty/util/dnd/Test.java b/src/chatty/util/dnd/Test.java new file mode 100644 index 000000000..f2f2c99e3 --- /dev/null +++ b/src/chatty/util/dnd/Test.java @@ -0,0 +1,126 @@ + +package chatty.util.dnd; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextPane; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * + * @author tduva + */ +public class Test { + + private static int counter = 0; + private static DockManager m; + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + m = new DockManager(new DockListener() { + @Override + public void activeContentChanged(DockPopout window, DockContent content, boolean focusChange) { + if (window == null) { + frame.setTitle(content.getTitle()); + } + else { + window.setTitle(content.getTitle()); + } + } + + @Override + public void popoutClosed(DockPopout window, List contents) { + + } + + @Override + public void popoutOpened(DockPopout popout, DockContent content) { + + } + }); +// m.setSetting(DockSetting.Type.TAB_PLACEMENT, "left"); + m.setSetting(DockSetting.Type.DIVIDER_SIZE, 7); + frame.add(m.getBase(), BorderLayout.CENTER); + + JMenuBar menubar = new JMenuBar(); + JMenu menu = new JMenu("Menu"); + Action action = new AbstractAction("Test") { + + @Override + public void actionPerformed(ActionEvent e) { + add(); + } + }; + menu.add(new JMenuItem(action)); + Action action2 = new AbstractAction("Test") { + + @Override + public void actionPerformed(ActionEvent e) { + m.setSetting(DockSetting.Type.POPOUT_TYPE, DockSetting.PopoutType.FRAME); + } + }; + menu.add(new JMenuItem(action2)); + menubar.add(menu); + frame.setJMenuBar(menubar); + + frame.setSize(1200, 500); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + for (int i=0;i<6;i++) { + add(); + } + DockContent c = add(); + m.setActiveContent(c); + }); + } + + private static DockContent add() { + counter++; + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + JTextPane text = new JTextPane(); + panel.add(text, BorderLayout.CENTER); + text.setText("Test Text " + counter); + DockContentContainer content = new DockContentContainer("Test " + ThreadLocalRandom.current().nextLong(10000000), new JScrollPane(panel), m); + text.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + content.setTitle(text.getText()); + if (text.getText().equals("test")) { + m.addContent(content); + } + } + + @Override + public void removeUpdate(DocumentEvent e) { + + } + + @Override + public void changedUpdate(DocumentEvent e) { + + } + }); + m.addContent(content); + return content; + } + +} diff --git a/src/chatty/util/ffz/FrankerFaceZ.java b/src/chatty/util/ffz/FrankerFaceZ.java index 733f07b83..220040c47 100644 --- a/src/chatty/util/ffz/FrankerFaceZ.java +++ b/src/chatty/util/ffz/FrankerFaceZ.java @@ -31,6 +31,9 @@ private enum Type { GLOBAL, ROOM, FEATURE_FRIDAY }; // State private boolean botNamesRequested; + private String botBadgeId = null; + // stream -> roomBadges(badgeId -> names) + private final Map>> roomBadgeUsernames = new HashMap<>(); /** * Feature Friday @@ -248,6 +251,7 @@ private void parseResult(Type type, String stream, String id, String result) { } else if (type == Type.ROOM) { // If type is ROOM, stream should be available emotes = FrankerFaceZParsing.parseRoomEmotes(result, stream); + addRoomBadgeUsernames(stream, FrankerFaceZParsing.parseRoomBadges(result)); Usericon modIcon = FrankerFaceZParsing.parseModIcon(result, stream); if (modIcon != null) { usericons.add(modIcon); @@ -354,8 +358,55 @@ public void requestBotNames() { if (result != null && responseCode == 200) { Set botNames = FrankerFaceZParsing.getBotNames(result); LOGGER.info("|[FFZ Bots] Found " + botNames.size() + " names"); - listener.botNamesReceived(botNames); + listener.botNamesReceived(null, botNames); + synchronized(roomBadgeUsernames) { + // Find bot badge id, so it can be used for room badges + botBadgeId = FrankerFaceZParsing.getBotBadgeId(result); + } + updateRoomBotNames(); } }); } + + /** + * Cache room badges, so bot names can be retrieved from it. + * + * @param stream + * @param names + */ + private void addRoomBadgeUsernames(String stream, Map> names) { + if (stream == null || names == null || names.isEmpty()) { + return; + } + synchronized(roomBadgeUsernames) { + roomBadgeUsernames.put(stream, names); + } + updateRoomBotNames(); + } + + /** + * Check the cached room badges for the bot badge id and add the bot names + * if present. Clear cached badges afterwards since they don't have any + * other use at the moment. + */ + private void updateRoomBotNames() { + Map> result = new HashMap<>(); + synchronized (roomBadgeUsernames) { + if (botBadgeId != null) { + for (Map.Entry>> room : roomBadgeUsernames.entrySet()) { + String stream = room.getKey(); + Set names = room.getValue().get(botBadgeId); + if (names != null) { + result.put(stream, names); + } + } + roomBadgeUsernames.clear(); + } + } + for (Map.Entry> entry : result.entrySet()) { + LOGGER.info("|[FFZ Bots] ("+entry.getKey()+"): Found " + entry.getValue().size() + " names"); + listener.botNamesReceived(entry.getKey(), entry.getValue()); + } + } + } diff --git a/src/chatty/util/ffz/FrankerFaceZListener.java b/src/chatty/util/ffz/FrankerFaceZListener.java index 984d4594f..d2409e623 100644 --- a/src/chatty/util/ffz/FrankerFaceZListener.java +++ b/src/chatty/util/ffz/FrankerFaceZListener.java @@ -20,7 +20,7 @@ public interface FrankerFaceZListener { */ public void channelEmoticonsReceived(EmoticonUpdate emotes); public void usericonsReceived(List icons); - public void botNamesReceived(Set botNames); + public void botNamesReceived(String stream, Set botNames); public void wsInfo(String info); public void authorizeUser(String code); public void wsUserInfo(String info); diff --git a/src/chatty/util/ffz/FrankerFaceZParsing.java b/src/chatty/util/ffz/FrankerFaceZParsing.java index 10ca293f6..0522f0ab3 100644 --- a/src/chatty/util/ffz/FrankerFaceZParsing.java +++ b/src/chatty/util/ffz/FrankerFaceZParsing.java @@ -5,7 +5,9 @@ import chatty.util.JSONUtil; import chatty.util.api.Emoticon; import chatty.util.api.usericons.UsericonFactory; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.json.simple.JSONArray; @@ -250,4 +252,61 @@ public static Set getBotNames(String json) { return result; } + /** + * Get the badge id. Used to get bot names. + * + * @param json + * @return A string containing the id, or null if an error occured + */ + public static String getBotBadgeId(String json) { + try { + JSONParser parser = new JSONParser(); + JSONObject root = (JSONObject)parser.parse(json); + JSONObject badge = (JSONObject)root.get("badge"); + Number id = (Number)badge.get("id"); + if (id == null) { + return null; + } + return String.valueOf(id); + } catch (Exception ex) { + LOGGER.warning("Error parsing bot badge id: "+ex); + } + return null; + } + + /** + * Parse the badges contained in the single room response. Currently only + * used to retrieve bot names. + * + * @param json + * @return A map with badge id as key and names as value (never null, may be + * empty) + */ + public static Map> parseRoomBadges(String json) { + Map> result = new HashMap<>(); + try { + JSONParser parser = new JSONParser(); + JSONObject o = (JSONObject)parser.parse(json); + JSONObject room = (JSONObject)o.get("room"); + JSONObject badges = (JSONObject)room.get("user_badges"); + for (Object key : badges.keySet()) { + Object value = badges.get(key); + if (key instanceof String && value instanceof JSONArray) { + String badgeId = (String) key; + JSONArray names = (JSONArray)value; + Set namesResult = new HashSet<>(); + for (Object item : names) { + if (item instanceof String) { + namesResult.add((String)item); + } + } + result.put(badgeId, namesResult); + } + } + } catch (Exception ex) { + LOGGER.warning("Error parsing room badges: "+ex); + } + return result; + } + } diff --git a/src/chatty/util/hotkeys/HotkeyManager.java b/src/chatty/util/hotkeys/HotkeyManager.java index 5dbd73f7f..6f137ca00 100644 --- a/src/chatty/util/hotkeys/HotkeyManager.java +++ b/src/chatty/util/hotkeys/HotkeyManager.java @@ -25,7 +25,9 @@ import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JDialog; +import javax.swing.JFrame; import javax.swing.JRootPane; +import javax.swing.JWindow; import javax.swing.KeyStroke; /** @@ -56,7 +58,7 @@ public class HotkeyManager { private final MainGui main; private final List hotkeys = new ArrayList<>(); private final Map actions = new LinkedHashMap<>(); - private final Map popouts = new WeakHashMap<>(); + private final Map popouts = new WeakHashMap<>(); /** * Whether global hotkeys are currently to be enabled (registered). @@ -147,9 +149,21 @@ public void registerAction(String id, String label, Action action) { * * @param popout */ - public void registerPopout(JDialog popout) { - popouts.put(popout, null); - addHotkeys(popout.getRootPane()); + public void registerPopout(Object popout) { + JRootPane pane = null; + if (popout instanceof JWindow) { + pane = ((JWindow)popout).getRootPane(); + } + else if (popout instanceof JFrame) { + pane = ((JFrame)popout).getRootPane(); + } + else if (popout instanceof JDialog) { + pane = ((JDialog)popout).getRootPane(); + } + if (pane != null) { + popouts.put(pane, null); + addHotkeys(pane); + } } /** @@ -303,8 +317,8 @@ private void addHotkeys(JRootPane pane) { if (isValidHotkey(hotkey) && hotkey.type == Type.REGULAR) { if (pane == null) { addHotkey(hotkey, main.getRootPane()); - for (JDialog popout : popouts.keySet()) { - addHotkey(hotkey, popout.getRootPane()); + for (JRootPane popoutPane : popouts.keySet()) { + addHotkey(hotkey, popoutPane); } } else { addHotkey(hotkey, pane); @@ -351,8 +365,8 @@ private void addGlobalHotkeys() { */ private void removeAllHotkeys() { removeHotkeys(main.getRootPane()); - for (JDialog popout : popouts.keySet()) { - removeHotkeys(popout.getRootPane()); + for (JRootPane popoutPane : popouts.keySet()) { + removeHotkeys(popoutPane); } removeGlobalHotkeys(); removeHotkeysFromActions(); diff --git a/version.py b/version.py index 5b654d636..fa21007b4 100644 --- a/version.py +++ b/version.py @@ -15,8 +15,8 @@ chatty = open(path, 'w'); line = re.sub( - r"public static final String VERSION = \"0.13.0.[0-9]+", - "public static final String VERSION = \"0.13.0.%s" % sub, + r"public static final String VERSION = \"0.14.0.[0-9]+", + "public static final String VERSION = \"0.14.0.%s" % sub, line ); #print(line)