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:
Gabriel Felipe Souza SantosandCursor committed 2026-10-27 11:08:21 -03:00
1 parent b10c196fc9
commit eeb1936f6d
178 files changed
+7641 -1481

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
View File
@@ -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>
+5
View File
@@ -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>
+8
View File
@@ -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>
+6
View File
@@ -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>
+73
View File
@@ -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
+98
View File
@@ -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
+25
View File
@@ -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>
+20
View File
@@ -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
+20
View File
@@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
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";
}
}
+5
View File
@@ -0,0 +1,5 @@
/dist/
/build/
/out/
/nbproject/private/
*.jar
+22
View File
@@ -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)
+6
View File
@@ -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>
+3
View File
@@ -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
View File
@@ -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>
+6
View File
@@ -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>
+115
View File
@@ -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">{
&quot;associatedIndex&quot;: 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>
+73
View File
@@ -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>
+3
View File
@@ -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
+25
View File
@@ -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");
}
}