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
+9
-11
@@ -1,13 +1,11 @@
|
|||||||
/pitiupi/build/
|
/pitiupi/build/
|
||||||
/pitiupi/dist/
|
/pitiupi/dist/
|
||||||
/pitiupi/plugins/*.jar
|
/pitiupi/nbproject/private/
|
||||||
/pitiupi.chat/build/
|
/plugins/chat/build/
|
||||||
/pitiupi.chat/dist/
|
/plugins/chat/dist/
|
||||||
/pitiupi.chat/nbproject/private/
|
/plugins/chat/nbproject/private/
|
||||||
/pitiupi.screenCast/build/
|
/plugins/screencast/build/
|
||||||
/pitiupi.screenCast/dist/
|
/plugins/screencast/dist/
|
||||||
/pitiupi.mpChat/out/
|
/plugins/screencast/nbproject/private/
|
||||||
/pitiupi.mpChat/dist/
|
/plugins/mpchat/out/
|
||||||
**/built-jar.properties
|
/plugins/mpchat/dist/
|
||||||
|
|
||||||
.idea/
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# pitiupi
|
# pitiupi
|
||||||
|
|
||||||
Ferramenta **P2P** para práticas de Redes de Computadores: núcleo em Java com arquitetura de **plugins** (chat, compartilhamento de tela, chat multi-protocolo).
|
Ferramenta **P2P** para práticas de Redes de Computadores: núcleo em Java com arquitetura de **plugins** (conversa na rede e compartilhamento de tela).
|
||||||
|
|
||||||
**Documentação completa:** [docs/README.md](docs/README.md) · **Quickstart:** [docs/documentation/00_quickstart.md](docs/documentation/00_quickstart.md)
|
**Documentação completa:** [docs/README.md](docs/README.md) · **Quickstart:** [docs/documentation/00_quickstart.md](docs/documentation/00_quickstart.md)
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ chmod +x scripts/pitiupi.sh # só na primeira vez
|
|||||||
./scripts/pitiupi.sh run --all
|
./scripts/pitiupi.sh run --all
|
||||||
```
|
```
|
||||||
|
|
||||||
Compila o núcleo, os plugins do [catálogo](plugins.registry.json), instala JARs em `pitiupi/plugins/` e abre a aplicação.
|
Compila o núcleo, os plugins do [catálogo](plugins.registry.json), cria symlinks em **`PITIUPI_HOME/plugins`** (padrão `~/pitiupi/plugins`) e abre a aplicação.
|
||||||
|
|
||||||
1. Login: **nome** e **porta** (ex.: `5000`).
|
1. Login: **nome** e **porta** (ex.: `5000`).
|
||||||
2. Menus dos plugins: barra **Plugin → Social | Jogos | Streaming** (cada plugin registra a categoria no código).
|
2. Abra plugins na aba **Plugins** (galeria) ou pelas ações na lista **Usuários nesta rede** (peer actions).
|
||||||
3. P2P no mesmo PC: segunda instância com **porta diferente** (ex.: `5001`).
|
3. P2P no mesmo PC: segunda instância com **porta diferente** (ex.: `5001`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -35,38 +35,37 @@ Novos módulos entram no repositório adicionando uma entrada no JSON (não é p
|
|||||||
| Campo | Significado |
|
| Campo | Significado |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `id` | Usado em `--plugins chat,meuJogo` |
|
| `id` | Usado em `--plugins chat,meuJogo` |
|
||||||
| `dir` | Pasta do módulo (ex.: `pitiupi.xadrez`) |
|
| `dir` | Pasta do módulo (ex.: `plugins/xadrez`) |
|
||||||
| `build` | `ant` ou `make` |
|
| `build` | `ant` ou `make` |
|
||||||
| `jar` | Caminho do JAR relativo a `dir` |
|
| `jar` | Caminho do JAR relativo a `dir` |
|
||||||
| `menuCategory` | `Social`, `Jogos` ou `Streaming` — filtro CLI e documentação |
|
| `menuCategory` | `Social`, `Jogos` ou `Streaming` — filtro CLI e documentação |
|
||||||
|
|
||||||
Plugins de referência hoje:
|
Plugins de referência hoje:
|
||||||
|
|
||||||
| id | Categoria (menu) | Pasta |
|
| id | Uso didático | Pasta |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `chat` | Social | `pitiupi.chat` |
|
| `chat` | Sala geral, peer por IP, TCP privado | `plugins/chat` |
|
||||||
| `mpchat` | Social | `pitiupi.mpChat` |
|
| `screencast` | Multicast ou unicast de tela | `plugins/screencast` |
|
||||||
| `screencast` | Streaming | `pitiupi.screenCast` |
|
|
||||||
|
|
||||||
A **categoria na interface** vem do `MenuCategory` no Java (`guiManager.createMenu(...)`), não do nome do JAR. O campo `menuCategory` no JSON alinha build/link com a doc.
|
Na interface, o texto das abas vem de `getDisplayName()` em cada plugin. O campo `menuCategory` no JSON alinha build/link com a documentação.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/pitiupi.sh run --all
|
./scripts/pitiupi.sh run --all
|
||||||
./scripts/pitiupi.sh run --category Social
|
./scripts/pitiupi.sh run --category Social
|
||||||
./scripts/pitiupi.sh run --plugins chat,screencast
|
./scripts/pitiupi.sh run --plugins chat,screencast
|
||||||
./scripts/pitiupi.sh discover # pastas pitiupi.* com SPI ainda não no catálogo
|
./scripts/pitiupi.sh discover # pastas em plugins/ com SPI ainda não no catálogo
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Novo plugin (resumo)
|
## Novo plugin (resumo)
|
||||||
|
|
||||||
1. Copiar estrutura de `pitiupi.chat` → `pitiupi.meuPlugin` ([tutorial](docs/documentation/10_tutorial_plugin.md)).
|
1. Copiar estrutura de `plugins/chat` → `plugins/meuPlugin` ([tutorial](docs/documentation/10_tutorial_plugin.md)).
|
||||||
2. Em `createPlugin`, registrar menu: `guiManager.createMenu(MenuCategory.Jogos, ...)` (ou Social / Streaming).
|
2. Em `createPlugin`, registrar painel com `PluginWorkspace` / `bindShellWorkspaceContent` e, se fizer sentido, **peer actions** na lista lateral.
|
||||||
3. Adicionar entrada em [`plugins.registry.json`](plugins.registry.json).
|
3. Adicionar entrada em [`plugins.registry.json`](plugins.registry.json).
|
||||||
4. `./scripts/pitiupi.sh run --plugins meuId`.
|
4. `./scripts/pitiupi.sh link --all` e reiniciar o app.
|
||||||
|
|
||||||
O núcleo carrega **todos os `*.jar`** em `pitiupi/plugins/` (pasta única). Detalhes: [pitiupi/plugins/README.md](pitiupi/plugins/README.md).
|
O núcleo carrega **todos os `*.jar`** em `PITIUPI_HOME/plugins`. Detalhes: [pitiupi/plugins/README.md](pitiupi/plugins/README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -74,14 +73,14 @@ O núcleo carrega **todos os `*.jar`** em `pitiupi/plugins/` (pasta única). Det
|
|||||||
|
|
||||||
| Ferramenta | Uso |
|
| Ferramenta | Uso |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **JDK** com `javac` | Compilação (OpenJDK **25** recomendado) |
|
| **JDK** com `javac` | Compilação (OpenJDK **25** recomendado; núcleo Java 17) |
|
||||||
| **Apache Ant** | Módulos NetBeans |
|
| **Apache Ant** | Módulos NetBeans |
|
||||||
| **GNU Make** | `pitiupi.mpChat` |
|
|
||||||
| **python3** | Script `scripts/pitiupi.sh` (ler o catálogo) |
|
| **python3** | Script `scripts/pitiupi.sh` (ler o catálogo) |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install openjdk-25-jdk ant make
|
sudo apt install openjdk-25-jdk ant
|
||||||
./scripts/pitiupi.sh check
|
./scripts/pitiupi.sh check
|
||||||
|
cd pitiupi && ant test # testes JUnit do núcleo
|
||||||
```
|
```
|
||||||
|
|
||||||
**GUI:** Swing — WSLg / `DISPLAY=:0`.
|
**GUI:** Swing — WSLg / `DISPLAY=:0`.
|
||||||
@@ -92,11 +91,11 @@ sudo apt install openjdk-25-jdk ant make
|
|||||||
|
|
||||||
| Comando | Ação |
|
| Comando | Ação |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `run` | Build + `plugins/` + app |
|
| `run` | Build + link + app |
|
||||||
| `build` | Build + `plugins/` |
|
| `build` | Build + link |
|
||||||
| `link` | Só symlinks (JARs já existem) |
|
| `link` | Só symlinks (JARs já existem) |
|
||||||
| `list` | Catálogo e status do JAR |
|
| `list` | Catálogo e status do JAR |
|
||||||
| `discover` | Sugestão de módulos `pitiupi.*` |
|
| `discover` | Sugestão de módulos com SPI |
|
||||||
| `check` | PATH + JSON válido |
|
| `check` | PATH + JSON válido |
|
||||||
|
|
||||||
Opções: `--all` (padrão), `--plugins ids`, `--category Social|Jogos|Streaming`.
|
Opções: `--all` (padrão), `--plugins ids`, `--category Social|Jogos|Streaming`.
|
||||||
@@ -105,7 +104,10 @@ Opções: `--all` (padrão), `--plugins ids`, `--category Social|Jogos|Streaming
|
|||||||
|
|
||||||
## Teste em rede
|
## Teste em rede
|
||||||
|
|
||||||
Duas instâncias, portas distintas, mesma LAN. Ver [docs/documentation/15_testes_e_gate.md](docs/documentation/15_testes_e_gate.md) e [appendix_portas_e_multicast.md](docs/documentation/appendix_portas_e_multicast.md).
|
- **Mesmo PC:** duas instâncias, portas distintas (ex. 5000 e 5001).
|
||||||
|
- **Dois PCs na mesma Wi‑Fi:** mesma porta (ex. 5000), nomes diferentes, mesmos JARs em `~/pitiupi/plugins` em cada máquina.
|
||||||
|
|
||||||
|
Ver [docs/documentation/15_testes_e_gate.md](docs/documentation/15_testes_e_gate.md) e [appendix_portas_e_multicast.md](docs/documentation/appendix_portas_e_multicast.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,14 @@
|
|||||||
| D6 | Mensagens `Serializable` em `byte[]` | Independência do transporte; plugins definem tipos | [07_mensagens_e_serializacao.md](documentation/07_mensagens_e_serializacao.md) |
|
| D6 | Mensagens `Serializable` em `byte[]` | Independência do transporte; plugins definem tipos | [07_mensagens_e_serializacao.md](documentation/07_mensagens_e_serializacao.md) |
|
||||||
| D7 | Plugins via JAR + Java SPI | Carregamento dinâmico sem recompilar o núcleo | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
| D7 | Plugins via JAR + Java SPI | Carregamento dinâmico sem recompilar o núcleo | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
| D8 | Classloader por JAR de plugin | Isolar dependências; desserialização com `PluginObjectInputStream` | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
| D8 | Classloader por JAR de plugin | Isolar dependências; desserialização com `PluginObjectInputStream` | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
|
| D9 | Plugins em `PITIUPI_HOME/plugins` | Dados do usuário fora do clone Git; `PITIUPI_HOME` ou `~/pitiupi` | [00_quickstart.md](documentation/00_quickstart.md) |
|
||||||
|
| D10 | Erros com `ErrorCode` | Mensagens PT na UI; `PitiupiException` no domínio | [13_guia_dev_e_agente.md](documentation/13_guia_dev_e_agente.md) |
|
||||||
|
| D11 | Reinício após instalar/remover JAR | Mutar `plugins/` exige reiniciar o app para recarregar SPI | [04_inicializacao_e_gui.md](documentation/04_inicializacao_e_gui.md) |
|
||||||
|
| D16 | UI em abas; sem `JFrame` de plugin | Conteúdo na `MainShellFrame`; galeria e peer actions abrem abas | [04_inicializacao_e_gui.md](documentation/04_inicializacao_e_gui.md) |
|
||||||
|
| D17 | Goodbye + timeout de presença | `GoodbyeMessage` em multicast ao fechar; timeout ~15 s mantido | [08_heartbeat_e_presenca.md](documentation/08_heartbeat_e_presenca.md) |
|
||||||
|
| D18 | Peer actions + convites | `PeerActionBroker`, `SessionInviteMessage`, badge (UI completa na shell) | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
|
| D19 | Chat unificado | Um JAR `plugins/chat` com sala geral, peer/IP e TCP privado; mpChat removido do catálogo | [11_plugins_referencia.md](documentation/11_plugins_referencia.md) |
|
||||||
|
| D20 | Peer actions por plugin no popup | Lista lateral agrupa ações de todos os plugins (submenus), não só a aba ativa | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
|
| D21 | `onShellTabClosed` | Fechar aba do plugin na shell notifica o plugin (ex.: parar screencast) | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||||
|
|
||||||
Novas decisões: adicionar linha nesta tabela e detalhar no arquivo técnico correspondente.
|
Novas decisões: adicionar linha nesta tabela e detalhar no arquivo técnico correspondente.
|
||||||
@@ -3,61 +3,45 @@
|
|||||||
## Pré-requisitos
|
## Pré-requisitos
|
||||||
|
|
||||||
- **JDK completo** (`java` e `javac`) — OpenJDK 25 recomendado; o núcleo usa `javac.source/target=17`, plugins Ant usam 25.
|
- **JDK completo** (`java` e `javac`) — OpenJDK 25 recomendado; o núcleo usa `javac.source/target=17`, plugins Ant usam 25.
|
||||||
- Apache Ant (`pitiupi`, `pitiupi.chat`, `pitiupi.screenCast`)
|
- Apache Ant (`pitiupi`, `plugins/chat`, `plugins/screencast`)
|
||||||
- GNU Make (`pitiupi.mpChat`)
|
|
||||||
|
|
||||||
Ubuntu / WSL:
|
Ubuntu / WSL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install openjdk-25-jdk ant make
|
sudo apt install openjdk-25-jdk ant
|
||||||
which javac ant make
|
which javac ant
|
||||||
```
|
```
|
||||||
|
|
||||||
Se `ant jar` falhar com *Cannot run program "javac"*, você instalou apenas JRE — instale `openjdk-*-jdk`.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Na raiz do clone:
|
Na raiz do clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi && ant jar
|
./scripts/pitiupi.sh build --all
|
||||||
cd ../pitiupi.chat && ant jar
|
|
||||||
cd ../pitiupi.screenCast && ant jar
|
|
||||||
cd ../pitiupi.mpChat && make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
O núcleo gera `pitiupi/dist/pitiupi.jar`. Cada plugin gera JAR em seu `dist/`.
|
Ou manualmente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd pitiupi && ant jar
|
||||||
|
cd plugins/chat && ant jar
|
||||||
|
cd plugins/screencast && ant jar
|
||||||
|
```
|
||||||
|
|
||||||
## Instalar plugins
|
## Instalar plugins
|
||||||
|
|
||||||
A aplicação lê JARs de `pitiupi/plugins/` (relativo ao diretório de trabalho ao executar o núcleo). Detalhes: [pitiupi/plugins/README.md](../../pitiupi/plugins/README.md).
|
Runtime: JARs em **`PITIUPI_HOME/plugins`** (padrão `~/pitiupi/plugins`).
|
||||||
|
|
||||||
Symlinks em desenvolvimento (Linux):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi/plugins
|
./scripts/pitiupi.sh link --all
|
||||||
ln -sf ../../pitiupi.chat/dist/pitiupi.chat.jar pitiupi.chat.jar
|
|
||||||
ln -sf ../../pitiupi.screenCast/dist/pitiupi.screenCast.jar pitiupi.screenCast.jar
|
|
||||||
ln -sf ../../pitiupi.mpChat/dist/mpChat.jar mpChat.jar
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Executar
|
## Executar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi
|
./scripts/pitiupi.sh run --all
|
||||||
java -jar dist/pitiupi.jar
|
|
||||||
```
|
```
|
||||||
|
|
||||||
No diálogo inicial informe **nome** e **porta** (UDP/TCP). Para duas instâncias no mesmo computador, use **portas diferentes** (ex.: 5000 e 5001).
|
Login com nome e porta; abra plugins pela aba **Plugins**. Clique em **Todos** ou em um peer na lista lateral para ver ações de cada plugin.
|
||||||
|
|
||||||
## Verificar P2P
|
Validação rápida do núcleo: `cd pitiupi && ant test`.
|
||||||
|
|
||||||
1. Abra duas instâncias (mesma rede ou mesmo host com portas distintas).
|
|
||||||
2. Confirme que os nomes aparecem no painel de usuários online.
|
|
||||||
3. Teste um plugin de referência (ex.: chat em `MenuCategory.Social`).
|
|
||||||
|
|
||||||
Problemas de multicast: [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md).
|
|
||||||
|
|
||||||
Gate de build (quatro módulos): [15_testes_e_gate.md](15_testes_e_gate.md).
|
|
||||||
|
|
||||||
Próximo passo para o TP: [10_tutorial_plugin.md](10_tutorial_plugin.md).
|
|
||||||
@@ -25,9 +25,9 @@ Exemplos de tema: chat temático, tabuleiro (xadrez/damas), compartilhamento de
|
|||||||
|
|
||||||
| Módulo | Ideia pedagógica |
|
| Módulo | Ideia pedagógica |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `pitiupi.chat` | Mensagens UDP + UI simples |
|
| `plugins/chat` | Mensagens UDP + UI simples |
|
||||||
| `pitiupi.screenCast` | Payload maior / streaming de imagem |
|
| `plugins/screencast` | Payload maior / streaming de imagem |
|
||||||
| `pitiupi.mpChat` | Multicast + TCP privado no mesmo plugin |
|
| `plugins/chat` | Multicast, unicast, TCP e canal TCP privado |
|
||||||
|
|
||||||
Detalhes: [11_plugins_referencia.md](11_plugins_referencia.md).
|
Detalhes: [11_plugins_referencia.md](11_plugins_referencia.md).
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
# Módulos do repositório
|
# Módulos do repositório
|
||||||
|
|
||||||
```
|
```
|
||||||
pitiupi/ # clone (este repositório)
|
pitiupi/ # clone (este repositório)
|
||||||
├── plugins.registry.json # catálogo de módulos (build/link via scripts/pitiupi.sh)
|
├── plugins.registry.json
|
||||||
├── scripts/pitiupi.sh
|
├── scripts/pitiupi.sh
|
||||||
├── pitiupi/ # núcleo P2P (executável)
|
├── pitiupi/ # núcleo P2P (executável)
|
||||||
├── pitiupi.chat/ # plugin referência — chat
|
├── plugins/
|
||||||
├── pitiupi.screenCast/ # plugin referência — captura de tela
|
│ ├── chat/ # chat unificado (UDP/multicast/TCP + TCP privado)
|
||||||
├── pitiupi.mpChat/ # plugin referência — chat multi-protocolo
|
│ ├── screencast/ # compartilhamento de tela
|
||||||
└── docs/ # documentação canônica
|
│ └── ping/ # exercício de aula (só doc)
|
||||||
|
└── docs/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Núcleo (`pitiupi/`)
|
## Núcleo (`pitiupi/`)
|
||||||
|
|
||||||
- **Build:** Apache Ant (`build.xml`, `nbproject/`).
|
- **Build:** Apache Ant.
|
||||||
- **Entrada:** `pitiupi.Main` → login → `MainWindow`.
|
- **Entrada:** `pitiupi.Main` → login → `MainShellFrame`.
|
||||||
- **Pacotes principais:** `pitiupi.net`, `pitiupi.control`, `pitiupi.GUI`, `pitiupi.models`.
|
- **Messaging:** `pitiupi.application.messaging.MessageDispatcher`.
|
||||||
|
|
||||||
## Plugins de referência
|
## Plugins de referência
|
||||||
|
|
||||||
| Pasta | Build | JAR típico | Classe SPI |
|
| Pasta | Build | JAR típico | SPI |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `pitiupi.chat` | `ant jar` | `dist/pitiupi.chat.jar` | `pitiupi.chat.plugin.ChatPlugin` |
|
| `plugins/chat` | `ant jar` | `dist/pitiupi.chat.jar` | `pitiupi.chat.plugin.ChatPlugin` |
|
||||||
| `pitiupi.screenCast` | `ant jar` | `dist/pitiupi.screenCast.jar` | `pitiupi.screenCast.plugin.ScreenCastPlugin` |
|
| `plugins/screencast` | `ant jar` | `dist/pitiupi.screenCast.jar` | `pitiupi.screenCast.plugin.ScreenCastPlugin` |
|
||||||
| `pitiupi.mpChat` | `make` | `dist/mpChat.jar` | `plugin.MultiProtocolChat` |
|
|
||||||
|
|
||||||
Todos dependem do JAR do núcleo no classpath de compilação.
|
Ver [10_tutorial_plugin.md](10_tutorial_plugin.md) e [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md).
|
||||||
|
|
||||||
## O que o aluno cria
|
|
||||||
|
|
||||||
Novo diretório irmão, por exemplo `pitiupi.xadrez/`, copiando a estrutura de `pitiupi.chat` (Ant + `META-INF/services`). Registre o módulo em `plugins.registry.json` e use `MenuCategory` adequado no código.
|
|
||||||
|
|
||||||
Não é obrigatório commitar o plugin do aluno neste repositório base; o professor pode definir fork ou entrega em zip.
|
|
||||||
|
|
||||||
Ver [03_build_ant_netbeans.md](03_build_ant_netbeans.md) e [10_tutorial_plugin.md](10_tutorial_plugin.md).
|
|
||||||
@@ -1,49 +1,17 @@
|
|||||||
# Build — Ant, NetBeans e Make
|
# Build — Ant e NetBeans
|
||||||
|
|
||||||
## Pré-requisitos e versões Java
|
| Módulo | Build | Java |
|
||||||
|
|
||||||
| Módulo | Build | `javac.source` / `target` (NetBeans) |
|
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `pitiupi` | Ant | 17 |
|
| `pitiupi` | Ant | 17 |
|
||||||
| `pitiupi.chat` | Ant | 25 |
|
| `plugins/chat` | Ant | 25 |
|
||||||
| `pitiupi.screenCast` | Ant | 25 |
|
| `plugins/screencast` | Ant | 25 |
|
||||||
| `pitiupi.mpChat` | Make | (sem `project.properties`; compila contra `pitiupi.jar`) |
|
|
||||||
|
|
||||||
Use um JDK que forneça `javac` (ex.: OpenJDK 25). Ant 1.10+ e Make no PATH.
|
|
||||||
|
|
||||||
## Núcleo e plugins NetBeans
|
|
||||||
|
|
||||||
Projetos com `nbproject/` e `build.xml`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi && ant jar
|
cd pitiupi && ant jar
|
||||||
cd pitiupi.chat && ant jar
|
cd plugins/chat && ant jar
|
||||||
cd pitiupi.screenCast && ant jar
|
cd plugins/screencast && ant jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Abrir no NetBeans: **File → Open Project** em cada pasta.
|
Classpath dos plugins: `../../pitiupi/dist/pitiupi.jar`.
|
||||||
|
|
||||||
O target `jar` produz o executável do núcleo e os JARs dos plugins. O classpath de compilação dos plugins aponta para `../pitiupi/dist/pitiupi.jar` (ver `project.properties` de cada módulo).
|
Catálogo: [`plugins.registry.json`](../../plugins.registry.json).
|
||||||
|
|
||||||
## pitiupi.mpChat (Makefile)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd pitiupi.mpChat
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
O Makefile compila com `javac -cp ../pitiupi/dist/pitiupi.jar`, copia `META-INF` e gera `dist/mpChat.jar`. **Compile o núcleo antes.**
|
|
||||||
|
|
||||||
## Diretório `plugins/`
|
|
||||||
|
|
||||||
A pasta `pitiupi/plugins/` contém os JARs carregados em runtime. Apenas arquivos `.jar` são lidos. Ver [pitiupi/plugins/README.md](../../pitiupi/plugins/README.md).
|
|
||||||
|
|
||||||
## Ordem recomendada de build
|
|
||||||
|
|
||||||
1. `pitiupi` (núcleo)
|
|
||||||
2. Plugins em qualquer ordem
|
|
||||||
3. Symlink ou cópia para `pitiupi/plugins/`
|
|
||||||
|
|
||||||
## Gate automatizado (docentes)
|
|
||||||
|
|
||||||
Quem mantém o meta-workspace Cursor pode usar o script de gate que executa `ant jar` nos três módulos Ant e `make` no mpChat (requer Ant/Make no PATH). Detalhes em [15_testes_e_gate.md](15_testes_e_gate.md).
|
|
||||||
@@ -20,38 +20,78 @@ distribuídas e desenvolvimento de projetos acadêmicos.
|
|||||||
|
|
||||||
A aplicação é dividida em três grupos principais:
|
A aplicação é dividida em três grupos principais:
|
||||||
|
|
||||||
- **Núcleo da aplicação**: a `MainWindow` coordena GUI, rede e plugins.
|
- **Núcleo da aplicação**: a `MainShellFrame` coordena GUI, rede e plugins.
|
||||||
- **Sistema de plugins**: extensões em JAR carregadas de `pitiupi/plugins/`.
|
- **Sistema de plugins**: extensões em JAR carregadas de `PITIUPI_HOME/plugins` (dev: `./scripts/pitiupi.sh link --all`).
|
||||||
- **Comunicação de rede**: UDP (`SocketUDP`) e TCP (`SocketTCP`, `TcpConnection`).
|
- **Comunicação de rede**: UDP (`SocketUDP`) e TCP (`SocketTCP`, `TcpConnection`).
|
||||||
|
|
||||||
## Inicialização da aplicação
|
## Inicialização da aplicação
|
||||||
|
|
||||||
1. A `MainWindow` é criada após o diálogo de login (nome e porta).
|
1. `Main` garante o layout em `PITIUPI_HOME` e abre o `LoginDialog` (nome e porta, PT-BR, ~30% da área útil do monitor, centralizado).
|
||||||
2. O `SocketUDP` associa-se à porta configurada e ingressa no grupo multicast.
|
2. Após confirmar o login, a `MainShellFrame` é exibida (1024×768, centralizada).
|
||||||
3. O `SocketTCP` cria um `ServerSocket` na mesma porta para conexões TCP.
|
3. O `SocketUDP` associa-se à porta configurada e ingressa no grupo multicast.
|
||||||
4. Os mecanismos de comunicação iniciam threads de recepção.
|
4. O `SocketTCP` cria um `ServerSocket` na mesma porta para conexões TCP.
|
||||||
5. O `HeartbeatManager` envia heartbeats periodicamente.
|
5. Os mecanismos de comunicação iniciam threads de recepção.
|
||||||
6. O painel de usuários online registra-se como listener do heartbeat.
|
6. O `HeartbeatManager` envia heartbeats periodicamente; o `NetworkUsersPanel` lista peers via `PresenceService`.
|
||||||
7. O `PluginManager` carrega os JARs em `plugins/` via SPI.
|
7. O `PluginManager` carrega os JARs em `plugins/` via SPI, com `ShellPluginWorkspace` ligado às abas centrais.
|
||||||
|
|
||||||
Ver também: [05_udp_multicast.md](05_udp_multicast.md), [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
Ver também: [05_udp_multicast.md](05_udp_multicast.md), [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
||||||
|
|
||||||
|
### Teste manual T-LOGIN-01 (login centralizado)
|
||||||
|
|
||||||
|
1. Compilar e executar: `./scripts/pitiupi.sh run --all` na raiz do repositório.
|
||||||
|
2. O `LoginDialog` deve aparecer **no centro** do monitor principal (WSL/KDE), não no canto superior esquerdo.
|
||||||
|
3. Após entrar, a `MainShellFrame` (1024×768) também deve abrir centralizada.
|
||||||
|
|
||||||
|
A posição exata depende do gerenciador de janelas; o núcleo recentra após exibir a janela.
|
||||||
|
|
||||||
|
### Atualizar plugin Chat (Onda E)
|
||||||
|
|
||||||
|
O JAR do módulo é `plugins/chat/dist/pitiupi.chat.jar` (catálogo `plugins.registry.json`). Não use `dist/chat.jar`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd plugins/chat && ant clean jar
|
||||||
|
./scripts/pitiupi.sh link --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso recria o symlink `~/pitiupi/plugins/chat.jar` → o JAR buildado. O comando `link` lista o destino real de cada `.jar`.
|
||||||
|
|
||||||
# Interface gráfica
|
# Interface gráfica
|
||||||
|
|
||||||
A interface gráfica da aplicação é baseada em Swing e é centralizada na
|
A interface gráfica é Swing, organizada em shell (`pitiupi.presentation.shell`):
|
||||||
classe `MainWindow`.
|
|
||||||
|
|
||||||
Durante a inicialização, cada plugin recebe `ConnectionManager` e `GuiManager`
|
| Região | Componente |
|
||||||
no método `createPlugin()`.
|
|--------|------------|
|
||||||
|
| Centro | `TabWorkspacePanel` — aba **Plugins** (galeria; abre plugin sob demanda), abas de plugin **fecháveis** (×), aba **Configurações** |
|
||||||
|
| Leste | `NetworkUsersPanel` — “Usuários nesta rede”, badges de convite, peer actions |
|
||||||
|
| Sul | Barra de status (porta e nome local) |
|
||||||
|
| Menu | `ShellMenuBar` — apenas **Ajuda → Sobre** (sair pelo X da janela) |
|
||||||
|
|
||||||
A extensão usual é registrar menus por categoria (`MenuCategory`). Ver
|
Plugins expõem `getName()` (técnico), `getIdentifier()` (SPI) e `getDisplayName()` (texto na galeria e nas abas).
|
||||||
[12_menus_unicast_multicast.md](12_menus_unicast_multicast.md).
|
|
||||||
|
A aba **Configurações** mostra em cima **Sua conexão** (nome, porta, IP local, ID da instância, contagem de peers; campos selecionáveis para copiar manualmente) e embaixo a tabela de JARs instalados (altura reduzida).
|
||||||
|
|
||||||
|
Durante a inicialização, cada plugin recebe `ConnectionManager` e `PluginWorkspace`
|
||||||
|
no método `createPlugin()`. Menus multicast na barra superior não são mais usados;
|
||||||
|
ações para todos os peers migram para **peer actions** (contexto na lista lateral).
|
||||||
|
Ao iniciar, só as abas **Plugins** e **Configurações** ficam visíveis. Cada plugin registra um painel com `bindShellWorkspaceContent`; a aba abre ao clicar na galeria ou via peer action. Referência: **Conversa na rede** (`ChatPanel`) e **Compartilhar tela** (`ScreenCastPanel`).
|
||||||
|
|
||||||
|
O **chat** usa campo de mensagem compacto (~2 linhas, quebra de linha; barra de rolagem vertical só se o texto não couber). O histórico ocupa a maior parte da altura da aba (divisor proporcional ao abrir a janela).
|
||||||
|
|
||||||
|
Canal TCP privado do chat fica no mesmo `ChatPanel` (histórico separado por conversa: sala geral, peer por IP, privado com toggle). A lista lateral usa `PeerActivityInbox` (bolinha vermelha e contagem; no menu, ● na ação correspondente).
|
||||||
|
|
||||||
|
Ao fechar a aba superior de **Compartilhar tela** (×), o núcleo chama `Plugin.onShellTabClosed()` — o plugin interrompe captura e envio (toggle desligado). Reabrir pela galeria deve mostrar compartilhamento inativo.
|
||||||
|
|
||||||
|
**TCP (transporte)** envia uma mensagem serializada pelo núcleo (`sendTcp`), na porta do login. **TCP privado** (toggle) abre conexão dedicada ao plugin entre dois peers; com o toggle ativo, o combo de transporte fica desabilitado e o envio usa esse canal. Na UI só se informa o **IP** do peer: UDP/TCP unicast do Pitiupi usa sempre a **mesma porta** que você escolheu no login (convenção de sala: todos com a mesma porta, ex. 5000 e 5001 em testes no mesmo PC).
|
||||||
|
|
||||||
|
Textos da UI (destino, combo de forma de envio e linha **“Como esta mensagem vai”**) usam linguagem simples e o termo técnico entre parênteses (multicast UDP, UDP unicast, TCP). Na sala, o transporte não é só multicast: o combo pode escolher cópias unicast ou TCP para cada pessoa, conforme o plugin.
|
||||||
|
|
||||||
|
Ver [12_menus_unicast_multicast.md](12_menus_unicast_multicast.md).
|
||||||
|
|
||||||
## Diagrama de comunicação
|
## Diagrama de comunicação
|
||||||
|
|
||||||
```
|
```
|
||||||
+------------------+
|
+------------------+
|
||||||
| MainWindow |
|
| MainShellFrame |
|
||||||
+--------+---------+
|
+--------+---------+
|
||||||
|
|
|
|
||||||
+--------------------+--------------------+
|
+--------------------+--------------------+
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ Métodos obrigatórios:
|
|||||||
| Método | Função |
|
| Método | Função |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `getName()`, `getAuthor()`, `getVersion()`, `getIdentifier()` | Identificação (o `identifier` distingue plugins em conexões TCP dedicadas) |
|
| `getName()`, `getAuthor()`, `getVersion()`, `getIdentifier()` | Identificação (o `identifier` distingue plugins em conexões TCP dedicadas) |
|
||||||
| `createPlugin(ConnectionManager, GuiManager)` | Inicialização: menus, janelas, uso da rede |
|
| `getDisplayName()` | Texto na galeria e título de aba (padrão: `getName()`) |
|
||||||
|
| `createPlugin(ConnectionManager, PluginWorkspace)` | Inicialização na shell: painel na aba, peer actions, rede |
|
||||||
|
| `createPlugin(ConnectionManager, GuiManager)` | Legado — adapter na carga; não usar em plugins novos |
|
||||||
| `getMessages()` | Lista de classes `Message` que o plugin desserializa |
|
| `getMessages()` | Lista de classes `Message` que o plugin desserializa |
|
||||||
| `receiveMessage(Message)` | Processamento após o `PluginManager` identificar o tipo |
|
| `receiveMessage(Message)` | Processamento após o `PluginManager` identificar o tipo |
|
||||||
|
|
||||||
Métodos opcionais (TCP avançado):
|
Métodos opcionais (ciclo de vida e TCP avançado):
|
||||||
|
|
||||||
| Método | Função |
|
| Método | Função |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `onShellTabClosed()` | Usuário fechou a aba do plugin no topo; liberar timers, captura, envio contínuo |
|
||||||
| `receiveAllTypes()` | Receber todas as mensagens públicas |
|
| `receiveAllTypes()` | Receber todas as mensagens públicas |
|
||||||
| `receivePrivateConnection(TcpConnection)` | Aceitar conexão TCP direcionada ao plugin |
|
| `receivePrivateConnection(TcpConnection)` | Aceitar conexão TCP direcionada ao plugin |
|
||||||
| `receiveBytes(TcpConnection, byte[])` | Ler payload bruto na conexão privada |
|
| `receiveBytes(TcpConnection, byte[])` | Ler payload bruto na conexão privada |
|
||||||
@@ -50,11 +53,27 @@ e colocados no diretório plugins da aplicação.
|
|||||||
|
|
||||||
### Carregamento de plugins
|
### Carregamento de plugins
|
||||||
|
|
||||||
O `PluginManager.loadPlugins()` (chamado pela `MainWindow`):
|
O `PluginManager.loadPlugins()` (chamado pela `MainShellFrame`):
|
||||||
|
|
||||||
1. Lista arquivos `.jar` em `pitiupi/plugins/` (diretório de trabalho = pasta do núcleo).
|
1. Lista arquivos `.jar` em `PITIUPI_HOME/plugins` (ver [00_quickstart.md](00_quickstart.md); symlinks de dev via `./scripts/pitiupi.sh link`).
|
||||||
2. Para cada JAR, cria um `URLClassLoader` e usa `ServiceLoader.load(Plugin.class, classLoader)`.
|
2. Para cada JAR, cria um `URLClassLoader` e usa `ServiceLoader.load(Plugin.class, classLoader)`.
|
||||||
3. Chama `createPlugin(connectionManager, guiManager)` em cada instância.
|
3. Chama `createPlugin(connectionManager, pluginWorkspace)` em cada instância (`ShellPluginWorkspace`).
|
||||||
4. Na recepção UDP/TCP, tenta desserializar com `PluginObjectInputStream` e despacha para plugins que declararam o tipo em `getMessages()`.
|
4. Na recepção UDP/TCP, tenta desserializar com `PluginObjectInputStream` e despacha para plugins que declararam o tipo em `getMessages()`.
|
||||||
|
|
||||||
|
Ao fechar uma aba dinâmica de plugin, `TabWorkspacePanel` notifica `PluginManager.notifyShellTabClosed(pluginId)` **antes** de remover o conteúdo, para o plugin encerrar trabalho em segundo plano.
|
||||||
|
|
||||||
Implementação: `pitiupi/src/pitiupi/control/plugins/PluginManager.java`.
|
Implementação: `pitiupi/src/pitiupi/control/plugins/PluginManager.java`.
|
||||||
|
|
||||||
|
## Shell e abas (PluginWorkspace)
|
||||||
|
|
||||||
|
Na `MainShellFrame`, `createPlugin(ConnectionManager, PluginWorkspace)` registra o painel com `bindShellWorkspaceContent(JPanel)` **sem** abrir aba no boot. A galeria **Plugins** e as peer actions abrem a aba sob demanda.
|
||||||
|
|
||||||
|
## Peer actions na lista lateral
|
||||||
|
|
||||||
|
`PeerActionBroker.getAllActionsFor(target)` agrupa ações de **todos** os plugins (submenu por `getDisplayName()`). Destinos: `SINGLE_PEER` (um usuário) ou `ALL_PEERS` (linha **Todos**).
|
||||||
|
|
||||||
|
## Envio didático (núcleo)
|
||||||
|
|
||||||
|
`PluginRoutingPanel` (núcleo) padroniza destino, forma de envio e dica embaixo para chat, screencast e futuros plugins.
|
||||||
|
|
||||||
|
`MessageDispatcher` envia com `TransportMode` e `SendTarget`. `PluginTransportCatalog` define quais transportes cada plugin mostra na UI (sala geral vs um IP). Os combos usam textos didáticos (`describeForUi`) com o termo técnico entre parênteses (multicast UDP, UDP unicast, TCP). Na sala geral o campo IP fica desativado; um resumo **“Como esta mensagem vai”** (chat) deixa explícito o transporte escolhido — na sala pode ser multicast ou cópias unicast/TCP para cada pessoa, conforme o plugin. Para um peer use o IP manualmente ou a peer action na lista lateral.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# Tutorial — criar um plugin
|
# Tutorial — criar um plugin
|
||||||
|
|
||||||
Este guia complementa [09_sistema_de_plugins.md](09_sistema_de_plugins.md). Use `pitiupi.chat` como modelo.
|
Este guia complementa [09_sistema_de_plugins.md](09_sistema_de_plugins.md). Use `plugins/chat` como modelo.
|
||||||
|
|
||||||
## 1. Novo projeto
|
## 1. Novo projeto
|
||||||
|
|
||||||
- Copie a pasta `pitiupi.chat` para `pitiupi.meuPlugin` (nome à escolha).
|
- Copie a pasta `plugins/chat` para `plugins/meuPlugin` (nome à escolha).
|
||||||
- Ajuste `project.properties` / `build.xml` com o nome do JAR de saída.
|
- Ajuste `project.properties` / `build.xml` com o nome do JAR de saída.
|
||||||
- Mantenha `javac.classpath` apontando para `../pitiupi/dist/pitiupi.jar`.
|
- Mantenha `javac.classpath` apontando para `../pitiupi/dist/pitiupi.jar`.
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ Duas instâncias, mesma rede, portas distintas se no mesmo host. Ver [00_quickst
|
|||||||
|
|
||||||
## TCP dedicado (avançado)
|
## TCP dedicado (avançado)
|
||||||
|
|
||||||
Se o plugin precisa de canal TCP com handshake por `getIdentifier()`, estude `pitiupi.mpChat` e os métodos opcionais em `Plugin` (`receivePrivateConnection`, `receiveBytes`). Ver [11_plugins_referencia.md](11_plugins_referencia.md).
|
Se o plugin precisa de canal TCP com handshake por `getIdentifier()`, estude `plugins/chat` (`receivePrivateConnection`, `receiveBytes`). Use `MessageDispatcher` para envios didáticos. Ver [11_plugins_referencia.md](11_plugins_referencia.md).
|
||||||
|
|
||||||
## Erros frequentes
|
## Erros frequentes
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,31 @@
|
|||||||
# Plugins de referência
|
# Plugins de referência
|
||||||
|
|
||||||
Três exemplos no repositório para comparar estilos de integração. Todos devem ser empacotados em `pitiupi/plugins/`.
|
Dois módulos buildáveis no catálogo; o chat unifica o antigo mpChat (multicast, unicast, TCP e canal TCP privado).
|
||||||
|
|
||||||
## Comparativo
|
## Comparativo
|
||||||
|
|
||||||
| | pitiupi.chat | pitiupi.screenCast | pitiupi.mpChat |
|
| | plugins/chat | plugins/screencast |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Build | Ant | Ant | Make |
|
| Build | Ant | Ant |
|
||||||
| id (`plugins.registry.json`) | `chat` | `screencast` | `mpchat` |
|
| id | `chat` | `screencast` |
|
||||||
| Classe | `ChatPlugin` | `ScreenCastPlugin` | `MultiProtocolChat` |
|
| Classe | `ChatPlugin` | `ScreenCastPlugin` |
|
||||||
| Categoria menu | `Social` | `Streaming` | `Social` (multicast + unicast) |
|
| Mensagem | `ChatMessage`, `LegacyMpChatMessage` | `ScreenCastMessage` |
|
||||||
| Mensagem | `ChatMessage` | `ScreenCastMessage` | `MpChatMessage` |
|
| UI | `ChatPanel` (destino + transporte) | `ScreenCastPanel` (destino + transporte) |
|
||||||
| Transporte principal | UDP (via `ConnectionManager`) | UDP / payloads maiores | UDP público + **TCP privado** |
|
| TCP privado | Toggle no `ChatPanel` + hooks `receiveBytes` (histórico `private:<IP>`) | — |
|
||||||
| TCP avançado | Não | Depende da versão do módulo | `receivePrivateConnection`, `receiveBytes` |
|
|
||||||
|
|
||||||
## pitiupi.chat
|
## plugins/chat
|
||||||
|
|
||||||
- **Referência canônica** alinhada ao contrato atual `Plugin` + `ConnectionManager` / `GuiManager`.
|
- Sala geral ou um peer (lista + IP).
|
||||||
- `createPlugin` registra `ChatMenu` em `MenuCategory.Social`.
|
- Transportes: multicast UDP, unicast UDP, TCP via `MessageDispatcher`.
|
||||||
- `getMessages()` retorna `ChatMessage.class`; `receiveMessage` atualiza `ChatWindow`.
|
- Peer actions: abrir sala geral; conversar com peer; canal TCP privado.
|
||||||
|
- Caminho: `plugins/chat/src/pitiupi/chat/plugin/ChatPlugin.java`.
|
||||||
|
|
||||||
Caminhos: `pitiupi.chat/src/pitiupi/chat/plugin/ChatPlugin.java`.
|
## plugins/screencast
|
||||||
|
|
||||||
## pitiupi.screenCast
|
- Compartilhamento contínuo com destino geral (multicast recomendado) ou um peer (unicast/TCP).
|
||||||
|
- Peer actions: compartilhar na rede; compartilhar com este peer.
|
||||||
|
- Caminho: `plugins/screencast/src/pitiupi/screenCast/plugin/ScreenCastPlugin.java`.
|
||||||
|
|
||||||
- Demonstra compartilhamento de tela / imagens (`ScreenCastWindow`, `ScreenCastMessage`).
|
## Exercício Ping
|
||||||
- Útil para discutir **tamanho de datagramas**, compressão (`ImageTransformation`) e limites UDP.
|
|
||||||
|
|
||||||
Caminhos: `pitiupi.screenCast/src/pitiupi/screenCast/`. Alinhado ao contrato `Plugin` + `ConnectionManager` / `GuiManager` (como `ChatPlugin`).
|
Material em [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md) e pasta `plugins/ping/` (sem JAR no repositório).
|
||||||
|
|
||||||
## pitiupi.mpChat
|
|
||||||
|
|
||||||
- Chat **público** (multicast / unicast UDP) e **privado** (TCP).
|
|
||||||
- `createPlugin(ConnectionManager, GuiManager)` como nos demais plugins; usa `ConnectionManager.connectPluginTcp` para abrir canal privado.
|
|
||||||
- Sobrescreve hooks TCP do `Plugin` (`receivePrivateConnection`, `receiveBytes`, `onSocketClosed`).
|
|
||||||
|
|
||||||
Caminhos: `pitiupi.mpChat/src/plugin/MultiProtocolChat.java`.
|
|
||||||
|
|
||||||
## Fluxo UDP (todos os plugins)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant App as MainWindow
|
|
||||||
participant UDP as SocketUDP
|
|
||||||
participant HB as HeartbeatManager
|
|
||||||
participant PM as PluginManager
|
|
||||||
participant Peer as OutroPeer
|
|
||||||
App->>UDP: join multicast
|
|
||||||
UDP->>HB: HeartbeatMessage
|
|
||||||
Peer->>UDP: Message plugin
|
|
||||||
UDP->>PM: handleMessage bytes
|
|
||||||
PM->>PM: deserialize + dispatch
|
|
||||||
```
|
|
||||||
|
|
||||||
Heartbeat não passa pelos plugins — ver [08_heartbeat_e_presenca.md](08_heartbeat_e_presenca.md).
|
|
||||||
|
|
||||||
## Próximo passo
|
|
||||||
|
|
||||||
Criar plugin próprio: [10_tutorial_plugin.md](10_tutorial_plugin.md).
|
|
||||||
@@ -1,35 +1,34 @@
|
|||||||
# Menus — unicast e multicast
|
# Menus, peer actions e transportes
|
||||||
|
|
||||||
## MenuCategory
|
## Interface atual (shell v1)
|
||||||
|
|
||||||
Enum em `pitiupi.models.Menus.MenuCategory`:
|
Plugins de referência **não** dependem da barra de menus superior para o fluxo principal:
|
||||||
|
|
||||||
| Valor | Uso sugerido |
|
- **Galeria** na aba **Plugins** abre cada extensão em aba própria (fechável com ×).
|
||||||
| --- | --- |
|
- **Peer actions** na lista **Usuários nesta rede** (clique em um peer ou em **Todos**) agrupam ações de todos os plugins instalados.
|
||||||
| `Jogos` | Plugins de jogos entre peers |
|
- Dentro da aba do plugin, **`PluginRoutingPanel`** padroniza destino (sala geral vs IP), forma de envio (multicast UDP, UDP unicast, TCP) e texto de ajuda.
|
||||||
| `Social` | Chat, presença, mensagens sociais |
|
|
||||||
| `Streaming` | Screen cast, mídia |
|
|
||||||
|
|
||||||
## Registrar menu (padrão atual)
|
Ver [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md) e [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
||||||
|
|
||||||
Via `GuiManager.createMenu(MenuCategory category, Menu menu)` — ver `ChatPlugin`:
|
## MenuCategory (legado)
|
||||||
|
|
||||||
```java
|
Enum em `pitiupi.models.Menus.MenuCategory` (`Jogos`, `Social`, `Streaming`) ainda aparece no **catálogo** (`menuCategory` em `plugins.registry.json`) e em plugins antigos que chamam `GuiManager.createMenu(...)`.
|
||||||
guiManager.createMenu(MenuCategory.Social, new ChatMenu(chatWindow));
|
|
||||||
```
|
|
||||||
|
|
||||||
A hierarquia de menus unicast/multicast do núcleo (`UnicastMenu`, `MulticastMenu`, etc.) permite ações por peer ou broadcast; plugins de referência encapsulam isso em classes `*Menu` próprias.
|
Novos plugins devem preferir `PluginWorkspace` + peer actions. O menu da shell expõe apenas **Ajuda → Sobre**.
|
||||||
|
|
||||||
## Unicast vs multicast na aplicação
|
## Unicast vs multicast na aplicação
|
||||||
|
|
||||||
| Modo | API típica | Semântica |
|
| Modo | API típica | Semântica |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Multicast | `ConnectionManager` → `SocketUDP.sendMulticast` | Todos no grupo na porta configurada |
|
| Multicast | `MessageDispatcher` / `SocketUDP.sendMulticast` | Todos no grupo na porta configurada |
|
||||||
| Unicast UDP | envio para IP de um peer | Um destinatário, sem estabelecer TCP |
|
| Unicast UDP | envio para IP de um peer | Um destinatário, sem estabelecer TCP |
|
||||||
| TCP | `ConnectionManager` / `SocketTCP` | Canal persistente, framing 4+N bytes |
|
| TCP | `ConnectionManager` / `SocketTCP` | Canal com framing 4+N bytes |
|
||||||
|
|
||||||
Detalhes de rede: [05_udp_multicast.md](05_udp_multicast.md), [06_tcp_conexoes.md](06_tcp_conexoes.md).
|
Detalhes de rede: [05_udp_multicast.md](05_udp_multicast.md), [06_tcp_conexoes.md](06_tcp_conexoes.md).
|
||||||
|
|
||||||
## mpChat
|
## Referência de código
|
||||||
|
|
||||||
`MulticastMpChatMenu` e `UnicastMpChatMenu` mostram como expor as duas modalidades no mesmo plugin — [11_plugins_referencia.md](11_plugins_referencia.md).
|
- Chat e screencast: peer actions em `ChatPlugin` e `ScreenCastPlugin`.
|
||||||
|
- Envio unificado: `pitiupi.application.messaging.MessageDispatcher` e `TransportMode`.
|
||||||
|
|
||||||
|
O antigo plugin **mpChat** foi unificado no módulo `plugins/chat` (mensagens `LegacyMpChatMessage` para compatibilidade TCP privado). Ver [11_plugins_referencia.md](11_plugins_referencia.md) e decisão D19 no [decision-register.md](../decision-register.md).
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Guia para desenvolvedor e agente
|
# Guia para desenvolvedor e agente
|
||||||
|
|
||||||
|
Esta documentação descreve **apenas** a ferramenta Pitiupi neste repositório: build, rede, plugins e convenções de código. Quem clona o projeto não precisa de pastas ou ferramentas de meta-desenvolvimento externas; mantenha referências técnicas e tutoriais limitados ao que está em `docs/` e no código do próprio repo.
|
||||||
|
|
||||||
## Leitura mínima antes de mudar código
|
## Leitura mínima antes de mudar código
|
||||||
|
|
||||||
1. [00_quickstart.md](00_quickstart.md) — build e execução
|
1. [00_quickstart.md](00_quickstart.md) — build e execução
|
||||||
@@ -19,16 +21,17 @@
|
|||||||
|
|
||||||
## Gate de build
|
## Gate de build
|
||||||
|
|
||||||
Compilar núcleo e os três plugins de referência:
|
Compilar núcleo e os dois plugins do catálogo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi && ant jar
|
cd pitiupi && ant jar && ant test
|
||||||
cd ../pitiupi.chat && ant jar
|
cd plugins/chat && ant jar
|
||||||
cd ../pitiupi.screenCast && ant jar
|
cd plugins/screencast && ant jar
|
||||||
cd ../pitiupi.mpChat && make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Requer JDK, Ant e Make no PATH. Teste manual em rede: [15_testes_e_gate.md](15_testes_e_gate.md).
|
Ou na raiz do clone: `./scripts/pitiupi.sh build --all`.
|
||||||
|
|
||||||
|
Requer JDK e Ant no PATH. Teste manual em rede: [15_testes_e_gate.md](15_testes_e_gate.md).
|
||||||
|
|
||||||
## Convenções
|
## Convenções
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
| Comportamento | Pacote / classe |
|
| Comportamento | Pacote / classe |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Login, porta, nome | `pitiupi.GUI.Login` |
|
| Login, porta, nome | `pitiupi.presentation.shell.LoginDialog` |
|
||||||
| Orquestração GUI + plugins | `pitiupi.GUI.MainWindow` |
|
| Shell principal (abas, galeria) | `pitiupi.presentation.shell.MainShellFrame`, `TabWorkspacePanel` |
|
||||||
| Barra de menus base | `pitiupi.GUI.MenuBar` |
|
| Menu Ajuda | `pitiupi.presentation.shell.ShellMenuBar` |
|
||||||
| Usuários online | `pitiupi.GUI.OnlineUsersPanel`, `HeartbeatManager` |
|
| Usuários online, peer actions | `pitiupi.presentation.shell.NetworkUsersPanel`, `PeerActionBroker` |
|
||||||
| Facade rede para plugins | `pitiupi.control.ConnectionManager` |
|
| Facade rede para plugins | `pitiupi.control.ConnectionManager` |
|
||||||
| Registro de menus | `pitiupi.control.GuiManager` |
|
| Workspace na aba (SPI) | `pitiupi.api.plugin.PluginWorkspace`, `ShellPluginWorkspace` |
|
||||||
|
| Menus legados (adapter) | `pitiupi.control.GuiManager` — deprecado; preferir peer actions |
|
||||||
| UDP | `pitiupi.net.udp.SocketUDP` |
|
| UDP | `pitiupi.net.udp.SocketUDP` |
|
||||||
| TCP servidor e mapa de conexões | `pitiupi.net.tcp.SocketTCP`, `TcpConnectionManager` |
|
| TCP servidor e mapa de conexões | `pitiupi.net.tcp.SocketTCP`, `TcpConnectionManager` |
|
||||||
| Conexão TCP por peer | `pitiupi.net.tcp.TcpConnection` |
|
| Conexão TCP por peer | `pitiupi.net.tcp.TcpConnection` |
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
```
|
```
|
||||||
pitiupi (núcleo)
|
pitiupi (núcleo)
|
||||||
↑ classpath
|
↑ classpath
|
||||||
pitiupi.chat | pitiupi.screenCast | pitiupi.mpChat
|
plugins/chat | plugins/screencast
|
||||||
```
|
```
|
||||||
|
|
||||||
Plugins **não** devem ser dependência do núcleo.
|
Plugins **não** devem ser dependência do núcleo.
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# Testes e gate
|
# Testes e gate
|
||||||
|
|
||||||
## Estado atual
|
## Gate automatizado (núcleo)
|
||||||
|
|
||||||
Não há suíte JUnit integrada ao repositório. A validação principal é:
|
O módulo `pitiupi/` inclui testes JUnit:
|
||||||
|
|
||||||
1. **Build** de todos os módulos.
|
```bash
|
||||||
2. **Teste manual** com duas instâncias na rede.
|
cd pitiupi && ant test
|
||||||
|
```
|
||||||
|
|
||||||
## Gate de build (recomendado)
|
Falha se `java`, `javac` ou Ant estiverem ausentes.
|
||||||
|
|
||||||
Na raiz do clone (todos os módulos do catálogo):
|
## Gate de build (produto)
|
||||||
|
|
||||||
|
Na raiz do clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/pitiupi.sh build --all
|
./scripts/pitiupi.sh build --all
|
||||||
@@ -19,26 +22,41 @@ Equivalente manual:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi && ant jar
|
cd pitiupi && ant jar
|
||||||
cd ../pitiupi.chat && ant jar
|
cd plugins/chat && ant jar
|
||||||
cd ../pitiupi.screenCast && ant jar
|
cd plugins/screencast && ant jar
|
||||||
cd ../pitiupi.mpChat && make
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Falha se `java`, **`javac`**, Ant, Make ou `python3` estiverem ausentes (instale `openjdk-*-jdk`, não só JRE).
|
Depois, para runtime local:
|
||||||
|
|
||||||
## Checklist manual (rede)
|
```bash
|
||||||
|
./scripts/pitiupi.sh link --all
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] Duas instâncias com portas distintas (mesmo host) ou dois hosts na mesma LAN
|
## Checklist manual — uma máquina (5000 / 5001)
|
||||||
- [ ] Nomes visíveis no painel de usuários em até ~15 s
|
|
||||||
- [ ] Plugin de referência troca mensagens entre os dois
|
- [ ] Duas instâncias com portas distintas; nomes diferentes
|
||||||
- [ ] Encerrar uma instância remove o peer da lista após timeout de heartbeat
|
- [ ] Peers visíveis na lista em até ~15 s
|
||||||
|
- [ ] Chat: sala geral e conversa por IP; tags de transporte coloridas no histórico
|
||||||
|
- [ ] ScreenCast: abrir aba, destino sala ou peer; fechar aba com × interrompe compartilhamento
|
||||||
|
- [ ] Compose do chat sem barra de rolagem vazio em janela ~1024×768
|
||||||
|
- [ ] Encerrar uma instância remove o peer após timeout de heartbeat
|
||||||
|
|
||||||
|
## Checklist manual — dois PCs (mesma Wi‑Fi)
|
||||||
|
|
||||||
|
Mesma **porta** em ambos (ex. 5000), **mesmos JARs** em `~/pitiupi/plugins` em cada máquina:
|
||||||
|
|
||||||
|
- [ ] Presença e linha **Todos**
|
||||||
|
- [ ] Troca de mensagens no chat (multicast e, se possível, unicast/TCP)
|
||||||
|
- [ ] Compartilhamento de tela entre os dois (ambiente com captura de tela funcional — no WSL a captura nativa pode falhar)
|
||||||
|
- [ ] Plugin Ping do aluno (tutorial [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md)) medindo RTT — recomendado antes da entrega final
|
||||||
|
|
||||||
## Checklist plugin de aluno
|
## Checklist plugin de aluno
|
||||||
|
|
||||||
- [ ] JAR em `plugins/` + SPI correto
|
- [ ] JAR em `PITIUPI_HOME/plugins` + SPI correto
|
||||||
- [ ] README do módulo com build e formato das mensagens
|
- [ ] README do módulo com build e formato das mensagens
|
||||||
|
- [ ] UI na aba (`PluginWorkspace`), sem `JFrame` principal
|
||||||
- [ ] Demonstração mínima professor ↔ aluno ou aluno ↔ aluno
|
- [ ] Demonstração mínima professor ↔ aluno ou aluno ↔ aluno
|
||||||
|
|
||||||
## Melhorias futuras
|
## Melhorias futuras
|
||||||
|
|
||||||
Ver [engineering_backlog.md](engineering_backlog.md) (testes automatizados, CI).
|
Ver [engineering_backlog.md](engineering_backlog.md) (CI contínuo, mais testes de rede sem sockets reais).
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Tutorial de aula — plugin Ping
|
||||||
|
|
||||||
|
Material para o aluno implementar um plugin **Ping/Pong** com medição de RTT (round-trip time). A solução em Java **não** entra no repositório oficial; apenas este tutorial e a pasta [`plugins/ping/`](../../plugins/ping/) (README + `.gitignore`).
|
||||||
|
|
||||||
|
Índice geral: [README.md](README.md).
|
||||||
|
|
||||||
|
## Objetivos
|
||||||
|
|
||||||
|
- Estender `Plugin` e registrar SPI.
|
||||||
|
- Mensagens `Serializable` que estendem `Message`.
|
||||||
|
- `ConnectionManager`: multicast UDP, unicast UDP e TCP.
|
||||||
|
- UI na aba central: `JPanel` + `bindShellWorkspaceContent` (sem `JFrame`); abrir pela galeria **Plugins** ou peer actions.
|
||||||
|
- Instalar JAR em `PITIUPI_HOME/plugins` e testar com um colega.
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- Pitiupi com login (nome + porta).
|
||||||
|
- [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
||||||
|
- JDK, Ant; núcleo em `pitiupi/dist/pitiupi.jar`.
|
||||||
|
|
||||||
|
## Estrutura sugerida (projeto do aluno)
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/ping/ # ou projeto fora do repo oficial
|
||||||
|
├── build.xml
|
||||||
|
├── nbproject/
|
||||||
|
├── src/
|
||||||
|
│ ├── META-INF/services/pitiupi.models.Plugin
|
||||||
|
│ └── br/ufsj/redes/ping/
|
||||||
|
│ ├── PingPlugin.java
|
||||||
|
│ ├── PingMessage.java
|
||||||
|
│ ├── PongMessage.java
|
||||||
|
│ └── PingPanel.java
|
||||||
|
└── dist/pitiupi.ping.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
Modelo Ant: copiar [`plugins/chat`](../chat) e ajustar classpath para `../../pitiupi/dist/pitiupi.jar`.
|
||||||
|
|
||||||
|
## Passo 1 — Projeto Ant
|
||||||
|
|
||||||
|
1. Copiar `plugins/chat`.
|
||||||
|
2. JAR de saída: `dist/pitiupi.ping.jar`.
|
||||||
|
3. Classpath de compilação: JAR do núcleo.
|
||||||
|
|
||||||
|
**Verificação:** `ant jar` sem erros.
|
||||||
|
|
||||||
|
## Passo 2 — `PingPlugin`
|
||||||
|
|
||||||
|
| Método | Exemplo |
|
||||||
|
| --- | --- |
|
||||||
|
| `getName()` | `"Ping"` |
|
||||||
|
| `getDisplayName()` | `"Ping na rede"` |
|
||||||
|
| `getIdentifier()` | `"pitiupi.ping"` (único na sala) |
|
||||||
|
|
||||||
|
Em `createPlugin(ConnectionManager cm, PluginWorkspace workspace)`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
pingPanel = new PingPanel(cm);
|
||||||
|
bindShellWorkspaceContent(pingPanel);
|
||||||
|
installPeerActions(workspace.peerActions());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Não** chame `openOrFocusTab` no boot — a aba abre quando o usuário clicar na galeria ou usar peer action.
|
||||||
|
|
||||||
|
Peer actions sugeridas: “Enviar ping” (peer único), “Ping para todos”.
|
||||||
|
|
||||||
|
**Verificação:** após instalar o JAR e reiniciar, só **Plugins** e **Configurações** no boot; botão na galeria abre a aba Ping.
|
||||||
|
|
||||||
|
## Passo 3 — Mensagens
|
||||||
|
|
||||||
|
`PingMessage` e `PongMessage` com `pingId` (UUID) e timestamps para RTT.
|
||||||
|
|
||||||
|
Registrar em `getMessages()`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
return List.of(PingMessage.class, PongMessage.class);
|
||||||
|
```
|
||||||
|
|
||||||
|
Em `receiveMessage`: ao receber ping, responder pong; ao receber pong, calcular RTT e atualizar a UI com `SwingUtilities.invokeLater`.
|
||||||
|
|
||||||
|
## Passo 4 — `PingPanel`
|
||||||
|
|
||||||
|
Botões para multicast, unicast UDP e TCP; área de log ou label com último RTT em ms. Para um peer, use campo IP como no `ChatPanel` ou peer action na lista lateral.
|
||||||
|
|
||||||
|
## Passo 5 — Transportes
|
||||||
|
|
||||||
|
Use `pitiupi.application.messaging.MessageDispatcher` no núcleo:
|
||||||
|
|
||||||
|
```java
|
||||||
|
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.broadcastAll(), TransportMode.MULTICAST_UDP);
|
||||||
|
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.singlePeer(addr), TransportMode.UNICAST_UDP);
|
||||||
|
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.singlePeer(addr), TransportMode.TCP);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Destino | Transporte didático | Observação |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Todos | Multicast UDP | Padrão em sala |
|
||||||
|
| Todos | Unicast UDP | Loop em cada peer online (demo) |
|
||||||
|
| Um peer | Unicast UDP / TCP | IP da lista ou manual |
|
||||||
|
|
||||||
|
Não alterar o endereço multicast do núcleo (`224.0.0.3`).
|
||||||
|
|
||||||
|
## Passo 6 — SPI e instalação
|
||||||
|
|
||||||
|
`META-INF/services/pitiupi.models.Plugin`:
|
||||||
|
|
||||||
|
```
|
||||||
|
br.ufsj.redes.ping.PingPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Copiar JAR para `~/pitiupi/plugins/` (ou instalar pela aba Configurações) e **reiniciar**.
|
||||||
|
|
||||||
|
## Critérios de sucesso
|
||||||
|
|
||||||
|
| # | Critério |
|
||||||
|
| --- | --- |
|
||||||
|
| C1 | JAR carrega sem exceção |
|
||||||
|
| C2 | Pelo menos um transporte funciona entre duas instâncias |
|
||||||
|
| C3 | RTT em ms após o pong |
|
||||||
|
| C4 | README do aluno com build e transporte usado |
|
||||||
|
|
||||||
|
## Anti-padrões
|
||||||
|
|
||||||
|
- Commitar solução no repo oficial.
|
||||||
|
- Busy-wait na EDT.
|
||||||
|
- Objetos não `Serializable`.
|
||||||
|
- Mesmo `getIdentifier()` que outro plugin.
|
||||||
|
|
||||||
|
## Referências
|
||||||
|
|
||||||
|
- [10_tutorial_plugin.md](10_tutorial_plugin.md)
|
||||||
|
- [05_udp_multicast.md](05_udp_multicast.md)
|
||||||
|
- [06_tcp_conexoes.md](06_tcp_conexoes.md)
|
||||||
|
- [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md)
|
||||||
@@ -9,7 +9,7 @@ Referência para **código Java**, **rede P2P** e **plugins**. Índice geral: [d
|
|||||||
| 0 | [00_quickstart.md](00_quickstart.md) | Build, plugins, duas instâncias |
|
| 0 | [00_quickstart.md](00_quickstart.md) | Build, plugins, duas instâncias |
|
||||||
| 1 | [01_visao_disciplina_e_tp.md](01_visao_disciplina_e_tp.md) | Uso na disciplina e trabalho prático |
|
| 1 | [01_visao_disciplina_e_tp.md](01_visao_disciplina_e_tp.md) | Uso na disciplina e trabalho prático |
|
||||||
| 2 | [02_modulos_do_repositorio.md](02_modulos_do_repositorio.md) | Núcleo e três plugins de referência |
|
| 2 | [02_modulos_do_repositorio.md](02_modulos_do_repositorio.md) | Núcleo e três plugins de referência |
|
||||||
| 3 | [03_build_ant_netbeans.md](03_build_ant_netbeans.md) | Ant, NetBeans, Makefile mpChat |
|
| 3 | [03_build_ant_netbeans.md](03_build_ant_netbeans.md) | Ant e NetBeans (núcleo + plugins) |
|
||||||
| 4 | [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md) | Boot, MainWindow, diagrama |
|
| 4 | [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md) | Boot, MainWindow, diagrama |
|
||||||
| 5 | [05_udp_multicast.md](05_udp_multicast.md) | SocketUDP |
|
| 5 | [05_udp_multicast.md](05_udp_multicast.md) | SocketUDP |
|
||||||
| 6 | [06_tcp_conexoes.md](06_tcp_conexoes.md) | SocketTCP, TcpConnection, framing |
|
| 6 | [06_tcp_conexoes.md](06_tcp_conexoes.md) | SocketTCP, TcpConnection, framing |
|
||||||
@@ -17,7 +17,8 @@ Referência para **código Java**, **rede P2P** e **plugins**. Índice geral: [d
|
|||||||
| 8 | [08_heartbeat_e_presenca.md](08_heartbeat_e_presenca.md) | HeartbeatManager |
|
| 8 | [08_heartbeat_e_presenca.md](08_heartbeat_e_presenca.md) | HeartbeatManager |
|
||||||
| 9 | [09_sistema_de_plugins.md](09_sistema_de_plugins.md) | Plugin, PluginManager, SPI |
|
| 9 | [09_sistema_de_plugins.md](09_sistema_de_plugins.md) | Plugin, PluginManager, SPI |
|
||||||
| 10 | [10_tutorial_plugin.md](10_tutorial_plugin.md) | Passo a passo novo plugin |
|
| 10 | [10_tutorial_plugin.md](10_tutorial_plugin.md) | Passo a passo novo plugin |
|
||||||
| 11 | [11_plugins_referencia.md](11_plugins_referencia.md) | chat, screenCast, mpChat |
|
| 11 | [11_plugins_referencia.md](11_plugins_referencia.md) | chat e screencast |
|
||||||
|
| 17 | [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md) | Exercício Ping (aula) |
|
||||||
| 12 | [12_menus_unicast_multicast.md](12_menus_unicast_multicast.md) | MenuCategory e ações |
|
| 12 | [12_menus_unicast_multicast.md](12_menus_unicast_multicast.md) | MenuCategory e ações |
|
||||||
| 13 | [13_guia_dev_e_agente.md](13_guia_dev_e_agente.md) | Leitura para dev/agente |
|
| 13 | [13_guia_dev_e_agente.md](13_guia_dev_e_agente.md) | Leitura para dev/agente |
|
||||||
| 14 | [14_arquitetura_e_manutencao.md](14_arquitetura_e_manutencao.md) | Onde editar cada comportamento |
|
| 14 | [14_arquitetura_e_manutencao.md](14_arquitetura_e_manutencao.md) | Onde editar cada comportamento |
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
| Item | Prioridade | Notas |
|
| Item | Prioridade | Notas |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Completar tutorial legado em `pitiupi/docs/PluginTutorial.md` | Média | Substituído por [10_tutorial_plugin.md](10_tutorial_plugin.md) |
|
|
||||||
| Testes JUnit para desserialização e framing TCP | Média | Sem rede real |
|
| Testes JUnit para desserialização e framing TCP | Média | Sem rede real |
|
||||||
| CI (GitHub Actions) com `ant jar` + `make` | Baixa | Requer imagem com Ant |
|
| CI (GitHub Actions) com `ant jar` + `ant test` | Baixa | Requer imagem com Ant/JDK |
|
||||||
|
| Captura de tela nativa no WSL | Baixa | Limitação de ambiente; validar em Windows/Linux com display |
|
||||||
|
|
||||||
Itens concluídos recentemente: alinhamento de `pitiupi.mpChat` a `ConnectionManager` / `GuiManager`; documentação de JDK mínimo no [README](../../README.md) e [00_quickstart.md](00_quickstart.md).
|
Itens concluídos recentemente: shell com abas e peer actions (D16–D18); chat unificado (D19); docs alinhadas à shell v1 (Onda G); suíte JUnit no núcleo (`ant test`); tutorial Ping em [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md).
|
||||||
|
|
||||||
Ao fechar um item, atualizar esta tabela e o doc técnico relacionado.
|
Ao fechar um item, atualizar esta tabela e o doc técnico relacionado.
|
||||||
Whitespace-only changes.
@@ -1,8 +0,0 @@
|
|||||||
compile.on.save=true
|
|
||||||
do.depend=false
|
|
||||||
do.jar=true
|
|
||||||
do.jlink=false
|
|
||||||
javac.debug=true
|
|
||||||
javadoc.preview=true
|
|
||||||
jlink.strip=false
|
|
||||||
user.properties.file=/home/flavio/snap/netbeans/145/build.properties
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.net;
|
|
||||||
|
|
||||||
|
|
||||||
import pitiupi.models.Messages.Message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
*/
|
|
||||||
public class ChatMessage extends Message {
|
|
||||||
|
|
||||||
private String text;
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
|
|
||||||
public ChatMessage(){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatMessage(String name, String text){
|
|
||||||
this.name = name;
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @return the name
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name the name to set
|
|
||||||
*/
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the text
|
|
||||||
*/
|
|
||||||
public String getText() {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param text the text to set
|
|
||||||
*/
|
|
||||||
public void setText(String text) {
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.plugin;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import pitiupi.chat.GUI.ChatMenu;
|
|
||||||
import pitiupi.chat.GUI.ChatWindow;
|
|
||||||
import pitiupi.chat.net.ChatMessage;
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
import pitiupi.control.GuiManager;
|
|
||||||
import pitiupi.models.Menus.MenuCategory;
|
|
||||||
import pitiupi.models.Messages.Message;
|
|
||||||
import pitiupi.models.Plugin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
*/
|
|
||||||
public class ChatPlugin extends Plugin {
|
|
||||||
|
|
||||||
private ChatWindow chatWindow;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
|
||||||
this.chatWindow = new ChatWindow(connectionManager);
|
|
||||||
guiManager.createMenu(MenuCategory.Social, new ChatMenu(chatWindow));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Class<? extends Message>> getMessages() {
|
|
||||||
return List.of(ChatMessage.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receiveMessage(Message message) {
|
|
||||||
if (message instanceof ChatMessage msg) {
|
|
||||||
this.chatWindow.receiveChatMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "Chat";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthor() {
|
|
||||||
return "Flávio Schiavoni";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVersion() {
|
|
||||||
return "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getIdentifier() {
|
|
||||||
return "Pitiupi.Chat.Example";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/pitiupi.mpChat.iml" filepath="$PROJECT_DIR$/pitiupi.mpChat.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
JAR = ../pitiupi/dist/pitiupi.jar
|
|
||||||
OUT_DIR = out
|
|
||||||
META-INF_DIR = src/META-INF
|
|
||||||
DIST = dist/mpChat.jar
|
|
||||||
|
|
||||||
SOURCES = $(shell find -name "*.java")
|
|
||||||
|
|
||||||
all:
|
|
||||||
mkdir -p "$(OUT_DIR)"
|
|
||||||
mkdir -p "$(dir $(DIST))"
|
|
||||||
javac -cp "$(JAR)" -d "$(OUT_DIR)" $(SOURCES)
|
|
||||||
cp -r "$(META-INF_DIR)" "$(OUT_DIR)"
|
|
||||||
jar cf "$(DIST)" -C "$(OUT_DIR)" .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf "$(OUT_DIR)"/*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
plugin.MultiProtocolChat
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package GUI;
|
|
||||||
|
|
||||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
|
||||||
|
|
||||||
public class MulticastMpChatMenu extends MulticastActionMenu {
|
|
||||||
PublicChatWindow publicChatWindow;
|
|
||||||
|
|
||||||
public MulticastMpChatMenu(PublicChatWindow publicChatWindow) {
|
|
||||||
this.publicChatWindow = publicChatWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
publicChatWindow.setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getText() {
|
|
||||||
return "MP-Chat";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package GUI;
|
|
||||||
|
|
||||||
import net.MpChatMessage;
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
import pitiupi.net.tcp.TcpConnection;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.awt.*;
|
|
||||||
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");
|
|
||||||
setSize(700, 500);
|
|
||||||
setLocationRelativeTo(null);
|
|
||||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
|
||||||
|
|
||||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
|
||||||
mainPanel.setBorder(
|
|
||||||
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(
|
|
||||||
BorderFactory.createTitledBorder("Mensagens")
|
|
||||||
);
|
|
||||||
|
|
||||||
mainPanel.add(
|
|
||||||
messagesScrollPane,
|
|
||||||
BorderLayout.CENTER
|
|
||||||
);
|
|
||||||
|
|
||||||
JPanel bottomPanel = new JPanel(
|
|
||||||
new BorderLayout(10, 10)
|
|
||||||
);
|
|
||||||
|
|
||||||
messageField = new JTextField();
|
|
||||||
|
|
||||||
JButton tcpButton = new JButton("TCP");
|
|
||||||
tcpButton.addActionListener(e -> sendTCP());
|
|
||||||
|
|
||||||
bottomPanel.add(
|
|
||||||
messageField,
|
|
||||||
BorderLayout.CENTER
|
|
||||||
);
|
|
||||||
|
|
||||||
bottomPanel.add(
|
|
||||||
tcpButton,
|
|
||||||
BorderLayout.EAST
|
|
||||||
);
|
|
||||||
|
|
||||||
mainPanel.add(
|
|
||||||
bottomPanel,
|
|
||||||
BorderLayout.SOUTH
|
|
||||||
);
|
|
||||||
|
|
||||||
setContentPane(mainPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendTCP() {
|
|
||||||
try {
|
|
||||||
String nome = connectionManager.getUserName();
|
|
||||||
|
|
||||||
MpChatMessage message = new MpChatMessage(
|
|
||||||
nome,
|
|
||||||
InetAddress.getLocalHost().getHostAddress(),
|
|
||||||
"TCP Private",
|
|
||||||
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(MpChatMessage 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()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package GUI;
|
|
||||||
|
|
||||||
import net.MpChatMessage;
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import javax.swing.text.BadLocationException;
|
|
||||||
import javax.swing.text.SimpleAttributeSet;
|
|
||||||
import javax.swing.text.StyleConstants;
|
|
||||||
import javax.swing.text.StyledDocument;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
public class PublicChatWindow extends JFrame {
|
|
||||||
private final ConnectionManager connectionManager;
|
|
||||||
|
|
||||||
private JTextPane receivedMessagesArea;
|
|
||||||
private JTextField messageField;
|
|
||||||
private JTextField ipField;
|
|
||||||
|
|
||||||
public PublicChatWindow(ConnectionManager connectionManager) {
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
|
|
||||||
initComponents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initComponents() {
|
|
||||||
setTitle("Public Chat");
|
|
||||||
setSize(700, 500);
|
|
||||||
setLocationRelativeTo(null);
|
|
||||||
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
|
||||||
|
|
||||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
|
||||||
|
|
||||||
mainPanel.setBorder(
|
|
||||||
BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
|
||||||
);
|
|
||||||
|
|
||||||
initReceivedMessagesPanel(mainPanel);
|
|
||||||
initBottomPanel(mainPanel);
|
|
||||||
|
|
||||||
setContentPane(mainPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initReceivedMessagesPanel(JPanel mainPanel) {
|
|
||||||
receivedMessagesArea = new JTextPane();
|
|
||||||
|
|
||||||
receivedMessagesArea.setEditable(false);
|
|
||||||
|
|
||||||
JScrollPane messagesScrollPane = new JScrollPane(receivedMessagesArea);
|
|
||||||
|
|
||||||
messagesScrollPane.setBorder(
|
|
||||||
BorderFactory.createTitledBorder("Mensagens recebidas")
|
|
||||||
);
|
|
||||||
|
|
||||||
mainPanel.add(messagesScrollPane, BorderLayout.CENTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initBottomPanel(JPanel mainPanel) {
|
|
||||||
messageField = new JTextField();
|
|
||||||
|
|
||||||
JPanel bottomPanel = new JPanel(
|
|
||||||
new BorderLayout(10, 10)
|
|
||||||
);
|
|
||||||
|
|
||||||
bottomPanel.add(messageField, BorderLayout.CENTER);
|
|
||||||
|
|
||||||
initIpField(bottomPanel);
|
|
||||||
initCommunicationButtons(bottomPanel);
|
|
||||||
|
|
||||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initIpField(JPanel bottomPanel) {
|
|
||||||
ipField = new JTextField();
|
|
||||||
ipField.setPreferredSize(new Dimension(130, 30));
|
|
||||||
|
|
||||||
JPanel ipPanel = new JPanel(
|
|
||||||
new BorderLayout(5, 5)
|
|
||||||
);
|
|
||||||
|
|
||||||
ipPanel.add(
|
|
||||||
new JLabel("IP destino:"),
|
|
||||||
BorderLayout.WEST
|
|
||||||
);
|
|
||||||
|
|
||||||
ipPanel.add(
|
|
||||||
ipField,
|
|
||||||
BorderLayout.CENTER
|
|
||||||
);
|
|
||||||
|
|
||||||
bottomPanel.add(ipPanel, BorderLayout.WEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCommunicationButtons(JPanel bottomPanel) {
|
|
||||||
JButton tcpButton = new JButton("TCP");
|
|
||||||
JButton udpButton = new JButton("UDP");
|
|
||||||
JButton multicastButton = new JButton("Multicast");
|
|
||||||
|
|
||||||
tcpButton.addActionListener(e -> sendTCP());
|
|
||||||
udpButton.addActionListener(e -> sendUDP());
|
|
||||||
multicastButton.addActionListener(e -> sendMulticast());
|
|
||||||
|
|
||||||
JPanel buttonsPanel = new JPanel(
|
|
||||||
new FlowLayout(FlowLayout.RIGHT, 5, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
buttonsPanel.add(tcpButton);
|
|
||||||
buttonsPanel.add(udpButton);
|
|
||||||
buttonsPanel.add(multicastButton);
|
|
||||||
|
|
||||||
bottomPanel.add(
|
|
||||||
buttonsPanel,
|
|
||||||
BorderLayout.EAST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MpChatMessage createMessage(String tipoMensagem) {
|
|
||||||
try {
|
|
||||||
String nome = connectionManager.getUserName();
|
|
||||||
String ip = InetAddress.getLocalHost().getHostAddress();
|
|
||||||
String message = messageField.getText();
|
|
||||||
|
|
||||||
return new MpChatMessage(
|
|
||||||
nome,
|
|
||||||
ip,
|
|
||||||
tipoMensagem,
|
|
||||||
message
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
throw new RuntimeException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void receiveMessage(MpChatMessage msg) {
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
String mensagem = String.format(
|
|
||||||
"[%s] %s (%s): %s%n",
|
|
||||||
msg.getTipoMensagem(),
|
|
||||||
msg.getNome(),
|
|
||||||
msg.getIp(),
|
|
||||||
msg.getMessage()
|
|
||||||
);
|
|
||||||
|
|
||||||
StyledDocument document = receivedMessagesArea.getStyledDocument();
|
|
||||||
|
|
||||||
SimpleAttributeSet attributes = new SimpleAttributeSet();
|
|
||||||
|
|
||||||
if (msg.getTipoMensagem().equals("TCP Unicast") || msg.getTipoMensagem().equals("UDP Unicast")) {
|
|
||||||
StyleConstants.setForeground(attributes, new Color(128, 0, 128));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.insertString(
|
|
||||||
document.getLength(),
|
|
||||||
mensagem,
|
|
||||||
attributes
|
|
||||||
);
|
|
||||||
|
|
||||||
receivedMessagesArea.setCaretPosition(
|
|
||||||
document.getLength()
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (BadLocationException ex) {
|
|
||||||
Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendTCP() {
|
|
||||||
try {
|
|
||||||
MpChatMessage msg = createMessage("TCP Unicast");
|
|
||||||
|
|
||||||
InetAddress ip = InetAddress.getByName(ipField.getText());
|
|
||||||
|
|
||||||
connectionManager.sendTcp(msg, ip);
|
|
||||||
receiveMessage(msg);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.getLogger(
|
|
||||||
PublicChatWindow.class.getName()
|
|
||||||
).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendUDP() {
|
|
||||||
try {
|
|
||||||
MpChatMessage msg = createMessage("UDP Unicast");
|
|
||||||
|
|
||||||
InetAddress ip = InetAddress.getByName(ipField.getText());
|
|
||||||
|
|
||||||
connectionManager.sendUdpUnicast(msg, ip);
|
|
||||||
receiveMessage(msg);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.getLogger(
|
|
||||||
PublicChatWindow.class.getName()
|
|
||||||
).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMulticast() {
|
|
||||||
try {
|
|
||||||
MpChatMessage msg = createMessage("UDP Multicast");
|
|
||||||
|
|
||||||
connectionManager.sendUdpMulticast(msg);
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.getLogger(
|
|
||||||
PublicChatWindow.class.getName()
|
|
||||||
).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package GUI;
|
|
||||||
|
|
||||||
import control.PrivateChatController;
|
|
||||||
import pitiupi.models.Menus.UnicastMenus.UnicastActionMenu;
|
|
||||||
import pitiupi.models.User;
|
|
||||||
|
|
||||||
public class UnicastMpChatMenu extends UnicastActionMenu {
|
|
||||||
|
|
||||||
private final PrivateChatController controller;
|
|
||||||
|
|
||||||
public UnicastMpChatMenu(
|
|
||||||
PrivateChatController controller
|
|
||||||
) {
|
|
||||||
this.controller = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(User user) {
|
|
||||||
controller.openChat(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getText() {
|
|
||||||
return "Start Private Chat";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
plugin.MultiProtocolChat
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package control;
|
|
||||||
|
|
||||||
import GUI.PrivateChatWindow;
|
|
||||||
import net.MpChatMessage;
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
import pitiupi.models.User;
|
|
||||||
import pitiupi.net.tcp.TcpConnection;
|
|
||||||
import plugin.MultiProtocolChat;
|
|
||||||
|
|
||||||
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 MultiProtocolChat multiProtocolChat;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Relaciona o endereço do usuário à janela de chat.
|
|
||||||
*/
|
|
||||||
private final Map<String, PrivateChatWindow> windows;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Endereços para os quais uma conexão já foi solicitada,
|
|
||||||
* mas ainda não chegou.
|
|
||||||
*/
|
|
||||||
private final Set<String> pendingConnections;
|
|
||||||
|
|
||||||
public PrivateChatController(
|
|
||||||
ConnectionManager connectionManager,
|
|
||||||
MultiProtocolChat multiProtocolChat
|
|
||||||
) {
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
this.multiProtocolChat = multiProtocolChat;
|
|
||||||
|
|
||||||
this.windows = new HashMap<>();
|
|
||||||
this.pendingConnections = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String addressKey(TcpConnection connection) {
|
|
||||||
return connection.getAddress().getHostAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openChat(User user) {
|
|
||||||
String address = user.getAddress().getHostAddress();
|
|
||||||
|
|
||||||
PrivateChatWindow window = windows.get(address);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Já existe uma janela para esse usuário.
|
|
||||||
*/
|
|
||||||
if (window != null) {
|
|
||||||
window.setVisible(true);
|
|
||||||
window.toFront();
|
|
||||||
window.requestFocus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Já estamos esperando uma conexão para esse usuário.
|
|
||||||
*/
|
|
||||||
if (pendingConnections.contains(address)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Ainda não existe conexão.
|
|
||||||
*/
|
|
||||||
pendingConnections.add(address);
|
|
||||||
|
|
||||||
multiProtocolChat.openConnection(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void receiveMessage(TcpConnection connection, MpChatMessage message) {
|
|
||||||
String address = addressKey(connection);
|
|
||||||
|
|
||||||
PrivateChatWindow window = windows.get(address);
|
|
||||||
|
|
||||||
if (window == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.receiveMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deve ser chamado quando uma nova conexão privada chegar.
|
|
||||||
*/
|
|
||||||
public void connectionReceived(TcpConnection connection) {
|
|
||||||
String address = addressKey(connection);
|
|
||||||
|
|
||||||
pendingConnections.remove(address);
|
|
||||||
|
|
||||||
PrivateChatWindow window = new PrivateChatWindow(
|
|
||||||
connectionManager,
|
|
||||||
connection
|
|
||||||
);
|
|
||||||
|
|
||||||
windows.put(address, window);
|
|
||||||
|
|
||||||
window.setVisible(true);
|
|
||||||
window.toFront();
|
|
||||||
window.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fecha a janela e remove o relacionamento.
|
|
||||||
*/
|
|
||||||
public void closeChat(TcpConnection connection) {
|
|
||||||
String address = addressKey(connection);
|
|
||||||
|
|
||||||
PrivateChatWindow window = windows.remove(address);
|
|
||||||
|
|
||||||
if (window != null) {
|
|
||||||
window.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package plugin;
|
|
||||||
|
|
||||||
import GUI.MulticastMpChatMenu;
|
|
||||||
import GUI.PublicChatWindow;
|
|
||||||
import GUI.UnicastMpChatMenu;
|
|
||||||
import control.PrivateChatController;
|
|
||||||
import net.MpChatMessage;
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
import pitiupi.control.GuiManager;
|
|
||||||
import pitiupi.models.Menus.MenuCategory;
|
|
||||||
import pitiupi.models.Messages.Message;
|
|
||||||
import pitiupi.models.Plugin;
|
|
||||||
import pitiupi.net.tcp.TcpConnection;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MultiProtocolChat extends Plugin {
|
|
||||||
private ConnectionManager connectionManager;
|
|
||||||
|
|
||||||
PrivateChatController privateChatController;
|
|
||||||
PublicChatWindow publicChatWindow;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "MultiProtocol chat";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthor() {
|
|
||||||
return "Gustavo Henrique";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVersion() {
|
|
||||||
return "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getIdentifier() {
|
|
||||||
return "Multi.Protocol.Chat.Pitiupi";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
|
|
||||||
privateChatController = new PrivateChatController(connectionManager, this);
|
|
||||||
|
|
||||||
publicChatWindow = new PublicChatWindow(connectionManager);
|
|
||||||
|
|
||||||
MulticastMpChatMenu multicastMpChatMenu = new MulticastMpChatMenu(publicChatWindow);
|
|
||||||
UnicastMpChatMenu unicastMpChatMenu = new UnicastMpChatMenu(privateChatController);
|
|
||||||
|
|
||||||
guiManager.createMenu(MenuCategory.Social, multicastMpChatMenu);
|
|
||||||
guiManager.createMenu(MenuCategory.Social, unicastMpChatMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Class<? extends Message>> getMessages() {
|
|
||||||
return List.of(MpChatMessage.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receiveMessage(Message message) {
|
|
||||||
if (message instanceof MpChatMessage mpChatMessage) {
|
|
||||||
publicChatWindow.receiveMessage(mpChatMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean receivePrivateConnection(TcpConnection connection) {
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
privateChatController.connectionReceived(connection);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean receiveBytes(TcpConnection connection, byte[] bytes) {
|
|
||||||
MpChatMessage mpChatMessage = parseMessage(bytes);
|
|
||||||
if (mpChatMessage == null) {
|
|
||||||
System.out.println("Invalid message received");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else System.out.println(mpChatMessage.getMessage());
|
|
||||||
|
|
||||||
privateChatController.receiveMessage(connection, mpChatMessage);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSocketClosed(TcpConnection connection) {
|
|
||||||
privateChatController.closeChat(connection);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openConnection(String ip) {
|
|
||||||
try {
|
|
||||||
connectionManager.connectPluginTcp(
|
|
||||||
InetAddress.getByName(ip),
|
|
||||||
getIdentifier(),
|
|
||||||
getIdentifier()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
System.out.println("Error opening connection: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MpChatMessage parseMessage(byte[] bytes) {
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
|
|
||||||
ObjectInputStream ois;
|
|
||||||
try {
|
|
||||||
ois = new ObjectInputStream(bis);
|
|
||||||
if (ois.readObject() instanceof MpChatMessage msg) {
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
} catch (IOException | ClassNotFoundException ex) {
|
|
||||||
System.out.println(ex.toString());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
compile.on.save=true
|
|
||||||
user.properties.file=/home/flavio/snap/netbeans/145/build.properties
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.GUI;
|
|
||||||
|
|
||||||
import pitiupi.control.ConnectionManager;
|
|
||||||
import pitiupi.control.GuiManager;
|
|
||||||
import pitiupi.screenCast.control.AlarmClock;
|
|
||||||
import pitiupi.screenCast.control.ImageTransformation;
|
|
||||||
import pitiupi.screenCast.control.ScreenShot;
|
|
||||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
|
||||||
import java.awt.AWTException;
|
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
*/
|
|
||||||
public class ScreenCastWindow extends javax.swing.JFrame {
|
|
||||||
|
|
||||||
private javax.swing.JScrollPane jScrollPane1;
|
|
||||||
private javax.swing.JTabbedPane tabbedPane;
|
|
||||||
private javax.swing.JCheckBox onlineButton;
|
|
||||||
|
|
||||||
private AlarmClock alarmClock;
|
|
||||||
private Timer timer;
|
|
||||||
public final static int TIME = 1000;
|
|
||||||
|
|
||||||
private float compressRatio;
|
|
||||||
private final ConnectionManager connectionManager;
|
|
||||||
private final GuiManager guiManager;
|
|
||||||
|
|
||||||
public ScreenCastWindow(ConnectionManager connectionManager, GuiManager guiManager) {
|
|
||||||
this.connectionManager = connectionManager;
|
|
||||||
this.guiManager = guiManager;
|
|
||||||
|
|
||||||
this.compressRatio = 0.5f;
|
|
||||||
this.initComponents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initComponents() {
|
|
||||||
this.setTitle("screen cast");
|
|
||||||
|
|
||||||
onlineButton = new javax.swing.JCheckBox("Sharing?");
|
|
||||||
onlineButton.setSelected(false);
|
|
||||||
onlineButton.addActionListener(this::onlineButtonActionPerformed);
|
|
||||||
this.getContentPane().add(onlineButton, java.awt.BorderLayout.NORTH);
|
|
||||||
|
|
||||||
jScrollPane1 = new javax.swing.JScrollPane();
|
|
||||||
tabbedPane = new javax.swing.JTabbedPane();
|
|
||||||
jScrollPane1.setViewportView(tabbedPane);
|
|
||||||
this.getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER);
|
|
||||||
|
|
||||||
this.pack();
|
|
||||||
|
|
||||||
this.timer = new Timer();
|
|
||||||
this.setSize(new java.awt.Dimension(1024, 768));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void shareMySheet(boolean status) {
|
|
||||||
if (status) {
|
|
||||||
this.timer = new Timer();
|
|
||||||
this.alarmClock = new AlarmClock(this);
|
|
||||||
this.timer.scheduleAtFixedRate(this.alarmClock, 0, ScreenCastWindow.TIME);
|
|
||||||
} else {
|
|
||||||
this.timer.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeCurrentTab() {
|
|
||||||
if (tabbedPane.getSelectedIndex() > 0) {
|
|
||||||
tabbedPane.remove(tabbedPane.getSelectedIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void exit() {
|
|
||||||
this.shareMySheet(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendScreenCastMessage() {
|
|
||||||
byte[] msg = null;
|
|
||||||
try {
|
|
||||||
BufferedImage image = ScreenShot.takeAShot();
|
|
||||||
BufferedImage rescaled = ImageTransformation.resize(image, 1024, 768);
|
|
||||||
byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio);
|
|
||||||
ScreenCastMessage message = new ScreenCastMessage(this.connectionManager.getUserName(), byteImage);
|
|
||||||
|
|
||||||
if (!connectionManager.sendUdpMulticast(message)) this.compressRatio -= 0.1f;
|
|
||||||
} catch (AWTException | IOException ex) {
|
|
||||||
Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void receiveScreenCastMessage(ScreenCastMessage message) {
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(message.getImageByte());
|
|
||||||
BufferedImage image;
|
|
||||||
try {
|
|
||||||
|
|
||||||
image = ImageIO.read(bais);
|
|
||||||
ImageIcon imageIcon = new ImageIcon(image);
|
|
||||||
|
|
||||||
JLabel myShoot = new JLabel(imageIcon);
|
|
||||||
|
|
||||||
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
|
|
||||||
String title = tabbedPane.getTitleAt(i);
|
|
||||||
if (title.equals(message.getName())) {
|
|
||||||
tabbedPane.setComponentAt(i, myShoot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabbedPane.addTab(message.getName(), myShoot);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.getLogger(ScreenCastWindow.class.getName()).log(Level.SEVERE, null, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onlineButtonActionPerformed(ActionEvent ae) {
|
|
||||||
this.shareMySheet(this.onlineButton.isSelected());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.util.TimerTask;
|
|
||||||
import pitiupi.screenCast.GUI.ScreenCastWindow;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
*/
|
|
||||||
public class AlarmClock extends TimerTask {
|
|
||||||
|
|
||||||
private final ScreenCastWindow screenCastWindow;
|
|
||||||
|
|
||||||
public AlarmClock(ScreenCastWindow mainWindow) {
|
|
||||||
this.screenCastWindow = mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
this.screenCastWindow.sendScreenCastMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.plugin;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
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.ScreenCastWindow;
|
|
||||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
*/
|
|
||||||
public class ScreenCastPlugin extends Plugin {
|
|
||||||
|
|
||||||
private ScreenCastWindow screenCastWindow;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "ScreenCast";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthor() {
|
|
||||||
return "Flávio Schiavoni";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVersion() {
|
|
||||||
return "0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getIdentifier() {
|
|
||||||
return "Pitiupi.ScreenCast.Example";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
|
||||||
this.screenCastWindow = new ScreenCastWindow(connectionManager, guiManager);
|
|
||||||
guiManager.createMenu(MenuCategory.Streaming, new ScreenCastMenu(screenCastWindow));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Class<? extends Message>> getMessages() {
|
|
||||||
return List.of(ScreenCastMessage.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receiveMessage(Message message) {
|
|
||||||
if (message instanceof ScreenCastMessage msg) {
|
|
||||||
this.screenCastWindow.receiveScreenCastMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,4 +70,26 @@
|
|||||||
nbproject/build-impl.xml file.
|
nbproject/build-impl.xml file.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<property name="junit.standalone" location="test/lib/junit-platform-console-standalone-1.10.2.jar"/>
|
||||||
|
|
||||||
|
<target name="test" depends="jar" description="JUnit 5 (console launcher)">
|
||||||
|
<mkdir dir="${build.test.classes.dir}"/>
|
||||||
|
<javac srcdir="test"
|
||||||
|
destdir="${build.test.classes.dir}"
|
||||||
|
classpath="${run.classpath}:${junit.standalone}"
|
||||||
|
includeantruntime="false"
|
||||||
|
source="17"
|
||||||
|
target="17"
|
||||||
|
encoding="UTF-8"/>
|
||||||
|
<java classname="org.junit.platform.console.ConsoleLauncher" fork="true" failonerror="true">
|
||||||
|
<classpath>
|
||||||
|
<pathelement path="${build.test.classes.dir}"/>
|
||||||
|
<pathelement path="${run.classpath}"/>
|
||||||
|
<pathelement location="${junit.standalone}"/>
|
||||||
|
</classpath>
|
||||||
|
<arg value="--scan-classpath"/>
|
||||||
|
<arg value="${build.test.classes.dir}"/>
|
||||||
|
</java>
|
||||||
|
</target>
|
||||||
</project>
|
</project>
|
||||||
Whitespace-only changes.
@@ -1,8 +0,0 @@
|
|||||||
compile.on.save=true
|
|
||||||
do.depend=false
|
|
||||||
do.jar=true
|
|
||||||
do.jlink=false
|
|
||||||
javac.debug=true
|
|
||||||
javadoc.preview=true
|
|
||||||
jlink.strip=false
|
|
||||||
user.properties.file=/home/flavio/snap/netbeans/145/build.properties
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
|
|
||||||
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
|
|
||||||
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
|
|
||||||
<group>
|
|
||||||
<file>file:/home/flavio/NetBeansProjects/pitiupi/src/pitiupi/control/PluginManager.java</file>
|
|
||||||
<file>file:/home/flavio/NetBeansProjects/pitiupi/src/pitiupi/control/ImageTransformation.java</file>
|
|
||||||
</group>
|
|
||||||
</open-files>
|
|
||||||
</project-private>
|
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
# Plugins instalados
|
# Plugins instalados (runtime)
|
||||||
|
|
||||||
O núcleo carrega **todos os arquivos `.jar`** desta pasta ao iniciar. Execute sempre a partir de `pitiupi/`:
|
O núcleo carrega **todos os arquivos `.jar`** de `PITIUPI_HOME/plugins` ao iniciar (padrão `~/pitiupi/plugins`). Variável de ambiente opcional: `PITIUPI_HOME`.
|
||||||
|
|
||||||
```bash
|
|
||||||
java -jar dist/pitiupi.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pasta única (sem subpastas por categoria)
|
|
||||||
|
|
||||||
Os JARs ficam lado a lado em `plugins/`. A divisão **chat / jogo / streaming** aparece no menu **Plugin → Social | Jogos | Streaming**, definida por cada plugin com `MenuCategory` no código.
|
|
||||||
|
|
||||||
## Instalação automática (recomendado)
|
## Instalação automática (recomendado)
|
||||||
|
|
||||||
Na raiz do repositório, o catálogo [`plugins.registry.json`](../../plugins.registry.json) lista módulos buildáveis:
|
Na raiz do repositório, o catálogo [`plugins.registry.json`](../../plugins.registry.json) lista módulos buildáveis:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./scripts/pitiupi.sh build --all
|
||||||
./scripts/pitiupi.sh link --all
|
./scripts/pitiupi.sh link --all
|
||||||
./scripts/pitiupi.sh build --plugins chat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Symlinks gerados: `plugins/{id}.jar` → `../../{módulo}/{jar}`.
|
Symlinks: `{PITIUPI_HOME}/plugins/{id}.jar` → JAR buildado em `plugins/{módulo}/dist/` no clone.
|
||||||
|
|
||||||
## Manual
|
## Manual
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd pitiupi/plugins
|
ln -sf /caminho/ao/clone/plugins/chat/dist/pitiupi.chat.jar ~/pitiupi/plugins/chat.jar
|
||||||
ln -sf ../../pitiupi.chat/dist/pitiupi.chat.jar chat.jar
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Não commite `.jar` — apenas este README e symlinks locais.
|
Não commite `.jar` binários no Git — apenas este README.
|
||||||
|
|
||||||
Novo módulo: registrar no catálogo e ver [docs/documentation/10_tutorial_plugin.md](../../docs/documentation/10_tutorial_plugin.md).
|
Novo módulo: registrar no catálogo e ver [docs/documentation/10_tutorial_plugin.md](../../docs/documentation/10_tutorial_plugin.md).
|
||||||
@@ -8,97 +8,76 @@ package pitiupi.GUI;
|
|||||||
import pitiupi.control.ConnectionManager;
|
import pitiupi.control.ConnectionManager;
|
||||||
import pitiupi.control.GuiManager;
|
import pitiupi.control.GuiManager;
|
||||||
import pitiupi.control.plugins.PluginManager;
|
import pitiupi.control.plugins.PluginManager;
|
||||||
|
import pitiupi.presentation.shell.NetworkUsersPanel;
|
||||||
|
import pitiupi.presentation.shell.ShellHost;
|
||||||
|
import pitiupi.presentation.shell.ShellMenuBar;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Janela principal da aplicação.
|
* @deprecated Substituído por {@link pitiupi.presentation.shell.MainShellFrame}.
|
||||||
*
|
|
||||||
* <p>Responsável por inicializar a interface gráfica e coordenar os
|
|
||||||
* principais componentes da aplicação, incluindo comunicação de rede,
|
|
||||||
* gerenciamento de heartbeat e carregamento de plugins.</p>
|
|
||||||
*
|
|
||||||
* <p>A instância desta classe é compartilhada com os plugins para permitir
|
|
||||||
* integração com a aplicação principal.</p>
|
|
||||||
*
|
|
||||||
* @author flavio
|
|
||||||
* @author tony
|
|
||||||
* @author Gustavo
|
|
||||||
*/
|
*/
|
||||||
public class MainWindow extends JFrame {
|
@Deprecated
|
||||||
|
public class MainWindow extends JFrame implements ShellHost {
|
||||||
|
|
||||||
private MenuBar menuBar;
|
|
||||||
private JLabel status;
|
private JLabel status;
|
||||||
OnlineUsersPanel onlineUsersPanel;
|
private final NetworkUsersPanel networkUsersPanel;
|
||||||
|
|
||||||
private final ConnectionManager connectionManager;
|
private final ConnectionManager connectionManager;
|
||||||
private final PluginManager pluginManager;
|
private final PluginManager pluginManager;
|
||||||
private final GuiManager guiManager;
|
private final GuiManager guiManager;
|
||||||
|
|
||||||
/**
|
|
||||||
* Cria a janela principal da aplicação.
|
|
||||||
*
|
|
||||||
* @param userName nome utilizado para identificação na rede.
|
|
||||||
* @param port porta utilizada na comunicação multicast.
|
|
||||||
*/
|
|
||||||
public MainWindow(String userName, int port) {
|
public MainWindow(String userName, int port) {
|
||||||
initMenu();
|
|
||||||
|
|
||||||
pluginManager = new PluginManager();
|
pluginManager = new PluginManager();
|
||||||
connectionManager = new ConnectionManager(this, userName, pluginManager, port);
|
connectionManager = new ConnectionManager(userName, pluginManager, port);
|
||||||
|
|
||||||
|
ShellMenuBar menuBar = new ShellMenuBar();
|
||||||
|
setJMenuBar(menuBar);
|
||||||
|
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
|
||||||
|
setTitle("Pitiupi");
|
||||||
|
|
||||||
|
networkUsersPanel = new NetworkUsersPanel(connectionManager);
|
||||||
|
getContentPane().add(new JScrollPane(new JTabbedPane()), BorderLayout.CENTER);
|
||||||
|
getContentPane().add(networkUsersPanel, BorderLayout.LINE_END);
|
||||||
|
|
||||||
initUserPanel();
|
|
||||||
initStatusPanel();
|
initStatusPanel();
|
||||||
|
|
||||||
guiManager = new GuiManager(this, menuBar, onlineUsersPanel);
|
guiManager = new GuiManager(this, menuBar, networkUsersPanel);
|
||||||
|
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
|
|
||||||
this.pack();
|
addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.setSize(new java.awt.Dimension(1024, 768));
|
pack();
|
||||||
}
|
setSize(new Dimension(1024, 768));
|
||||||
|
|
||||||
private void initMenu() {
|
|
||||||
menuBar = new MenuBar(this);
|
|
||||||
|
|
||||||
this.setJMenuBar(menuBar);
|
|
||||||
this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
|
|
||||||
this.setTitle("Pitiupi");
|
|
||||||
|
|
||||||
javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane();
|
|
||||||
javax.swing.JTabbedPane tabbedPane = new javax.swing.JTabbedPane();
|
|
||||||
|
|
||||||
jScrollPane1.setViewportView(tabbedPane);
|
|
||||||
|
|
||||||
this.getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initUserPanel() {
|
|
||||||
onlineUsersPanel = new OnlineUsersPanel();
|
|
||||||
this.connectionManager.addPeerListener(onlineUsersPanel);
|
|
||||||
this.getContentPane().add(onlineUsersPanel, java.awt.BorderLayout.LINE_END);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initStatusPanel() {
|
private void initStatusPanel() {
|
||||||
JPanel statusPanel = new JPanel();
|
JPanel statusPanel = new JPanel();
|
||||||
status = new JLabel();
|
status = new JLabel();
|
||||||
|
|
||||||
statusPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
|
statusPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
|
||||||
updateStatus();
|
updateStatus();
|
||||||
statusPanel.add(status);
|
statusPanel.add(status);
|
||||||
|
getContentPane().add(statusPanel, BorderLayout.PAGE_END);
|
||||||
this.getContentPane().add(statusPanel, BorderLayout.PAGE_END);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void exit() {
|
public void exit() {
|
||||||
this.setVisible(false);
|
connectionManager.shutdownGracefully();
|
||||||
this.dispose();
|
setVisible(false);
|
||||||
|
dispose();
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ConnectionManager getConnectionManager() {
|
public ConnectionManager getConnectionManager() {
|
||||||
return connectionManager;
|
return connectionManager;
|
||||||
}
|
}
|
||||||
@@ -107,13 +86,18 @@ public class MainWindow extends JFrame {
|
|||||||
pluginManager.loadPlugins(connectionManager, guiManager);
|
pluginManager.loadPlugins(connectionManager, guiManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setUserName(String userName) {
|
public void setUserName(String userName) {
|
||||||
connectionManager.setUserName(userName);
|
connectionManager.setUserName(userName);
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatus() {
|
@Override
|
||||||
this.status.setText("Connected on port " + connectionManager.getPort() + " name " + connectionManager.getUserName());
|
public void setStatusMessage(String message) {
|
||||||
|
status.setText(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStatus() {
|
||||||
|
status.setText("Connected on port " + connectionManager.getPort() + " name " + connectionManager.getUserName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
|
|||||||
initMenu();
|
initMenu();
|
||||||
|
|
||||||
setLayout(new BorderLayout());
|
setLayout(new BorderLayout());
|
||||||
setBorder(BorderFactory.createTitledBorder("Online"));
|
setBorder(BorderFactory.createTitledBorder("Usuários nesta rede"));
|
||||||
setPreferredSize(new Dimension(160, 0));
|
setPreferredSize(new Dimension(160, 0));
|
||||||
|
|
||||||
listModel = new DefaultListModel<>();
|
listModel = new DefaultListModel<>();
|
||||||
@@ -64,6 +64,10 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userList.setSelectedIndex(index);
|
userList.setSelectedIndex(index);
|
||||||
|
User selected = listModel.getElementAt(index);
|
||||||
|
if (selected != null && selected.isBroadcastTarget()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
menu.show(userList, e.getX(), e.getY());
|
menu.show(userList, e.getX(), e.getY());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -73,7 +77,7 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
|
|||||||
menuItem.addActionListener(e -> {
|
menuItem.addActionListener(e -> {
|
||||||
User user = userList.getSelectedValue();
|
User user = userList.getSelectedValue();
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null && !user.isBroadcastTarget() && user.getAddress() != null) {
|
||||||
action.accept(user);
|
action.accept(user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -92,6 +96,7 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
|
|||||||
public void onPeersChanged(List<User> peers) {
|
public void onPeersChanged(List<User> peers) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
listModel.clear();
|
listModel.clear();
|
||||||
|
listModel.addElement(User.broadcastAll());
|
||||||
for (User peer : peers) {
|
for (User peer : peers) {
|
||||||
listModel.addElement(peer);
|
listModel.addElement(peer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
package pitiupi;
|
package pitiupi;
|
||||||
|
|
||||||
import java.net.UnknownHostException;
|
import pitiupi.platform.LegacyPluginMigration;
|
||||||
|
import pitiupi.platform.PitiupiHome;
|
||||||
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@@ -44,12 +46,21 @@ public class Main {
|
|||||||
java.awt.EventQueue.invokeLater(new Runnable() {
|
java.awt.EventQueue.invokeLater(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
pitiupi.GUI.Login login = new pitiupi.GUI.Login();
|
try {
|
||||||
login.setVisible(true);
|
PitiupiHome.get().ensureLayout();
|
||||||
|
LegacyPluginMigration.maybeMigrateFromLegacyCwd();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "Falha ao preparar PITIUPI_HOME", e);
|
||||||
|
}
|
||||||
|
|
||||||
pitiupi.GUI.MainWindow main;
|
pitiupi.presentation.shell.LoginDialog login = new pitiupi.presentation.shell.LoginDialog();
|
||||||
main = new pitiupi.GUI.MainWindow(login.getName(), login.getPort());
|
if (!login.showLogin()) {
|
||||||
main.setVisible(true);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pitiupi.presentation.shell.MainShellFrame shell =
|
||||||
|
new pitiupi.presentation.shell.MainShellFrame(login.getUserName(), login.getPort());
|
||||||
|
shell.showOnScreen();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.application.invites.SessionInviteKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convite aceito pelo usuário na UI.
|
||||||
|
*/
|
||||||
|
public final class InviteDetails {
|
||||||
|
|
||||||
|
private final String inviteId;
|
||||||
|
private final String fromInstanceId;
|
||||||
|
private final String fromDisplayName;
|
||||||
|
private final String toInstanceId;
|
||||||
|
private final String pluginId;
|
||||||
|
private final SessionInviteKind kind;
|
||||||
|
private final String payload;
|
||||||
|
|
||||||
|
public InviteDetails(
|
||||||
|
String inviteId,
|
||||||
|
String fromInstanceId,
|
||||||
|
String fromDisplayName,
|
||||||
|
String toInstanceId,
|
||||||
|
String pluginId,
|
||||||
|
SessionInviteKind kind,
|
||||||
|
String payload
|
||||||
|
) {
|
||||||
|
this.inviteId = inviteId;
|
||||||
|
this.fromInstanceId = fromInstanceId;
|
||||||
|
this.fromDisplayName = fromDisplayName;
|
||||||
|
this.toInstanceId = toInstanceId;
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.kind = kind;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInviteId() {
|
||||||
|
return inviteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromInstanceId() {
|
||||||
|
return fromInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromDisplayName() {
|
||||||
|
return fromDisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToInstanceId() {
|
||||||
|
return toInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionInviteKind getKind() {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.application.invites.InviteInboxService;
|
||||||
|
import pitiupi.application.peeractions.PeerActionBroker;
|
||||||
|
import pitiupi.application.peeractions.PluginPeerActionsRegistrar;
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
import pitiupi.control.GuiManager;
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptador Onda C: expõe workspace e mantém {@link GuiManager} para plugins legados.
|
||||||
|
*/
|
||||||
|
public final class LegacyPluginWorkspace implements PluginWorkspace {
|
||||||
|
|
||||||
|
private final String pluginId;
|
||||||
|
private final ConnectionManager connectionManager;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final PeerActionBroker peerActionBroker;
|
||||||
|
private final InviteInboxService inviteInboxService;
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final PluginPeerActionsRegistrar registrar;
|
||||||
|
|
||||||
|
public LegacyPluginWorkspace(
|
||||||
|
String pluginId,
|
||||||
|
Plugin plugin,
|
||||||
|
ConnectionManager connectionManager,
|
||||||
|
GuiManager guiManager,
|
||||||
|
PeerActionBroker peerActionBroker,
|
||||||
|
InviteInboxService inviteInboxService
|
||||||
|
) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.peerActionBroker = peerActionBroker;
|
||||||
|
this.inviteInboxService = inviteInboxService;
|
||||||
|
this.registrar = new PluginPeerActionsRegistrar(peerActionBroker, pluginId);
|
||||||
|
peerActionBroker.registerPlugin(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiManager guiManager() {
|
||||||
|
return guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String pluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionManager connectionManager() {
|
||||||
|
return connectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerActionsRegistrar peerActions() {
|
||||||
|
return registrar;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void openOrFocusTab(String pluginInstanceId, String title, JComponent content) {
|
||||||
|
peerActionBroker.setActivePlugin(pluginInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatusMessage(String text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliverInviteAccepted(InviteDetails invite) {
|
||||||
|
plugin.onInviteAccepted(invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindInviteDelivery() {
|
||||||
|
inviteInboxService.setInviteHandler(pluginId, plugin::onInviteAccepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.application.peeractions.PeerAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registro de ações contextuais na lista de usuários (plugin da aba ativa).
|
||||||
|
*/
|
||||||
|
public interface PeerActionsRegistrar {
|
||||||
|
|
||||||
|
void register(PeerAction action);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Superfície exposta ao plugin na aba central (evolução da API legada).
|
||||||
|
*/
|
||||||
|
public interface PluginWorkspace {
|
||||||
|
|
||||||
|
ConnectionManager connectionManager();
|
||||||
|
|
||||||
|
PeerActionsRegistrar peerActions();
|
||||||
|
|
||||||
|
void openOrFocusTab(String pluginInstanceId, String title, JComponent content);
|
||||||
|
|
||||||
|
default void openOrFocusTab(String pluginId) {
|
||||||
|
openOrFocusTab(pluginId, pluginId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void openOrFocusTab(String pluginInstanceId, String title) {
|
||||||
|
openOrFocusTab(pluginInstanceId, title, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStatusMessage(String text);
|
||||||
|
|
||||||
|
void deliverInviteAccepted(InviteDetails invite);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package pitiupi.api.plugin;
|
||||||
|
|
||||||
|
import pitiupi.application.invites.InviteInboxService;
|
||||||
|
import pitiupi.application.peeractions.PeerActionBroker;
|
||||||
|
import pitiupi.application.peeractions.PluginPeerActionsRegistrar;
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
import pitiupi.control.GuiManager;
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
|
import pitiupi.presentation.shell.ShellHost;
|
||||||
|
import pitiupi.presentation.shell.TabWorkspacePanel;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workspace com abas e barra de status (Onda D).
|
||||||
|
*/
|
||||||
|
public final class ShellPluginWorkspace implements PluginWorkspace {
|
||||||
|
|
||||||
|
private final String pluginId;
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final ConnectionManager connectionManager;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final TabWorkspacePanel tabWorkspace;
|
||||||
|
private final ShellHost shellHost;
|
||||||
|
private final PluginPeerActionsRegistrar registrar;
|
||||||
|
|
||||||
|
public ShellPluginWorkspace(
|
||||||
|
String pluginId,
|
||||||
|
Plugin plugin,
|
||||||
|
ConnectionManager connectionManager,
|
||||||
|
GuiManager guiManager,
|
||||||
|
PeerActionBroker peerActionBroker,
|
||||||
|
InviteInboxService inviteInboxService,
|
||||||
|
TabWorkspacePanel tabWorkspace,
|
||||||
|
ShellHost shellHost
|
||||||
|
) {
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
this.guiManager = guiManager;
|
||||||
|
this.tabWorkspace = tabWorkspace;
|
||||||
|
this.shellHost = shellHost;
|
||||||
|
this.registrar = new PluginPeerActionsRegistrar(peerActionBroker, pluginId);
|
||||||
|
peerActionBroker.registerPlugin(pluginId);
|
||||||
|
peerActionBroker.registerPluginMetadata(pluginId, plugin.getDisplayName());
|
||||||
|
inviteInboxService.setInviteHandler(pluginId, plugin::onInviteAccepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiManager guiManager() {
|
||||||
|
return guiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionManager connectionManager() {
|
||||||
|
return connectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeerActionsRegistrar peerActions() {
|
||||||
|
return registrar;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void openOrFocusTab(String pluginInstanceId) {
|
||||||
|
openOrFocusTab(pluginInstanceId, plugin.getDisplayName(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void openOrFocusTab(String pluginInstanceId, String title, JComponent content) {
|
||||||
|
JComponent panel = content != null ? content : plugin.getShellWorkspaceContent();
|
||||||
|
tabWorkspace.openOrFocusTab(pluginInstanceId, title, panel);
|
||||||
|
connectionManager.getPeerActionBroker().setActivePlugin(pluginInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatusMessage(String text) {
|
||||||
|
shellHost.setStatusMessage(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deliverInviteAccepted(InviteDetails invite) {
|
||||||
|
plugin.onInviteAccepted(invite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package pitiupi.application.activity;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pendências de atividade por peer (mensagem, tela, etc.) para badges na lista lateral.
|
||||||
|
*/
|
||||||
|
public final class PeerActivityInbox {
|
||||||
|
|
||||||
|
public static final String BROADCAST_PEER_KEY = "__broadcast__";
|
||||||
|
|
||||||
|
private final Map<String, Map<String, Integer>> countByPeerAndAction = new ConcurrentHashMap<>();
|
||||||
|
private Runnable onChanged = () -> {
|
||||||
|
};
|
||||||
|
|
||||||
|
public void setOnChanged(Runnable onChanged) {
|
||||||
|
this.onChanged = onChanged != null ? onChanged : () -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void record(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
|
String peerKey = peerKey(peerInstanceId);
|
||||||
|
String actionKey = actionKey(pluginId, peerActionId);
|
||||||
|
countByPeerAndAction.computeIfAbsent(peerKey, k -> new ConcurrentHashMap<>())
|
||||||
|
.merge(actionKey, 1, Integer::sum);
|
||||||
|
onChanged.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordBroadcast(String pluginId, String peerActionId) {
|
||||||
|
record(BROADCAST_PEER_KEY, pluginId, peerActionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int totalForPeer(String peerInstanceId) {
|
||||||
|
return sum(countByPeerAndAction.getOrDefault(peerKey(peerInstanceId), Map.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int countForAction(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
|
return countByPeerAndAction
|
||||||
|
.getOrDefault(peerKey(peerInstanceId), Map.of())
|
||||||
|
.getOrDefault(actionKey(pluginId, peerActionId), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAction(String peerInstanceId, String pluginId, String peerActionId) {
|
||||||
|
Map<String, Integer> actions = countByPeerAndAction.get(peerKey(peerInstanceId));
|
||||||
|
if (actions != null) {
|
||||||
|
actions.remove(actionKey(pluginId, peerActionId));
|
||||||
|
if (actions.isEmpty()) {
|
||||||
|
countByPeerAndAction.remove(peerKey(peerInstanceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChanged.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int sum(Map<String, Integer> map) {
|
||||||
|
int total = 0;
|
||||||
|
for (Integer v : map.values()) {
|
||||||
|
total += v;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String peerKey(String peerInstanceId) {
|
||||||
|
return peerInstanceId != null ? peerInstanceId : BROADCAST_PEER_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String actionKey(String pluginId, String peerActionId) {
|
||||||
|
return pluginId + ":" + peerActionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package pitiupi.application.invites;
|
||||||
|
|
||||||
|
import pitiupi.api.plugin.InviteDetails;
|
||||||
|
import pitiupi.control.HeartbeatManager;
|
||||||
|
import pitiupi.models.Messages.SessionInviteMessage;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caixa de convites recebidos; incrementa contador por remetente na lista.
|
||||||
|
*/
|
||||||
|
public final class InviteInboxService {
|
||||||
|
|
||||||
|
private final HeartbeatManager heartbeatManager;
|
||||||
|
private final Map<String, Integer> notificationCountByFromInstance = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, SessionInviteMessage> pendingByInviteId = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Consumer<InviteDetails>> handlersByPluginId = new ConcurrentHashMap<>();
|
||||||
|
private Runnable onNotificationsChanged = () -> {
|
||||||
|
};
|
||||||
|
private volatile String lastAcceptedPluginId;
|
||||||
|
|
||||||
|
public InviteInboxService(HeartbeatManager heartbeatManager) {
|
||||||
|
this.heartbeatManager = heartbeatManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnNotificationsChanged(Runnable onNotificationsChanged) {
|
||||||
|
this.onNotificationsChanged = onNotificationsChanged != null ? onNotificationsChanged : () -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInviteHandler(String pluginId, Consumer<InviteDetails> handler) {
|
||||||
|
handlersByPluginId.put(pluginId, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receive(SessionInviteMessage message) {
|
||||||
|
if (message.getFromInstanceId().equals(heartbeatManager.getInstanceId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String to = message.getToInstanceId();
|
||||||
|
if (to != null && !to.isBlank() && !to.equals(heartbeatManager.getInstanceId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingByInviteId.put(message.getInviteId(), message);
|
||||||
|
notificationCountByFromInstance.merge(message.getFromInstanceId(), 1, Integer::sum);
|
||||||
|
onNotificationsChanged.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNotificationCount(String fromInstanceId) {
|
||||||
|
return notificationCountByFromInstance.getOrDefault(fromInstanceId, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean acceptInvite(String inviteId) {
|
||||||
|
SessionInviteMessage message = pendingByInviteId.remove(inviteId);
|
||||||
|
if (message == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
decrementNotification(message.getFromInstanceId());
|
||||||
|
InviteDetails details = new InviteDetails(
|
||||||
|
message.getInviteId(),
|
||||||
|
message.getFromInstanceId(),
|
||||||
|
message.getFromDisplayName(),
|
||||||
|
message.getToInstanceId(),
|
||||||
|
message.getPluginId(),
|
||||||
|
message.getKind(),
|
||||||
|
message.getPayload()
|
||||||
|
);
|
||||||
|
lastAcceptedPluginId = message.getPluginId();
|
||||||
|
Consumer<InviteDetails> handler = handlersByPluginId.get(message.getPluginId());
|
||||||
|
if (handler != null) {
|
||||||
|
handler.accept(details);
|
||||||
|
}
|
||||||
|
onNotificationsChanged.run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String acceptFirstInviteFrom(String fromInstanceId) {
|
||||||
|
for (Map.Entry<String, SessionInviteMessage> e : pendingByInviteId.entrySet()) {
|
||||||
|
if (e.getValue().getFromInstanceId().equals(fromInstanceId)) {
|
||||||
|
if (acceptInvite(e.getKey())) {
|
||||||
|
return lastAcceptedPluginId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastAcceptedPluginId() {
|
||||||
|
return lastAcceptedPluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decrementNotification(String fromInstanceId) {
|
||||||
|
notificationCountByFromInstance.computeIfPresent(fromInstanceId, (k, v) -> v <= 1 ? null : v - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package pitiupi.application.invites;
|
||||||
|
|
||||||
|
public enum SessionInviteKind {
|
||||||
|
CHAT,
|
||||||
|
SCREEN,
|
||||||
|
PING,
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package pitiupi.application.messaging;
|
||||||
|
|
||||||
|
import pitiupi.application.presence.UserPeerView;
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
import pitiupi.models.Messages.Message;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envio didático multicast / unicast / TCP para sala geral ou um peer.
|
||||||
|
*/
|
||||||
|
public final class MessageDispatcher {
|
||||||
|
|
||||||
|
private MessageDispatcher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int send(
|
||||||
|
ConnectionManager connectionManager,
|
||||||
|
Message message,
|
||||||
|
SendTarget target,
|
||||||
|
TransportMode mode
|
||||||
|
) {
|
||||||
|
if (target instanceof SendTarget.BroadcastAll) {
|
||||||
|
return sendToAll(connectionManager, message, mode);
|
||||||
|
}
|
||||||
|
if (target instanceof SendTarget.SinglePeer single) {
|
||||||
|
return sendToOne(connectionManager, message, mode, single.address()) ? 1 : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int sendToAll(ConnectionManager connectionManager, Message message, TransportMode mode) {
|
||||||
|
if (mode == TransportMode.MULTICAST_UDP) {
|
||||||
|
return connectionManager.sendUdpMulticast(message) ? 1 : 0;
|
||||||
|
}
|
||||||
|
List<InetAddress> peers = onlinePeerAddresses(connectionManager);
|
||||||
|
int sent = 0;
|
||||||
|
for (InetAddress address : peers) {
|
||||||
|
if (sendToOne(connectionManager, message, mode, address)) {
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sendToOne(
|
||||||
|
ConnectionManager connectionManager,
|
||||||
|
Message message,
|
||||||
|
TransportMode mode,
|
||||||
|
InetAddress address
|
||||||
|
) {
|
||||||
|
if (address == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return switch (mode) {
|
||||||
|
case MULTICAST_UDP -> connectionManager.sendUdpMulticast(message);
|
||||||
|
case UNICAST_UDP -> connectionManager.sendUdpUnicast(message, address);
|
||||||
|
case TCP -> connectionManager.sendTcp(message, address);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<InetAddress> onlinePeerAddresses(ConnectionManager connectionManager) {
|
||||||
|
List<InetAddress> addresses = new ArrayList<>();
|
||||||
|
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||||
|
if (view.isBroadcastTarget() || view.isSelf()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
view.getInetAddress().ifPresent(addresses::add);
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package pitiupi.application.messaging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rótulos de destino na UI (chat, screencast, etc.).
|
||||||
|
*/
|
||||||
|
public final class MessagingDestinationUi {
|
||||||
|
|
||||||
|
public static final String EVERYONE_IN_ROOM = "Todos na sala";
|
||||||
|
public static final String SINGLE_PERSON = "Para uma pessoa (IP)";
|
||||||
|
|
||||||
|
private static final String PRIVATE_TCP_SUMMARY =
|
||||||
|
"Conversa só entre vocês (TCP privado)";
|
||||||
|
|
||||||
|
private MessagingDestinationUi() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String summarizeSendMode(boolean everyoneInRoom, TransportMode mode, boolean privateTcp) {
|
||||||
|
if (!everyoneInRoom && privateTcp) {
|
||||||
|
return PRIVATE_TCP_SUMMARY;
|
||||||
|
}
|
||||||
|
if (mode == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mode.describeForUi(everyoneInRoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package pitiupi.application.messaging;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transportes expostos na UI por tipo de plugin e destino (didático).
|
||||||
|
*/
|
||||||
|
public final class PluginTransportCatalog {
|
||||||
|
|
||||||
|
public enum PluginKind {
|
||||||
|
CHAT,
|
||||||
|
SCREEN_CAST
|
||||||
|
}
|
||||||
|
|
||||||
|
private PluginTransportCatalog() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<TransportMode> forBroadcast(PluginKind kind) {
|
||||||
|
return switch (kind) {
|
||||||
|
case CHAT -> List.of(
|
||||||
|
TransportMode.MULTICAST_UDP,
|
||||||
|
TransportMode.UNICAST_UDP,
|
||||||
|
TransportMode.TCP
|
||||||
|
);
|
||||||
|
case SCREEN_CAST -> List.of(
|
||||||
|
TransportMode.MULTICAST_UDP,
|
||||||
|
TransportMode.UNICAST_UDP
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<TransportMode> forSinglePeer(PluginKind kind) {
|
||||||
|
return switch (kind) {
|
||||||
|
case CHAT, SCREEN_CAST -> List.of(
|
||||||
|
TransportMode.UNICAST_UDP,
|
||||||
|
TransportMode.TCP
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package pitiupi.application.messaging;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destino de envio: sala geral ou um endereço IP.
|
||||||
|
*/
|
||||||
|
public sealed interface SendTarget permits SendTarget.BroadcastAll, SendTarget.SinglePeer {
|
||||||
|
|
||||||
|
record BroadcastAll() implements SendTarget {
|
||||||
|
}
|
||||||
|
|
||||||
|
record SinglePeer(InetAddress address) implements SendTarget {
|
||||||
|
}
|
||||||
|
|
||||||
|
static SendTarget broadcastAll() {
|
||||||
|
return new BroadcastAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
static SendTarget singlePeer(InetAddress address) {
|
||||||
|
return new SinglePeer(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package pitiupi.application.messaging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transportes expostos na UI didática dos plugins.
|
||||||
|
* {@link #describeForUi(boolean)} é o resumo didático ao lado do destino;
|
||||||
|
* {@link #getLabel()} e {@link #toString()} são os nomes curtos no combo (UDP unicast, TCP, …).
|
||||||
|
*/
|
||||||
|
public enum TransportMode {
|
||||||
|
MULTICAST_UDP("UDP multicast"),
|
||||||
|
UNICAST_UDP("UDP unicast"),
|
||||||
|
TCP("TCP");
|
||||||
|
|
||||||
|
private final String transcriptTag;
|
||||||
|
|
||||||
|
TransportMode(String transcriptTag) {
|
||||||
|
this.transcriptTag = transcriptTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag no histórico (ex.: {@code [UDP multicast]}). */
|
||||||
|
public String getLabel() {
|
||||||
|
return transcriptTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texto do combo e resumos — linguagem simples com o termo técnico entre parênteses.
|
||||||
|
*
|
||||||
|
* @param everyoneInRoom {@code true} quando o destino é a sala (todos); {@code false} para um IP.
|
||||||
|
*/
|
||||||
|
public String describeForUi(boolean everyoneInRoom) {
|
||||||
|
return switch (this) {
|
||||||
|
case MULTICAST_UDP -> everyoneInRoom
|
||||||
|
? "Todo mundo na sala de uma vez (UDP multicast)"
|
||||||
|
: "Para todos de uma vez (UDP multicast)";
|
||||||
|
case UNICAST_UDP -> everyoneInRoom
|
||||||
|
? "Uma cópia para cada pessoa na sala (UDP unicast)"
|
||||||
|
: "Direto para o IP da pessoa (UDP unicast)";
|
||||||
|
case TCP -> everyoneInRoom
|
||||||
|
? "Uma cópia por conexão para cada pessoa na sala (TCP)"
|
||||||
|
: "Direto para o IP por conexão (TCP)";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return transcriptTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ações de um plugin para o menu contextual da lista lateral.
|
||||||
|
*/
|
||||||
|
public record GroupedPeerActions(String pluginId, String pluginDisplayName, List<PeerAction> actions) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ação exibida no menu contextual da lista de usuários.
|
||||||
|
*/
|
||||||
|
public final class PeerAction {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String label;
|
||||||
|
private final Set<PeerActionTarget> targets;
|
||||||
|
private final PeerActionHandler handler;
|
||||||
|
|
||||||
|
public PeerAction(String id, String label, Set<PeerActionTarget> targets, PeerActionHandler handler) {
|
||||||
|
this.id = id;
|
||||||
|
this.label = label;
|
||||||
|
this.targets = EnumSet.copyOf(targets);
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PeerAction of(String id, String label, PeerActionTarget target, PeerActionHandler handler) {
|
||||||
|
return new PeerAction(id, label, Set.of(target), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PeerActionTarget> getTargets() {
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PeerActionHandler getHandler() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supports(PeerActionTarget target) {
|
||||||
|
return targets.contains(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registro de peer actions por plugin; lista lateral pode exibir todos os plugins.
|
||||||
|
*/
|
||||||
|
public final class PeerActionBroker {
|
||||||
|
|
||||||
|
private final Map<String, List<PeerAction>> actionsByPlugin = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, String> displayNameByPlugin = new ConcurrentHashMap<>();
|
||||||
|
private volatile String activePluginId;
|
||||||
|
|
||||||
|
public void registerPlugin(String pluginId) {
|
||||||
|
actionsByPlugin.putIfAbsent(pluginId, new ArrayList<>());
|
||||||
|
if (activePluginId == null) {
|
||||||
|
activePluginId = pluginId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPluginMetadata(String pluginId, String displayName) {
|
||||||
|
displayNameByPlugin.put(pluginId, displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivePlugin(String pluginId) {
|
||||||
|
this.activePluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActivePluginId() {
|
||||||
|
return activePluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerAction(String pluginId, PeerAction action) {
|
||||||
|
actionsByPlugin.computeIfAbsent(pluginId, k -> new ArrayList<>()).add(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PeerAction> getActionsFor(PeerActionTarget target) {
|
||||||
|
if (activePluginId == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return filterActions(actionsByPlugin.getOrDefault(activePluginId, List.of()), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GroupedPeerActions> getAllActionsFor(PeerActionTarget target) {
|
||||||
|
List<GroupedPeerActions> groups = new ArrayList<>();
|
||||||
|
List<String> pluginIds = new ArrayList<>(actionsByPlugin.keySet());
|
||||||
|
pluginIds.sort(Comparator.comparing(id -> displayNameByPlugin.getOrDefault(id, id)));
|
||||||
|
|
||||||
|
for (String pluginId : pluginIds) {
|
||||||
|
List<PeerAction> filtered = filterActions(actionsByPlugin.getOrDefault(pluginId, List.of()), target);
|
||||||
|
if (!filtered.isEmpty()) {
|
||||||
|
groups.add(new GroupedPeerActions(
|
||||||
|
pluginId,
|
||||||
|
displayNameByPlugin.getOrDefault(pluginId, pluginId),
|
||||||
|
filtered
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PeerAction> filterActions(List<PeerAction> registered, PeerActionTarget target) {
|
||||||
|
List<PeerAction> filtered = new ArrayList<>();
|
||||||
|
for (PeerAction action : registered) {
|
||||||
|
if (action.supports(target)) {
|
||||||
|
filtered.add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearPlugin(String pluginId) {
|
||||||
|
actionsByPlugin.remove(pluginId);
|
||||||
|
displayNameByPlugin.remove(pluginId);
|
||||||
|
if (pluginId.equals(activePluginId)) {
|
||||||
|
activePluginId = actionsByPlugin.keySet().stream().findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PeerActionHandler {
|
||||||
|
|
||||||
|
void run(PeerTarget target);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Público-alvo de uma peer action.
|
||||||
|
*/
|
||||||
|
public enum PeerActionTarget {
|
||||||
|
SINGLE_PEER,
|
||||||
|
ALL_PEERS
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
import pitiupi.application.presence.UserPeerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destino escolhido na lista (um peer ou Todos).
|
||||||
|
*/
|
||||||
|
public sealed interface PeerTarget permits PeerTarget.Single, PeerTarget.All {
|
||||||
|
|
||||||
|
record Single(UserPeerView peer) implements PeerTarget {
|
||||||
|
}
|
||||||
|
|
||||||
|
record All() implements PeerTarget {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package pitiupi.application.peeractions;
|
||||||
|
|
||||||
|
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||||
|
|
||||||
|
public final class PluginPeerActionsRegistrar implements PeerActionsRegistrar {
|
||||||
|
|
||||||
|
private final PeerActionBroker broker;
|
||||||
|
private final String pluginId;
|
||||||
|
|
||||||
|
public PluginPeerActionsRegistrar(PeerActionBroker broker, String pluginId) {
|
||||||
|
this.broker = broker;
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(PeerAction action) {
|
||||||
|
broker.registerAction(pluginId, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package pitiupi.application.presence;
|
||||||
|
|
||||||
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
|
import pitiupi.application.invites.InviteInboxService;
|
||||||
|
import pitiupi.control.HeartbeatManager;
|
||||||
|
import pitiupi.models.Messages.GoodbyeMessage;
|
||||||
|
import pitiupi.models.Messages.HeartbeatMessage;
|
||||||
|
import pitiupi.models.User;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orquestra presença na camada de aplicação (sem Swing).
|
||||||
|
*/
|
||||||
|
public final class PresenceService {
|
||||||
|
|
||||||
|
private final HeartbeatManager heartbeatManager;
|
||||||
|
private final InviteInboxService inviteInboxService;
|
||||||
|
private final PeerActivityInbox peerActivityInbox;
|
||||||
|
|
||||||
|
public PresenceService(HeartbeatManager heartbeatManager) {
|
||||||
|
this(heartbeatManager, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PresenceService(HeartbeatManager heartbeatManager, InviteInboxService inviteInboxService) {
|
||||||
|
this(heartbeatManager, inviteInboxService, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PresenceService(
|
||||||
|
HeartbeatManager heartbeatManager,
|
||||||
|
InviteInboxService inviteInboxService,
|
||||||
|
PeerActivityInbox peerActivityInbox
|
||||||
|
) {
|
||||||
|
this.heartbeatManager = heartbeatManager;
|
||||||
|
this.inviteInboxService = inviteInboxService;
|
||||||
|
this.peerActivityInbox = peerActivityInbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onHeartbeat(HeartbeatMessage msg, InetAddress from) {
|
||||||
|
heartbeatManager.receiveHeartbeat(msg, from);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGoodbye(GoodbyeMessage msg) {
|
||||||
|
heartbeatManager.receiveGoodbye(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserPeerView> listPeerViews() {
|
||||||
|
List<UserPeerView> views = new ArrayList<>();
|
||||||
|
int broadcastActivity = peerActivityInbox != null
|
||||||
|
? peerActivityInbox.totalForPeer(PeerActivityInbox.BROADCAST_PEER_KEY)
|
||||||
|
: 0;
|
||||||
|
views.add(new UserPeerView("Todos", null, null, broadcastActivity, false, true));
|
||||||
|
String selfId = heartbeatManager.getInstanceId();
|
||||||
|
for (User user : heartbeatManager.getPeers()) {
|
||||||
|
boolean isSelf = selfId.equals(user.getInstanceId());
|
||||||
|
int notifications = 0;
|
||||||
|
if (inviteInboxService != null && user.getInstanceId() != null) {
|
||||||
|
notifications += inviteInboxService.getNotificationCount(user.getInstanceId());
|
||||||
|
}
|
||||||
|
if (peerActivityInbox != null && user.getInstanceId() != null) {
|
||||||
|
notifications += peerActivityInbox.totalForPeer(user.getInstanceId());
|
||||||
|
}
|
||||||
|
views.add(new UserPeerView(
|
||||||
|
user.getUserName(),
|
||||||
|
user.getInstanceId(),
|
||||||
|
user.getAddress(),
|
||||||
|
notifications,
|
||||||
|
isSelf,
|
||||||
|
false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPeerView broadcastTarget() {
|
||||||
|
return UserPeerView.broadcastTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package pitiupi.application.presence;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO de peer para a UI (lista lateral e peer actions futuras).
|
||||||
|
*/
|
||||||
|
public final class UserPeerView {
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String instanceId;
|
||||||
|
private final InetAddress inetAddress;
|
||||||
|
private final int notificationCount;
|
||||||
|
private final boolean isSelf;
|
||||||
|
private final boolean isBroadcastTarget;
|
||||||
|
|
||||||
|
public UserPeerView(
|
||||||
|
String displayName,
|
||||||
|
String instanceId,
|
||||||
|
InetAddress inetAddress,
|
||||||
|
int notificationCount,
|
||||||
|
boolean isSelf,
|
||||||
|
boolean isBroadcastTarget
|
||||||
|
) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
this.inetAddress = inetAddress;
|
||||||
|
this.notificationCount = notificationCount;
|
||||||
|
this.isSelf = isSelf;
|
||||||
|
this.isBroadcastTarget = isBroadcastTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<InetAddress> getInetAddress() {
|
||||||
|
return Optional.ofNullable(inetAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNotificationCount() {
|
||||||
|
return notificationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSelf() {
|
||||||
|
return isSelf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBroadcastTarget() {
|
||||||
|
return isBroadcastTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserPeerView broadcastTarget() {
|
||||||
|
return new UserPeerView("Todos", null, null, 0, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
package pitiupi.control;
|
package pitiupi.control;
|
||||||
|
|
||||||
import pitiupi.GUI.MainWindow;
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
|
import pitiupi.application.invites.InviteInboxService;
|
||||||
|
import pitiupi.application.peeractions.PeerActionBroker;
|
||||||
|
import pitiupi.application.presence.PresenceService;
|
||||||
|
import pitiupi.models.Messages.SessionInviteMessage;
|
||||||
import pitiupi.control.plugins.PluginManager;
|
import pitiupi.control.plugins.PluginManager;
|
||||||
|
import pitiupi.models.Messages.GoodbyeMessage;
|
||||||
import pitiupi.models.Messages.HeartbeatMessage;
|
import pitiupi.models.Messages.HeartbeatMessage;
|
||||||
import pitiupi.models.Messages.Message;
|
import pitiupi.models.Messages.Message;
|
||||||
import pitiupi.net.tcp.SocketTCP;
|
import pitiupi.net.tcp.SocketTCP;
|
||||||
@@ -9,6 +14,7 @@ import pitiupi.net.tcp.TcpConnection;
|
|||||||
import pitiupi.net.udp.SocketUDP;
|
import pitiupi.net.udp.SocketUDP;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class ConnectionManager {
|
public class ConnectionManager {
|
||||||
private final String pitiupiIdentifier;
|
private final String pitiupiIdentifier;
|
||||||
@@ -16,13 +22,20 @@ public class ConnectionManager {
|
|||||||
private final PluginManager pluginManager;
|
private final PluginManager pluginManager;
|
||||||
|
|
||||||
private final HeartbeatManager heartbeatManager;
|
private final HeartbeatManager heartbeatManager;
|
||||||
|
private final PresenceService presenceService;
|
||||||
|
private final PeerActionBroker peerActionBroker;
|
||||||
|
private final InviteInboxService inviteInboxService;
|
||||||
|
private final PeerActivityInbox peerActivityInbox;
|
||||||
|
|
||||||
private SocketTCP socketTCP;
|
private SocketTCP socketTCP;
|
||||||
private SocketUDP socketUDP;
|
private SocketUDP socketUDP;
|
||||||
private String username;
|
private String username;
|
||||||
private int port;
|
private int port;
|
||||||
|
private volatile boolean shutdownComplete;
|
||||||
|
private Consumer<String> inviteAcceptedUiListener = pluginId -> {
|
||||||
|
};
|
||||||
|
|
||||||
public ConnectionManager(MainWindow main, String username, PluginManager pluginManager, int port) {
|
public ConnectionManager(String username, PluginManager pluginManager, int port) {
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
|
|
||||||
this.username = username;
|
this.username = username;
|
||||||
@@ -33,7 +46,64 @@ public class ConnectionManager {
|
|||||||
reconnect();
|
reconnect();
|
||||||
|
|
||||||
heartbeatManager = new HeartbeatManager(this);
|
heartbeatManager = new HeartbeatManager(this);
|
||||||
|
peerActionBroker = new PeerActionBroker();
|
||||||
|
inviteInboxService = new InviteInboxService(heartbeatManager);
|
||||||
|
peerActivityInbox = new PeerActivityInbox();
|
||||||
|
presenceService = new PresenceService(heartbeatManager, inviteInboxService, peerActivityInbox);
|
||||||
|
inviteInboxService.setOnNotificationsChanged(heartbeatManager::notifyPeerListeners);
|
||||||
|
peerActivityInbox.setOnChanged(heartbeatManager::notifyPeerListeners);
|
||||||
heartbeatManager.start();
|
heartbeatManager.start();
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdownGracefully, "pitiupi-goodbye"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PresenceService getPresenceService() {
|
||||||
|
return presenceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PeerActionBroker getPeerActionBroker() {
|
||||||
|
return peerActionBroker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InviteInboxService getInviteInboxService() {
|
||||||
|
return inviteInboxService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PeerActivityInbox getPeerActivityInbox() {
|
||||||
|
return peerActivityInbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInviteAcceptedUiListener(Consumer<String> listener) {
|
||||||
|
this.inviteAcceptedUiListener = listener != null ? listener : pluginId -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyInviteAcceptedForUi(String pluginId) {
|
||||||
|
if (pluginId != null) {
|
||||||
|
inviteAcceptedUiListener.accept(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeartbeatManager getHeartbeatManager() {
|
||||||
|
return heartbeatManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receiveSessionInvite(SessionInviteMessage message) {
|
||||||
|
inviteInboxService.receive(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdownGracefully() {
|
||||||
|
if (shutdownComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shutdownComplete = true;
|
||||||
|
heartbeatManager.sendGoodbyeAndStop();
|
||||||
|
if (socketUDP != null) {
|
||||||
|
socketUDP.close();
|
||||||
|
}
|
||||||
|
if (socketTCP != null) {
|
||||||
|
socketTCP.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TcpConnection connectPluginTcp(InetAddress destinationAdress, String senderId, String receiverId) {
|
public TcpConnection connectPluginTcp(InetAddress destinationAdress, String senderId, String receiverId) {
|
||||||
@@ -72,7 +142,11 @@ public class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void receiveHeartbeat(HeartbeatMessage message, InetAddress destinationAddress) {
|
public void receiveHeartbeat(HeartbeatMessage message, InetAddress destinationAddress) {
|
||||||
heartbeatManager.receiveHeartbeat(message, destinationAddress);
|
presenceService.onHeartbeat(message, destinationAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receiveGoodbye(GoodbyeMessage message) {
|
||||||
|
presenceService.onGoodbye(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPort(int port) {
|
public void setPort(int port) {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package pitiupi.control;
|
package pitiupi.control;
|
||||||
|
|
||||||
import pitiupi.GUI.MainWindow;
|
import pitiupi.presentation.shell.NetworkUsersPanel;
|
||||||
import pitiupi.GUI.MenuBar;
|
import pitiupi.presentation.shell.ShellHost;
|
||||||
import pitiupi.GUI.OnlineUsersPanel;
|
|
||||||
import pitiupi.models.Menus.*;
|
import pitiupi.models.Menus.*;
|
||||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
||||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||||
@@ -16,39 +15,25 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class GuiManager {
|
public class GuiManager {
|
||||||
private MainWindow mainWindow;
|
private final ShellHost shellHost;
|
||||||
private MenuBar menuBar;
|
private final NetworkUsersPanel networkUsersPanel;
|
||||||
private OnlineUsersPanel onlineUsersPanel;
|
|
||||||
|
|
||||||
private Logger logger;
|
private Logger logger;
|
||||||
|
|
||||||
public GuiManager(MainWindow mainWindow, MenuBar menuBar, OnlineUsersPanel onlineUsersPanel) {
|
public GuiManager(ShellHost shellHost, javax.swing.JMenuBar menuBar, NetworkUsersPanel networkUsersPanel) {
|
||||||
this.mainWindow = mainWindow;
|
this.shellHost = shellHost;
|
||||||
this.menuBar = menuBar;
|
this.networkUsersPanel = networkUsersPanel;
|
||||||
this.onlineUsersPanel = onlineUsersPanel;
|
|
||||||
this.logger = Logger.getLogger(GuiManager.class.getName());
|
this.logger = Logger.getLogger(GuiManager.class.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createMenu(MenuCategory category, Menu menu) {
|
public void createMenu(MenuCategory category, Menu menu) {
|
||||||
System.out.println(
|
|
||||||
menu.getClass().getSuperclass().getSuperclass()
|
|
||||||
);
|
|
||||||
|
|
||||||
System.out.println(
|
|
||||||
MulticastMenu.class
|
|
||||||
);
|
|
||||||
|
|
||||||
System.out.println(
|
|
||||||
menu.getClass().getSuperclass().getSuperclass()
|
|
||||||
== MulticastMenu.class
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
JMenuItem newMenu = buildMenu(menu);
|
JMenuItem newMenu = buildMenu(menu);
|
||||||
if (menu instanceof MulticastMenu) {
|
if (menu instanceof MulticastMenu) {
|
||||||
menuBar.addPluginMenu(category, newMenu);
|
// Onda D: ações multicast migram para peer actions / abas (Onda E).
|
||||||
}
|
}
|
||||||
if (menu instanceof UnicastMenu) {
|
if (menu instanceof UnicastMenu) {
|
||||||
onlineUsersPanel.addUnicastMenu(category, newMenu);
|
networkUsersPanel.addUnicastMenu(category, newMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException e) {
|
catch (IllegalArgumentException e) {
|
||||||
@@ -86,7 +71,7 @@ public class GuiManager {
|
|||||||
if (menu instanceof UnicastActionMenu unicastActionMenu) {
|
if (menu instanceof UnicastActionMenu unicastActionMenu) {
|
||||||
JMenuItem newMenu = new JMenuItem(menu.getText());
|
JMenuItem newMenu = new JMenuItem(menu.getText());
|
||||||
|
|
||||||
onlineUsersPanel.initUnicastMenu(newMenu, unicastActionMenu::onClick);
|
networkUsersPanel.initUnicastMenu(newMenu, unicastActionMenu::onClick);
|
||||||
return newMenu;
|
return newMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import pitiupi.models.User;
|
import pitiupi.models.User;
|
||||||
|
import pitiupi.models.Messages.GoodbyeMessage;
|
||||||
import pitiupi.models.Messages.HeartbeatMessage;
|
import pitiupi.models.Messages.HeartbeatMessage;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,17 @@ public class HeartbeatManager {
|
|||||||
this.connectionManager = connectionManager;
|
this.connectionManager = connectionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testes unitários sem rede (não chamar {@link #start()}).
|
||||||
|
*/
|
||||||
|
public static HeartbeatManager createForTesting() {
|
||||||
|
return new HeartbeatManager(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inicia o mecanismo de heartbeat.
|
* Inicia o mecanismo de heartbeat.
|
||||||
*/
|
*/
|
||||||
@@ -59,10 +71,31 @@ public class HeartbeatManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void announce() {
|
private void announce() {
|
||||||
|
if (connectionManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
HeartbeatMessage msg = new HeartbeatMessage(connectionManager.getUserName(), instanceId);
|
HeartbeatMessage msg = new HeartbeatMessage(connectionManager.getUserName(), instanceId);
|
||||||
connectionManager.sendUdpMulticast(msg);
|
connectionManager.sendUdpMulticast(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendGoodbyeAndStop() {
|
||||||
|
if (connectionManager != null) {
|
||||||
|
GoodbyeMessage msg = new GoodbyeMessage(connectionManager.getUserName(), instanceId);
|
||||||
|
connectionManager.sendUdpMulticast(msg);
|
||||||
|
}
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receiveGoodbye(GoodbyeMessage msg) {
|
||||||
|
if (msg.getInstanceId().equals(instanceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean removed = peers.remove(msg.getInstanceId()) != null;
|
||||||
|
if (removed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chamado pelo Socket quando um pacote é identificado como heartbeat
|
// Chamado pelo Socket quando um pacote é identificado como heartbeat
|
||||||
/**
|
/**
|
||||||
* Processa uma mensagem de heartbeat recebida de outro usuário.
|
* Processa uma mensagem de heartbeat recebida de outro usuário.
|
||||||
@@ -85,7 +118,7 @@ public class HeartbeatManager {
|
|||||||
String key = msg.getInstanceId();
|
String key = msg.getInstanceId();
|
||||||
boolean isNew = !peers.containsKey(key);
|
boolean isNew = !peers.containsKey(key);
|
||||||
|
|
||||||
User peer = peers.computeIfAbsent(key, k -> new User(msg.getUserName(), from, msg.getTimestamp()));
|
User peer = peers.computeIfAbsent(key, k -> new User(msg.getUserName(), from, msg.getTimestamp(), msg.getInstanceId()));
|
||||||
peer.touch(System.currentTimeMillis());
|
peer.touch(System.currentTimeMillis());
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
@@ -138,4 +171,11 @@ public class HeartbeatManager {
|
|||||||
public List<User> getPeers() {
|
public List<User> getPeers() {
|
||||||
return new ArrayList<>(peers.values());
|
return new ArrayList<>(peers.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-notifica listeners (ex.: contadores de convite na lista).
|
||||||
|
*/
|
||||||
|
public void notifyPeerListeners() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package pitiupi.control.plugins;
|
package pitiupi.control.plugins;
|
||||||
|
|
||||||
import pitiupi.Main;
|
import pitiupi.Main;
|
||||||
|
import pitiupi.api.plugin.LegacyPluginWorkspace;
|
||||||
|
import pitiupi.api.plugin.PluginWorkspace;
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
import pitiupi.control.ConnectionManager;
|
import pitiupi.control.ConnectionManager;
|
||||||
import pitiupi.control.GuiManager;
|
import pitiupi.control.GuiManager;
|
||||||
|
import pitiupi.errors.ErrorCode;
|
||||||
import pitiupi.models.Messages.Message;
|
import pitiupi.models.Messages.Message;
|
||||||
|
import pitiupi.platform.PitiupiHome;
|
||||||
|
import pitiupi.plugins.store.PluginMetadataWriter;
|
||||||
import pitiupi.net.tcp.TcpConnection;
|
import pitiupi.net.tcp.TcpConnection;
|
||||||
import pitiupi.models.Plugin;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -15,6 +19,7 @@ import java.net.URLClassLoader;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.ServiceLoader;
|
import java.util.ServiceLoader;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -24,12 +29,14 @@ public class PluginManager {
|
|||||||
private final HashSet<String> loadedJars;
|
private final HashSet<String> loadedJars;
|
||||||
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
private final PluginMetadataWriter metadataWriter;
|
||||||
|
|
||||||
public PluginManager() {
|
public PluginManager() {
|
||||||
this.loadedPlugins = new ArrayList<>();
|
this.loadedPlugins = new ArrayList<>();
|
||||||
this.loadedJars = new HashSet<>();
|
this.loadedJars = new HashSet<>();
|
||||||
|
|
||||||
logger = Logger.getLogger(getClass().getName());
|
logger = Logger.getLogger(getClass().getName());
|
||||||
|
this.metadataWriter = new PluginMetadataWriter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleMessage(byte[] msg) {
|
public void handleMessage(byte[] msg) {
|
||||||
@@ -90,8 +97,39 @@ public class PluginManager {
|
|||||||
plugin.onSocketClosed(connection);
|
plugin.onSocketClosed(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Plugin> getLoadedPlugins() {
|
||||||
|
return loadedPlugins.stream().map(LoadedPlugin::getPlugin).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyShellTabClosed(String pluginId) {
|
||||||
|
if (pluginId == null || pluginId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Plugin plugin = getPlugin(pluginId);
|
||||||
|
if (plugin != null) {
|
||||||
|
plugin.onShellTabClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use {@link #loadPlugins(ConnectionManager, GuiManager, Function)} com {@link pitiupi.api.plugin.ShellPluginWorkspace} */
|
||||||
|
@Deprecated
|
||||||
public void loadPlugins(ConnectionManager connectionManager, GuiManager guiManager) {
|
public void loadPlugins(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||||
File pluginDir = new File("plugins");
|
loadPlugins(connectionManager, guiManager, plugin -> new LegacyPluginWorkspace(
|
||||||
|
plugin.getIdentifier(),
|
||||||
|
plugin,
|
||||||
|
connectionManager,
|
||||||
|
guiManager,
|
||||||
|
connectionManager.getPeerActionBroker(),
|
||||||
|
connectionManager.getInviteInboxService()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadPlugins(
|
||||||
|
ConnectionManager connectionManager,
|
||||||
|
GuiManager guiManager,
|
||||||
|
Function<Plugin, PluginWorkspace> workspaceFactory
|
||||||
|
) {
|
||||||
|
File pluginDir = PitiupiHome.get().pluginsDirectory().toFile();
|
||||||
File[] jarFiles = pluginDir.listFiles((dir, name) -> name.endsWith(".jar"));
|
File[] jarFiles = pluginDir.listFiles((dir, name) -> name.endsWith(".jar"));
|
||||||
|
|
||||||
if (jarFiles == null || jarFiles.length == 0) {
|
if (jarFiles == null || jarFiles.length == 0) {
|
||||||
@@ -118,13 +156,20 @@ public class PluginManager {
|
|||||||
for (Plugin plugin : serviceLoader) {
|
for (Plugin plugin : serviceLoader) {
|
||||||
LoadedPlugin loadedPlugin = new LoadedPlugin(plugin, classLoader);
|
LoadedPlugin loadedPlugin = new LoadedPlugin(plugin, classLoader);
|
||||||
|
|
||||||
plugin.createPlugin(connectionManager, guiManager);
|
PluginWorkspace workspace = workspaceFactory.apply(plugin);
|
||||||
|
plugin.createPlugin(connectionManager, workspace);
|
||||||
loadedPlugins.add(loadedPlugin);
|
loadedPlugins.add(loadedPlugin);
|
||||||
loadedJars.add(file.getName());
|
loadedJars.add(file.getName());
|
||||||
|
try {
|
||||||
|
metadataWriter.write(plugin, file.toPath());
|
||||||
|
} catch (Exception metaEx) {
|
||||||
|
logger.log(Level.WARNING, "Failed to write plugin metadata for " + file.getName(), metaEx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.log(Level.WARNING, "Failed to load plugin " + file.getName(), e);
|
logger.log(Level.WARNING, "Failed to load plugin " + file.getName() + ": "
|
||||||
|
+ ErrorCode.PLUGIN_LOAD_FAILED.getUserMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package pitiupi.errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Códigos de erro expostos à UI (mensagens em português).
|
||||||
|
*/
|
||||||
|
public enum ErrorCode {
|
||||||
|
PLUGIN_INSTALL_FAILED("Não foi possível instalar o plugin."),
|
||||||
|
PLUGIN_REMOVE_FAILED("Não foi possível remover o plugin."),
|
||||||
|
PLUGIN_LOAD_FAILED("Falha ao carregar um plugin."),
|
||||||
|
INVALID_PATH("Caminho de arquivo inválido.");
|
||||||
|
|
||||||
|
private final String userMessage;
|
||||||
|
|
||||||
|
ErrorCode(String userMessage) {
|
||||||
|
this.userMessage = userMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserMessage() {
|
||||||
|
return userMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package pitiupi.errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exceção de domínio com código para a interface.
|
||||||
|
*/
|
||||||
|
public final class PitiupiException extends Exception {
|
||||||
|
|
||||||
|
private final ErrorCode code;
|
||||||
|
|
||||||
|
public PitiupiException(ErrorCode code) {
|
||||||
|
super(code.getUserMessage());
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PitiupiException(ErrorCode code, String detail) {
|
||||||
|
super(detail);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PitiupiException(ErrorCode code, Throwable cause) {
|
||||||
|
super(code.getUserMessage(), cause);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorCode getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserMessage() {
|
||||||
|
return code.getUserMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package pitiupi.models.Messages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mensagem de saída graciosa enviada em multicast ao fechar a aplicação.
|
||||||
|
*/
|
||||||
|
public class GoodbyeMessage extends Message {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final String userName;
|
||||||
|
private final String instanceId;
|
||||||
|
|
||||||
|
public GoodbyeMessage(String userName, String instanceId) {
|
||||||
|
super(true);
|
||||||
|
this.userName = userName;
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package pitiupi.models.Messages;
|
||||||
|
|
||||||
|
import pitiupi.application.invites.SessionInviteKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convite de sessão (chat, tela, etc.) sem abrir UI no peer remoto.
|
||||||
|
*/
|
||||||
|
public class SessionInviteMessage extends Message {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final String inviteId;
|
||||||
|
private final String fromInstanceId;
|
||||||
|
private final String fromDisplayName;
|
||||||
|
private final String toInstanceId;
|
||||||
|
private final String pluginId;
|
||||||
|
private final SessionInviteKind kind;
|
||||||
|
private final String payload;
|
||||||
|
|
||||||
|
public SessionInviteMessage(
|
||||||
|
String inviteId,
|
||||||
|
String fromInstanceId,
|
||||||
|
String fromDisplayName,
|
||||||
|
String toInstanceId,
|
||||||
|
String pluginId,
|
||||||
|
SessionInviteKind kind,
|
||||||
|
String payload
|
||||||
|
) {
|
||||||
|
super(true);
|
||||||
|
this.inviteId = inviteId;
|
||||||
|
this.fromInstanceId = fromInstanceId;
|
||||||
|
this.fromDisplayName = fromDisplayName;
|
||||||
|
this.toInstanceId = toInstanceId;
|
||||||
|
this.pluginId = pluginId;
|
||||||
|
this.kind = kind;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInviteId() {
|
||||||
|
return inviteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromInstanceId() {
|
||||||
|
return fromInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFromDisplayName() {
|
||||||
|
return fromDisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToInstanceId() {
|
||||||
|
return toInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPluginId() {
|
||||||
|
return pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionInviteKind getKind() {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
package pitiupi.models;
|
package pitiupi.models;
|
||||||
|
|
||||||
|
import pitiupi.api.plugin.InviteDetails;
|
||||||
|
import pitiupi.api.plugin.PeerActionsRegistrar;
|
||||||
|
import pitiupi.api.plugin.PluginWorkspace;
|
||||||
|
import pitiupi.api.plugin.ShellPluginWorkspace;
|
||||||
import pitiupi.control.ConnectionManager;
|
import pitiupi.control.ConnectionManager;
|
||||||
import pitiupi.control.GuiManager;
|
import pitiupi.control.GuiManager;
|
||||||
import pitiupi.models.Messages.Message;
|
import pitiupi.models.Messages.Message;
|
||||||
import pitiupi.net.tcp.TcpConnection;
|
import pitiupi.net.tcp.TcpConnection;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,11 +25,34 @@ import java.util.List;
|
|||||||
* @author flavio
|
* @author flavio
|
||||||
*/
|
*/
|
||||||
public abstract class Plugin {
|
public abstract class Plugin {
|
||||||
|
|
||||||
|
private JComponent shellWorkspaceContent;
|
||||||
|
|
||||||
public abstract String getName();
|
public abstract String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nome amigável na UI (galeria, abas). Padrão: {@link #getName()}.
|
||||||
|
*/
|
||||||
|
public String getDisplayName() {
|
||||||
|
return getName();
|
||||||
|
}
|
||||||
|
|
||||||
public abstract String getAuthor();
|
public abstract String getAuthor();
|
||||||
public abstract String getVersion();
|
public abstract String getVersion();
|
||||||
public abstract String getIdentifier();
|
public abstract String getIdentifier();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra o painel principal da aba central (shell). Não abre a aba — use
|
||||||
|
* {@link pitiupi.api.plugin.PluginWorkspace#openOrFocusTab} na galeria ou peer actions.
|
||||||
|
*/
|
||||||
|
protected final void bindShellWorkspaceContent(JComponent content) {
|
||||||
|
this.shellWorkspaceContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final JComponent getShellWorkspaceContent() {
|
||||||
|
return shellWorkspaceContent;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inicializa o plugin.
|
* Inicializa o plugin.
|
||||||
*
|
*
|
||||||
@@ -35,6 +63,28 @@ public abstract class Plugin {
|
|||||||
*/
|
*/
|
||||||
public abstract void createPlugin(ConnectionManager connectionManager, GuiManager guiManager);
|
public abstract void createPlugin(ConnectionManager connectionManager, GuiManager guiManager);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicialização via workspace (Onda C+). O adaptador chama o legado e {@link #installPeerActions}.
|
||||||
|
*/
|
||||||
|
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||||
|
if (workspace instanceof ShellPluginWorkspace shell) {
|
||||||
|
createPlugin(connectionManager, shell.guiManager());
|
||||||
|
installPeerActions(workspace.peerActions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onInviteAccepted(InviteDetails invite) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aba central do plugin fechada na shell (× na aba superior). Use para parar timers, captura, etc.
|
||||||
|
*/
|
||||||
|
public void onShellTabClosed() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retorna uma mensagem utilizada pelo plugin.
|
* Retorna uma mensagem utilizada pelo plugin.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12,14 +12,34 @@ import java.net.InetAddress;
|
|||||||
*/
|
*/
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
|
public static final String BROADCAST_INSTANCE_ID = "__broadcast__";
|
||||||
|
|
||||||
private final String userName;
|
private final String userName;
|
||||||
private final InetAddress address;
|
private final InetAddress address;
|
||||||
private volatile long lastSeen;
|
private volatile long lastSeen;
|
||||||
|
private final String instanceId;
|
||||||
|
|
||||||
public User(String userName, InetAddress address, long lastSeen) {
|
public User(String userName, InetAddress address, long lastSeen) {
|
||||||
|
this(userName, address, lastSeen, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User(String userName, InetAddress address, long lastSeen, String instanceId) {
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static User broadcastAll() {
|
||||||
|
return new User("Todos", null, 0, BROADCAST_INSTANCE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBroadcastTarget() {
|
||||||
|
return BROADCAST_INSTANCE_ID.equals(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserName() {
|
public String getUserName() {
|
||||||
@@ -45,6 +65,9 @@ public class User {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
if (isBroadcastTarget() || address == null) {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
return userName + " (" + address.getHostAddress() + ")";
|
return userName + " (" + address.getHostAddress() + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,9 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import pitiupi.control.ConnectionManager;
|
import pitiupi.control.ConnectionManager;
|
||||||
import pitiupi.control.plugins.PluginManager;
|
import pitiupi.control.plugins.PluginManager;
|
||||||
|
import pitiupi.models.Messages.GoodbyeMessage;
|
||||||
import pitiupi.models.Messages.HeartbeatMessage;
|
import pitiupi.models.Messages.HeartbeatMessage;
|
||||||
|
import pitiupi.models.Messages.SessionInviteMessage;
|
||||||
import pitiupi.models.Messages.Message;
|
import pitiupi.models.Messages.Message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,11 +153,19 @@ public class SocketUDP extends Thread {
|
|||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData());
|
Object coreMessage = tryParseCoreUdpMessage(msgPacket.getData());
|
||||||
if (heartbeat != null) {
|
if (coreMessage instanceof HeartbeatMessage heartbeat) {
|
||||||
connectionManager.receiveHeartbeat(heartbeat, msgPacket.getAddress());
|
connectionManager.receiveHeartbeat(heartbeat, msgPacket.getAddress());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (coreMessage instanceof GoodbyeMessage goodbye) {
|
||||||
|
connectionManager.receiveGoodbye(goodbye);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (coreMessage instanceof SessionInviteMessage invite) {
|
||||||
|
connectionManager.receiveSessionInvite(invite);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
pluginManager.handleMessage(msgPacket.getData());
|
pluginManager.handleMessage(msgPacket.getData());
|
||||||
}
|
}
|
||||||
@@ -168,14 +178,19 @@ public class SocketUDP extends Thread {
|
|||||||
* @return a mensagem de heartbeat caso os dados correspondam a uma;
|
* @return a mensagem de heartbeat caso os dados correspondam a uma;
|
||||||
* {@code null} caso contrário.
|
* {@code null} caso contrário.
|
||||||
*/
|
*/
|
||||||
private HeartbeatMessage tryParseHeartbeat(byte[] data) {
|
private Object tryParseCoreUdpMessage(byte[] data) {
|
||||||
try {
|
try {
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||||
ObjectInput in = new ObjectInputStream(bais);
|
ObjectInput in = new ObjectInputStream(bais);
|
||||||
Object obj = in.readObject();
|
Object obj = in.readObject();
|
||||||
return (obj instanceof HeartbeatMessage) ? (HeartbeatMessage) obj : null;
|
if (obj instanceof HeartbeatMessage
|
||||||
|
|| obj instanceof GoodbyeMessage
|
||||||
|
|| obj instanceof SessionInviteMessage) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package pitiupi.platform;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leitura e gravação de {@code config.properties} na raiz do PITIUPI_HOME.
|
||||||
|
*/
|
||||||
|
public final class ConfigLoader {
|
||||||
|
|
||||||
|
private final Path configFile;
|
||||||
|
private final Properties properties = new Properties();
|
||||||
|
|
||||||
|
public ConfigLoader(Path homeRoot) {
|
||||||
|
this.configFile = homeRoot.resolve("config.properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadOrCreateDefaults() throws IOException {
|
||||||
|
if (Files.isRegularFile(configFile)) {
|
||||||
|
try (InputStream in = Files.newInputStream(configFile)) {
|
||||||
|
properties.load(in);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
properties.setProperty("migration.cwdPluginsPrompt", "true");
|
||||||
|
store();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void store() throws IOException {
|
||||||
|
Files.createDirectories(configFile.getParent());
|
||||||
|
try (OutputStream out = Files.newOutputStream(configFile)) {
|
||||||
|
properties.store(out, "Pitiupi configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(String key, boolean defaultValue) {
|
||||||
|
String v = properties.getProperty(key);
|
||||||
|
if (v == null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBoolean(String key, boolean value) throws IOException {
|
||||||
|
properties.setProperty(key, value ? "true" : "false");
|
||||||
|
store();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMigrationCwdPluginsDone() {
|
||||||
|
return getBoolean("migration.cwdPluginsDone", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMigrationCwdPluginsDone(boolean done) throws IOException {
|
||||||
|
setBoolean("migration.cwdPluginsDone", done);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMigrationCwdPluginsPromptEnabled() {
|
||||||
|
return getBoolean("migration.cwdPluginsPrompt", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMigrationCwdPluginsPromptEnabled(boolean enabled) throws IOException {
|
||||||
|
setBoolean("migration.cwdPluginsPrompt", enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package pitiupi.platform;
|
||||||
|
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migração única de {@code ./plugins} (CWD legado) para {@code PITIUPI_HOME/plugins}.
|
||||||
|
*/
|
||||||
|
public final class LegacyPluginMigration {
|
||||||
|
|
||||||
|
private LegacyPluginMigration() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void maybeMigrateFromLegacyCwd() throws IOException {
|
||||||
|
PitiupiHome home = PitiupiHome.get();
|
||||||
|
ConfigLoader config = home.config();
|
||||||
|
|
||||||
|
if (config.isMigrationCwdPluginsDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path legacyDir = Path.of(System.getProperty("user.dir"), "plugins").normalize();
|
||||||
|
Path targetDir = home.pluginsDirectory();
|
||||||
|
|
||||||
|
boolean legacyHasJars = hasJarFiles(legacyDir);
|
||||||
|
boolean targetEmpty = !hasJarFiles(targetDir);
|
||||||
|
|
||||||
|
if (!legacyHasJars || !targetEmpty) {
|
||||||
|
config.setMigrationCwdPluginsDone(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean copy = false;
|
||||||
|
if (GraphicsEnvironmentHelper.isHeadless() || !config.isMigrationCwdPluginsPromptEnabled()) {
|
||||||
|
copy = false;
|
||||||
|
} else {
|
||||||
|
int choice = JOptionPane.showConfirmDialog(
|
||||||
|
null,
|
||||||
|
"Encontramos plugins na pasta antiga (" + legacyDir + ").\n"
|
||||||
|
+ "Copiar para " + targetDir + "?",
|
||||||
|
"Migração de plugins",
|
||||||
|
JOptionPane.YES_NO_OPTION
|
||||||
|
);
|
||||||
|
copy = choice == JOptionPane.YES_OPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy) {
|
||||||
|
copyJarsAndMeta(legacyDir, targetDir);
|
||||||
|
} else {
|
||||||
|
config.setMigrationCwdPluginsPromptEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setMigrationCwdPluginsDone(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasJarFiles(Path dir) throws IOException {
|
||||||
|
if (!Files.isDirectory(dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (Stream<Path> s = Files.list(dir)) {
|
||||||
|
return s.anyMatch(p -> p.getFileName().toString().endsWith(".jar"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyJarsAndMeta(Path from, Path to) throws IOException {
|
||||||
|
Files.createDirectories(to);
|
||||||
|
try (Stream<Path> s = Files.list(from)) {
|
||||||
|
s.filter(p -> {
|
||||||
|
String name = p.getFileName().toString();
|
||||||
|
return name.endsWith(".jar") || name.endsWith(".plugin.meta");
|
||||||
|
}).forEach(src -> {
|
||||||
|
try {
|
||||||
|
Files.copy(src, to.resolve(src.getFileName()), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evita referência direta a java.awt.GraphicsEnvironment em testes headless.
|
||||||
|
*/
|
||||||
|
static final class GraphicsEnvironmentHelper {
|
||||||
|
static boolean isHeadless() {
|
||||||
|
return java.awt.GraphicsEnvironment.isHeadless();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package pitiupi.platform;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raiz de dados do usuário ({@code PITIUPI_HOME} ou {@code ~/pitiupi}).
|
||||||
|
*/
|
||||||
|
public final class PitiupiHome {
|
||||||
|
|
||||||
|
private static final PitiupiHome DEFAULT = new PitiupiHome(resolveRoot());
|
||||||
|
|
||||||
|
private final Path root;
|
||||||
|
private ConfigLoader configLoader;
|
||||||
|
|
||||||
|
private PitiupiHome(Path root) {
|
||||||
|
this.root = root.normalize().toAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PitiupiHome get() {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instância com raiz explícita (testes e ferramentas).
|
||||||
|
*/
|
||||||
|
public static PitiupiHome open(Path root) {
|
||||||
|
return new PitiupiHome(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path resolveRoot() {
|
||||||
|
String env = System.getenv("PITIUPI_HOME");
|
||||||
|
if (env != null && !env.isBlank()) {
|
||||||
|
return expandUserHome(env.trim());
|
||||||
|
}
|
||||||
|
return Paths.get(System.getProperty("user.home"), "pitiupi");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path expandUserHome(String path) {
|
||||||
|
if (path.startsWith("~/")) {
|
||||||
|
return Paths.get(System.getProperty("user.home"), path.substring(2));
|
||||||
|
}
|
||||||
|
if ("~".equals(path)) {
|
||||||
|
return Paths.get(System.getProperty("user.home"));
|
||||||
|
}
|
||||||
|
return Paths.get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path rootDirectory() {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path pluginsDirectory() {
|
||||||
|
return root.resolve("plugins");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path cacheDirectory() {
|
||||||
|
return root.resolve("cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path logsDirectory() {
|
||||||
|
return root.resolve("logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path configFile() {
|
||||||
|
return root.resolve("config.properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigLoader config() throws IOException {
|
||||||
|
if (configLoader == null) {
|
||||||
|
configLoader = new ConfigLoader(root);
|
||||||
|
configLoader.loadOrCreateDefaults();
|
||||||
|
}
|
||||||
|
return configLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria diretórios padrão se ausentes.
|
||||||
|
*/
|
||||||
|
public void ensureLayout() throws IOException {
|
||||||
|
Files.createDirectories(pluginsDirectory());
|
||||||
|
Files.createDirectories(cacheDirectory());
|
||||||
|
Files.createDirectories(logsDirectory());
|
||||||
|
config();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package pitiupi.plugins.store;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Referência a um JAR instalado e metadados opcionais.
|
||||||
|
*/
|
||||||
|
public final class InstalledPluginRef {
|
||||||
|
|
||||||
|
private final Path jarPath;
|
||||||
|
private final String fileName;
|
||||||
|
private final Optional<String> arquivoId;
|
||||||
|
private final Optional<String> version;
|
||||||
|
private final Optional<String> name;
|
||||||
|
private final Optional<String> displayName;
|
||||||
|
private final Optional<String> identifier;
|
||||||
|
private final Optional<String> author;
|
||||||
|
|
||||||
|
public InstalledPluginRef(
|
||||||
|
Path jarPath,
|
||||||
|
String fileName,
|
||||||
|
Optional<String> arquivoId,
|
||||||
|
Optional<String> version,
|
||||||
|
Optional<String> name,
|
||||||
|
Optional<String> displayName,
|
||||||
|
Optional<String> identifier,
|
||||||
|
Optional<String> author
|
||||||
|
) {
|
||||||
|
this.jarPath = jarPath;
|
||||||
|
this.fileName = fileName;
|
||||||
|
this.arquivoId = arquivoId;
|
||||||
|
this.version = version;
|
||||||
|
this.name = name;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getJarPath() {
|
||||||
|
return jarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getArquivoId() {
|
||||||
|
return arquivoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getIdentifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package pitiupi.plugins.store;
|
||||||
|
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grava {@code *.plugin.meta} após carregar um plugin com sucesso.
|
||||||
|
*/
|
||||||
|
public final class PluginMetadataWriter {
|
||||||
|
|
||||||
|
public void write(Plugin plugin, Path jarPath) throws IOException {
|
||||||
|
Path metaPath = metaPathFor(jarPath);
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.setProperty("identifier", plugin.getIdentifier());
|
||||||
|
props.setProperty("name", plugin.getName());
|
||||||
|
props.setProperty("displayName", plugin.getDisplayName());
|
||||||
|
props.setProperty("author", plugin.getAuthor());
|
||||||
|
props.setProperty("version", plugin.getVersion());
|
||||||
|
String arquivoId = deriveArquivoId(jarPath, plugin);
|
||||||
|
props.setProperty("arquivoId", arquivoId);
|
||||||
|
|
||||||
|
try (OutputStream out = Files.newOutputStream(metaPath)) {
|
||||||
|
props.store(out, "Pitiupi plugin metadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Path metaPathFor(Path jarPath) {
|
||||||
|
return Path.of(jarPath.toString() + ".plugin.meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String deriveArquivoId(Path jarPath, Plugin plugin) {
|
||||||
|
String file = jarPath.getFileName().toString();
|
||||||
|
if (file.endsWith(".jar")) {
|
||||||
|
String base = file.substring(0, file.length() - 4);
|
||||||
|
if (base.startsWith("pitiupi.")) {
|
||||||
|
return base.substring("pitiupi.".length());
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return plugin.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package pitiupi.plugins.store;
|
||||||
|
|
||||||
|
import pitiupi.errors.ErrorCode;
|
||||||
|
import pitiupi.errors.PitiupiException;
|
||||||
|
import pitiupi.platform.PitiupiHome;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instalação e remoção local de JARs em {@code PITIUPI_HOME/plugins}.
|
||||||
|
*/
|
||||||
|
public final class PluginStore {
|
||||||
|
|
||||||
|
private final Path pluginsDir;
|
||||||
|
private final Path cacheDir;
|
||||||
|
|
||||||
|
public PluginStore() {
|
||||||
|
PitiupiHome home = PitiupiHome.get();
|
||||||
|
this.pluginsDir = home.pluginsDirectory();
|
||||||
|
this.cacheDir = home.cacheDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginStore(Path pluginsDir, Path cacheDir) {
|
||||||
|
this.pluginsDir = pluginsDir.normalize().toAbsolutePath();
|
||||||
|
this.cacheDir = cacheDir.normalize().toAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InstalledPluginRef> listInstalled() throws IOException {
|
||||||
|
if (!Files.isDirectory(pluginsDir)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<InstalledPluginRef> result = new ArrayList<>();
|
||||||
|
try (Stream<Path> stream = Files.list(pluginsDir)) {
|
||||||
|
stream.filter(p -> p.getFileName().toString().endsWith(".jar"))
|
||||||
|
.forEach(jar -> result.add(toRef(jar)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path installFromFile(Path sourceJar) throws PitiupiException, IOException {
|
||||||
|
if (sourceJar == null || !Files.isRegularFile(sourceJar)) {
|
||||||
|
throw new PitiupiException(ErrorCode.INVALID_PATH);
|
||||||
|
}
|
||||||
|
String fileName = sourceJar.getFileName().toString();
|
||||||
|
if (!fileName.endsWith(".jar") || fileName.contains("..")) {
|
||||||
|
throw new PitiupiException(ErrorCode.INVALID_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createDirectories(pluginsDir);
|
||||||
|
Files.createDirectories(cacheDir);
|
||||||
|
|
||||||
|
Path dest = pluginsDir.resolve(fileName).normalize();
|
||||||
|
if (!dest.startsWith(pluginsDir.normalize().toAbsolutePath())) {
|
||||||
|
throw new PitiupiException(ErrorCode.INVALID_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path tmp = cacheDir.resolve(fileName + "." + UUID.randomUUID() + ".part");
|
||||||
|
try {
|
||||||
|
Files.copy(sourceJar, tmp, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
Files.move(tmp, dest, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
return dest;
|
||||||
|
} catch (IOException e) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(tmp);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// best effort
|
||||||
|
}
|
||||||
|
throw new PitiupiException(ErrorCode.PLUGIN_INSTALL_FAILED, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(Path jarInPluginsDir) throws PitiupiException, IOException {
|
||||||
|
Path normalized = jarInPluginsDir.normalize().toAbsolutePath();
|
||||||
|
Path pluginsAbs = pluginsDir.normalize().toAbsolutePath();
|
||||||
|
if (!normalized.startsWith(pluginsAbs) || !normalized.getFileName().toString().endsWith(".jar")) {
|
||||||
|
throw new PitiupiException(ErrorCode.INVALID_PATH);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(normalized);
|
||||||
|
Files.deleteIfExists(PluginMetadataWriter.metaPathFor(normalized));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PitiupiException(ErrorCode.PLUGIN_REMOVE_FAILED, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean exists(String arquivoId) throws IOException {
|
||||||
|
return findJarByArquivoId(arquivoId).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Path> findJarByArquivoId(String id) throws IOException {
|
||||||
|
if (id == null || id.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
for (InstalledPluginRef ref : listInstalled()) {
|
||||||
|
if (ref.getArquivoId().map(id::equals).orElse(false)) {
|
||||||
|
return Optional.of(ref.getJarPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InstalledPluginRef toRef(Path jar) {
|
||||||
|
Properties meta = loadMeta(PluginMetadataWriter.metaPathFor(jar));
|
||||||
|
String fileName = jar.getFileName().toString();
|
||||||
|
return new InstalledPluginRef(
|
||||||
|
jar,
|
||||||
|
fileName,
|
||||||
|
optional(meta, "arquivoId"),
|
||||||
|
optional(meta, "version"),
|
||||||
|
optional(meta, "name"),
|
||||||
|
optional(meta, "displayName"),
|
||||||
|
optional(meta, "identifier"),
|
||||||
|
optional(meta, "author")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> optional(Properties meta, String key) {
|
||||||
|
String v = meta.getProperty(key);
|
||||||
|
if (v == null || v.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties loadMeta(Path metaPath) {
|
||||||
|
Properties props = new Properties();
|
||||||
|
if (!Files.isRegularFile(metaPath)) {
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
try (InputStream in = Files.newInputStream(metaPath)) {
|
||||||
|
props.load(in);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// listagem tolerante
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reinício da aplicação após mutação de plugins (D11).
|
||||||
|
*/
|
||||||
|
public final class ApplicationRestarter {
|
||||||
|
|
||||||
|
private ApplicationRestarter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void promptRestartIfNeeded(java.awt.Component parent) {
|
||||||
|
int choice = JOptionPane.showConfirmDialog(
|
||||||
|
parent,
|
||||||
|
"Para aplicar a instalação ou remoção, reinicie o Pitiupi.\nReiniciar agora?",
|
||||||
|
"Reinício necessário",
|
||||||
|
JOptionPane.YES_NO_OPTION
|
||||||
|
);
|
||||||
|
if (choice == JOptionPane.YES_OPTION) {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void restart() {
|
||||||
|
try {
|
||||||
|
String javaHome = System.getProperty("java.home");
|
||||||
|
String javaBin = javaHome + "/bin/java";
|
||||||
|
List<String> cmd = new ArrayList<>();
|
||||||
|
cmd.add(javaBin);
|
||||||
|
for (String arg : ManagementArgs.currentJvmArgs()) {
|
||||||
|
cmd.add(arg);
|
||||||
|
}
|
||||||
|
cmd.add("-jar");
|
||||||
|
String jar = ManagementArgs.locateRunningJar();
|
||||||
|
if (jar == null) {
|
||||||
|
JOptionPane.showMessageDialog(null,
|
||||||
|
"Feche e abra o Pitiupi novamente (reinício automático indisponível neste modo).");
|
||||||
|
System.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmd.add(jar);
|
||||||
|
new ProcessBuilder(cmd).inheritIO().start();
|
||||||
|
System.exit(0);
|
||||||
|
} catch (IOException e) {
|
||||||
|
JOptionPane.showMessageDialog(null,
|
||||||
|
"Não foi possível reiniciar automaticamente. Feche e abra o Pitiupi novamente.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ManagementArgs {
|
||||||
|
private static List<String> currentJvmArgs() {
|
||||||
|
return List.copyOf(java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String locateRunningJar() {
|
||||||
|
return pitiupi.Main.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import java.awt.Component;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTabbedPane;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abas com botão fechar circular (estilo shell). Conteúdo real fica fora (strip-only).
|
||||||
|
*/
|
||||||
|
public final class ClosableTabbedPane {
|
||||||
|
|
||||||
|
private final StripOnlyTabbedPane tabbedPane = new StripOnlyTabbedPane();
|
||||||
|
private final List<String> orderedKeys = new ArrayList<>();
|
||||||
|
private final Map<String, Integer> keyToIndex = new HashMap<>();
|
||||||
|
private Consumer<String> onTabSelected = key -> {
|
||||||
|
};
|
||||||
|
private Consumer<String> onTabClosed = key -> {
|
||||||
|
};
|
||||||
|
private boolean suppressSelectionEvents;
|
||||||
|
private String closeFallbackKey = PluginSessionKeys.ROOM;
|
||||||
|
|
||||||
|
public ClosableTabbedPane() {
|
||||||
|
tabbedPane.addChangeListener(e -> {
|
||||||
|
if (suppressSelectionEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onTabSelected.accept(selectedKey());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCloseFallbackKey(String closeFallbackKey) {
|
||||||
|
this.closeFallbackKey = closeFallbackKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JTabbedPane getTabbedPane() {
|
||||||
|
return tabbedPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTabCount() {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
return tabbedPane.getTabCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnTabSelected(Consumer<String> onTabSelected) {
|
||||||
|
this.onTabSelected = onTabSelected != null ? onTabSelected : key -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnTabClosed(Consumer<String> onTabClosed) {
|
||||||
|
this.onTabClosed = onTabClosed != null ? onTabClosed : key -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String selectedKey() {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
int idx = tabbedPane.getSelectedIndex();
|
||||||
|
if (idx < 0 || idx >= orderedKeys.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return orderedKeys.get(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectTab(String key) {
|
||||||
|
selectTabInternal(key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectTabSilently(String key) {
|
||||||
|
selectTabInternal(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectTabInternal(String key, boolean silent) {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
int index = indexOfKey(key);
|
||||||
|
if (index < 0 || index >= tabbedPane.getTabCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean prev = suppressSelectionEvents;
|
||||||
|
suppressSelectionEvents = silent;
|
||||||
|
try {
|
||||||
|
tabbedPane.setSelectedIndex(index);
|
||||||
|
} finally {
|
||||||
|
suppressSelectionEvents = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addOrSelectTab(String key, String title, JComponent content, boolean closable) {
|
||||||
|
addOrSelectTab(key, title, title, content, closable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addOrSelectTab(String key, String title, String tooltip, JComponent content, boolean closable) {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
dedupeKey(key);
|
||||||
|
int index = indexOfKey(key);
|
||||||
|
if (index >= 0 && index < tabbedPane.getTabCount()) {
|
||||||
|
setTabHeader(index, title, tooltip, closable, key);
|
||||||
|
tabbedPane.setSelectedIndex(index);
|
||||||
|
reindex();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tabbedPane.addTab(title, null);
|
||||||
|
int newIndex = tabbedPane.getTabCount() - 1;
|
||||||
|
orderedKeys.add(key);
|
||||||
|
setTabHeader(newIndex, title, tooltip, closable, key);
|
||||||
|
reindex();
|
||||||
|
tabbedPane.setSelectedIndex(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeTab(String key) {
|
||||||
|
closeTab(key, closeFallbackKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeTab(String key, String fallbackKey) {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
int index = indexOfKey(key);
|
||||||
|
if (index < 0 || index >= tabbedPane.getTabCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tabbedPane.removeTabAt(index);
|
||||||
|
if (index < orderedKeys.size()) {
|
||||||
|
orderedKeys.remove(index);
|
||||||
|
}
|
||||||
|
keyToIndex.remove(key);
|
||||||
|
reindex();
|
||||||
|
onTabClosed.accept(key);
|
||||||
|
String next = null;
|
||||||
|
if (fallbackKey != null && !fallbackKey.isBlank() && hasTab(fallbackKey)) {
|
||||||
|
next = fallbackKey;
|
||||||
|
} else if (closeFallbackKey != null && !closeFallbackKey.isBlank() && hasTab(closeFallbackKey)) {
|
||||||
|
next = closeFallbackKey;
|
||||||
|
} else {
|
||||||
|
next = firstTabKey();
|
||||||
|
}
|
||||||
|
if (next != null) {
|
||||||
|
selectTab(next);
|
||||||
|
} else if (!suppressSelectionEvents) {
|
||||||
|
onTabSelected.accept(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTab(String key) {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
int index = indexOfKey(key);
|
||||||
|
return index >= 0 && index < tabbedPane.getTabCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTabTitle(String key, String title) {
|
||||||
|
setTabTitle(key, title, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTabTitle(String key, String title, String tooltip) {
|
||||||
|
reconcileKeyListWithTabPane();
|
||||||
|
int index = indexOfKey(key);
|
||||||
|
if (index < 0 || index >= tabbedPane.getTabCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Component tabComponent = tabbedPane.getTabComponentAt(index);
|
||||||
|
if (tabComponent instanceof JPanel header) {
|
||||||
|
for (Component child : header.getComponents()) {
|
||||||
|
if (child instanceof JLabel label) {
|
||||||
|
label.setText(title);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String tip = tooltip != null && !tooltip.isBlank() ? tooltip : title;
|
||||||
|
tabbedPane.setToolTipTextAt(index, tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
int stripHeight() {
|
||||||
|
return tabbedPane.stripHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstTabKey() {
|
||||||
|
if (orderedKeys.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return orderedKeys.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int indexOfKey(String key) {
|
||||||
|
Integer mapped = keyToIndex.get(key);
|
||||||
|
if (mapped != null && mapped < orderedKeys.size() && key.equals(orderedKeys.get(mapped))) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < orderedKeys.size(); i++) {
|
||||||
|
if (key.equals(orderedKeys.get(i))) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileKeyListWithTabPane() {
|
||||||
|
while (orderedKeys.size() > tabbedPane.getTabCount()) {
|
||||||
|
orderedKeys.remove(orderedKeys.size() - 1);
|
||||||
|
}
|
||||||
|
reindex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dedupeKey(String key) {
|
||||||
|
List<Integer> indices = new ArrayList<>();
|
||||||
|
for (int i = 0; i < orderedKeys.size(); i++) {
|
||||||
|
if (key.equals(orderedKeys.get(i))) {
|
||||||
|
indices.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indices.size() <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = indices.size() - 1; i >= 1; i--) {
|
||||||
|
int idx = indices.get(i);
|
||||||
|
if (idx >= 0 && idx < tabbedPane.getTabCount()) {
|
||||||
|
tabbedPane.removeTabAt(idx);
|
||||||
|
}
|
||||||
|
if (idx < orderedKeys.size()) {
|
||||||
|
orderedKeys.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reindex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTabHeader(int tabIndex, String title, String tooltip, boolean closable, String key) {
|
||||||
|
if (tabIndex < 0 || tabIndex >= tabbedPane.getTabCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JPanel header = new JPanel(new BorderLayout(4, 0));
|
||||||
|
header.setOpaque(false);
|
||||||
|
header.setBorder(new EmptyBorder(0, 6, 0, 2));
|
||||||
|
JLabel label = new JLabel(title);
|
||||||
|
header.add(label, BorderLayout.CENTER);
|
||||||
|
if (closable) {
|
||||||
|
header.add(ShellUi.roundTabCloseButton(() -> closeTab(key, closeFallbackKey)), BorderLayout.EAST);
|
||||||
|
}
|
||||||
|
tabbedPane.setTabComponentAt(tabIndex, header);
|
||||||
|
String tip = tooltip != null && !tooltip.isBlank() ? tooltip : title;
|
||||||
|
tabbedPane.setToolTipTextAt(tabIndex, tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reindex() {
|
||||||
|
keyToIndex.clear();
|
||||||
|
for (int i = 0; i < orderedKeys.size(); i++) {
|
||||||
|
keyToIndex.put(orderedKeys.get(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endereço IPv4 local preferencial para compartilhar com peers.
|
||||||
|
*/
|
||||||
|
public final class LocalNetworkInfo {
|
||||||
|
|
||||||
|
private LocalNetworkInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String bestLocalIpv4() {
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface ni = interfaces.nextElement();
|
||||||
|
if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
InetAddress addr = addresses.nextElement();
|
||||||
|
if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
|
||||||
|
return addr.getHostAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InetAddress local = InetAddress.getLocalHost();
|
||||||
|
return local.getHostAddress();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code true} se o IP informado é deste host (inclui loopback e interfaces ativas).
|
||||||
|
*/
|
||||||
|
public static boolean isLocalIpv4(String ip) {
|
||||||
|
if (ip == null || ip.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String trimmed = ip.trim();
|
||||||
|
if ("127.0.0.1".equals(trimmed) || "localhost".equalsIgnoreCase(trimmed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String canonical = InetAddress.getByName(trimmed).getHostAddress();
|
||||||
|
if ("127.0.0.1".equals(canonical)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (canonical.equals(bestLocalIpv4())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface ni = interfaces.nextElement();
|
||||||
|
if (!ni.isUp()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
InetAddress addr = addresses.nextElement();
|
||||||
|
if (addr instanceof Inet4Address && canonical.equals(addr.getHostAddress())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return trimmed.equals(bestLocalIpv4());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifica mensagens e atividade originadas neste host (eco de rede).
|
||||||
|
*/
|
||||||
|
public final class LocalUserIdentity {
|
||||||
|
|
||||||
|
private LocalUserIdentity() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLocalSender(ConnectionManager connectionManager, String senderName, String senderIp) {
|
||||||
|
if (connectionManager == null || senderName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String localName = connectionManager.getUserName();
|
||||||
|
if (localName == null || !localName.equals(senderName.trim())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (senderIp == null || senderIp.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return LocalNetworkInfo.isLocalIpv4(canonicalIpv4(senderIp));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String canonicalIpv4(String ip) {
|
||||||
|
try {
|
||||||
|
return InetAddress.getByName(ip.trim()).getHostAddress();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return ip.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JDialog;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JSpinner;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.GridBagLayout;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login modal centralizado (~30% da área útil do monitor).
|
||||||
|
*/
|
||||||
|
public final class LoginDialog extends JDialog {
|
||||||
|
|
||||||
|
private static final Preferences prefs = Preferences.userNodeForPackage(LoginDialog.class);
|
||||||
|
private static final String PREF_NOME = "lastUserName";
|
||||||
|
private static final String PREF_PORTA = "lastPort";
|
||||||
|
|
||||||
|
private final JTextField nameField = new JTextField(24);
|
||||||
|
private final JSpinner portField = new JSpinner();
|
||||||
|
private boolean accepted;
|
||||||
|
|
||||||
|
public LoginDialog() {
|
||||||
|
super((java.awt.Frame) null, "Pitiupi", true);
|
||||||
|
setLocationByPlatform(false);
|
||||||
|
UiGeometry.installCenterOnShow(this);
|
||||||
|
buildUi();
|
||||||
|
applyPreferredSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUi() {
|
||||||
|
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
|
||||||
|
int portaSalva = prefs.getInt(PREF_PORTA, 5000);
|
||||||
|
portField.setModel(new javax.swing.SpinnerNumberModel(portaSalva, 1, 65535, 1));
|
||||||
|
nameField.setText(nomeSalvo);
|
||||||
|
|
||||||
|
JPanel form = new JPanel(new GridBagLayout());
|
||||||
|
form.setBorder(new EmptyBorder(16, 24, 8, 24));
|
||||||
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
|
gbc.insets = new Insets(8, 8, 8, 8);
|
||||||
|
gbc.anchor = GridBagConstraints.WEST;
|
||||||
|
|
||||||
|
gbc.gridx = 0;
|
||||||
|
gbc.gridy = 0;
|
||||||
|
form.add(new JLabel("Seu nome na rede:"), gbc);
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
gbc.weightx = 1;
|
||||||
|
form.add(nameField, gbc);
|
||||||
|
|
||||||
|
gbc.gridx = 0;
|
||||||
|
gbc.gridy = 1;
|
||||||
|
gbc.weightx = 0;
|
||||||
|
gbc.fill = GridBagConstraints.NONE;
|
||||||
|
form.add(new JLabel("Porta:"), gbc);
|
||||||
|
gbc.gridx = 1;
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
gbc.weightx = 1;
|
||||||
|
form.add(portField, gbc);
|
||||||
|
|
||||||
|
JButton enterButton = new JButton("Entrar");
|
||||||
|
JButton cancelButton = new JButton("Cancelar");
|
||||||
|
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
buttons.setBorder(new EmptyBorder(8, 16, 16, 16));
|
||||||
|
buttons.add(enterButton);
|
||||||
|
buttons.add(cancelButton);
|
||||||
|
|
||||||
|
enterButton.addActionListener(e -> onEnter());
|
||||||
|
cancelButton.addActionListener(e -> System.exit(0));
|
||||||
|
getRootPane().setDefaultButton(enterButton);
|
||||||
|
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
add(form, BorderLayout.CENTER);
|
||||||
|
add(buttons, BorderLayout.SOUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPreferredSize() {
|
||||||
|
pack();
|
||||||
|
java.awt.Dimension fraction = UiGeometry.fractionOfScreen(0.3, 0.3);
|
||||||
|
int width = Math.max(fraction.width, getWidth());
|
||||||
|
int height = Math.max(240, getHeight());
|
||||||
|
setSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onEnter() {
|
||||||
|
try {
|
||||||
|
portField.commitEdit();
|
||||||
|
} catch (java.text.ParseException ex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = nameField.getText().trim();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int port = (Integer) portField.getValue();
|
||||||
|
if (port <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefs.put(PREF_NOME, name);
|
||||||
|
prefs.putInt(PREF_PORTA, port);
|
||||||
|
accepted = true;
|
||||||
|
setVisible(false);
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean showLogin() {
|
||||||
|
applyPreferredSize();
|
||||||
|
validate();
|
||||||
|
UiGeometry.centerOnScreen(this);
|
||||||
|
setVisible(true);
|
||||||
|
SwingUtilities.invokeLater(() -> UiGeometry.centerOnScreen(this));
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return nameField.getText().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return (Integer) portField.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.api.plugin.ShellPluginWorkspace;
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
import pitiupi.control.GuiManager;
|
||||||
|
import pitiupi.control.plugins.PluginManager;
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Janela principal pós-login (shell com abas).
|
||||||
|
*/
|
||||||
|
public final class MainShellFrame extends JFrame implements ShellHost {
|
||||||
|
|
||||||
|
private final PluginManager pluginManager = new PluginManager();
|
||||||
|
private final ConnectionManager connectionManager;
|
||||||
|
private final NetworkUsersPanel networkUsersPanel;
|
||||||
|
private final GuiManager guiManager;
|
||||||
|
private final TabWorkspacePanel tabWorkspace;
|
||||||
|
private final PluginGalleryPanel galleryPanel;
|
||||||
|
private final SettingsPanel settingsPanel;
|
||||||
|
private final JLabel statusLabel = new JLabel(" ");
|
||||||
|
|
||||||
|
public MainShellFrame(String userName, int port) {
|
||||||
|
setTitle("Pitiupi");
|
||||||
|
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
|
||||||
|
addWindowListener(new java.awt.event.WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(java.awt.event.WindowEvent e) {
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connectionManager = new ConnectionManager(userName, pluginManager, port);
|
||||||
|
settingsPanel = new SettingsPanel(connectionManager);
|
||||||
|
PluginGalleryPanel gallery = new PluginGalleryPanel((pluginId, title) -> { });
|
||||||
|
tabWorkspace = new TabWorkspacePanel(gallery, settingsPanel);
|
||||||
|
galleryPanel = gallery;
|
||||||
|
galleryPanel.setOpenHandler(this::openPluginTabFromGallery);
|
||||||
|
|
||||||
|
networkUsersPanel = new NetworkUsersPanel(connectionManager);
|
||||||
|
ShellMenuBar menuBar = new ShellMenuBar();
|
||||||
|
setJMenuBar(menuBar);
|
||||||
|
|
||||||
|
guiManager = new GuiManager(this, menuBar, networkUsersPanel);
|
||||||
|
|
||||||
|
JPanel center = new JPanel(new BorderLayout());
|
||||||
|
center.add(tabWorkspace.getRootPanel(), BorderLayout.CENTER);
|
||||||
|
getContentPane().add(center, BorderLayout.CENTER);
|
||||||
|
getContentPane().add(networkUsersPanel, BorderLayout.EAST);
|
||||||
|
|
||||||
|
JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
|
statusPanel.add(statusLabel);
|
||||||
|
getContentPane().add(statusPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
tabWorkspace.setOnActivePluginChanged(activeId -> {
|
||||||
|
if (activeId != null) {
|
||||||
|
connectionManager.getPeerActionBroker().setActivePlugin(activeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tabWorkspace.setOnSettingsTabSelected(settingsPanel::onTabSelected);
|
||||||
|
tabWorkspace.setOnPluginTabClosed(pluginManager::notifyShellTabClosed);
|
||||||
|
connectionManager.setInviteAcceptedUiListener(this::focusPluginTab);
|
||||||
|
|
||||||
|
loadPlugins();
|
||||||
|
updateStatus(userName, port);
|
||||||
|
setLocationByPlatform(false);
|
||||||
|
UiGeometry.installCenterOnShow(this);
|
||||||
|
pack();
|
||||||
|
setSize(1024, 768);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showOnScreen() {
|
||||||
|
UiGeometry.centerOnScreen(this);
|
||||||
|
setVisible(true);
|
||||||
|
javax.swing.SwingUtilities.invokeLater(() -> UiGeometry.centerOnScreen(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPlugins() {
|
||||||
|
pluginManager.loadPlugins(connectionManager, guiManager, (plugin) -> new ShellPluginWorkspace(
|
||||||
|
plugin.getIdentifier(),
|
||||||
|
plugin,
|
||||||
|
connectionManager,
|
||||||
|
guiManager,
|
||||||
|
connectionManager.getPeerActionBroker(),
|
||||||
|
connectionManager.getInviteInboxService(),
|
||||||
|
tabWorkspace,
|
||||||
|
this
|
||||||
|
));
|
||||||
|
List<Plugin> plugins = pluginManager.getLoadedPlugins();
|
||||||
|
galleryPanel.setPlugins(plugins);
|
||||||
|
settingsPanel.setLoadedPlugins(plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus(String userName, int port) {
|
||||||
|
setStatusMessage("Conectado na rede · porta " + port + " · você: " + userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPluginTabFromGallery(String pluginId, String title) {
|
||||||
|
JComponent content = pluginManager.getLoadedPlugins().stream()
|
||||||
|
.filter(p -> p.getIdentifier().equals(pluginId))
|
||||||
|
.map(Plugin::getShellWorkspaceContent)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
tabWorkspace.openOrFocusTab(pluginId, title, content);
|
||||||
|
connectionManager.getPeerActionBroker().setActivePlugin(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void focusPluginTab(String pluginId) {
|
||||||
|
String title = pluginManager.getLoadedPlugins().stream()
|
||||||
|
.filter(p -> p.getIdentifier().equals(pluginId))
|
||||||
|
.map(Plugin::getDisplayName)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(pluginId);
|
||||||
|
openPluginTabFromGallery(pluginId, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exit() {
|
||||||
|
connectionManager.shutdownGracefully();
|
||||||
|
setVisible(false);
|
||||||
|
dispose();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
connectionManager.setUserName(userName);
|
||||||
|
updateStatus(userName, connectionManager.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConnectionManager getConnectionManager() {
|
||||||
|
return connectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatusMessage(String message) {
|
||||||
|
statusLabel.setText(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.application.activity.PeerActivityInbox;
|
||||||
|
import pitiupi.application.peeractions.GroupedPeerActions;
|
||||||
|
import pitiupi.application.peeractions.PeerAction;
|
||||||
|
import pitiupi.application.peeractions.PeerActionBroker;
|
||||||
|
import pitiupi.application.peeractions.PeerActionTarget;
|
||||||
|
import pitiupi.application.peeractions.PeerTarget;
|
||||||
|
import pitiupi.application.presence.UserPeerView;
|
||||||
|
import pitiupi.control.ConnectionManager;
|
||||||
|
import pitiupi.control.GuiManager;
|
||||||
|
import pitiupi.control.PeerListener;
|
||||||
|
import pitiupi.models.Menus.MenuCategory;
|
||||||
|
import pitiupi.models.User;
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JMenu;
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JPopupMenu;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista lateral com Todos, badges e peer actions.
|
||||||
|
*/
|
||||||
|
public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
||||||
|
|
||||||
|
private final DefaultListModel<DisplayPeer> listModel = new DefaultListModel<>();
|
||||||
|
private final JList<DisplayPeer> userList = new JList<>(listModel);
|
||||||
|
private final JPopupMenu menu = new JPopupMenu();
|
||||||
|
private final Map<MenuCategory, JMenu> legacyCategories = new HashMap<>();
|
||||||
|
private final ConnectionManager connectionManager;
|
||||||
|
|
||||||
|
public NetworkUsersPanel(ConnectionManager connectionManager) {
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
setPreferredSize(new Dimension(200, 0));
|
||||||
|
|
||||||
|
for (MenuCategory category : MenuCategory.values()) {
|
||||||
|
JMenu cat = new JMenu(category.toString());
|
||||||
|
menu.add(cat);
|
||||||
|
legacyCategories.put(category, cat);
|
||||||
|
}
|
||||||
|
|
||||||
|
userList.setCellRenderer(new DefaultListCellRenderer() {
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(
|
||||||
|
JList<?> list,
|
||||||
|
Object value,
|
||||||
|
int index,
|
||||||
|
boolean isSelected,
|
||||||
|
boolean cellHasFocus
|
||||||
|
) {
|
||||||
|
JLabel label = (JLabel) super.getListCellRendererComponent(
|
||||||
|
list, value, index, isSelected, cellHasFocus);
|
||||||
|
if (value instanceof DisplayPeer peer) {
|
||||||
|
label.setText(ShellUi.formatPeerListLabel(
|
||||||
|
peer.view.getDisplayName(),
|
||||||
|
peer.view.getNotificationCount()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JScrollPane userScroll = new JScrollPane(userList);
|
||||||
|
ShellUi.borderlessScroll(userScroll);
|
||||||
|
add(ShellUi.sidebarPanel("Usuários nesta rede", userScroll), BorderLayout.CENTER);
|
||||||
|
userList.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
int index = userList.locationToIndex(e.getPoint());
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userList.setSelectedIndex(index);
|
||||||
|
DisplayPeer peer = listModel.getElementAt(index);
|
||||||
|
PeerActionTarget target = peer.view.isBroadcastTarget()
|
||||||
|
? PeerActionTarget.ALL_PEERS
|
||||||
|
: PeerActionTarget.SINGLE_PEER;
|
||||||
|
showPeerActions(peer, target, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connectionManager.addPeerListener(this);
|
||||||
|
refreshFromPresence();
|
||||||
|
connectionManager.getInviteInboxService().setOnNotificationsChanged(this::refreshFromPresence);
|
||||||
|
connectionManager.getPeerActivityInbox().setOnChanged(this::refreshFromPresence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPeerActions(DisplayPeer peer, PeerActionTarget target, int x, int y) {
|
||||||
|
JPopupMenu popup = new JPopupMenu();
|
||||||
|
PeerActionBroker broker = connectionManager.getPeerActionBroker();
|
||||||
|
PeerTarget peerTarget = target == PeerActionTarget.ALL_PEERS
|
||||||
|
? new PeerTarget.All()
|
||||||
|
: new PeerTarget.Single(peer.view);
|
||||||
|
|
||||||
|
PeerActivityInbox activity = connectionManager.getPeerActivityInbox();
|
||||||
|
String activityPeerKey = peer.view.isBroadcastTarget()
|
||||||
|
? PeerActivityInbox.BROADCAST_PEER_KEY
|
||||||
|
: peer.view.getInstanceId();
|
||||||
|
|
||||||
|
for (GroupedPeerActions group : broker.getAllActionsFor(target)) {
|
||||||
|
JMenu pluginMenu = new JMenu(group.pluginDisplayName());
|
||||||
|
for (PeerAction action : group.actions()) {
|
||||||
|
int pending = activityPeerKey != null
|
||||||
|
? activity.countForAction(activityPeerKey, group.pluginId(), action.getId())
|
||||||
|
: 0;
|
||||||
|
JMenuItem item = new JMenuItem(
|
||||||
|
ShellUi.formatActionLabelWithBadge(action.getLabel(), pending)
|
||||||
|
);
|
||||||
|
item.addActionListener(e -> {
|
||||||
|
if (activityPeerKey != null) {
|
||||||
|
activity.clearAction(activityPeerKey, group.pluginId(), action.getId());
|
||||||
|
}
|
||||||
|
broker.setActivePlugin(group.pluginId());
|
||||||
|
action.getHandler().run(peerTarget);
|
||||||
|
});
|
||||||
|
pluginMenu.add(item);
|
||||||
|
}
|
||||||
|
popup.add(pluginMenu);
|
||||||
|
}
|
||||||
|
if (peer.view.getNotificationCount() > 0 && !peer.view.isBroadcastTarget()) {
|
||||||
|
JMenuItem accept = new JMenuItem("Aceitar convite");
|
||||||
|
accept.addActionListener(e -> {
|
||||||
|
String pluginId = connectionManager.getInviteInboxService()
|
||||||
|
.acceptFirstInviteFrom(peer.view.getInstanceId());
|
||||||
|
if (pluginId != null) {
|
||||||
|
connectionManager.getPeerActionBroker().setActivePlugin(pluginId);
|
||||||
|
connectionManager.notifyInviteAcceptedForUi(pluginId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
popup.add(accept);
|
||||||
|
}
|
||||||
|
for (JMenu legacy : legacyCategories.values()) {
|
||||||
|
if (legacy.getItemCount() > 0 && target == PeerActionTarget.SINGLE_PEER && !peer.view.isBroadcastTarget()) {
|
||||||
|
popup.addSeparator();
|
||||||
|
JMenu copy = new JMenu(legacy.getText());
|
||||||
|
for (int i = 0; i < legacy.getItemCount(); i++) {
|
||||||
|
copy.add(legacy.getItem(i));
|
||||||
|
}
|
||||||
|
popup.add(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (popup.getComponentCount() > 0) {
|
||||||
|
popup.show(userList, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initUnicastMenu(JMenuItem menuItem, Consumer<User> action) {
|
||||||
|
menuItem.addActionListener(e -> {
|
||||||
|
DisplayPeer selected = userList.getSelectedValue();
|
||||||
|
if (selected == null || selected.view.isBroadcastTarget()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
User user = selected.toUser();
|
||||||
|
if (user.getAddress() != null) {
|
||||||
|
action.accept(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUnicastMenu(MenuCategory category, JMenuItem menuItem) {
|
||||||
|
legacyCategories.get(category).add(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPeersChanged(List<User> peers) {
|
||||||
|
refreshFromPresence();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshFromPresence() {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
listModel.clear();
|
||||||
|
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||||
|
listModel.addElement(new DisplayPeer(view));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
record DisplayPeer(UserPeerView view) {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String name = view.getDisplayName();
|
||||||
|
if (view.getNotificationCount() > 0) {
|
||||||
|
return name + " (" + view.getNotificationCount() + ")";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
User toUser() {
|
||||||
|
return new User(
|
||||||
|
view.getDisplayName(),
|
||||||
|
view.getInetAddress().orElse(null),
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
view.getInstanceId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entrada de IP inline para aba rascunho (Nova conversa).
|
||||||
|
*/
|
||||||
|
public final class PeerDraftPanel extends JPanel {
|
||||||
|
|
||||||
|
private final JTextField ipField = new JTextField(24);
|
||||||
|
private final Consumer<String> onCommit;
|
||||||
|
|
||||||
|
public PeerDraftPanel(String title, String hint, Consumer<String> onCommit) {
|
||||||
|
this.onCommit = onCommit;
|
||||||
|
setLayout(new BorderLayout(8, 8));
|
||||||
|
JPanel center = new JPanel(new BorderLayout(6, 6));
|
||||||
|
center.add(new JLabel(title), BorderLayout.NORTH);
|
||||||
|
center.add(ShellUi.mutedLabel(hint), BorderLayout.CENTER);
|
||||||
|
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
|
||||||
|
row.add(new JLabel("IP do peer:"));
|
||||||
|
row.add(ipField);
|
||||||
|
JButton open = new JButton("Abrir conversa");
|
||||||
|
open.addActionListener(e -> commit());
|
||||||
|
row.add(open);
|
||||||
|
center.add(row, BorderLayout.SOUTH);
|
||||||
|
add(center, BorderLayout.NORTH);
|
||||||
|
ipField.addActionListener(e -> commit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void focusIpField() {
|
||||||
|
ipField.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearIpField() {
|
||||||
|
ipField.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void commit() {
|
||||||
|
String ip = ipField.getText();
|
||||||
|
if (ip == null || ip.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onCommit != null) {
|
||||||
|
onCommit.accept(ip.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.models.Plugin;
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aba Plugins: tiles compactos para abrir/focar aba do plugin.
|
||||||
|
*/
|
||||||
|
public final class PluginGalleryPanel extends JPanel {
|
||||||
|
|
||||||
|
private BiConsumer<String, String> onPluginSelected;
|
||||||
|
|
||||||
|
public PluginGalleryPanel(BiConsumer<String, String> onPluginSelected) {
|
||||||
|
this.onPluginSelected = onPluginSelected;
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
setBorder(new EmptyBorder(6, 6, 6, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpenHandler(BiConsumer<String, String> onPluginSelected) {
|
||||||
|
this.onPluginSelected = onPluginSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlugins(List<Plugin> plugins) {
|
||||||
|
removeAll();
|
||||||
|
if (plugins.isEmpty()) {
|
||||||
|
add(new JLabel(
|
||||||
|
"<html><center>Escolha um plugin para abrir aqui.<br/>"
|
||||||
|
+ "Tudo acontece nesta área — sem janelas extras.</center></html>",
|
||||||
|
SwingConstants.CENTER
|
||||||
|
), BorderLayout.CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JPanel grid = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||||
|
List<Plugin> ordered = plugins.stream()
|
||||||
|
.sorted(Comparator.comparingInt(PluginGalleryPanel::galleryOrder))
|
||||||
|
.toList();
|
||||||
|
for (Plugin plugin : ordered) {
|
||||||
|
String display = plugin.getDisplayName();
|
||||||
|
JButton btn = new JButton(display);
|
||||||
|
ShellUi.applyPluginGalleryTile(btn);
|
||||||
|
btn.setToolTipText(buildTooltip(plugin));
|
||||||
|
btn.addActionListener(e -> onPluginSelected.accept(plugin.getIdentifier(), display));
|
||||||
|
grid.add(btn);
|
||||||
|
}
|
||||||
|
JScrollPane scroll = new JScrollPane(grid);
|
||||||
|
ShellUi.borderlessScroll(scroll);
|
||||||
|
add(scroll, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildTooltip(Plugin plugin) {
|
||||||
|
return "<html>"
|
||||||
|
+ plugin.getDisplayName()
|
||||||
|
+ "<br/><i>" + plugin.getName() + "</i>"
|
||||||
|
+ "<br/>ID: " + plugin.getIdentifier()
|
||||||
|
+ "<br/>v" + plugin.getVersion() + " · " + plugin.getAuthor()
|
||||||
|
+ "</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int galleryOrder(Plugin plugin) {
|
||||||
|
if (plugin.getIdentifier() != null && plugin.getIdentifier().toLowerCase().contains("chat")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (plugin.getIdentifier() != null && plugin.getIdentifier().toLowerCase().contains("screencast")) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.application.messaging.MessagingDestinationUi;
|
||||||
|
import pitiupi.application.messaging.PluginTransportCatalog;
|
||||||
|
import pitiupi.application.messaging.TransportMode;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
import javax.swing.DefaultComboBoxModel;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transporte e contexto da sessão ativa (aba). Destino vem da aba lateral / lista de sessões.
|
||||||
|
*/
|
||||||
|
public final class PluginRoutingPanel extends JPanel {
|
||||||
|
|
||||||
|
public record RoutingSnapshot(
|
||||||
|
boolean everyoneInRoom,
|
||||||
|
String peerIp,
|
||||||
|
TransportMode transport,
|
||||||
|
boolean privateTcpOn
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PluginTransportCatalog.PluginKind pluginKind;
|
||||||
|
private final JLabel sessionBanner = new JLabel();
|
||||||
|
private final JLabel receiveScopeHint = new JLabel();
|
||||||
|
private final JLabel destinationModeHint = new JLabel();
|
||||||
|
private final JLabel ipLabel = new JLabel("IP do peer:");
|
||||||
|
private final JTextField ipField = new JTextField(18);
|
||||||
|
private final JComboBox<TransportMode> transportCombo = new JComboBox<>();
|
||||||
|
private final JToggleButton privateTcpToggle = new JToggleButton("TCP privado (desligado)");
|
||||||
|
private final JLabel ipHint;
|
||||||
|
private final JPanel rowPeer = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
|
||||||
|
private final JPanel rowHint = new JPanel(new BorderLayout());
|
||||||
|
private final List<Consumer<RoutingSnapshot>> listeners = new ArrayList<>();
|
||||||
|
private Consumer<Boolean> privateTcpListener = on -> {
|
||||||
|
};
|
||||||
|
private boolean everyoneInRoom = true;
|
||||||
|
private TransportMode lastRoomTransport = TransportMode.MULTICAST_UDP;
|
||||||
|
private TransportMode lastPeerTransport = TransportMode.UNICAST_UDP;
|
||||||
|
|
||||||
|
public PluginRoutingPanel(
|
||||||
|
PluginTransportCatalog.PluginKind pluginKind,
|
||||||
|
String peerHintHtml,
|
||||||
|
boolean showPrivateTcpToggle
|
||||||
|
) {
|
||||||
|
this.pluginKind = pluginKind;
|
||||||
|
this.ipHint = new JLabel(peerHintHtml);
|
||||||
|
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
|
||||||
|
ShellUi.applySectionTitleBorder(this, "Destino e transporte");
|
||||||
|
buildUi(showPrivateTcpToggle);
|
||||||
|
bindRoomSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRoutingListener(Consumer<RoutingSnapshot> listener) {
|
||||||
|
if (listener != null) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivateTcpListener(Consumer<Boolean> listener) {
|
||||||
|
this.privateTcpListener = listener != null ? listener : on -> {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoutingSnapshot snapshot() {
|
||||||
|
String ip = ipField.getText().trim();
|
||||||
|
TransportMode mode = (TransportMode) transportCombo.getSelectedItem();
|
||||||
|
return new RoutingSnapshot(everyoneInRoom, ip, mode, privateTcpToggle.isSelected() && !everyoneInRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindRoomSession() {
|
||||||
|
everyoneInRoom = true;
|
||||||
|
ipField.setText("");
|
||||||
|
sessionBanner.setText("<html><b>Sessão:</b> Sala geral (Todos)</html>");
|
||||||
|
setReceiveScopeHint(null);
|
||||||
|
applyRoutingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindPeerSession(String ip, String displayName) {
|
||||||
|
bindPeerSession(ip, displayName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindPeerSession(String ip, String displayName, String receiveScopeHtml) {
|
||||||
|
everyoneInRoom = false;
|
||||||
|
if (ip != null) {
|
||||||
|
ipField.setText(ip);
|
||||||
|
}
|
||||||
|
String who = displayName != null && !displayName.isBlank() ? displayName : ip;
|
||||||
|
sessionBanner.setText(SessionPeerLabels.sessionBannerHtml(who, ip));
|
||||||
|
setReceiveScopeHint(receiveScopeHtml);
|
||||||
|
applyRoutingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceiveScopeHint(String html) {
|
||||||
|
if (html == null || html.isBlank()) {
|
||||||
|
receiveScopeHint.setText("");
|
||||||
|
receiveScopeHint.setVisible(false);
|
||||||
|
} else {
|
||||||
|
receiveScopeHint.setText(html);
|
||||||
|
receiveScopeHint.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivateTcpOn(boolean on) {
|
||||||
|
privateTcpToggle.setSelected(on);
|
||||||
|
updatePrivateToggleLabel();
|
||||||
|
applyRoutingState();
|
||||||
|
privateTcpListener.accept(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectTransport(TransportMode mode) {
|
||||||
|
if (mode != null) {
|
||||||
|
transportCombo.setSelectedItem(mode);
|
||||||
|
rememberSelectedTransport(mode);
|
||||||
|
}
|
||||||
|
updateDestinationModeHint();
|
||||||
|
fireRoutingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildUi(boolean showPrivateTcpToggle) {
|
||||||
|
JPanel rowSession = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
|
||||||
|
rowSession.add(sessionBanner);
|
||||||
|
rowSession.add(destinationModeHint);
|
||||||
|
receiveScopeHint.setVisible(false);
|
||||||
|
rowSession.add(receiveScopeHint);
|
||||||
|
|
||||||
|
privateTcpToggle.setFocusPainted(false);
|
||||||
|
transportCombo.addActionListener(e -> {
|
||||||
|
TransportMode selected = (TransportMode) transportCombo.getSelectedItem();
|
||||||
|
rememberSelectedTransport(selected);
|
||||||
|
updateDestinationModeHint();
|
||||||
|
fireRoutingChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
rowPeer.add(ipLabel);
|
||||||
|
rowPeer.add(ipField);
|
||||||
|
rowPeer.add(new JLabel("Forma de envio:"));
|
||||||
|
rowPeer.add(transportCombo);
|
||||||
|
|
||||||
|
privateTcpToggle.setVisible(showPrivateTcpToggle);
|
||||||
|
privateTcpToggle.setToolTipText(
|
||||||
|
"Ligado: conversa só entre vocês, em conexão dedicada (TCP privado). "
|
||||||
|
+ "Desligado: use a forma de envio escolhida ao lado."
|
||||||
|
);
|
||||||
|
privateTcpToggle.addActionListener(e -> {
|
||||||
|
updatePrivateToggleLabel();
|
||||||
|
updateDestinationModeHint();
|
||||||
|
privateTcpListener.accept(privateTcpToggle.isSelected());
|
||||||
|
applyRoutingState();
|
||||||
|
});
|
||||||
|
if (showPrivateTcpToggle) {
|
||||||
|
rowPeer.add(privateTcpToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowHint.setBorder(BorderFactory.createEmptyBorder(0, 8, 2, 8));
|
||||||
|
rowHint.add(ipHint, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
add(rowSession);
|
||||||
|
add(rowPeer);
|
||||||
|
add(rowHint);
|
||||||
|
|
||||||
|
ipField.addActionListener(e -> applyRoutingState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyRoutingState() {
|
||||||
|
ipLabel.setVisible(!everyoneInRoom);
|
||||||
|
ipField.setVisible(!everyoneInRoom);
|
||||||
|
ipField.setEnabled(!everyoneInRoom);
|
||||||
|
privateTcpToggle.setEnabled(!everyoneInRoom);
|
||||||
|
rowHint.setVisible(!everyoneInRoom);
|
||||||
|
if (everyoneInRoom) {
|
||||||
|
privateTcpToggle.setSelected(false);
|
||||||
|
updatePrivateToggleLabel();
|
||||||
|
}
|
||||||
|
boolean privateOn = privateTcpToggle.isSelected();
|
||||||
|
transportCombo.setEnabled(!everyoneInRoom && (!privateTcpToggle.isVisible() || !privateOn));
|
||||||
|
|
||||||
|
List<TransportMode> modes = everyoneInRoom
|
||||||
|
? PluginTransportCatalog.forBroadcast(pluginKind)
|
||||||
|
: PluginTransportCatalog.forSinglePeer(pluginKind);
|
||||||
|
DefaultComboBoxModel<TransportMode> model = new DefaultComboBoxModel<>();
|
||||||
|
for (TransportMode mode : modes) {
|
||||||
|
model.addElement(mode);
|
||||||
|
}
|
||||||
|
transportCombo.setModel(model);
|
||||||
|
TransportMode preferred = everyoneInRoom ? lastRoomTransport : lastPeerTransport;
|
||||||
|
if (preferred != null && modes.contains(preferred)) {
|
||||||
|
transportCombo.setSelectedItem(preferred);
|
||||||
|
} else if (!modes.isEmpty()) {
|
||||||
|
transportCombo.setSelectedIndex(0);
|
||||||
|
rememberSelectedTransport((TransportMode) transportCombo.getSelectedItem());
|
||||||
|
}
|
||||||
|
updateDestinationModeHint();
|
||||||
|
fireRoutingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDestinationModeHint() {
|
||||||
|
RoutingSnapshot snap = snapshot();
|
||||||
|
String line = MessagingDestinationUi.summarizeSendMode(
|
||||||
|
snap.everyoneInRoom(),
|
||||||
|
snap.transport(),
|
||||||
|
snap.privateTcpOn()
|
||||||
|
);
|
||||||
|
destinationModeHint.setText("<html><i>— " + line + "</i></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePrivateToggleLabel() {
|
||||||
|
privateTcpToggle.setText(privateTcpToggle.isSelected()
|
||||||
|
? "TCP privado (ligado)"
|
||||||
|
: "TCP privado (desligado)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireRoutingChanged() {
|
||||||
|
RoutingSnapshot snap = snapshot();
|
||||||
|
for (Consumer<RoutingSnapshot> listener : listeners) {
|
||||||
|
listener.accept(snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberSelectedTransport(TransportMode mode) {
|
||||||
|
if (mode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (everyoneInRoom) {
|
||||||
|
lastRoomTransport = mode;
|
||||||
|
} else {
|
||||||
|
lastPeerTransport = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escape(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return text.replace("&", "&").replace("<", "<");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chaves de abas de sessão (chat, screencast).
|
||||||
|
*/
|
||||||
|
public final class PluginSessionKeys {
|
||||||
|
|
||||||
|
public static final String ROOM = "room";
|
||||||
|
public static final String ADD_PEER = "__add_peer__";
|
||||||
|
public static final String PEER_DRAFT = "peer:__draft__";
|
||||||
|
|
||||||
|
private PluginSessionKeys() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String peer(String ip) {
|
||||||
|
return "peer:" + ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome compartilhado dos plugins de sessão: roteamento, faixa de abas e corpo.
|
||||||
|
*/
|
||||||
|
public final class PluginSessionWorkspace extends JPanel {
|
||||||
|
|
||||||
|
private final PluginRoutingPanel routingPanel;
|
||||||
|
private final SessionTabBar sessionTabBar = new SessionTabBar();
|
||||||
|
private final JPanel bodyHost = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
public PluginSessionWorkspace(PluginRoutingPanel routingPanel, JComponent pluginToolbar) {
|
||||||
|
this.routingPanel = routingPanel;
|
||||||
|
setLayout(new BorderLayout(0, 0));
|
||||||
|
|
||||||
|
JPanel top = new JPanel();
|
||||||
|
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
|
||||||
|
top.add(routingPanel);
|
||||||
|
if (pluginToolbar != null) {
|
||||||
|
top.add(pluginToolbar);
|
||||||
|
}
|
||||||
|
add(top, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JPanel sessionColumn = new JPanel(new BorderLayout(0, 0));
|
||||||
|
sessionColumn.add(sessionTabBar.getPanel(), BorderLayout.NORTH);
|
||||||
|
sessionColumn.add(new SessionBodyChrome(bodyHost), BorderLayout.CENTER);
|
||||||
|
add(sessionColumn, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginRoutingPanel getRoutingPanel() {
|
||||||
|
return routingPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionTabBar getSessionTabBar() {
|
||||||
|
return sessionTabBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JPanel getBodyHost() {
|
||||||
|
return bodyHost;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package pitiupi.presentation.shell;
|
||||||
|
|
||||||
|
import pitiupi.presentation.shell.ui.ShellUi;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.border.MatteBorder;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envolve o corpo da sessão com separador leve abaixo da faixa de abas.
|
||||||
|
*/
|
||||||
|
public final class SessionBodyChrome extends JPanel {
|
||||||
|
|
||||||
|
public SessionBodyChrome(JPanel bodyHost) {
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
new MatteBorder(1, 0, 0, 0, ShellUi.shellEdgeColor()),
|
||||||
|
new EmptyBorder(2, 2, 2, 2)
|
||||||
|
));
|
||||||
|
add(bodyHost, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user