Entrega v1 aula: shell com abas, PITIUPI_HOME e plugins de referência.
Consolida núcleo (presença, peer actions, MessageDispatcher), chat e screencast na galeria, documentação PT-BR e testes JUnit; remove mpChat do catálogo em favor do chat unificado. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b10c196fc9
commit
eeb1936f6d
178 files changed
+7641
-1481
No files matched your search
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AntConfiguration">
|
||||
<buildFile url="file://$PROJECT_DIR$/build.xml" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+3
@@ -0,0 +1,3 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+115
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/../pitiupi.testSocket/Makefile" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 7
|
||||
}</component>
|
||||
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feature/separacao-gerenciadores",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Gustavo/OneDrive/Documentos/Faculdade/2026_2/Monitoria/pitiupi/pitiupi.screenCast",
|
||||
"project.structure.last.edited": "Modules",
|
||||
"project.structure.proportion": "0.15",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "preferences.keymap"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager">
|
||||
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="Tox" factoryName="Tox">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="JetRunConfigurationType">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
|
||||
<module name="pitiupi.screenCast" />
|
||||
<option name="filePath" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-30f59d01ecdd-37e91769500f-intellij.indexing.shared.core-IU-261.26222.65" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-IU-261.26222.65" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="" />
|
||||
<created>1784745206049</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1784745206049</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- You may freely edit this file. See commented blocks below for -->
|
||||
<!-- some examples of how to customize the build. -->
|
||||
<!-- (If you delete it and reopen the project it will be recreated.) -->
|
||||
<!-- By default, only the Clean and Build commands use this build script. -->
|
||||
<!-- Commands such as Run, Debug, and Test only use this build script if -->
|
||||
<!-- the Compile on Save feature is turned off for the project. -->
|
||||
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
|
||||
<!-- in the project's Project Properties dialog box.-->
|
||||
<project name="pitiupi.screenCast" default="default" basedir=".">
|
||||
<description>Builds, tests, and runs the project pitiupi.screenCast.</description>
|
||||
<import file="nbproject/build-impl.xml"/>
|
||||
<!--
|
||||
|
||||
There exist several targets which are by default empty and which can be
|
||||
used for execution of your tasks. These targets are usually executed
|
||||
before and after some main targets. They are:
|
||||
|
||||
-pre-init: called before initialization of project properties
|
||||
-post-init: called after initialization of project properties
|
||||
-pre-compile: called before javac compilation
|
||||
-post-compile: called after javac compilation
|
||||
-pre-compile-single: called before javac compilation of single file
|
||||
-post-compile-single: called after javac compilation of single file
|
||||
-pre-compile-test: called before javac compilation of JUnit tests
|
||||
-post-compile-test: called after javac compilation of JUnit tests
|
||||
-pre-compile-test-single: called before javac compilation of single JUnit test
|
||||
-post-compile-test-single: called after javac compilation of single JUunit test
|
||||
-pre-jar: called before JAR building
|
||||
-post-jar: called after JAR building
|
||||
-post-clean: called after cleaning build products
|
||||
|
||||
(Targets beginning with '-' are not intended to be called on their own.)
|
||||
|
||||
Example of inserting an obfuscator after compilation could look like this:
|
||||
|
||||
<target name="-post-compile">
|
||||
<obfuscate>
|
||||
<fileset dir="${build.classes.dir}"/>
|
||||
</obfuscate>
|
||||
</target>
|
||||
|
||||
For list of available properties check the imported
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
|
||||
Another way to customize the build is by overriding existing main targets.
|
||||
The targets of interest are:
|
||||
|
||||
-init-macrodef-javac: defines macro for javac compilation
|
||||
-init-macrodef-junit: defines macro for junit execution
|
||||
-init-macrodef-debug: defines macro for class debugging
|
||||
-init-macrodef-java: defines macro for class execution
|
||||
-do-jar: JAR building
|
||||
run: execution of project
|
||||
-javadoc-build: Javadoc generation
|
||||
test-report: JUnit report generation
|
||||
|
||||
An example of overriding the target for project execution could look like this:
|
||||
|
||||
<target name="run" depends="pitiupi.screenCast-impl.jar">
|
||||
<exec dir="bin" executable="launcher.exe">
|
||||
<arg file="${dist.jar}"/>
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
Notice that the overridden target depends on the jar target and not only on
|
||||
the compile target as the regular run target does. Again, for a list of available
|
||||
properties which you can use, check the target you are overriding in the
|
||||
nbproject/build-impl.xml file.
|
||||
|
||||
-->
|
||||
</project>
|
||||
@@ -0,0 +1,3 @@
|
||||
Manifest-Version: 1.0
|
||||
X-COMMENT: Main-Class will be added automatically by build
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
build.xml.data.CRC32=96758119
|
||||
build.xml.script.CRC32=70726a23
|
||||
build.xml.stylesheet.CRC32=f85dc8f2@1.119.0.48
|
||||
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||
nbproject/build-impl.xml.data.CRC32=96758119
|
||||
nbproject/build-impl.xml.script.CRC32=0f906282
|
||||
nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.119.0.48
|
||||
@@ -0,0 +1,98 @@
|
||||
annotation.processing.enabled=true
|
||||
annotation.processing.enabled.in.editor=false
|
||||
annotation.processing.processor.options=
|
||||
annotation.processing.processors.list=
|
||||
annotation.processing.run.all.processors=true
|
||||
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||
build.classes.dir=${build.dir}/classes
|
||||
build.classes.excludes=**/*.java,**/*.form
|
||||
# This directory is removed when the project is cleaned:
|
||||
build.dir=build
|
||||
build.generated.dir=${build.dir}/generated
|
||||
build.generated.sources.dir=${build.dir}/generated-sources
|
||||
# Only compile against the classpath explicitly listed here:
|
||||
build.sysclasspath=ignore
|
||||
build.test.classes.dir=${build.dir}/test/classes
|
||||
build.test.results.dir=${build.dir}/test/results
|
||||
# Uncomment to specify the preferred debugger connection transport:
|
||||
#debug.transport=dt_socket
|
||||
debug.classpath=\
|
||||
${run.classpath}
|
||||
debug.modulepath=\
|
||||
${run.modulepath}
|
||||
debug.test.classpath=\
|
||||
${run.test.classpath}
|
||||
debug.test.modulepath=\
|
||||
${run.test.modulepath}
|
||||
# Files in build.classes.dir which should be excluded from distribution jar
|
||||
dist.archive.excludes=
|
||||
# This directory is removed when the project is cleaned:
|
||||
dist.dir=dist
|
||||
dist.jar=${dist.dir}/pitiupi.screenCast.jar
|
||||
dist.javadoc.dir=${dist.dir}/javadoc
|
||||
dist.jlink.dir=${dist.dir}/jlink
|
||||
dist.jlink.output=${dist.jlink.dir}/pitiupi.screenCast
|
||||
excludes=
|
||||
includes=**
|
||||
jar.compress=false
|
||||
javac.classpath=\
|
||||
${reference.pitiupi.jar}
|
||||
# Space-separated list of extra javac options
|
||||
javac.compilerargs=
|
||||
javac.deprecation=false
|
||||
javac.external.vm=true
|
||||
javac.modulepath=
|
||||
javac.processormodulepath=
|
||||
javac.processorpath=\
|
||||
${javac.classpath}
|
||||
javac.source=25
|
||||
javac.target=25
|
||||
javac.test.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
javac.test.modulepath=\
|
||||
${javac.modulepath}
|
||||
javac.test.processorpath=\
|
||||
${javac.test.classpath}
|
||||
javadoc.additionalparam=
|
||||
javadoc.author=false
|
||||
javadoc.encoding=${source.encoding}
|
||||
javadoc.html5=false
|
||||
javadoc.noindex=false
|
||||
javadoc.nonavbar=false
|
||||
javadoc.notree=false
|
||||
javadoc.private=false
|
||||
javadoc.splitindex=true
|
||||
javadoc.use=true
|
||||
javadoc.version=false
|
||||
javadoc.windowtitle=
|
||||
# The jlink additional root modules to resolve
|
||||
jlink.additionalmodules=
|
||||
# The jlink additional command line parameters
|
||||
jlink.additionalparam=
|
||||
jlink.launcher=true
|
||||
jlink.launcher.name=pitiupi.screenCast
|
||||
main.class=
|
||||
manifest.file=manifest.mf
|
||||
meta.inf.dir=${src.dir}/META-INF
|
||||
mkdist.disabled=false
|
||||
platform.active=default_platform
|
||||
project.pitiupi=../../pitiupi
|
||||
reference.pitiupi.jar=${project.pitiupi}/dist/pitiupi.jar
|
||||
run.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
# Space-separated list of JVM arguments used when running the project.
|
||||
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
|
||||
# To set system properties for unit tests define test-sys-prop.name=value:
|
||||
run.jvmargs=
|
||||
run.modulepath=\
|
||||
${javac.modulepath}
|
||||
run.test.classpath=\
|
||||
${javac.test.classpath}:\
|
||||
${build.test.classes.dir}
|
||||
run.test.modulepath=\
|
||||
${javac.test.modulepath}
|
||||
source.encoding=UTF-8
|
||||
src.dir=src
|
||||
test.src.dir=test
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||
<type>org.netbeans.modules.java.j2seproject</type>
|
||||
<configuration>
|
||||
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||
<name>pitiupi.screenCast</name>
|
||||
<source-roots>
|
||||
<root id="src.dir"/>
|
||||
</source-roots>
|
||||
<test-roots>
|
||||
<root id="test.src.dir"/>
|
||||
</test-roots>
|
||||
</data>
|
||||
<references xmlns="http://www.netbeans.org/ns/ant-project-references/1">
|
||||
<reference>
|
||||
<foreign-project>pitiupi</foreign-project>
|
||||
<artifact-type>jar</artifact-type>
|
||||
<script>build.xml</script>
|
||||
<target>jar</target>
|
||||
<clean-target>clean</clean-target>
|
||||
<id>jar</id>
|
||||
</reference>
|
||||
</references>
|
||||
</configuration>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
pitiupi.screenCast.plugin.ScreenCastPlugin
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="module-library">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
|
||||
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
|
||||
*/
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenCastMenu extends MulticastSubMenu {
|
||||
|
||||
private final ScreenCastShowMenu showMenu;
|
||||
|
||||
public ScreenCastMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.showMenu = new ScreenCastShowMenu(screenCastPanel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Screen Cast";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MulticastMenu> getMenus() {
|
||||
return List.of(showMenu);
|
||||
}
|
||||
|
||||
private static final class ScreenCastShowMenu extends MulticastActionMenu {
|
||||
private final ScreenCastPanel screenCastPanel;
|
||||
|
||||
ScreenCastShowMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.screenCastPanel = screenCastPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "ScreenCast Window";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
screenCastPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import pitiupi.presentation.shell.PluginSessionKeys;
|
||||
|
||||
/**
|
||||
* Abas em que o usuário pode ligar envio de tela (outbound).
|
||||
*/
|
||||
final class ScreenCastOutboundTabs {
|
||||
|
||||
private ScreenCastOutboundTabs() {
|
||||
}
|
||||
|
||||
static boolean isOutboundShareTabKey(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
return false;
|
||||
}
|
||||
if (key.startsWith("view:")) {
|
||||
return false;
|
||||
}
|
||||
return PluginSessionKeys.ROOM.equals(key) || key.startsWith("peer:");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
import pitiupi.application.messaging.MessageDispatcher;
|
||||
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||
import pitiupi.application.messaging.SendTarget;
|
||||
import pitiupi.application.messaging.TransportMode;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.presentation.shell.PeerDraftPanel;
|
||||
import pitiupi.presentation.shell.PluginRoutingPanel;
|
||||
import pitiupi.presentation.shell.PluginSessionKeys;
|
||||
import pitiupi.presentation.shell.PluginSessionWorkspace;
|
||||
import pitiupi.presentation.shell.SessionPeerLabels;
|
||||
import pitiupi.presentation.shell.SessionTabBar;
|
||||
import pitiupi.presentation.shell.ui.ShellUi;
|
||||
|
||||
import pitiupi.screenCast.control.AlarmClock;
|
||||
import pitiupi.screenCast.control.ImageTransformation;
|
||||
import pitiupi.screenCast.control.ScreenCaptureDiagnostics;
|
||||
import pitiupi.screenCast.control.ScreenShot;
|
||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.event.HierarchyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
/**
|
||||
* Painel de compartilhamento de tela.
|
||||
* Backlog UX: prévia dividida (recebendo + enviando) na mesma aba peer; multi-share simultâneo.
|
||||
*/
|
||||
public class ScreenCastPanel extends JPanel {
|
||||
|
||||
public static final int TIME = 1000;
|
||||
private static final int MAX_WIDTH = 800;
|
||||
private static final int MAX_HEIGHT = 600;
|
||||
private static final String CARD_DRAFT = "__draft__";
|
||||
private static final String IDLE_ROOM_HTML =
|
||||
"<html><p>Ative <b>Compartilhamento da sua tela</b> para enviar para todos na rede (multicast).</p></html>";
|
||||
private static final String IDLE_PEER_HTML =
|
||||
"<html><p>Ative o compartilhamento para enviar só para este peer (unicast).</p></html>";
|
||||
|
||||
private AlarmClock alarmClock;
|
||||
private Timer timer;
|
||||
private float compressRatio;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PluginSessionWorkspace workspace;
|
||||
private final PluginRoutingPanel routingPanel;
|
||||
private final SessionTabBar sessionTabs;
|
||||
private final JToggleButton shareToggle = new JToggleButton("Compartilhamento da sua tela (desligado)");
|
||||
private final Map<String, Boolean> sharingByTabKey = new HashMap<>();
|
||||
private boolean suppressShareToggleEvents;
|
||||
private String shareContextTabKey;
|
||||
private String lastSessionKey = PluginSessionKeys.ROOM;
|
||||
private final Map<String, String> peerIpsByViewKey = new HashMap<>();
|
||||
private final Map<String, String> peerDisplayNames = new HashMap<>();
|
||||
private final Map<String, ScreenCastReceiveScope> receiveScopes = new HashMap<>();
|
||||
private final CardLayout bodyLayout = new CardLayout();
|
||||
private final JPanel bodyCards = new JPanel(bodyLayout);
|
||||
private final Map<String, JPanel> bodyPanels = new HashMap<>();
|
||||
private PeerDraftPanel draftPanel;
|
||||
|
||||
public ScreenCastPanel(ConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.compressRatio = 0.55f;
|
||||
this.routingPanel = new PluginRoutingPanel(
|
||||
PluginTransportCatalog.PluginKind.SCREEN_CAST,
|
||||
"<html><i>Receba pela lista lateral (notificação) ou use + Nova para um IP. "
|
||||
+ "A aba Todos envia em multicast; abas de peer usam unicast.</i></html>",
|
||||
false
|
||||
);
|
||||
this.workspace = new PluginSessionWorkspace(routingPanel, buildShareToolbar());
|
||||
this.sessionTabs = workspace.getSessionTabBar();
|
||||
sessionTabs.setCloseFallbackKey(PluginSessionKeys.ROOM);
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private JPanel buildShareToolbar() {
|
||||
shareToggle.setFocusPainted(false);
|
||||
shareToggle.addActionListener(e -> {
|
||||
if (suppressShareToggleEvents) {
|
||||
return;
|
||||
}
|
||||
String key = sessionTabs.selectedKey();
|
||||
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
|
||||
return;
|
||||
}
|
||||
sharingByTabKey.put(key, shareToggle.isSelected());
|
||||
updateShareToggleLabel();
|
||||
applySharingForActiveTab();
|
||||
});
|
||||
JPanel shareRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
|
||||
shareRow.add(shareToggle);
|
||||
return shareRow;
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout());
|
||||
draftPanel = new PeerDraftPanel(
|
||||
"Nova sessão por IP",
|
||||
"Informe o IP para enviar ou receber unicast. O nome na aba atualiza quando o peer for conhecido.",
|
||||
this::commitDraftIp
|
||||
);
|
||||
bodyCards.add(draftPanel, CARD_DRAFT);
|
||||
workspace.getBodyHost().add(bodyCards, BorderLayout.CENTER);
|
||||
add(workspace, BorderLayout.CENTER);
|
||||
timer = new Timer();
|
||||
addHierarchyListener(e -> {
|
||||
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing()) {
|
||||
onSessionTabSelected(sessionTabs.selectedKey());
|
||||
}
|
||||
});
|
||||
initSessionTabs();
|
||||
}
|
||||
|
||||
private void initSessionTabs() {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.ROOM,
|
||||
"Todos",
|
||||
"Transmissão para todos na rede (multicast)",
|
||||
new JPanel(),
|
||||
false
|
||||
);
|
||||
sessionTabs.setOnTabSelected(this::onSessionTabSelected);
|
||||
sessionTabs.setOnAddPeerRequested(this::openDraftPeerTab);
|
||||
sessionTabs.setOnTabClosed(this::onSessionTabClosed);
|
||||
ensureBodyPanel(PluginSessionKeys.ROOM);
|
||||
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||
}
|
||||
|
||||
private void onSessionTabClosed(String key) {
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
||||
}
|
||||
sharingByTabKey.remove(key);
|
||||
}
|
||||
|
||||
private void restoreSessionAfterDraftClosed() {
|
||||
String restore = lastSessionKey != null && sessionTabs.hasTab(lastSessionKey)
|
||||
? lastSessionKey
|
||||
: PluginSessionKeys.ROOM;
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(restore)) {
|
||||
restore = PluginSessionKeys.ROOM;
|
||||
}
|
||||
sessionTabs.selectTabSilently(restore);
|
||||
onSessionTabSelected(restore);
|
||||
}
|
||||
|
||||
private void onSessionTabSelected(String key) {
|
||||
persistShareStateForContextTab();
|
||||
if (key != null && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
lastSessionKey = key;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
cancelShareTimer();
|
||||
applyShareToggleForTab(key);
|
||||
showBodyCard(CARD_DRAFT);
|
||||
draftPanel.focusIpField();
|
||||
return;
|
||||
}
|
||||
if (key == null) {
|
||||
key = PluginSessionKeys.ROOM;
|
||||
}
|
||||
showBodyCard(key);
|
||||
applyRoutingForSessionKey(key);
|
||||
applyShareToggleForTab(key);
|
||||
applySharingForActiveTab();
|
||||
if (!isSharingForTab(key)) {
|
||||
refreshOutboundBodyForTab(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void persistShareStateForContextTab() {
|
||||
if (shareContextTabKey != null && ScreenCastOutboundTabs.isOutboundShareTabKey(shareContextTabKey)) {
|
||||
sharingByTabKey.put(shareContextTabKey, shareToggle.isSelected());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyShareToggleForTab(String key) {
|
||||
shareContextTabKey = ScreenCastOutboundTabs.isOutboundShareTabKey(key) ? key : null;
|
||||
suppressShareToggleEvents = true;
|
||||
try {
|
||||
if (shareContextTabKey == null) {
|
||||
shareToggle.setEnabled(false);
|
||||
shareToggle.setSelected(false);
|
||||
} else {
|
||||
shareToggle.setEnabled(true);
|
||||
shareToggle.setSelected(sharingByTabKey.getOrDefault(shareContextTabKey, false));
|
||||
}
|
||||
updateShareToggleLabel();
|
||||
} finally {
|
||||
suppressShareToggleEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setShareToggleSelectedSilently(boolean selected) {
|
||||
suppressShareToggleEvents = true;
|
||||
try {
|
||||
shareToggle.setSelected(selected);
|
||||
updateShareToggleLabel();
|
||||
} finally {
|
||||
suppressShareToggleEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSharingForTab(String key) {
|
||||
return ScreenCastOutboundTabs.isOutboundShareTabKey(key)
|
||||
&& sharingByTabKey.getOrDefault(key, false);
|
||||
}
|
||||
|
||||
private void applySharingForActiveTab() {
|
||||
cancelShareTimer();
|
||||
String key = sessionTabs.selectedKey();
|
||||
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
|
||||
return;
|
||||
}
|
||||
boolean on = sharingByTabKey.getOrDefault(key, shareToggle.isSelected());
|
||||
sharingByTabKey.put(key, on);
|
||||
if (!on) {
|
||||
refreshOutboundBodyForTab(key);
|
||||
return;
|
||||
}
|
||||
syncRoutingFromActiveSessionTab();
|
||||
timer = new Timer();
|
||||
alarmClock = new AlarmClock(this);
|
||||
timer.scheduleAtFixedRate(alarmClock, 0, TIME);
|
||||
String previewKey = previewTabKey();
|
||||
ensureBodyPanel(previewKey);
|
||||
setBodyMessage(previewKey, ShellUi.mutedLabel("Capturando tela…"));
|
||||
showBodyCard(previewKey);
|
||||
}
|
||||
|
||||
private void refreshOutboundBodyForTab(String key) {
|
||||
if (key == null || PluginSessionKeys.PEER_DRAFT.equals(key) || key.startsWith("view:")) {
|
||||
return;
|
||||
}
|
||||
if (isSharingForTab(key)) {
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
} else if (key.startsWith("peer:")) {
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
|
||||
}
|
||||
}
|
||||
|
||||
private void syncRoutingFromActiveSessionTab() {
|
||||
applyRoutingForSessionKey(sessionTabs.selectedKey());
|
||||
}
|
||||
|
||||
private void applyRoutingForSessionKey(String key) {
|
||||
if (key == null) {
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
|
||||
return;
|
||||
}
|
||||
if (PluginSessionKeys.ROOM.equals(key)) {
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (key.startsWith("peer:")) {
|
||||
String ip = key.substring("peer:".length());
|
||||
String name = peerDisplayNames.getOrDefault(ip, ip);
|
||||
routingPanel.bindPeerSession(ip, name);
|
||||
routingPanel.setReceiveScopeHint(null);
|
||||
return;
|
||||
}
|
||||
if (key.startsWith("view:")) {
|
||||
String ip = peerIpsByViewKey.get(key);
|
||||
String title = key.substring("view:".length());
|
||||
ScreenCastReceiveScope scope = receiveScopes.getOrDefault(key, ScreenCastReceiveScope.BROADCAST);
|
||||
if (ip != null && !ip.isBlank()) {
|
||||
routingPanel.bindPeerSession(ip, title, scope.bannerHtml());
|
||||
} else {
|
||||
routingPanel.bindPeerSession("", title, scope.bannerHtml());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openDraftPeerTab() {
|
||||
String previous = sessionTabs.selectedKey();
|
||||
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
|
||||
lastSessionKey = previous;
|
||||
}
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.PEER_DRAFT,
|
||||
"Nova conversa",
|
||||
"Informe o IP do peer",
|
||||
new JPanel(),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
sessionTabs.selectTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
showBodyCard(CARD_DRAFT);
|
||||
draftPanel.focusIpField();
|
||||
}
|
||||
|
||||
private void commitDraftIp(String ip) {
|
||||
if (draftPanel != null) {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
if (sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
|
||||
sessionTabs.closeTab(PluginSessionKeys.PEER_DRAFT);
|
||||
}
|
||||
openPeerTab(ip, ip);
|
||||
}
|
||||
|
||||
private void openPeerTab(String ip, String displayName) {
|
||||
String canonical = canonicalPeerIp(ip);
|
||||
String resolved = SessionPeerLabels.resolveDisplayName(connectionManager, canonical, displayName);
|
||||
String key = PluginSessionKeys.peer(canonical);
|
||||
peerDisplayNames.put(canonical, resolved);
|
||||
ensureBodyPanel(key);
|
||||
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
|
||||
if (sessionTabs.hasTab(key)) {
|
||||
sessionTabs.setTabTitle(key, resolved, canonical);
|
||||
sessionTabs.selectTab(key);
|
||||
} else {
|
||||
sessionTabs.addOrSelectTab(key, resolved, canonical, new JPanel(), true);
|
||||
}
|
||||
routingPanel.bindPeerSession(canonical, resolved);
|
||||
showBodyCard(key);
|
||||
}
|
||||
|
||||
private static String canonicalPeerIp(String ip) {
|
||||
if (ip == null || ip.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(ip.trim()).getHostAddress();
|
||||
} catch (Exception ex) {
|
||||
return ip.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public void focusMulticastSharing() {
|
||||
ensureTodosTab();
|
||||
sharingByTabKey.put(PluginSessionKeys.ROOM, true);
|
||||
sessionTabs.selectTab(PluginSessionKeys.ROOM);
|
||||
routingPanel.bindRoomSession();
|
||||
routingPanel.selectTransport(TransportMode.MULTICAST_UDP);
|
||||
setShareToggleSelectedSilently(true);
|
||||
applySharingForActiveTab();
|
||||
}
|
||||
|
||||
public void focusPeerSharing(UserPeerView peer) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
return;
|
||||
}
|
||||
peer.getInetAddress().ifPresent(addr -> {
|
||||
String ip = addr.getHostAddress();
|
||||
openPeerTab(ip, peer.getDisplayName());
|
||||
String key = PluginSessionKeys.peer(canonicalPeerIp(ip));
|
||||
sharingByTabKey.put(key, true);
|
||||
routingPanel.selectTransport(TransportMode.UNICAST_UDP);
|
||||
setShareToggleSelectedSilently(true);
|
||||
applySharingForActiveTab();
|
||||
});
|
||||
}
|
||||
|
||||
public void focusPeerAddress(UserPeerView peer) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
|
||||
return;
|
||||
}
|
||||
peer.getInetAddress().ifPresent(addr -> openPeerTab(addr.getHostAddress(), peer.getDisplayName()));
|
||||
}
|
||||
|
||||
private void ensureTodosTab() {
|
||||
if (!sessionTabs.hasTab(PluginSessionKeys.ROOM)) {
|
||||
sessionTabs.addOrSelectTab(
|
||||
PluginSessionKeys.ROOM,
|
||||
"Todos",
|
||||
"Transmissão para todos na rede (multicast)",
|
||||
new JPanel(),
|
||||
false
|
||||
);
|
||||
ensureBodyPanel(PluginSessionKeys.ROOM);
|
||||
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShareToggleLabel() {
|
||||
shareToggle.setText(shareToggle.isSelected()
|
||||
? "Compartilhamento da sua tela (ligado)"
|
||||
: "Compartilhamento da sua tela (desligado)");
|
||||
}
|
||||
|
||||
private void cancelShareTimer() {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void stopSharing() {
|
||||
sharingByTabKey.replaceAll((k, v) -> false);
|
||||
setShareToggleSelectedSilently(false);
|
||||
cancelShareTimer();
|
||||
refreshOutboundBodyForTab(sessionTabs.selectedKey());
|
||||
}
|
||||
|
||||
public void sendScreenCastMessage() {
|
||||
String activeKey = sessionTabs.selectedKey();
|
||||
if (!isSharingForTab(activeKey)) {
|
||||
return;
|
||||
}
|
||||
syncRoutingFromActiveSessionTab();
|
||||
try {
|
||||
BufferedImage image = ScreenShot.takeAShot();
|
||||
if (image == null || image.getWidth() < 1 || image.getHeight() < 1) {
|
||||
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.WARNING, "Captura de tela vazia");
|
||||
showCaptureWarning();
|
||||
return;
|
||||
}
|
||||
boolean black = ScreenCaptureDiagnostics.isLikelyBlackFrame(image);
|
||||
BufferedImage rescaled = ImageTransformation.resize(image, MAX_WIDTH, MAX_HEIGHT);
|
||||
byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio);
|
||||
if (byteImage == null || byteImage.length < 32) {
|
||||
return;
|
||||
}
|
||||
ScreenCastMessage message = new ScreenCastMessage(connectionManager.getUserName(), byteImage);
|
||||
|
||||
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
|
||||
TransportMode mode = routing.transport();
|
||||
SendTarget target = resolveSendTarget(routing);
|
||||
if (target == null || mode == null) {
|
||||
return;
|
||||
}
|
||||
int sent = MessageDispatcher.send(connectionManager, message, target, mode);
|
||||
if (sent == 0 && mode == TransportMode.MULTICAST_UDP) {
|
||||
compressRatio = Math.max(0.25f, compressRatio - 0.1f);
|
||||
}
|
||||
if (black) {
|
||||
showCaptureWarning();
|
||||
} else {
|
||||
showLocalPreview(rescaled);
|
||||
}
|
||||
} catch (AWTException | IOException ex) {
|
||||
Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex);
|
||||
showCaptureWarning();
|
||||
} catch (Exception ex) {
|
||||
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void showCaptureWarning() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JLabel label = new JLabel(ScreenCaptureDiagnostics.blackFrameUserMessage());
|
||||
String key = previewTabKey();
|
||||
setBodyContent(key, label);
|
||||
showBodyCard(key);
|
||||
});
|
||||
}
|
||||
|
||||
private void showLocalPreview(BufferedImage image) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String key = previewTabKey();
|
||||
JLabel label = new JLabel(new ImageIcon(image));
|
||||
setBodyContent(key, new JScrollPane(label));
|
||||
showBodyCard(key);
|
||||
});
|
||||
}
|
||||
|
||||
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
|
||||
if (routing.everyoneInRoom()) {
|
||||
return SendTarget.broadcastAll();
|
||||
}
|
||||
String ip = routing.peerIp();
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return SendTarget.singlePeer(InetAddress.getByName(ip));
|
||||
}
|
||||
|
||||
public void receiveScreenCastMessage(ScreenCastMessage message, ScreenCastReceiveScope scope) {
|
||||
if (isOwnOutboundFrame(message)) {
|
||||
return;
|
||||
}
|
||||
byte[] bytes = message.getImageByte();
|
||||
String viewKey = "view:" + message.getName();
|
||||
if (bytes == null || bytes.length < 32) {
|
||||
SwingUtilities.invokeLater(() -> updateTabError(viewKey, message.getName(), "Frame vazio ou corrompido.", scope));
|
||||
return;
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
|
||||
if (image == null) {
|
||||
updateTabError(viewKey, message.getName(), "Não foi possível decodificar a imagem.", scope);
|
||||
return;
|
||||
}
|
||||
receiveScopes.put(viewKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
|
||||
resolveSenderIp(message.getName(), viewKey);
|
||||
sessionTabs.addOrSelectTab(viewKey, message.getName(), message.getName(), new JPanel(), true);
|
||||
setBodyContent(viewKey, new JScrollPane(new JLabel(new ImageIcon(image))));
|
||||
sessionTabs.selectTab(viewKey);
|
||||
showBodyCard(viewKey);
|
||||
} catch (IOException ex) {
|
||||
updateTabError(viewKey, message.getName(), "Erro ao ler imagem: " + ex.getMessage(), scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void resolveSenderIp(String senderName, String viewKey) {
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (view.isBroadcastTarget()) {
|
||||
continue;
|
||||
}
|
||||
if (senderName != null && senderName.equals(view.getDisplayName())) {
|
||||
view.getInetAddress().ifPresent(addr -> peerIpsByViewKey.put(viewKey, addr.getHostAddress()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabError(String tabKey, String title, String error, ScreenCastReceiveScope scope) {
|
||||
receiveScopes.put(tabKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
|
||||
JLabel label = new JLabel("<html><p style='color:#c62828'>" + error + "</p></html>");
|
||||
setBodyContent(tabKey, label);
|
||||
sessionTabs.addOrSelectTab(tabKey, title, title, new JPanel(), true);
|
||||
showBodyCard(tabKey);
|
||||
}
|
||||
|
||||
private void ensureBodyPanel(String key) {
|
||||
if (!bodyPanels.containsKey(key)) {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
bodyPanels.put(key, panel);
|
||||
bodyCards.add(panel, key);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBodyContent(String key, java.awt.Component content) {
|
||||
ensureBodyPanel(key);
|
||||
JPanel panel = bodyPanels.get(key);
|
||||
panel.removeAll();
|
||||
panel.add(content, BorderLayout.CENTER);
|
||||
panel.revalidate();
|
||||
panel.repaint();
|
||||
}
|
||||
|
||||
private void setBodyMessage(String key, JLabel label) {
|
||||
setBodyContent(key, label);
|
||||
}
|
||||
|
||||
private String previewTabKey() {
|
||||
String selected = sessionTabs.selectedKey();
|
||||
if (selected != null && !PluginSessionKeys.PEER_DRAFT.equals(selected)) {
|
||||
return selected;
|
||||
}
|
||||
return PluginSessionKeys.ROOM;
|
||||
}
|
||||
|
||||
private boolean isOwnOutboundFrame(ScreenCastMessage message) {
|
||||
if (message == null || message.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
String localName = connectionManager.getUserName();
|
||||
return localName != null && localName.equals(message.getName().trim());
|
||||
}
|
||||
|
||||
private void showBodyCard(String card) {
|
||||
bodyLayout.show(bodyCards, card);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pitiupi.screenCast.GUI;
|
||||
|
||||
public enum ScreenCastReceiveScope {
|
||||
BROADCAST,
|
||||
DIRECT;
|
||||
|
||||
public String bannerHtml() {
|
||||
if (this == BROADCAST) {
|
||||
return "<html><i>Transmissão para todos na rede</i></html>";
|
||||
}
|
||||
return "<html><i>Transmissão direcionada a você</i></html>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.util.TimerTask;
|
||||
import pitiupi.screenCast.GUI.ScreenCastPanel;
|
||||
|
||||
public class AlarmClock extends TimerTask {
|
||||
|
||||
private final ScreenCastPanel screenCastPanel;
|
||||
|
||||
public AlarmClock(ScreenCastPanel screenCastPanel) {
|
||||
this.screenCastPanel = screenCastPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
screenCastPanel.sendScreenCastMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ImageTransformation {
|
||||
|
||||
public static byte[] toByteArray(BufferedImage image) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "jpg", baos);
|
||||
byte[] data = baos.toByteArray();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static byte[] compress(BufferedImage image, float scale) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
|
||||
ImageWriter writer = writers.next();
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionQuality(scale);
|
||||
ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
|
||||
writer.setOutput(ios);
|
||||
writer.write(null, new IIOImage(image, null, null), param);
|
||||
byte[] data = baos.toByteArray();
|
||||
writer.dispose();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static BufferedImage resize(BufferedImage img, int maxWidth, int maxHeight) throws IOException {
|
||||
int scaledWidth = maxWidth;
|
||||
int scaledHeight = (int) (img.getHeight() * ((double) scaledWidth / img.getWidth()));
|
||||
|
||||
if (scaledHeight > maxHeight) {
|
||||
scaledHeight = maxHeight;
|
||||
scaledWidth = (int) (img.getWidth() * ((double) scaledHeight / img.getHeight()));
|
||||
|
||||
if (scaledWidth > maxWidth) {
|
||||
scaledWidth = maxWidth;
|
||||
scaledHeight = maxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
Image resized = img.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
|
||||
BufferedImage buffered = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = buffered.createGraphics();
|
||||
graphics.drawImage(resized, 0, 0, null);
|
||||
graphics.dispose();
|
||||
return buffered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class ScreenCaptureDiagnostics {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ScreenCaptureDiagnostics.class.getName());
|
||||
private static boolean environmentLogged;
|
||||
|
||||
private ScreenCaptureDiagnostics() {
|
||||
}
|
||||
|
||||
public static boolean isLikelyBlackFrame(BufferedImage image) {
|
||||
if (image == null || image.getWidth() < 8 || image.getHeight() < 8) {
|
||||
return true;
|
||||
}
|
||||
int stepX = Math.max(1, image.getWidth() / 16);
|
||||
int stepY = Math.max(1, image.getHeight() / 16);
|
||||
int dark = 0;
|
||||
int samples = 0;
|
||||
for (int y = 0; y < image.getHeight(); y += stepY) {
|
||||
for (int x = 0; x < image.getWidth(); x += stepX) {
|
||||
int rgb = image.getRGB(x, y);
|
||||
int r = (rgb >> 16) & 0xff;
|
||||
int g = (rgb >> 8) & 0xff;
|
||||
int b = rgb & 0xff;
|
||||
if (r < 12 && g < 12 && b < 12) {
|
||||
dark++;
|
||||
}
|
||||
samples++;
|
||||
}
|
||||
}
|
||||
return samples > 0 && dark >= samples * 0.92;
|
||||
}
|
||||
|
||||
public static String blackFrameUserMessage() {
|
||||
logEnvironmentOnce();
|
||||
String wayland = System.getenv("WAYLAND_DISPLAY");
|
||||
String display = System.getenv("DISPLAY");
|
||||
return "<html><p><b>Captura retornou tela preta.</b></p>"
|
||||
+ "<p>Em WSL/WSLg o <code>java.awt.Robot</code> costuma não capturar o desktop.</p>"
|
||||
+ "<ul>"
|
||||
+ "<li>Teste o Pitiupi no <b>Windows nativo</b> (fora do WSL) para compartilhar a tela real.</li>"
|
||||
+ "<li>No WSL, confira WSLg: <code>WAYLAND_DISPLAY="
|
||||
+ (wayland != null ? wayland : "—")
|
||||
+ "</code>, <code>DISPLAY="
|
||||
+ (display != null ? display : "—")
|
||||
+ "</code>.</li>"
|
||||
+ "<li>Se os frames ainda saem pretos, o envio na rede pode ocorrer, mas o preview local não reflete sua tela.</li>"
|
||||
+ "</ul></html>";
|
||||
}
|
||||
|
||||
private static void logEnvironmentOnce() {
|
||||
if (environmentLogged) {
|
||||
return;
|
||||
}
|
||||
environmentLogged = true;
|
||||
try {
|
||||
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||
LOG.log(Level.INFO, "Screen capture env: {0}, displays={1}, DISPLAY={2}, WAYLAND_DISPLAY={3}",
|
||||
new Object[]{
|
||||
env.getClass().getSimpleName(),
|
||||
env.getScreenDevices().length,
|
||||
System.getenv("DISPLAY"),
|
||||
System.getenv("WAYLAND_DISPLAY")
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LOG.log(Level.WARNING, "Screen capture env unavailable", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.control;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Robot;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenShot {
|
||||
|
||||
public static BufferedImage takeAShot() throws AWTException, IOException {
|
||||
Robot robot = new Robot();
|
||||
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
|
||||
BufferedImage output = robot.createScreenCapture(screenRect);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.screenCast.net;
|
||||
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenCastMessage extends Message {
|
||||
|
||||
private String name;
|
||||
private byte[] imageByte;
|
||||
|
||||
public ScreenCastMessage() {
|
||||
}
|
||||
|
||||
public ScreenCastMessage(String name, byte[] imageByte) {
|
||||
this.name = name;
|
||||
this.imageByte = imageByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the imageByte
|
||||
*/
|
||||
public byte[] getImageByte() {
|
||||
return imageByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageByte the imageByte to set
|
||||
*/
|
||||
public void setImageByte(byte[] imageByte) {
|
||||
this.imageByte = imageByte;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package pitiupi.screenCast.plugin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.application.activity.PeerActivityInbox;
|
||||
import pitiupi.application.peeractions.PeerAction;
|
||||
import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.application.peeractions.PeerActionTarget;
|
||||
import pitiupi.application.peeractions.PeerTarget;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Menus.MenuCategory;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.screenCast.GUI.ScreenCastMenu;
|
||||
import pitiupi.screenCast.GUI.ScreenCastPanel;
|
||||
import pitiupi.screenCast.GUI.ScreenCastReceiveScope;
|
||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
||||
|
||||
public class ScreenCastPlugin extends Plugin {
|
||||
|
||||
private ScreenCastPanel screenCastPanel;
|
||||
private PluginWorkspace workspace;
|
||||
private ConnectionManager connectionManager;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ScreenCast";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Compartilhar tela";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "Flávio Schiavoni";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return "0.1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "Pitiupi.ScreenCast.Example";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.workspace = workspace;
|
||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||
bindShellWorkspaceContent(screenCastPanel);
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||
guiManager.createMenu(MenuCategory.Streaming, new ScreenCastMenu(screenCastPanel));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShellTabClosed() {
|
||||
if (screenCastPanel != null) {
|
||||
screenCastPanel.stopSharing();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
registrar.register(PeerAction.of(
|
||||
"screencast.multicast",
|
||||
"Compartilhar na rede (multicast)",
|
||||
PeerActionTarget.ALL_PEERS,
|
||||
target -> {
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
screenCastPanel.focusMulticastSharing();
|
||||
}
|
||||
));
|
||||
registrar.register(PeerAction.of(
|
||||
"screencast.peer",
|
||||
"Compartilhar com este peer",
|
||||
PeerActionTarget.SINGLE_PEER,
|
||||
target -> {
|
||||
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
|
||||
if (target instanceof PeerTarget.Single single) {
|
||||
screenCastPanel.focusPeerSharing(single.peer());
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Message>> getMessages() {
|
||||
return List.of(ScreenCastMessage.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveMessage(Message message) {
|
||||
if (message instanceof ScreenCastMessage msg && screenCastPanel != null) {
|
||||
if (isOwnOutboundFrame(msg)) {
|
||||
return;
|
||||
}
|
||||
ScreenCastReceiveScope scope = classifyIncoming(msg);
|
||||
recordScreenCastActivity(msg, scope);
|
||||
screenCastPanel.receiveScreenCastMessage(msg, scope);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOwnOutboundFrame(ScreenCastMessage msg) {
|
||||
if (connectionManager == null || msg.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
String localName = connectionManager.getUserName();
|
||||
return localName != null && localName.equals(msg.getName().trim());
|
||||
}
|
||||
|
||||
private ScreenCastReceiveScope classifyIncoming(ScreenCastMessage msg) {
|
||||
if (connectionManager == null || msg.getName() == null) {
|
||||
return ScreenCastReceiveScope.BROADCAST;
|
||||
}
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (!view.isBroadcastTarget()
|
||||
&& msg.getName().equals(view.getDisplayName())
|
||||
&& view.getInstanceId() != null) {
|
||||
return ScreenCastReceiveScope.DIRECT;
|
||||
}
|
||||
}
|
||||
return ScreenCastReceiveScope.BROADCAST;
|
||||
}
|
||||
|
||||
private void recordScreenCastActivity(ScreenCastMessage msg, ScreenCastReceiveScope scope) {
|
||||
if (connectionManager == null) {
|
||||
return;
|
||||
}
|
||||
PeerActivityInbox inbox = connectionManager.getPeerActivityInbox();
|
||||
if (scope == ScreenCastReceiveScope.DIRECT) {
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (!view.isBroadcastTarget()
|
||||
&& msg.getName() != null
|
||||
&& msg.getName().equals(view.getDisplayName())
|
||||
&& view.getInstanceId() != null) {
|
||||
inbox.record(view.getInstanceId(), getIdentifier(), "screencast.peer");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
inbox.recordBroadcast(getIdentifier(), "screencast.multicast");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user