Entrega v1 aula: shell com abas, PITIUPI_HOME e plugins de referência.
Consolida núcleo (presença, peer actions, MessageDispatcher), chat e screencast na galeria, documentação PT-BR e testes JUnit; remove mpChat do catálogo em favor do chat unificado. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b10c196fc9
commit
eeb1936f6d
178 files changed
+7641
-1481
No files matched your search
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="pitiupi.chat">
|
||||
<CLASSES>
|
||||
<root url="jar://$PROJECT_DIR$/dist/pitiupi.chat.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/pitiupi.chat.iml" filepath="$PROJECT_DIR$/pitiupi.chat.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- You may freely edit this file. See commented blocks below for -->
|
||||
<!-- some examples of how to customize the build. -->
|
||||
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||
<!-- the Compile on Save feature is turned off for the project. -->
|
||||
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||
<!-- in the project's Project Properties dialog box.-->
|
||||
<project name="pitiupi.chat" default="default" basedir=".">
|
||||
<description>Builds, tests, and runs the project pitiupi.chat.</description>
|
||||
<import file="nbproject/build-impl.xml"/>
|
||||
<!--
|
||||
|
||||
There exist several targets which are by default empty and which can be
|
||||
used for execution of your tasks. These targets are usually executed
|
||||
before and after some main targets. They are:
|
||||
|
||||
-pre-init: called before initialization of project properties
|
||||
-post-init: called after initialization of project properties
|
||||
-pre-compile: called before javac compilation
|
||||
-post-compile: called after javac compilation
|
||||
-pre-compile-single: called before javac compilation of single file
|
||||
-post-compile-single: called after javac compilation of single file
|
||||
-pre-compile-test: called before javac compilation of JUnit tests
|
||||
-post-compile-test: called after javac compilation of JUnit tests
|
||||
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||
-pre-jar: called before JAR building
|
||||
-post-jar: called after JAR building
|
||||
-post-clean: called after cleaning build products
|
||||
|
||||
(Targets beginning with '-' are not intended to be called on their own.)
|
||||
|
||||
Example of inserting an obfuscator after compilation could look like this:
|
||||
|
||||
<target name="-post-compile">
|
||||
<obfuscate>
|
||||
<fileset dir="${build.classes.dir}"/>
|
||||
</obfuscate>
|
||||
</target>
|
||||
|
||||
For list of available properties check the imported
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
|
||||
Another way to customize the build is by overriding existing main targets.
|
||||
The targets of interest are:
|
||||
|
||||
-init-macrodef-javac: defines macro for javac compilation
|
||||
-init-macrodef-junit: defines macro for junit execution
|
||||
-init-macrodef-debug: defines macro for class debugging
|
||||
-init-macrodef-java: defines macro for class execution
|
||||
-do-jar: JAR building
|
||||
run: execution of project
|
||||
-javadoc-build: Javadoc generation
|
||||
test-report: JUnit report generation
|
||||
|
||||
An example of overriding the target for project execution could look like this:
|
||||
|
||||
<target name="run" depends="pitiupi.chat-impl.jar">
|
||||
<exec dir="bin" executable="launcher.exe">
|
||||
<arg file="${dist.jar}"/>
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
Notice that the overridden target depends on the jar target and not only on
|
||||
the compile target as the regular run target does. Again, for a list of available
|
||||
properties which you can use, check the target you are overriding in the
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
-->
|
||||
</project>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
build.xml.data.CRC32=3f66efc2
|
||||
build.xml.script.CRC32=3953f1a4
|
||||
build.xml.stylesheet.CRC32=f85dc8f2@1.119.0.48
|
||||
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||
nbproject/build-impl.xml.data.CRC32=3f66efc2
|
||||
nbproject/build-impl.xml.script.CRC32=a4594806
|
||||
nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.119.0.48
|
||||
@@ -0,0 +1,98 @@
|
||||
annotation.processing.enabled=true
|
||||
annotation.processing.enabled.in.editor=false
|
||||
annotation.processing.processors.list=
|
||||
annotation.processing.run.all.processors=true
|
||||
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||
application.title=pitiupi.chat
|
||||
application.vendor=flavio
|
||||
build.classes.dir=${build.dir}/classes
|
||||
build.classes.excludes=**/*.java,**/*.form
|
||||
# This directory is removed when the project is cleaned:
|
||||
build.dir=build
|
||||
build.generated.dir=${build.dir}/generated
|
||||
build.generated.sources.dir=${build.dir}/generated-sources
|
||||
# Only compile against the classpath explicitly listed here:
|
||||
build.sysclasspath=ignore
|
||||
build.test.classes.dir=${build.dir}/test/classes
|
||||
build.test.results.dir=${build.dir}/test/results
|
||||
# Uncomment to specify the preferred debugger connection transport:
|
||||
#debug.transport=dt_socket
|
||||
debug.classpath=\
|
||||
${run.classpath}
|
||||
debug.modulepath=\
|
||||
${run.modulepath}
|
||||
debug.test.classpath=\
|
||||
${run.test.classpath}
|
||||
debug.test.modulepath=\
|
||||
${run.test.modulepath}
|
||||
# Files in build.classes.dir which should be excluded from distribution jar
|
||||
dist.archive.excludes=
|
||||
# This directory is removed when the project is cleaned:
|
||||
dist.dir=dist
|
||||
dist.jar=${dist.dir}/pitiupi.chat.jar
|
||||
dist.javadoc.dir=${dist.dir}/javadoc
|
||||
dist.jlink.dir=${dist.dir}/jlink
|
||||
dist.jlink.output=${dist.jlink.dir}/pitiupi.chat
|
||||
endorsed.classpath=
|
||||
excludes=
|
||||
includes=**
|
||||
jar.compress=false
|
||||
javac.classpath=\
|
||||
${reference.pitiupi.jar}
|
||||
# Space-separated list of extra javac options
|
||||
javac.compilerargs=
|
||||
javac.deprecation=false
|
||||
javac.external.vm=true
|
||||
javac.modulepath=
|
||||
javac.processormodulepath=
|
||||
javac.processorpath=\
|
||||
${javac.classpath}
|
||||
javac.source=25
|
||||
javac.target=25
|
||||
javac.test.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
javac.test.modulepath=\
|
||||
${javac.modulepath}
|
||||
javac.test.processorpath=\
|
||||
${javac.test.classpath}
|
||||
javadoc.additionalparam=
|
||||
javadoc.author=false
|
||||
javadoc.encoding=${source.encoding}
|
||||
javadoc.html5=false
|
||||
javadoc.noindex=false
|
||||
javadoc.nonavbar=false
|
||||
javadoc.notree=false
|
||||
javadoc.private=false
|
||||
javadoc.splitindex=true
|
||||
javadoc.use=true
|
||||
javadoc.version=false
|
||||
javadoc.windowtitle=
|
||||
# The jlink additional root modules to resolve
|
||||
jlink.additionalmodules=
|
||||
# The jlink additional command line parameters
|
||||
jlink.additionalparam=
|
||||
jlink.launcher=true
|
||||
jlink.launcher.name=pitiupi.chat
|
||||
meta.inf.dir=${src.dir}/META-INF
|
||||
mkdist.disabled=true
|
||||
platform.active=default_platform
|
||||
project.pitiupi=../../pitiupi
|
||||
reference.pitiupi.jar=${project.pitiupi}/dist/pitiupi.jar
|
||||
run.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
# Space-separated list of JVM arguments used when running the project.
|
||||
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
|
||||
# To set system properties for unit tests define test-sys-prop.name=value:
|
||||
run.jvmargs=
|
||||
run.modulepath=\
|
||||
${javac.modulepath}
|
||||
run.test.classpath=\
|
||||
${javac.test.classpath}:\
|
||||
${build.test.classes.dir}
|
||||
run.test.modulepath=\
|
||||
${javac.test.modulepath}
|
||||
source.encoding=UTF-8
|
||||
src.dir=src
|
||||
test.src.dir=test
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||
<type>org.netbeans.modules.java.j2seproject</type>
|
||||
<configuration>
|
||||
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||
<name>pitiupi.chat</name>
|
||||
<source-roots>
|
||||
<root id="src.dir"/>
|
||||
</source-roots>
|
||||
<test-roots>
|
||||
<root id="test.src.dir"/>
|
||||
</test-roots>
|
||||
</data>
|
||||
<references xmlns="http://www.netbeans.org/ns/ant-project-references/1">
|
||||
<reference>
|
||||
<foreign-project>pitiupi</foreign-project>
|
||||
<artifact-type>jar</artifact-type>
|
||||
<script>build.xml</script>
|
||||
<target>jar</target>
|
||||
<clean-target>clean</clean-target>
|
||||
<id>jar</id>
|
||||
</reference>
|
||||
</references>
|
||||
</configuration>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module-library">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1 @@
|
||||
pitiupi.chat.plugin.ChatPlugin
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="module-library">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
|
||||
*/
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ChatMenu extends MulticastSubMenu {
|
||||
ChatMulticastMenu chatMulticastMenu;
|
||||
|
||||
public ChatMenu(ChatWindow chatWindow) {
|
||||
this.chatMulticastMenu = new ChatMulticastMenu(chatWindow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MulticastMenu> getMenus() {
|
||||
return List.of(chatMulticastMenu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;;
|
||||
|
||||
public class ChatMulticastMenu extends MulticastActionMenu {
|
||||
ChatWindow chatWindow;
|
||||
|
||||
public ChatMulticastMenu(ChatWindow chatWindow) {
|
||||
this.chatWindow = chatWindow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Show Chat Window";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
chatWindow.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.application.messaging.MessageDispatcher;
|
||||
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||
import pitiupi.application.messaging.SendTarget;
|
||||
import pitiupi.application.messaging.TransportMode;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.chat.control.PrivateChatController;
|
||||
import pitiupi.chat.control.PrivateChatUiSink;
|
||||
import pitiupi.chat.net.ChatMessage;
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.presentation.shell.LocalNetworkInfo;
|
||||
import pitiupi.presentation.shell.LocalUserIdentity;
|
||||
import pitiupi.presentation.shell.PeerDraftPanel;
|
||||
import pitiupi.presentation.shell.PluginRoutingPanel;
|
||||
import pitiupi.presentation.shell.PluginSessionKeys;
|
||||
import pitiupi.presentation.shell.PluginSessionWorkspace;
|
||||
import pitiupi.presentation.shell.SessionPeerLabels;
|
||||
import pitiupi.presentation.shell.SessionTabBar;
|
||||
import pitiupi.presentation.shell.ui.ShellUi;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.HierarchyEvent;
|
||||
import javax.swing.text.html.HTMLEditorKit;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.net.InetAddress;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
|
||||
private static final String ROOM_KEY = "room";
|
||||
private static final String CARD_MAIN = "main";
|
||||
private static final String CARD_DRAFT = "draft";
|
||||
|
||||
private final JEditorPane transcript = new JEditorPane();
|
||||
private final JTextArea input = new JTextArea();
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PluginSessionWorkspace workspace;
|
||||
private final PluginRoutingPanel routingPanel;
|
||||
private final SessionTabBar sessionTabs;
|
||||
private final CardLayout bodyLayout = new CardLayout();
|
||||
private final JPanel bodyCards = new JPanel(bodyLayout);
|
||||
private String lastSessionKey = PluginSessionKeys.ROOM;
|
||||
private final Map<String, StringBuilder> transcripts = new HashMap<>();
|
||||
private final Map<String, String> peerDisplayNames = new HashMap<>();
|
||||
private String activeConversationKey = ROOM_KEY;
|
||||
private PrivateChatController privateChatController;
|
||||
private PeerDraftPanel draftPanel;
|
||||
private Runnable pendingTranscriptFlush;
|
||||
|
||||
public ChatPanel(ConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.routingPanel = new PluginRoutingPanel(
|
||||
PluginTransportCatalog.PluginKind.CHAT,
|
||||
"<html><i>Use o IP do peer ou escolha alguém na lista lateral → Conversar. "
|
||||
+ "Envios usam a porta do login (barra de status); na sala, mesma porta para todos.</i></html>",
|
||||
true
|
||||
);
|
||||
routingPanel.setToolTipText(
|
||||
"UDP/TCP unicast usa o IP do peer e a porta " + connectionManager.getPort()
|
||||
+ " (a que você informou ao entrar)."
|
||||
);
|
||||
this.workspace = new PluginSessionWorkspace(routingPanel, null);
|
||||
this.sessionTabs = workspace.getSessionTabBar();
|
||||
setLayout(new BorderLayout(5, 5));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
buildUi();
|
||||
initSessionTabs();
|
||||
routingPanel.addRoutingListener(s -> applyConversationKeyFromRouting());
|
||||
routingPanel.setPrivateTcpListener(selected -> {
|
||||
if (selected) {
|
||||
User user = resolveUserForSingle();
|
||||
if (user != null && privateChatController != null) {
|
||||
privateChatController.openChat(user);
|
||||
}
|
||||
}
|
||||
applyConversationKeyFromRouting();
|
||||
});
|
||||
selectRoomSession();
|
||||
}
|
||||
|
||||
private void initSessionTabs() {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.ROOM,
|
||||
"Sala geral",
|
||||
"Sala geral (Todos)",
|
||||
new JPanel(),
|
||||
false
|
||||
);
|
||||
sessionTabs.setCloseFallbackKey(PluginSessionKeys.ROOM);
|
||||
sessionTabs.setOnTabSelected(this::onSessionTabSelected);
|
||||
sessionTabs.setOnAddPeerRequested(this::openDraftPeerTab);
|
||||
sessionTabs.setOnTabClosed(this::onSessionTabClosed);
|
||||
}
|
||||
|
||||
private void onSessionTabClosed(String key) {
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyConversationKeyFromRouting() {
|
||||
String next = resolveConversationKey();
|
||||
if (!next.equals(activeConversationKey)) {
|
||||
switchActiveConversation(next);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSessionTabSelected(String key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
lastSessionKey = key;
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
showBodyCard(CARD_DRAFT);
|
||||
if (draftPanel != null) {
|
||||
draftPanel.focusIpField();
|
||||
}
|
||||
return;
|
||||
}
|
||||
showBodyCard(CARD_MAIN);
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
routingPanel.bindRoomSession();
|
||||
switchActiveConversation(ROOM_KEY);
|
||||
return;
|
||||
}
|
||||
if (key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
String ip = key.substring("peer:".length());
|
||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||
routingPanel.bindPeerSession(ip, name);
|
||||
switchActiveConversation(resolveConversationKey());
|
||||
}
|
||||
}
|
||||
|
||||
private void openDraftPeerTab() {
|
||||
String previous = sessionTabs.selectedKey();
|
||||
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
|
||||
lastSessionKey = previous;
|
||||
}
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.PEER_DRAFT,
|
||||
"Nova conversa",
|
||||
"Informe o IP do peer",
|
||||
new JPanel(),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
sessionTabs.selectTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
showBodyCard(CARD_DRAFT);
|
||||
if (draftPanel != null) {
|
||||
draftPanel.focusIpField();
|
||||
}
|
||||
}
|
||||
|
||||
private void commitDraftIp(String ip) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
if (sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.closeTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
showBodyCard(CARD_MAIN);
|
||||
openPeerTab(canonicalPeerIp(ip), ip);
|
||||
}
|
||||
|
||||
private void syncRoutingFromActiveSessionTab() {
|
||||
String key = sessionTabs.selectedKey();
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
routingPanel.bindRoomSession();
|
||||
if (!ROOM_KEY.equals(activeConversationKey)) {
|
||||
switchActiveConversation(ROOM_KEY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key != null && key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
String ip = key.substring("peer:".length());
|
||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||
routingPanel.bindPeerSession(ip, name);
|
||||
String conv = resolveConversationKey();
|
||||
if (!conv.equals(activeConversationKey)) {
|
||||
switchActiveConversation(conv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreSessionAfterDraftClosed() {
|
||||
showBodyCard(CARD_MAIN);
|
||||
String restore = lastSessionKey != null && sessionTabs.hasTab(lastSessionKey)
|
||||
? lastSessionKey
|
||||
: PluginSessionKeys.ROOM;
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(restore)) {
|
||||
restore = PluginSessionKeys.ROOM;
|
||||
}
|
||||
sessionTabs.selectTabSilently(restore);
|
||||
if (PluginSessionKeys.ROOM.equals(restore)) {
|
||||
routingPanel.bindRoomSession();
|
||||
switchActiveConversation(ROOM_KEY);
|
||||
} else if (restore.startsWith("peer:")) {
|
||||
String ip = restore.substring("peer:".length());
|
||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||
routingPanel.bindPeerSession(ip, name);
|
||||
switchActiveConversation(resolveConversationKey());
|
||||
}
|
||||
}
|
||||
|
||||
private void openPeerTab(String ip, String displayName) {
|
||||
String canonical = canonicalPeerIp(ip);
|
||||
String resolved = SessionPeerLabels.resolveDisplayName(connectionManager, canonical, displayName);
|
||||
peerDisplayNames.put(canonical, resolved);
|
||||
String tabKey = PluginSessionKeys.peer(canonical);
|
||||
if (sessionTabs.hasTab(tabKey)) {
|
||||
sessionTabs.setTabTitle(tabKey, resolved, canonical);
|
||||
sessionTabs.selectTab(tabKey);
|
||||
} else {
|
||||
sessionTabs.addOrSelectTab(tabKey, resolved, canonical, new JPanel(), true);
|
||||
}
|
||||
routingPanel.bindPeerSession(canonical, resolved);
|
||||
switchActiveConversation(resolveConversationKey());
|
||||
showBodyCard(CARD_MAIN);
|
||||
}
|
||||
|
||||
private static String canonicalPeerIp(String ip) {
|
||||
if (ip == null || ip.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(ip.trim()).getHostAddress();
|
||||
} catch (Exception ex) {
|
||||
return ip.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectRoomSession() {
|
||||
routingPanel.bindRoomSession();
|
||||
switchActiveConversation(ROOM_KEY);
|
||||
showBodyCard(CARD_MAIN);
|
||||
}
|
||||
|
||||
public void setPrivateChatController(PrivateChatController privateChatController) {
|
||||
this.privateChatController = privateChatController;
|
||||
}
|
||||
|
||||
public void focusGeneralRoom() {
|
||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||
}
|
||||
|
||||
public void focusPeer(UserPeerView peer) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
focusGeneralRoom();
|
||||
return;
|
||||
}
|
||||
peer.getInetAddress().ifPresent(addr ->
|
||||
openPeerTab(addr.getHostAddress(), peer.getDisplayName()));
|
||||
}
|
||||
|
||||
public void enablePrivateTcpForPeer(UserPeerView peer) {
|
||||
focusPeer(peer);
|
||||
routingPanel.setPrivateTcpOn(true);
|
||||
switchActiveConversation(resolveConversationKey());
|
||||
}
|
||||
|
||||
public void deliverIncomingMessage(ChatMessage message) {
|
||||
if (isEchoFromLocalUser(message)) {
|
||||
return;
|
||||
}
|
||||
String bucket = bucketForIncoming(message);
|
||||
if (!ROOM_KEY.equals(bucket) && message.getSenderIp() != null && !message.getSenderIp().isBlank()) {
|
||||
String canonical = canonicalPeerIp(message.getSenderIp());
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.peer(canonical))) {
|
||||
openPeerTab(canonical, message.getName());
|
||||
}
|
||||
bucket = peerKey(canonical);
|
||||
}
|
||||
appendChatLine(bucket, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrivateChannelReady(String peerIp) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
appendSystemLine(privateKey(peerIp), "Canal TCP privado conectado com " + peerIp + ".");
|
||||
if (privateKey(peerIp).equals(activeConversationKey)) {
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrivateChannelClosed(String peerIp) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
appendSystemLine(privateKey(peerIp), "Canal TCP privado encerrado (" + peerIp + ").");
|
||||
if (privateKey(peerIp).equals(activeConversationKey)) {
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendPrivateLine(String peerIp, LegacyMpChatMessage message, boolean outgoing) {
|
||||
SwingUtilities.invokeLater(() -> appendLegacyLine(privateKey(peerIp), message));
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
split.setResizeWeight(0.84);
|
||||
split.setOneTouchExpandable(true);
|
||||
transcript.setContentType("text/html");
|
||||
transcript.setEditable(false);
|
||||
|
||||
input.setColumns(20);
|
||||
input.setRows(2);
|
||||
input.setLineWrap(true);
|
||||
input.setWrapStyleWord(true);
|
||||
input.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "send");
|
||||
input.getActionMap().put("send", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
JButton send = new JButton("Enviar");
|
||||
send.setFocusPainted(false);
|
||||
send.addActionListener(e -> sendMessage());
|
||||
|
||||
JScrollPane inputScroll = new JScrollPane(input);
|
||||
inputScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
|
||||
inputScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
ShellUi.borderlessScroll(inputScroll);
|
||||
|
||||
JPanel compose = new JPanel(new BorderLayout(0, 4));
|
||||
compose.setPreferredSize(new Dimension(0, 72));
|
||||
compose.add(inputScroll, BorderLayout.CENTER);
|
||||
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
|
||||
actions.add(send);
|
||||
compose.add(actions, BorderLayout.SOUTH);
|
||||
|
||||
split.setTopComponent(new JScrollPane(transcript));
|
||||
split.setBottomComponent(compose);
|
||||
split.addHierarchyListener(e -> {
|
||||
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
|
||||
&& split.isShowing()
|
||||
&& split.getHeight() > 0) {
|
||||
split.setDividerLocation(0.84);
|
||||
}
|
||||
});
|
||||
|
||||
draftPanel = new PeerDraftPanel(
|
||||
"Nova conversa por IP",
|
||||
"Informe o endereço IP do peer. A aba será atualizada com o nome quando a conversa abrir.",
|
||||
this::commitDraftIp
|
||||
);
|
||||
|
||||
bodyCards.add(split, CARD_MAIN);
|
||||
bodyCards.add(draftPanel, CARD_DRAFT);
|
||||
workspace.getBodyHost().add(bodyCards, BorderLayout.CENTER);
|
||||
|
||||
add(workspace, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private void showBodyCard(String card) {
|
||||
bodyLayout.show(bodyCards, card);
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
if (input.getText().trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
syncRoutingFromActiveSessionTab();
|
||||
String text = input.getText();
|
||||
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
|
||||
try {
|
||||
if (routing.privateTcpOn() && !routing.everyoneInRoom()) {
|
||||
String peerIp = canonicalPeerIp(routing.peerIp());
|
||||
if (LocalNetworkInfo.isLocalIpv4(peerIp)) {
|
||||
appendSystemLine(
|
||||
activeConversationKey,
|
||||
"Canal TCP privado com você mesmo não está disponível; use sala geral ou UDP unicast."
|
||||
);
|
||||
return;
|
||||
}
|
||||
User user = resolveUserForSingle();
|
||||
if (user == null) {
|
||||
appendSystemLine(activeConversationKey, "Informe o IP do peer para o canal TCP privado.");
|
||||
return;
|
||||
}
|
||||
if (privateChatController != null && privateChatController.sendText(user, text)) {
|
||||
input.setText("");
|
||||
return;
|
||||
}
|
||||
if (privateChatController != null) {
|
||||
privateChatController.openChat(user);
|
||||
}
|
||||
appendSystemLine(privateKey(user.getAddress().getHostAddress()),
|
||||
"Canal TCP privado conectando… envie de novo em instantes.");
|
||||
return;
|
||||
}
|
||||
|
||||
TransportMode mode = routing.transport();
|
||||
SendTarget target = resolveSendTarget(routing);
|
||||
if (target == null || mode == null) {
|
||||
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
|
||||
return;
|
||||
}
|
||||
String ip = LocalNetworkInfo.bestLocalIpv4();
|
||||
ChatMessage message = new ChatMessage(
|
||||
connectionManager.getUserName(),
|
||||
text,
|
||||
mode.getLabel(),
|
||||
ip
|
||||
);
|
||||
int sent = MessageDispatcher.send(connectionManager, message, target, mode);
|
||||
appendChatLine(activeConversationKey, message);
|
||||
if (!routing.everyoneInRoom()) {
|
||||
String peerIp = canonicalPeerIp(routing.peerIp());
|
||||
if (LocalNetworkInfo.isLocalIpv4(peerIp) && sent == 0) {
|
||||
if (mode == TransportMode.TCP) {
|
||||
appendSystemLine(
|
||||
activeConversationKey,
|
||||
"TCP para o próprio host não é suportado; use sala geral ou UDP unicast."
|
||||
);
|
||||
} else {
|
||||
appendSystemLine(
|
||||
activeConversationKey,
|
||||
"Mensagem registrada aqui; eco pela rede pode não voltar no mesmo PC."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
input.setText("");
|
||||
} catch (Exception ex) {
|
||||
appendSystemLine(activeConversationKey, "Erro ao enviar: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
|
||||
if (routing.everyoneInRoom()) {
|
||||
return SendTarget.broadcastAll();
|
||||
}
|
||||
String ip = canonicalPeerIp(routing.peerIp());
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return SendTarget.singlePeer(InetAddress.getByName(ip));
|
||||
}
|
||||
|
||||
private User resolveUserForSingle() {
|
||||
try {
|
||||
String ip = canonicalPeerIp(routingPanel.snapshot().peerIp());
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
InetAddress address = InetAddress.getByName(ip);
|
||||
String displayName = ip;
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (view.getInetAddress().isPresent()
|
||||
&& view.getInetAddress().get().equals(address)) {
|
||||
displayName = view.getDisplayName();
|
||||
return new User(displayName, address, System.currentTimeMillis(), view.getInstanceId());
|
||||
}
|
||||
}
|
||||
return new User(displayName, address, System.currentTimeMillis(), null);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void receiveChatMessage(ChatMessage message) {
|
||||
runOnEdt(() -> deliverIncomingMessage(message));
|
||||
}
|
||||
|
||||
public void receiveLegacyMessage(String tipo, String nome, String ip, String text) {
|
||||
runOnEdt(() -> {
|
||||
LegacyMpChatMessage legacy = new LegacyMpChatMessage(nome, ip, tipo, text);
|
||||
String bucket = ip != null && !ip.isBlank() ? privateKey(canonicalPeerIp(ip)) : ROOM_KEY;
|
||||
appendLegacyLine(bucket, legacy);
|
||||
});
|
||||
}
|
||||
|
||||
private static void runOnEdt(Runnable action) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
action.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(action);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendChatLine(String bucket, ChatMessage message) {
|
||||
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
String tag = message.getTransportLabel() != null ? message.getTransportLabel() : "—";
|
||||
String ip = message.getSenderIp() != null ? message.getSenderIp() : "—";
|
||||
String line = dt.format(new Date()) + " " + ChatTranscriptHtml.coloredTransportTag(tag)
|
||||
+ " <b>" + ChatTranscriptHtml.escape(message.getName()) + "</b> ("
|
||||
+ ChatTranscriptHtml.escape(ip) + "): " + ChatTranscriptHtml.escape(message.getText());
|
||||
prependLine(bucket, line);
|
||||
}
|
||||
|
||||
private void appendLegacyLine(String bucket, LegacyMpChatMessage message) {
|
||||
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
|
||||
String tag = message.getTipoMensagem() != null ? message.getTipoMensagem() : "Legado";
|
||||
String line = dt.format(new Date()) + " " + ChatTranscriptHtml.coloredTransportTag(tag)
|
||||
+ " <b>" + ChatTranscriptHtml.escape(message.getNome()) + "</b> ("
|
||||
+ ChatTranscriptHtml.escape(message.getIp()) + "): "
|
||||
+ ChatTranscriptHtml.escape(message.getMessage());
|
||||
prependLine(bucket, line);
|
||||
}
|
||||
|
||||
private void appendSystemLine(String bucket, String text) {
|
||||
prependLine(bucket, "<i>" + ChatTranscriptHtml.escape(text) + "</i>");
|
||||
}
|
||||
|
||||
private void prependLine(String bucket, String htmlLine) {
|
||||
StringBuilder buf = transcripts.computeIfAbsent(bucket, k -> new StringBuilder());
|
||||
buf.insert(0, htmlLine + "<br>");
|
||||
if (bucket.equals(activeConversationKey)) {
|
||||
scheduleFlushTranscriptToPane();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleFlushTranscriptToPane() {
|
||||
if (pendingTranscriptFlush != null) {
|
||||
return;
|
||||
}
|
||||
pendingTranscriptFlush = () -> {
|
||||
pendingTranscriptFlush = null;
|
||||
flushTranscriptToPane();
|
||||
};
|
||||
SwingUtilities.invokeLater(pendingTranscriptFlush);
|
||||
}
|
||||
|
||||
private void flushTranscriptToPane() {
|
||||
StringBuilder buf = transcripts.get(activeConversationKey);
|
||||
String inner = buf != null ? buf.toString() : "";
|
||||
String html = ChatTranscriptHtml.wrapDocument(inner);
|
||||
try {
|
||||
transcript.setText(html);
|
||||
} catch (RuntimeException ex) {
|
||||
resetTranscriptEditor();
|
||||
try {
|
||||
transcript.setText(html);
|
||||
} catch (RuntimeException ignored) {
|
||||
// evita quebrar a EDT se o HTML continuar inválido
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetTranscriptEditor() {
|
||||
transcript.setEditorKit(new HTMLEditorKit());
|
||||
transcript.setContentType("text/html");
|
||||
transcript.setEditable(false);
|
||||
}
|
||||
|
||||
private void switchActiveConversation(String newKey) {
|
||||
if (newKey == null) {
|
||||
newKey = ROOM_KEY;
|
||||
}
|
||||
if (newKey.equals(activeConversationKey)) {
|
||||
return;
|
||||
}
|
||||
activeConversationKey = newKey;
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
|
||||
private String resolveConversationKey() {
|
||||
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
|
||||
if (routing.everyoneInRoom()) {
|
||||
return ROOM_KEY;
|
||||
}
|
||||
String ip = canonicalPeerIp(routing.peerIp());
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return ROOM_KEY;
|
||||
}
|
||||
if (routing.privateTcpOn()) {
|
||||
return privateKey(ip);
|
||||
}
|
||||
return peerKey(ip);
|
||||
}
|
||||
|
||||
private static String peerKey(String ip) {
|
||||
return "peer:" + ip;
|
||||
}
|
||||
|
||||
private static String privateKey(String ip) {
|
||||
return "private:" + ip;
|
||||
}
|
||||
|
||||
private String bucketForIncoming(ChatMessage message) {
|
||||
String tag = message.getTransportLabel();
|
||||
if (tag != null && tag.toLowerCase().contains("multicast")) {
|
||||
return ROOM_KEY;
|
||||
}
|
||||
String ip = message.getSenderIp();
|
||||
if (ip == null || ip.isBlank()) {
|
||||
return ROOM_KEY;
|
||||
}
|
||||
return peerKey(canonicalPeerIp(ip));
|
||||
}
|
||||
|
||||
private boolean isEchoFromLocalUser(ChatMessage message) {
|
||||
if (message == null) {
|
||||
return false;
|
||||
}
|
||||
return LocalUserIdentity.isLocalSender(
|
||||
connectionManager,
|
||||
message.getName(),
|
||||
message.getSenderIp()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.application.messaging.TransportMode;
|
||||
|
||||
/**
|
||||
* HTML do histórico com cores por transporte.
|
||||
*/
|
||||
public final class ChatTranscriptHtml {
|
||||
|
||||
private ChatTranscriptHtml() {
|
||||
}
|
||||
|
||||
public static String coloredTransportTag(String transportLabel) {
|
||||
if (transportLabel == null || transportLabel.isBlank()) {
|
||||
return "<span style='color:#616161'>[—]</span>";
|
||||
}
|
||||
String normalized = transportLabel.trim().toLowerCase();
|
||||
String color;
|
||||
if (normalized.contains("privado")) {
|
||||
color = "#6a1b9a";
|
||||
} else if (normalized.contains("multicast")) {
|
||||
color = "#2e7d32";
|
||||
} else if (normalized.contains("udp")) {
|
||||
color = "#1565c0";
|
||||
} else if (normalized.contains("tcp")) {
|
||||
color = "#e65100";
|
||||
} else {
|
||||
color = "#616161";
|
||||
}
|
||||
return "<span style='color:" + color + ";font-weight:bold'>[" + escape(transportLabel) + "]</span>";
|
||||
}
|
||||
|
||||
public static String tagForMode(TransportMode mode) {
|
||||
return coloredTransportTag(mode != null ? mode.getLabel() : "—");
|
||||
}
|
||||
|
||||
public static String escape(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
public static String wrapDocument(String innerHtml) {
|
||||
if (innerHtml == null || innerHtml.isBlank()) {
|
||||
return "<html><body style='margin:0'></body></html>";
|
||||
}
|
||||
return "<html><body style='margin:0'>" + innerHtml + "</body></html>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import pitiupi.chat.net.ChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ChatWindow extends JDialog {
|
||||
|
||||
private JButton jButton1;
|
||||
private JEditorPane jEditorPane1;
|
||||
private JPanel jPanel1;
|
||||
private JPanel jPanel2;
|
||||
private JScrollPane jScrollPane1;
|
||||
private JScrollPane jScrollPane2;
|
||||
private JSplitPane jSplitPane1;
|
||||
private JTextArea jTextArea1;
|
||||
|
||||
private final ConnectionManager connectionManager;
|
||||
|
||||
public ChatWindow(ConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
initComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from within the constructor to initialize the form.
|
||||
* WARNING: Do NOT modify this code. The content of this method is always
|
||||
* regenerated by the Form Editor.
|
||||
*/
|
||||
private void initComponents() {
|
||||
|
||||
this.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
|
||||
this.setTitle("Chat Window");
|
||||
this.setBounds(new java.awt.Rectangle(0, 0, 800, 600));
|
||||
this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5));
|
||||
this.setPreferredSize(new java.awt.Dimension(800, 600));
|
||||
|
||||
jSplitPane1 = new JSplitPane();
|
||||
jScrollPane1 = new JScrollPane();
|
||||
jEditorPane1 = new JEditorPane();
|
||||
jPanel1 = new JPanel();
|
||||
jScrollPane2 = new JScrollPane();
|
||||
jTextArea1 = new JTextArea();
|
||||
jPanel2 = new JPanel();
|
||||
jButton1 = new JButton();
|
||||
|
||||
jSplitPane1.setDividerLocation(500);
|
||||
jSplitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT);
|
||||
jEditorPane1.setContentType("text/html");
|
||||
jPanel1.setLayout(new java.awt.BorderLayout());
|
||||
jTextArea1.setColumns(20);
|
||||
jTextArea1.setRows(5);
|
||||
jTextArea1.addKeyListener(new java.awt.event.KeyAdapter() {
|
||||
@Override
|
||||
public void keyTyped(java.awt.event.KeyEvent evt) {
|
||||
if (evt.getKeyChar() == '\n') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
});
|
||||
jButton1.setText("Send");
|
||||
|
||||
jScrollPane1.setViewportView(jEditorPane1);
|
||||
jScrollPane2.setViewportView(jTextArea1);
|
||||
jSplitPane1.setTopComponent(jScrollPane1);
|
||||
|
||||
jPanel1.add(jScrollPane2, java.awt.BorderLayout.CENTER);
|
||||
jPanel2.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT));
|
||||
jButton1.addActionListener((java.awt.event.ActionEvent evt) -> {
|
||||
sendMessage();
|
||||
});
|
||||
jPanel2.add(jButton1);
|
||||
jPanel1.add(jPanel2, java.awt.BorderLayout.PAGE_END);
|
||||
jSplitPane1.setRightComponent(jPanel1);
|
||||
getContentPane().add(jSplitPane1, java.awt.BorderLayout.CENTER);
|
||||
pack();
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
if (jTextArea1.getText().trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ChatMessage message = new ChatMessage(connectionManager.getUserName(), jTextArea1.getText());
|
||||
this.sendChatMessage(message);
|
||||
jTextArea1.setText("");
|
||||
}
|
||||
|
||||
public void sendChatMessage(Message message) {
|
||||
connectionManager.sendUdpMulticast(message);
|
||||
}
|
||||
|
||||
public void receiveChatMessage(ChatMessage message) {
|
||||
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyyy hh:mm:ss");
|
||||
Date date = new Date();
|
||||
this.receiveChatMessage(dt.format(date) + " "
|
||||
+ "<b>" + message.getName() + "</b>: "
|
||||
+ message.getText() + "<br>");
|
||||
}
|
||||
|
||||
public void receiveChatMessage(String message) {
|
||||
jEditorPane1.setText(message + jEditorPane1.getText());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.net.InetAddress;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class PrivateChatWindow extends JFrame {
|
||||
|
||||
private final ConnectionManager connectionManager;
|
||||
private final TcpConnection connection;
|
||||
private final JTextArea receivedMessagesArea;
|
||||
private final JTextField messageField;
|
||||
|
||||
public PrivateChatWindow(ConnectionManager connectionManager, TcpConnection connection) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.connection = connection;
|
||||
|
||||
setTitle("Chat privado (TCP)");
|
||||
setSize(700, 500);
|
||||
setLocationRelativeTo(null);
|
||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
receivedMessagesArea = new JTextArea();
|
||||
receivedMessagesArea.setEditable(false);
|
||||
receivedMessagesArea.setLineWrap(true);
|
||||
receivedMessagesArea.setWrapStyleWord(true);
|
||||
|
||||
JScrollPane messagesScrollPane = new JScrollPane(receivedMessagesArea);
|
||||
messagesScrollPane.setBorder(javax.swing.BorderFactory.createTitledBorder("Mensagens"));
|
||||
mainPanel.add(messagesScrollPane, BorderLayout.CENTER);
|
||||
|
||||
messageField = new JTextField();
|
||||
JButton tcpButton = new JButton("Enviar TCP");
|
||||
tcpButton.addActionListener(e -> sendTCP());
|
||||
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(10, 10));
|
||||
bottomPanel.add(messageField, BorderLayout.CENTER);
|
||||
bottomPanel.add(tcpButton, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
}
|
||||
|
||||
public void sendChatLine(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
return;
|
||||
}
|
||||
messageField.setText(text);
|
||||
sendTCP();
|
||||
}
|
||||
|
||||
private void sendTCP() {
|
||||
try {
|
||||
LegacyMpChatMessage message = new LegacyMpChatMessage(
|
||||
connectionManager.getUserName(),
|
||||
pitiupi.presentation.shell.LocalNetworkInfo.bestLocalIpv4(),
|
||||
"TCP privado",
|
||||
messageField.getText()
|
||||
);
|
||||
connection.send(message.toByteArray());
|
||||
receiveMessage(message);
|
||||
messageField.setText("");
|
||||
} catch (Exception ex) {
|
||||
Logger.getLogger(PrivateChatWindow.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void receiveMessage(LegacyMpChatMessage message) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String texto = String.format(
|
||||
"[%s] %s (%s): %s%n",
|
||||
message.getTipoMensagem(),
|
||||
message.getNome(),
|
||||
message.getIp(),
|
||||
message.getMessage()
|
||||
);
|
||||
receivedMessagesArea.append(texto);
|
||||
receivedMessagesArea.setCaretPosition(receivedMessagesArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package pitiupi.chat.control;
|
||||
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
import pitiupi.presentation.shell.LocalNetworkInfo;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class PrivateChatController {
|
||||
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PrivateChatHost host;
|
||||
private final Map<String, TcpConnection> connections = new HashMap<>();
|
||||
private final Set<String> pendingConnections = new HashSet<>();
|
||||
private PrivateChatUiSink uiSink;
|
||||
|
||||
public PrivateChatController(ConnectionManager connectionManager, PrivateChatHost host) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public void setUiSink(PrivateChatUiSink uiSink) {
|
||||
this.uiSink = uiSink;
|
||||
}
|
||||
|
||||
private static String addressKey(TcpConnection connection) {
|
||||
return connection.getAddress().getHostAddress();
|
||||
}
|
||||
|
||||
public void openChat(User user) {
|
||||
if (user == null || user.getAddress() == null) {
|
||||
return;
|
||||
}
|
||||
String address = user.getAddress().getHostAddress();
|
||||
if (connections.containsKey(address)) {
|
||||
if (uiSink != null) {
|
||||
uiSink.onPrivateChannelReady(address);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pendingConnections.contains(address)) {
|
||||
return;
|
||||
}
|
||||
pendingConnections.add(address);
|
||||
host.openConnection(address);
|
||||
}
|
||||
|
||||
public void receiveMessage(TcpConnection connection, LegacyMpChatMessage message) {
|
||||
if (uiSink != null) {
|
||||
uiSink.appendPrivateLine(addressKey(connection), message, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void connectionReceived(TcpConnection connection) {
|
||||
String address = addressKey(connection);
|
||||
pendingConnections.remove(address);
|
||||
connections.put(address, connection);
|
||||
if (uiSink != null) {
|
||||
uiSink.onPrivateChannelReady(address);
|
||||
}
|
||||
}
|
||||
|
||||
public void closeChat(TcpConnection connection) {
|
||||
String address = addressKey(connection);
|
||||
connections.remove(address);
|
||||
if (uiSink != null) {
|
||||
uiSink.onPrivateChannelClosed(address);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendText(User user, String text) {
|
||||
if (user == null || user.getAddress() == null || text == null || text.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String address = user.getAddress().getHostAddress();
|
||||
TcpConnection connection = connections.get(address);
|
||||
if (connection == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
LegacyMpChatMessage message = new LegacyMpChatMessage(
|
||||
connectionManager.getUserName(),
|
||||
LocalNetworkInfo.bestLocalIpv4(),
|
||||
"TCP privado",
|
||||
text
|
||||
);
|
||||
connection.send(message.toByteArray());
|
||||
if (uiSink != null) {
|
||||
uiSink.appendPrivateLine(address, message, true);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
System.out.println("Error sending private TCP: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasOpenChannel(User user) {
|
||||
if (user == null || user.getAddress() == null) {
|
||||
return false;
|
||||
}
|
||||
return connections.containsKey(user.getAddress().getHostAddress());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package pitiupi.chat.control;
|
||||
|
||||
public interface PrivateChatHost {
|
||||
String getPluginIdentifier();
|
||||
|
||||
void openConnection(String ip);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package pitiupi.chat.control;
|
||||
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
|
||||
/**
|
||||
* TCP privado integrado ao painel principal (sem janela extra).
|
||||
*/
|
||||
public interface PrivateChatUiSink {
|
||||
|
||||
void onPrivateChannelReady(String peerIp);
|
||||
|
||||
void onPrivateChannelClosed(String peerIp);
|
||||
|
||||
void appendPrivateLine(String peerIp, LegacyMpChatMessage message, boolean outgoing);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package pitiupi.chat.net;
|
||||
|
||||
import pitiupi.application.messaging.TransportMode;
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
public class ChatMessage extends Message {
|
||||
|
||||
private String text;
|
||||
private String name;
|
||||
private String transportLabel;
|
||||
private String senderIp;
|
||||
|
||||
public ChatMessage() {
|
||||
}
|
||||
|
||||
public ChatMessage(String name, String text) {
|
||||
this(name, text, TransportMode.MULTICAST_UDP.getLabel(), null);
|
||||
}
|
||||
|
||||
public ChatMessage(String name, String text, String transportLabel, String senderIp) {
|
||||
this.name = name;
|
||||
this.text = text;
|
||||
this.transportLabel = transportLabel;
|
||||
this.senderIp = senderIp;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public String getTransportLabel() {
|
||||
return transportLabel;
|
||||
}
|
||||
|
||||
public void setTransportLabel(String transportLabel) {
|
||||
this.transportLabel = transportLabel;
|
||||
}
|
||||
|
||||
public String getSenderIp() {
|
||||
return senderIp;
|
||||
}
|
||||
|
||||
public void setSenderIp(String senderIp) {
|
||||
this.senderIp = senderIp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package pitiupi.chat.net;
|
||||
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
/**
|
||||
* Formato legado do antigo plugin mpChat (TCP privado e salas antigas).
|
||||
*/
|
||||
public class LegacyMpChatMessage extends Message {
|
||||
private String nome;
|
||||
private String ip;
|
||||
private String tipoMensagem;
|
||||
private String message;
|
||||
|
||||
public LegacyMpChatMessage() {
|
||||
}
|
||||
|
||||
public LegacyMpChatMessage(String nome, String ip, String tipoMensagem, String message) {
|
||||
this.nome = nome;
|
||||
this.ip = ip;
|
||||
this.tipoMensagem = tipoMensagem;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getNome() {
|
||||
return nome;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public String getTipoMensagem() {
|
||||
return tipoMensagem;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package pitiupi.chat.plugin;
|
||||
|
||||
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.application.activity.PeerActivityInbox;
|
||||
import pitiupi.application.peeractions.PeerAction;
|
||||
import pitiupi.application.peeractions.PeerActionTarget;
|
||||
import pitiupi.application.peeractions.PeerTarget;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.chat.GUI.ChatPanel;
|
||||
import pitiupi.chat.control.PrivateChatController;
|
||||
import pitiupi.chat.control.PrivateChatHost;
|
||||
import pitiupi.chat.net.ChatMessage;
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.presentation.shell.LocalUserIdentity;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatPlugin extends Plugin implements PrivateChatHost {
|
||||
|
||||
private ChatPanel chatPanel;
|
||||
private PluginWorkspace workspace;
|
||||
private ConnectionManager connectionManager;
|
||||
private PrivateChatController privateChatController;
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.workspace = workspace;
|
||||
this.privateChatController = new PrivateChatController(connectionManager, this);
|
||||
this.chatPanel = new ChatPanel(connectionManager);
|
||||
chatPanel.setPrivateChatController(privateChatController);
|
||||
privateChatController.setUiSink(chatPanel);
|
||||
bindShellWorkspaceContent(chatPanel);
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
registrar.register(PeerAction.of(
|
||||
"chat.focus",
|
||||
"Abrir sala geral",
|
||||
PeerActionTarget.ALL_PEERS,
|
||||
target -> {
|
||||
chatPanel.focusGeneralRoom();
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
}
|
||||
));
|
||||
registrar.register(PeerAction.of(
|
||||
"chat.peer",
|
||||
"Conversar com este peer",
|
||||
PeerActionTarget.SINGLE_PEER,
|
||||
this::openChatForPeer
|
||||
));
|
||||
registrar.register(PeerAction.of(
|
||||
"chat.private.tcp",
|
||||
"Ativar TCP privado",
|
||||
PeerActionTarget.SINGLE_PEER,
|
||||
target -> {
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
if (target instanceof PeerTarget.Single single) {
|
||||
chatPanel.enablePrivateTcpForPeer(single.peer());
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private void openChatForPeer(PeerTarget target) {
|
||||
if (!(target instanceof PeerTarget.Single single)) {
|
||||
return;
|
||||
}
|
||||
chatPanel.focusPeer(single.peer());
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
}
|
||||
|
||||
private static User toUser(UserPeerView peer) {
|
||||
if (peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
return null;
|
||||
}
|
||||
return peer.getInetAddress()
|
||||
.map(addr -> new User(peer.getDisplayName(), addr, System.currentTimeMillis(), peer.getInstanceId()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Message>> getMessages() {
|
||||
return List.of(ChatMessage.class, LegacyMpChatMessage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveMessage(Message message) {
|
||||
if (message instanceof ChatMessage msg && chatPanel != null) {
|
||||
recordChatActivity(msg);
|
||||
SwingUtilities.invokeLater(() -> chatPanel.receiveChatMessage(msg));
|
||||
} else if (message instanceof LegacyMpChatMessage legacy && chatPanel != null) {
|
||||
SwingUtilities.invokeLater(() -> chatPanel.receiveLegacyMessage(
|
||||
legacy.getTipoMensagem(),
|
||||
legacy.getNome(),
|
||||
legacy.getIp(),
|
||||
legacy.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void recordChatActivity(ChatMessage msg) {
|
||||
if (LocalUserIdentity.isLocalSender(connectionManager, msg.getName(), msg.getSenderIp())) {
|
||||
return;
|
||||
}
|
||||
PeerActivityInbox inbox = connectionManager.getPeerActivityInbox();
|
||||
String tag = msg.getTransportLabel();
|
||||
boolean multicast = tag != null && tag.toLowerCase().contains("multicast");
|
||||
if (multicast) {
|
||||
inbox.recordBroadcast(getIdentifier(), "chat.focus");
|
||||
return;
|
||||
}
|
||||
String instanceId = peerInstanceIdForActivity(msg.getSenderIp());
|
||||
if (instanceId != null) {
|
||||
inbox.record(instanceId, getIdentifier(), "chat.peer");
|
||||
}
|
||||
}
|
||||
|
||||
private String peerInstanceIdForActivity(String ip) {
|
||||
if (ip == null || ip.isBlank() || connectionManager == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
InetAddress address = InetAddress.getByName(ip);
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (view.isSelf()) {
|
||||
continue;
|
||||
}
|
||||
if (view.getInetAddress().isPresent() && view.getInetAddress().get().equals(address)) {
|
||||
return view.getInstanceId();
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receivePrivateConnection(TcpConnection connection) {
|
||||
SwingUtilities.invokeLater(() -> privateChatController.connectionReceived(connection));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receiveBytes(TcpConnection connection, byte[] bytes) {
|
||||
LegacyMpChatMessage legacy = parseLegacy(bytes);
|
||||
if (legacy == null) {
|
||||
return true;
|
||||
}
|
||||
privateChatController.receiveMessage(connection, legacy);
|
||||
String instanceId = peerInstanceIdForActivity(connection.getAddress().getHostAddress());
|
||||
if (instanceId != null) {
|
||||
connectionManager.getPeerActivityInbox().record(instanceId, getIdentifier(), "chat.private.tcp");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSocketClosed(TcpConnection connection) {
|
||||
privateChatController.closeChat(connection);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openConnection(String ip) {
|
||||
try {
|
||||
connectionManager.connectPluginTcp(
|
||||
InetAddress.getByName(ip),
|
||||
getIdentifier(),
|
||||
getIdentifier()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error opening connection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginIdentifier() {
|
||||
return getIdentifier();
|
||||
}
|
||||
|
||||
private LegacyMpChatMessage parseLegacy(byte[] bytes) {
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
|
||||
if (ois.readObject() instanceof LegacyMpChatMessage msg) {
|
||||
return msg;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException ex) {
|
||||
System.out.println(ex.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Conversa na rede";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "Flávio Schiavoni";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "Pitiupi.Chat.Example";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/dist/
|
||||
/build/
|
||||
/out/
|
||||
/nbproject/private/
|
||||
*.jar
|
||||
@@ -0,0 +1,22 @@
|
||||
# Plugin Ping (exercício de aula)
|
||||
|
||||
Material didático para o aluno **implementar** um plugin de Ping/Pong com medição de RTT. Este diretório no repositório oficial contém apenas planejamento e referências — **não** inclua código Java ou JAR da solução no commit.
|
||||
|
||||
## Onde está o tutorial
|
||||
|
||||
Tutorial canônico (passo a passo): [docs/documentation/17_tutorial_ping_aula.md](../../docs/documentation/17_tutorial_ping_aula.md).
|
||||
|
||||
## Como o aluno trabalha
|
||||
|
||||
1. Copiar a estrutura de [`plugins/chat`](../chat) para um projeto local (ou criar `plugins/ping` com `src/` apenas no fork pessoal).
|
||||
2. Implementar `PingPlugin`, mensagens e `PingPanel` (`JPanel` + `bindShellWorkspaceContent`).
|
||||
3. Build: `ant jar` → `dist/pitiupi.ping.jar`.
|
||||
4. Instalar em `PITIUPI_HOME/plugins/` e reiniciar o Pitiupi.
|
||||
5. Abrir o plugin pela aba **Plugins** na galeria (aba não abre sozinha no boot).
|
||||
|
||||
Quando o JAR estiver pronto para o time, registre uma entrada em [`plugins.registry.json`](../../plugins.registry.json) na raiz do clone (id `ping`, `dir`: `plugins/ping`, etc.).
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Núcleo compilado: `../../pitiupi/dist/pitiupi.jar`
|
||||
- Leitura: [09_sistema_de_plugins.md](../../docs/documentation/09_sistema_de_plugins.md)
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AntConfiguration">
|
||||
<buildFile url="file://$PROJECT_DIR$/build.xml" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+3
@@ -0,0 +1,3 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+115
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/../pitiupi.testSocket/Makefile" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 7
|
||||
}</component>
|
||||
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feature/separacao-gerenciadores",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Gustavo/OneDrive/Documentos/Faculdade/2026_2/Monitoria/pitiupi/pitiupi.screenCast",
|
||||
"project.structure.last.edited": "Modules",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "preferences.keymap"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager">
|
||||
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="Tox" factoryName="Tox">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="JetRunConfigurationType">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="filePath" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-30f59d01ecdd-37e91769500f-intellij.indexing.shared.core-IU-261.26222.65" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-IU-261.26222.65" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="" />
|
||||
<created>1784745206049</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1784745206049</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- You may freely edit this file. See commented blocks below for -->
|
||||
<!-- some examples of how to customize the build. -->
|
||||
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||
<!-- the Compile on Save feature is turned off for the project. -->
|
||||
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||
<!-- in the project's Project Properties dialog box.-->
|
||||
<project name="pitiupi.screenCast" default="default" basedir=".">
|
||||
<description>Builds, tests, and runs the project pitiupi.screenCast.</description>
|
||||
<import file="nbproject/build-impl.xml"/>
|
||||
<!--
|
||||
|
||||
There exist several targets which are by default empty and which can be
|
||||
used for execution of your tasks. These targets are usually executed
|
||||
before and after some main targets. They are:
|
||||
|
||||
-pre-init: called before initialization of project properties
|
||||
-post-init: called after initialization of project properties
|
||||
-pre-compile: called before javac compilation
|
||||
-post-compile: called after javac compilation
|
||||
-pre-compile-single: called before javac compilation of single file
|
||||
-post-compile-single: called after javac compilation of single file
|
||||
-pre-compile-test: called before javac compilation of JUnit tests
|
||||
-post-compile-test: called after javac compilation of JUnit tests
|
||||
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||
-pre-jar: called before JAR building
|
||||
-post-jar: called after JAR building
|
||||
-post-clean: called after cleaning build products
|
||||
|
||||
(Targets beginning with '-' are not intended to be called on their own.)
|
||||
|
||||
Example of inserting an obfuscator after compilation could look like this:
|
||||
|
||||
<target name="-post-compile">
|
||||
<obfuscate>
|
||||
<fileset dir="${build.classes.dir}"/>
|
||||
</obfuscate>
|
||||
</target>
|
||||
|
||||
For list of available properties check the imported
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
|
||||
Another way to customize the build is by overriding existing main targets.
|
||||
The targets of interest are:
|
||||
|
||||
-init-macrodef-javac: defines macro for javac compilation
|
||||
-init-macrodef-junit: defines macro for junit execution
|
||||
-init-macrodef-debug: defines macro for class debugging
|
||||
-init-macrodef-java: defines macro for class execution
|
||||
-do-jar: JAR building
|
||||
run: execution of project
|
||||
-javadoc-build: Javadoc generation
|
||||
test-report: JUnit report generation
|
||||
|
||||
An example of overriding the target for project execution could look like this:
|
||||
|
||||
<target name="run" depends="pitiupi.screenCast-impl.jar">
|
||||
<exec dir="bin" executable="launcher.exe">
|
||||
<arg file="${dist.jar}"/>
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
Notice that the overridden target depends on the jar target and not only on
|
||||
the compile target as the regular run target does. Again, for a list of available
|
||||
properties which you can use, check the target you are overriding in the
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
-->
|
||||
</project>
|
||||
@@ -0,0 +1,3 @@
|
||||
Manifest-Version: 1.0
|
||||
X-COMMENT: Main-Class will be added automatically by build
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
build.xml.data.CRC32=96758119
|
||||
build.xml.script.CRC32=70726a23
|
||||
build.xml.stylesheet.CRC32=f85dc8f2@1.119.0.48
|
||||
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||
nbproject/build-impl.xml.data.CRC32=96758119
|
||||
nbproject/build-impl.xml.script.CRC32=0f906282
|
||||
nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.119.0.48
|
||||
@@ -0,0 +1,98 @@
|
||||
annotation.processing.enabled=true
|
||||
annotation.processing.enabled.in.editor=false
|
||||
annotation.processing.processor.options=
|
||||
annotation.processing.processors.list=
|
||||
annotation.processing.run.all.processors=true
|
||||
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||
build.classes.dir=${build.dir}/classes
|
||||
build.classes.excludes=**/*.java,**/*.form
|
||||
# This directory is removed when the project is cleaned:
|
||||
build.dir=build
|
||||
build.generated.dir=${build.dir}/generated
|
||||
build.generated.sources.dir=${build.dir}/generated-sources
|
||||
# Only compile against the classpath explicitly listed here:
|
||||
build.sysclasspath=ignore
|
||||
build.test.classes.dir=${build.dir}/test/classes
|
||||
build.test.results.dir=${build.dir}/test/results
|
||||
# Uncomment to specify the preferred debugger connection transport:
|
||||
#debug.transport=dt_socket
|
||||
debug.classpath=\
|
||||
${run.classpath}
|
||||
debug.modulepath=\
|
||||
${run.modulepath}
|
||||
debug.test.classpath=\
|
||||
${run.test.classpath}
|
||||
debug.test.modulepath=\
|
||||
${run.test.modulepath}
|
||||
# Files in build.classes.dir which should be excluded from distribution jar
|
||||
dist.archive.excludes=
|
||||
# This directory is removed when the project is cleaned:
|
||||
dist.dir=dist
|
||||
dist.jar=${dist.dir}/pitiupi.screenCast.jar
|
||||
dist.javadoc.dir=${dist.dir}/javadoc
|
||||
dist.jlink.dir=${dist.dir}/jlink
|
||||
dist.jlink.output=${dist.jlink.dir}/pitiupi.screenCast
|
||||
excludes=
|
||||
includes=**
|
||||
jar.compress=false
|
||||
javac.classpath=\
|
||||
${reference.pitiupi.jar}
|
||||
# Space-separated list of extra javac options
|
||||
javac.compilerargs=
|
||||
javac.deprecation=false
|
||||
javac.external.vm=true
|
||||
javac.modulepath=
|
||||
javac.processormodulepath=
|
||||
javac.processorpath=\
|
||||
${javac.classpath}
|
||||
javac.source=25
|
||||
javac.target=25
|
||||
javac.test.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
javac.test.modulepath=\
|
||||
${javac.modulepath}
|
||||
javac.test.processorpath=\
|
||||
${javac.test.classpath}
|
||||
javadoc.additionalparam=
|
||||
javadoc.author=false
|
||||
javadoc.encoding=${source.encoding}
|
||||
javadoc.html5=false
|
||||
javadoc.noindex=false
|
||||
javadoc.nonavbar=false
|
||||
javadoc.notree=false
|
||||
javadoc.private=false
|
||||
javadoc.splitindex=true
|
||||
javadoc.use=true
|
||||
javadoc.version=false
|
||||
javadoc.windowtitle=
|
||||
# The jlink additional root modules to resolve
|
||||
jlink.additionalmodules=
|
||||
# The jlink additional command line parameters
|
||||
jlink.additionalparam=
|
||||
jlink.launcher=true
|
||||
jlink.launcher.name=pitiupi.screenCast
|
||||
main.class=
|
||||
manifest.file=manifest.mf
|
||||
meta.inf.dir=${src.dir}/META-INF
|
||||
mkdist.disabled=false
|
||||
platform.active=default_platform
|
||||
project.pitiupi=../../pitiupi
|
||||
reference.pitiupi.jar=${project.pitiupi}/dist/pitiupi.jar
|
||||
run.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
# Space-separated list of JVM arguments used when running the project.
|
||||
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
|
||||
# To set system properties for unit tests define test-sys-prop.name=value:
|
||||
run.jvmargs=
|
||||
run.modulepath=\
|
||||
${javac.modulepath}
|
||||
run.test.classpath=\
|
||||
${javac.test.classpath}:\
|
||||
${build.test.classes.dir}
|
||||
run.test.modulepath=\
|
||||
${javac.test.modulepath}
|
||||
source.encoding=UTF-8
|
||||
src.dir=src
|
||||
test.src.dir=test
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||
<type>org.netbeans.modules.java.j2seproject</type>
|
||||
<configuration>
|
||||
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||
<name>pitiupi.screenCast</name>
|
||||
<source-roots>
|
||||
<root id="src.dir"/>
|
||||
</source-roots>
|
||||
<test-roots>
|
||||
<root id="test.src.dir"/>
|
||||
</test-roots>
|
||||
</data>
|
||||
<references xmlns="http://www.netbeans.org/ns/ant-project-references/1">
|
||||
<reference>
|
||||
<foreign-project>pitiupi</foreign-project>
|
||||
<artifact-type>jar</artifact-type>
|
||||
<script>build.xml</script>
|
||||
<target>jar</target>
|
||||
<clean-target>clean</clean-target>
|
||||
<id>jar</id>
|
||||
</reference>
|
||||
</references>
|
||||
</configuration>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
pitiupi.screenCast.plugin.ScreenCastPlugin
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="module-library">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
|
||||
*/
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenCastMenu extends MulticastSubMenu {
|
||||
|
||||
private final ScreenCastShowMenu showMenu;
|
||||
|
||||
public ScreenCastMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.showMenu = new ScreenCastShowMenu(screenCastPanel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Screen Cast";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MulticastMenu> getMenus() {
|
||||
return List.of(showMenu);
|
||||
}
|
||||
|
||||
private static final class ScreenCastShowMenu extends MulticastActionMenu {
|
||||
private final ScreenCastPanel screenCastPanel;
|
||||
|
||||
ScreenCastShowMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.screenCastPanel = screenCastPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "ScreenCast Window";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
screenCastPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import pitiupi.presentation.shell.PluginSessionKeys;
|
||||
|
||||
/**
|
||||
* Abas em que o usuário pode ligar envio de tela (outbound).
|
||||
*/
|
||||
final class ScreenCastOutboundTabs {
|
||||
|
||||
private ScreenCastOutboundTabs() {
|
||||
}
|
||||
|
||||
static boolean isOutboundShareTabKey(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
return false;
|
||||
}
|
||||
if (key.startsWith("view:")) {
|
||||
return false;
|
||||
}
|
||||
return PluginSessionKeys.ROOM.equals(key) || key.startsWith("peer:");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import pitiupi.application.messaging.MessageDispatcher;
|
||||
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||
import pitiupi.application.messaging.SendTarget;
|
||||
import pitiupi.application.messaging.TransportMode;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.presentation.shell.PeerDraftPanel;
|
||||
import pitiupi.presentation.shell.PluginRoutingPanel;
|
||||
import pitiupi.presentation.shell.PluginSessionKeys;
|
||||
import pitiupi.presentation.shell.PluginSessionWorkspace;
|
||||
import pitiupi.presentation.shell.SessionPeerLabels;
|
||||
import pitiupi.presentation.shell.SessionTabBar;
|
||||
import pitiupi.presentation.shell.ui.ShellUi;
|
||||
|
||||
import pitiupi.screenCast.control.AlarmClock;
|
||||
import pitiupi.screenCast.control.ImageTransformation;
|
||||
import pitiupi.screenCast.control.ScreenCaptureDiagnostics;
|
||||
import pitiupi.screenCast.control.ScreenShot;
|
||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.event.HierarchyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
/**
|
||||
* Painel de compartilhamento de tela.
|
||||
* Backlog UX: prévia dividida (recebendo + enviando) na mesma aba peer; multi-share simultâneo.
|
||||
*/
|
||||
public class ScreenCastPanel extends JPanel {
|
||||
|
||||
public static final int TIME = 1000;
|
||||
private static final int MAX_WIDTH = 800;
|
||||
private static final int MAX_HEIGHT = 600;
|
||||
private static final String CARD_DRAFT = "__draft__";
|
||||
private static final String IDLE_ROOM_HTML =
|
||||
"<html><p>Ative <b>Compartilhamento da sua tela</b> para enviar para todos na rede (multicast).</p></html>";
|
||||
private static final String IDLE_PEER_HTML =
|
||||
"<html><p>Ative o compartilhamento para enviar só para este peer (unicast).</p></html>";
|
||||
|
||||
private AlarmClock alarmClock;
|
||||
private Timer timer;
|
||||
private float compressRatio;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PluginSessionWorkspace workspace;
|
||||
private final PluginRoutingPanel routingPanel;
|
||||
private final SessionTabBar sessionTabs;
|
||||
private final JToggleButton shareToggle = new JToggleButton("Compartilhamento da sua tela (desligado)");
|
||||
private final Map<String, Boolean> sharingByTabKey = new HashMap<>();
|
||||
private boolean suppressShareToggleEvents;
|
||||
private String shareContextTabKey;
|
||||
private String lastSessionKey = PluginSessionKeys.ROOM;
|
||||
private final Map<String, String> peerIpsByViewKey = new HashMap<>();
|
||||
private final Map<String, String> peerDisplayNames = new HashMap<>();
|
||||
private final Map<String, ScreenCastReceiveScope> receiveScopes = new HashMap<>();
|
||||
private final CardLayout bodyLayout = new CardLayout();
|
||||
private final JPanel bodyCards = new JPanel(bodyLayout);
|
||||
private final Map<String, JPanel> bodyPanels = new HashMap<>();
|
||||
private PeerDraftPanel draftPanel;
|
||||
|
||||
public ScreenCastPanel(ConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.compressRatio = 0.55f;
|
||||
this.routingPanel = new PluginRoutingPanel(
|
||||
PluginTransportCatalog.PluginKind.SCREEN_CAST,
|
||||
"<html><i>Receba pela lista lateral (notificação) ou use + Nova para um IP. "
|
||||
+ "A aba Todos envia em multicast; abas de peer usam unicast.</i></html>",
|
||||
false
|
||||
);
|
||||
this.workspace = new PluginSessionWorkspace(routingPanel, buildShareToolbar());
|
||||
this.sessionTabs = workspace.getSessionTabBar();
|
||||
sessionTabs.setCloseFallbackKey(PluginSessionKeys.ROOM);
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private JPanel buildShareToolbar() {
|
||||
shareToggle.setFocusPainted(false);
|
||||
shareToggle.addActionListener(e -> {
|
||||
if (suppressShareToggleEvents) {
|
||||
return;
|
||||
}
|
||||
String key = sessionTabs.selectedKey();
|
||||
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
|
||||
return;
|
||||
}
|
||||
sharingByTabKey.put(key, shareToggle.isSelected());
|
||||
updateShareToggleLabel();
|
||||
applySharingForActiveTab();
|
||||
});
|
||||
JPanel shareRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
|
||||
shareRow.add(shareToggle);
|
||||
return shareRow;
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout());
|
||||
draftPanel = new PeerDraftPanel(
|
||||
"Nova sessão por IP",
|
||||
"Informe o IP para enviar ou receber unicast. O nome na aba atualiza quando o peer for conhecido.",
|
||||
this::commitDraftIp
|
||||
);
|
||||
bodyCards.add(draftPanel, CARD_DRAFT);
|
||||
workspace.getBodyHost().add(bodyCards, BorderLayout.CENTER);
|
||||
add(workspace, BorderLayout.CENTER);
|
||||
timer = new Timer();
|
||||
addHierarchyListener(e -> {
|
||||
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing()) {
|
||||
onSessionTabSelected(sessionTabs.selectedKey());
|
||||
}
|
||||
});
|
||||
initSessionTabs();
|
||||
}
|
||||
|
||||
private void initSessionTabs() {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.ROOM,
|
||||
"Todos",
|
||||
"Transmissão para todos na rede (multicast)",
|
||||
new JPanel(),
|
||||
false
|
||||
);
|
||||
sessionTabs.setOnTabSelected(this::onSessionTabSelected);
|
||||
sessionTabs.setOnAddPeerRequested(this::openDraftPeerTab);
|
||||
sessionTabs.setOnTabClosed(this::onSessionTabClosed);
|
||||
ensureBodyPanel(PluginSessionKeys.ROOM);
|
||||
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||
}
|
||||
|
||||
private void onSessionTabClosed(String key) {
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
||||
}
|
||||
sharingByTabKey.remove(key);
|
||||
}
|
||||
|
||||
private void restoreSessionAfterDraftClosed() {
|
||||
String restore = lastSessionKey != null && sessionTabs.hasTab(lastSessionKey)
|
||||
? lastSessionKey
|
||||
: PluginSessionKeys.ROOM;
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(restore)) {
|
||||
restore = PluginSessionKeys.ROOM;
|
||||
}
|
||||
sessionTabs.selectTabSilently(restore);
|
||||
onSessionTabSelected(restore);
|
||||
}
|
||||
|
||||
private void onSessionTabSelected(String key) {
|
||||
persistShareStateForContextTab();
|
||||
if (key != null && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
lastSessionKey = key;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
cancelShareTimer();
|
||||
applyShareToggleForTab(key);
|
||||
showBodyCard(CARD_DRAFT);
|
||||
draftPanel.focusIpField();
|
||||
return;
|
||||
}
|
||||
if (key == null) {
|
||||
key = PluginSessionKeys.ROOM;
|
||||
}
|
||||
showBodyCard(key);
|
||||
applyRoutingForSessionKey(key);
|
||||
applyShareToggleForTab(key);
|
||||
applySharingForActiveTab();
|
||||
if (!isSharingForTab(key)) {
|
||||
refreshOutboundBodyForTab(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistShareStateForContextTab() {
|
||||
if (shareContextTabKey != null && ScreenCastOutboundTabs.isOutboundShareTabKey(shareContextTabKey)) {
|
||||
sharingByTabKey.put(shareContextTabKey, shareToggle.isSelected());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyShareToggleForTab(String key) {
|
||||
shareContextTabKey = ScreenCastOutboundTabs.isOutboundShareTabKey(key) ? key : null;
|
||||
suppressShareToggleEvents = true;
|
||||
try {
|
||||
if (shareContextTabKey == null) {
|
||||
shareToggle.setEnabled(false);
|
||||
shareToggle.setSelected(false);
|
||||
} else {
|
||||
shareToggle.setEnabled(true);
|
||||
shareToggle.setSelected(sharingByTabKey.getOrDefault(shareContextTabKey, false));
|
||||
}
|
||||
updateShareToggleLabel();
|
||||
} finally {
|
||||
suppressShareToggleEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setShareToggleSelectedSilently(boolean selected) {
|
||||
suppressShareToggleEvents = true;
|
||||
try {
|
||||
shareToggle.setSelected(selected);
|
||||
updateShareToggleLabel();
|
||||
} finally {
|
||||
suppressShareToggleEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSharingForTab(String key) {
|
||||
return ScreenCastOutboundTabs.isOutboundShareTabKey(key)
|
||||
&& sharingByTabKey.getOrDefault(key, false);
|
||||
}
|
||||
|
||||
private void applySharingForActiveTab() {
|
||||
cancelShareTimer();
|
||||
String key = sessionTabs.selectedKey();
|
||||
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
|
||||
return;
|
||||
}
|
||||
boolean on = sharingByTabKey.getOrDefault(key, shareToggle.isSelected());
|
||||
sharingByTabKey.put(key, on);
|
||||
if (!on) {
|
||||
refreshOutboundBodyForTab(key);
|
||||
return;
|
||||
}
|
||||
syncRoutingFromActiveSessionTab();
|
||||
timer = new Timer();
|
||||
alarmClock = new AlarmClock(this);
|
||||
timer.scheduleAtFixedRate(alarmClock, 0, TIME);
|
||||
String previewKey = previewTabKey();
|
||||
ensureBodyPanel(previewKey);
|
||||
setBodyMessage(previewKey, ShellUi.mutedLabel("Capturando tela…"));
|
||||
showBodyCard(previewKey);
|
||||
}
|
||||
|
||||
private void refreshOutboundBodyForTab(String key) {
|
||||
if (key == null || PluginSessionKeys.PEER_DRAFT.equals(key) || key.startsWith("view:")) {
|
||||
return;
|
||||
}
|
||||
if (isSharingForTab(key)) {
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
} else if (key.startsWith("peer:")) {
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
|
||||
}
|
||||
}
|
||||
|
||||
private void syncRoutingFromActiveSessionTab() {
|
||||
applyRoutingForSessionKey(sessionTabs.selectedKey());
|
||||
}
|
||||
|
||||
private void applyRoutingForSessionKey(String key) {
|
||||
if (key == null) {
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (key.startsWith("peer:")) {
|
||||
String ip = key.substring("peer:".length());
|
||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||
routingPanel.bindPeerSession(ip, name);
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (key.startsWith("view:")) {
|
||||
String ip = peerIpsByViewKey.get(key);
|
||||
String title = key.substring("view:".length());
|
||||
ScreenCastReceiveScope scope = receiveScopes.getOrDefault(key, ScreenCastReceiveScope.BROADCAST);
|
||||
if (ip != null && !ip.isBlank()) {
|
||||
routingPanel.bindPeerSession(ip, title, scope.bannerHtml());
|
||||
} else {
|
||||
routingPanel.bindPeerSession("", title, scope.bannerHtml());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openDraftPeerTab() {
|
||||
String previous = sessionTabs.selectedKey();
|
||||
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
|
||||
lastSessionKey = previous;
|
||||
}
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.PEER_DRAFT,
|
||||
"Nova conversa",
|
||||
"Informe o IP do peer",
|
||||
new JPanel(),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
sessionTabs.selectTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
showBodyCard(CARD_DRAFT);
|
||||
draftPanel.focusIpField();
|
||||
}
|
||||
|
||||
private void commitDraftIp(String ip) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
if (sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.closeTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
openPeerTab(ip, ip);
|
||||
}
|
||||
|
||||
private void openPeerTab(String ip, String displayName) {
|
||||
String canonical = canonicalPeerIp(ip);
|
||||
String resolved = SessionPeerLabels.resolveDisplayName(connectionManager, canonical, displayName);
|
||||
String key = PluginSessionKeys.peer(canonical);
|
||||
peerDisplayNames.put(canonical, resolved);
|
||||
ensureBodyPanel(key);
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
|
||||
if (sessionTabs.hasTab(key)) {
|
||||
sessionTabs.setTabTitle(key, resolved, canonical);
|
||||
sessionTabs.selectTab(key);
|
||||
} else {
|
||||
sessionTabs.addOrSelectTab(key, resolved, canonical, new JPanel(), true);
|
||||
}
|
||||
routingPanel.bindPeerSession(canonical, resolved);
|
||||
showBodyCard(key);
|
||||
}
|
||||
|
||||
private static String canonicalPeerIp(String ip) {
|
||||
if (ip == null || ip.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(ip.trim()).getHostAddress();
|
||||
} catch (Exception ex) {
|
||||
return ip.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public void focusMulticastSharing() {
|
||||
ensureTodosTab();
|
||||
sharingByTabKey.put(PluginSessionKeys.ROOM, true);
|
||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.selectTransport(TransportMode.MULTICAST_UDP);
|
||||
setShareToggleSelectedSilently(true);
|
||||
applySharingForActiveTab();
|
||||
}
|
||||
|
||||
public void focusPeerSharing(UserPeerView peer) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
return;
|
||||
}
|
||||
peer.getInetAddress().ifPresent(addr -> {
|
||||
String ip = addr.getHostAddress();
|
||||
openPeerTab(ip, peer.getDisplayName());
|
||||
String key = PluginSessionKeys.peer(canonicalPeerIp(ip));
|
||||
sharingByTabKey.put(key, true);
|
||||
routingPanel.selectTransport(TransportMode.UNICAST_UDP);
|
||||
setShareToggleSelectedSilently(true);
|
||||
applySharingForActiveTab();
|
||||
});
|
||||
}
|
||||
|
||||
public void focusPeerAddress(UserPeerView peer) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
return;
|
||||
}
|
||||
peer.getInetAddress().ifPresent(addr -> openPeerTab(addr.getHostAddress(), peer.getDisplayName()));
|
||||
}
|
||||
|
||||
private void ensureTodosTab() {
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.ROOM)) {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.ROOM,
|
||||
"Todos",
|
||||
"Transmissão para todos na rede (multicast)",
|
||||
new JPanel(),
|
||||
false
|
||||
);
|
||||
ensureBodyPanel(PluginSessionKeys.ROOM);
|
||||
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShareToggleLabel() {
|
||||
shareToggle.setText(shareToggle.isSelected()
|
||||
? "Compartilhamento da sua tela (ligado)"
|
||||
: "Compartilhamento da sua tela (desligado)");
|
||||
}
|
||||
|
||||
private void cancelShareTimer() {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopSharing() {
|
||||
sharingByTabKey.replaceAll((k, v) -> false);
|
||||
setShareToggleSelectedSilently(false);
|
||||
cancelShareTimer();
|
||||
refreshOutboundBodyForTab(sessionTabs.selectedKey());
|
||||
}
|
||||
|
||||
public void sendScreenCastMessage() {
|
||||
String activeKey = sessionTabs.selectedKey();
|
||||
if (!isSharingForTab(activeKey)) {
|
||||
return;
|
||||
}
|
||||
syncRoutingFromActiveSessionTab();
|
||||
try {
|
||||
BufferedImage image = ScreenShot.takeAShot();
|
||||
if (image == null || image.getWidth() < 1 || image.getHeight() < 1) {
|
||||
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.WARNING, "Captura de tela vazia");
|
||||
showCaptureWarning();
|
||||
return;
|
||||
}
|
||||
boolean black = ScreenCaptureDiagnostics.isLikelyBlackFrame(image);
|
||||
BufferedImage rescaled = ImageTransformation.resize(image, MAX_WIDTH, MAX_HEIGHT);
|
||||
byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio);
|
||||
if (byteImage == null || byteImage.length < 32) {
|
||||
return;
|
||||
}
|
||||
ScreenCastMessage message = new ScreenCastMessage(connectionManager.getUserName(), byteImage);
|
||||
|
||||
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
|
||||
TransportMode mode = routing.transport();
|
||||
SendTarget target = resolveSendTarget(routing);
|
||||
if (target == null || mode == null) {
|
||||
return;
|
||||
}
|
||||
int sent = MessageDispatcher.send(connectionManager, message, target, mode);
|
||||
if (sent == 0 && mode == TransportMode.MULTICAST_UDP) {
|
||||
compressRatio = Math.max(0.25f, compressRatio - 0.1f);
|
||||
}
|
||||
if (black) {
|
||||
showCaptureWarning();
|
||||
} else {
|
||||
showLocalPreview(rescaled);
|
||||
}
|
||||
} catch (AWTException | IOException ex) {
|
||||
Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex);
|
||||
showCaptureWarning();
|
||||
} catch (Exception ex) {
|
||||
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void showCaptureWarning() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JLabel label = new JLabel(ScreenCaptureDiagnostics.blackFrameUserMessage());
|
||||
String key = previewTabKey();
|
||||
setBodyContent(key, label);
|
||||
showBodyCard(key);
|
||||
});
|
||||
}
|
||||
|
||||
private void showLocalPreview(BufferedImage image) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String key = previewTabKey();
|
||||
JLabel label = new JLabel(new ImageIcon(image));
|
||||
setBodyContent(key, new JScrollPane(label));
|
||||
showBodyCard(key);
|
||||
});
|
||||
}
|
||||
|
||||
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
|
||||
if (routing.everyoneInRoom()) {
|
||||
return SendTarget.broadcastAll();
|
||||
}
|
||||
String ip = routing.peerIp();
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return SendTarget.singlePeer(InetAddress.getByName(ip));
|
||||
}
|
||||
|
||||
public void receiveScreenCastMessage(ScreenCastMessage message, ScreenCastReceiveScope scope) {
|
||||
if (isOwnOutboundFrame(message)) {
|
||||
return;
|
||||
}
|
||||
byte[] bytes = message.getImageByte();
|
||||
String viewKey = "view:" + message.getName();
|
||||
if (bytes == null || bytes.length < 32) {
|
||||
SwingUtilities.invokeLater(() -> updateTabError(viewKey, message.getName(), "Frame vazio ou corrompido.", scope));
|
||||
return;
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
|
||||
if (image == null) {
|
||||
updateTabError(viewKey, message.getName(), "Não foi possível decodificar a imagem.", scope);
|
||||
return;
|
||||
}
|
||||
receiveScopes.put(viewKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
|
||||
resolveSenderIp(message.getName(), viewKey);
|
||||
sessionTabs.addOrSelectTab(viewKey, message.getName(), message.getName(), new JPanel(), true);
|
||||
setBodyContent(viewKey, new JScrollPane(new JLabel(new ImageIcon(image))));
|
||||
sessionTabs.selectTab(viewKey);
|
||||
showBodyCard(viewKey);
|
||||
} catch (IOException ex) {
|
||||
updateTabError(viewKey, message.getName(), "Erro ao ler imagem: " + ex.getMessage(), scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void resolveSenderIp(String senderName, String viewKey) {
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (view.isBroadcastTarget()) {
|
||||
continue;
|
||||
}
|
||||
if (senderName != null && senderName.equals(view.getDisplayName())) {
|
||||
view.getInetAddress().ifPresent(addr -> peerIpsByViewKey.put(viewKey, addr.getHostAddress()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabError(String tabKey, String title, String error, ScreenCastReceiveScope scope) {
|
||||
receiveScopes.put(tabKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
|
||||
JLabel label = new JLabel("<html><p style='color:#c62828'>" + error + "</p></html>");
|
||||
setBodyContent(tabKey, label);
|
||||
sessionTabs.addOrSelectTab(tabKey, title, title, new JPanel(), true);
|
||||
showBodyCard(tabKey);
|
||||
}
|
||||
|
||||
private void ensureBodyPanel(String key) {
|
||||
if (!bodyPanels.containsKey(key)) {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
bodyPanels.put(key, panel);
|
||||
bodyCards.add(panel, key);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBodyContent(String key, java.awt.Component content) {
|
||||
ensureBodyPanel(key);
|
||||
JPanel panel = bodyPanels.get(key);
|
||||
panel.removeAll();
|
||||
panel.add(content, BorderLayout.CENTER);
|
||||
panel.revalidate();
|
||||
panel.repaint();
|
||||
}
|
||||
|
||||
private void setBodyMessage(String key, JLabel label) {
|
||||
setBodyContent(key, label);
|
||||
}
|
||||
|
||||
private String previewTabKey() {
|
||||
String selected = sessionTabs.selectedKey();
|
||||
if (selected != null && !PluginSessionKeys.PEER_DRAFT.equals(selected)) {
|
||||
return selected;
|
||||
}
|
||||
return PluginSessionKeys.ROOM;
|
||||
}
|
||||
|
||||
private boolean isOwnOutboundFrame(ScreenCastMessage message) {
|
||||
if (message == null || message.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
String localName = connectionManager.getUserName();
|
||||
return localName != null && localName.equals(message.getName().trim());
|
||||
}
|
||||
|
||||
private void showBodyCard(String card) {
|
||||
bodyLayout.show(bodyCards, card);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
public enum ScreenCastReceiveScope {
|
||||
BROADCAST,
|
||||
DIRECT;
|
||||
|
||||
public String bannerHtml() {
|
||||
if (this == BROADCAST) {
|
||||
return "<html><i>Transmissão para todos na rede</i></html>";
|
||||
}
|
||||
return "<html><i>Transmissão direcionada a você</i></html>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.util.TimerTask;
|
||||
import pitiupi.screenCast.GUI.ScreenCastPanel;
|
||||
|
||||
public class AlarmClock extends TimerTask {
|
||||
|
||||
private final ScreenCastPanel screenCastPanel;
|
||||
|
||||
public AlarmClock(ScreenCastPanel screenCastPanel) {
|
||||
this.screenCastPanel = screenCastPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
screenCastPanel.sendScreenCastMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ImageTransformation {
|
||||
|
||||
public static byte[] toByteArray(BufferedImage image) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "jpg", baos);
|
||||
byte[] data = baos.toByteArray();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static byte[] compress(BufferedImage image, float scale) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
|
||||
ImageWriter writer = writers.next();
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionQuality(scale);
|
||||
ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
|
||||
writer.setOutput(ios);
|
||||
writer.write(null, new IIOImage(image, null, null), param);
|
||||
byte[] data = baos.toByteArray();
|
||||
writer.dispose();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static BufferedImage resize(BufferedImage img, int maxWidth, int maxHeight) throws IOException {
|
||||
int scaledWidth = maxWidth;
|
||||
int scaledHeight = (int) (img.getHeight() * ((double) scaledWidth / img.getWidth()));
|
||||
|
||||
if (scaledHeight > maxHeight) {
|
||||
scaledHeight = maxHeight;
|
||||
scaledWidth = (int) (img.getWidth() * ((double) scaledHeight / img.getHeight()));
|
||||
|
||||
if (scaledWidth > maxWidth) {
|
||||
scaledWidth = maxWidth;
|
||||
scaledHeight = maxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
Image resized = img.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
|
||||
BufferedImage buffered = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = buffered.createGraphics();
|
||||
graphics.drawImage(resized, 0, 0, null);
|
||||
graphics.dispose();
|
||||
return buffered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class ScreenCaptureDiagnostics {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ScreenCaptureDiagnostics.class.getName());
|
||||
private static boolean environmentLogged;
|
||||
|
||||
private ScreenCaptureDiagnostics() {
|
||||
}
|
||||
|
||||
public static boolean isLikelyBlackFrame(BufferedImage image) {
|
||||
if (image == null || image.getWidth() < 8 || image.getHeight() < 8) {
|
||||
return true;
|
||||
}
|
||||
int stepX = Math.max(1, image.getWidth() / 16);
|
||||
int stepY = Math.max(1, image.getHeight() / 16);
|
||||
int dark = 0;
|
||||
int samples = 0;
|
||||
for (int y = 0; y < image.getHeight(); y += stepY) {
|
||||
for (int x = 0; x < image.getWidth(); x += stepX) {
|
||||
int rgb = image.getRGB(x, y);
|
||||
int r = (rgb >> 16) & 0xff;
|
||||
int g = (rgb >> 8) & 0xff;
|
||||
int b = rgb & 0xff;
|
||||
if (r < 12 && g < 12 && b < 12) {
|
||||
dark++;
|
||||
}
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
return samples > 0 && dark >= samples * 0.92;
|
||||
}
|
||||
|
||||
public static String blackFrameUserMessage() {
|
||||
logEnvironmentOnce();
|
||||
String wayland = System.getenv("WAYLAND_DISPLAY");
|
||||
String display = System.getenv("DISPLAY");
|
||||
return "<html><p><b>Captura retornou tela preta.</b></p>"
|
||||
+ "<p>Em WSL/WSLg o <code>java.awt.Robot</code> costuma não capturar o desktop.</p>"
|
||||
+ "<ul>"
|
||||
+ "<li>Teste o Pitiupi no <b>Windows nativo</b> (fora do WSL) para compartilhar a tela real.</li>"
|
||||
+ "<li>No WSL, confira WSLg: <code>WAYLAND_DISPLAY="
|
||||
+ (wayland != null ? wayland : "—")
|
||||
+ "</code>, <code>DISPLAY="
|
||||
+ (display != null ? display : "—")
|
||||
+ "</code>.</li>"
|
||||
+ "<li>Se os frames ainda saem pretos, o envio na rede pode ocorrer, mas o preview local não reflete sua tela.</li>"
|
||||
+ "</ul></html>";
|
||||
}
|
||||
|
||||
private static void logEnvironmentOnce() {
|
||||
if (environmentLogged) {
|
||||
return;
|
||||
}
|
||||
environmentLogged = true;
|
||||
try {
|
||||
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||
LOG.log(Level.INFO, "Screen capture env: {0}, displays={1}, DISPLAY={2}, WAYLAND_DISPLAY={3}",
|
||||
new Object[]{
|
||||
env.getClass().getSimpleName(),
|
||||
env.getScreenDevices().length,
|
||||
System.getenv("DISPLAY"),
|
||||
System.getenv("WAYLAND_DISPLAY")
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LOG.log(Level.WARNING, "Screen capture env unavailable", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Robot;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenShot {
|
||||
|
||||
public static BufferedImage takeAShot() throws AWTException, IOException {
|
||||
Robot robot = new Robot();
|
||||
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
|
||||
BufferedImage output = robot.createScreenCapture(screenRect);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.net;
|
||||
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenCastMessage extends Message {
|
||||
|
||||
private String name;
|
||||
private byte[] imageByte;
|
||||
|
||||
public ScreenCastMessage() {
|
||||
}
|
||||
|
||||
public ScreenCastMessage(String name, byte[] imageByte) {
|
||||
this.name = name;
|
||||
this.imageByte = imageByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the imageByte
|
||||
*/
|
||||
public byte[] getImageByte() {
|
||||
return imageByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageByte the imageByte to set
|
||||
*/
|
||||
public void setImageByte(byte[] imageByte) {
|
||||
this.imageByte = imageByte;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package pitiupi.screenCast.plugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.application.activity.PeerActivityInbox;
|
||||
import pitiupi.application.peeractions.PeerAction;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.application.peeractions.PeerActionTarget;
|
||||
import pitiupi.application.peeractions.PeerTarget;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Menus.MenuCategory;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.screenCast.GUI.ScreenCastMenu;
|
||||
import pitiupi.screenCast.GUI.ScreenCastPanel;
|
||||
import pitiupi.screenCast.GUI.ScreenCastReceiveScope;
|
||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
||||
|
||||
public class ScreenCastPlugin extends Plugin {
|
||||
|
||||
private ScreenCastPanel screenCastPanel;
|
||||
private PluginWorkspace workspace;
|
||||
private ConnectionManager connectionManager;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ScreenCast";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Compartilhar tela";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "Flávio Schiavoni";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "Pitiupi.ScreenCast.Example";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.workspace = workspace;
|
||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||
bindShellWorkspaceContent(screenCastPanel);
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||
guiManager.createMenu(MenuCategory.Streaming, new ScreenCastMenu(screenCastPanel));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShellTabClosed() {
|
||||
if (screenCastPanel != null) {
|
||||
screenCastPanel.stopSharing();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
registrar.register(PeerAction.of(
|
||||
"screencast.multicast",
|
||||
"Compartilhar na rede (multicast)",
|
||||
PeerActionTarget.ALL_PEERS,
|
||||
target -> {
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
screenCastPanel.focusMulticastSharing();
|
||||
}
|
||||
));
|
||||
registrar.register(PeerAction.of(
|
||||
"screencast.peer",
|
||||
"Compartilhar com este peer",
|
||||
PeerActionTarget.SINGLE_PEER,
|
||||
target -> {
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
if (target instanceof PeerTarget.Single single) {
|
||||
screenCastPanel.focusPeerSharing(single.peer());
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Message>> getMessages() {
|
||||
return List.of(ScreenCastMessage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveMessage(Message message) {
|
||||
if (message instanceof ScreenCastMessage msg && screenCastPanel != null) {
|
||||
if (isOwnOutboundFrame(msg)) {
|
||||
return;
|
||||
}
|
||||
ScreenCastReceiveScope scope = classifyIncoming(msg);
|
||||
recordScreenCastActivity(msg, scope);
|
||||
screenCastPanel.receiveScreenCastMessage(msg, scope);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOwnOutboundFrame(ScreenCastMessage msg) {
|
||||
if (connectionManager == null || msg.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
String localName = connectionManager.getUserName();
|
||||
return localName != null && localName.equals(msg.getName().trim());
|
||||
}
|
||||
|
||||
private ScreenCastReceiveScope classifyIncoming(ScreenCastMessage msg) {
|
||||
if (connectionManager == null || msg.getName() == null) {
|
||||
return ScreenCastReceiveScope.BROADCAST;
|
||||
}
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (!view.isBroadcastTarget()
|
||||
&& msg.getName().equals(view.getDisplayName())
|
||||
&& view.getInstanceId() != null) {
|
||||
return ScreenCastReceiveScope.DIRECT;
|
||||
}
|
||||
}
|
||||
return ScreenCastReceiveScope.BROADCAST;
|
||||
}
|
||||
|
||||
private void recordScreenCastActivity(ScreenCastMessage msg, ScreenCastReceiveScope scope) {
|
||||
if (connectionManager == null) {
|
||||
return;
|
||||
}
|
||||
PeerActivityInbox inbox = connectionManager.getPeerActivityInbox();
|
||||
if (scope == ScreenCastReceiveScope.DIRECT) {
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (!view.isBroadcastTarget()
|
||||
&& msg.getName() != null
|
||||
&& msg.getName().equals(view.getDisplayName())
|
||||
&& view.getInstanceId() != null) {
|
||||
inbox.record(view.getInstanceId(), getIdentifier(), "screencast.peer");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
inbox.recordBroadcast(getIdentifier(), "screencast.multicast");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user