refactor(plugins): SPI workspace-only e remoção de menus legados
Remove GuiManager e pitiupi.models.Menus; atualiza chat/screencast, docs e testes (ConfigLoader, TcpConnectionManager). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7c8a476253
commit
42e70af48b
33 files changed
+104
-444
No files matched your search
+1
-1
@@ -16,7 +16,7 @@ Entrada na raiz do repositório: [README.md](../README.md).
|
||||
| --- | --- |
|
||||
| **Aluno (TP)** | [documentation/00_quickstart.md](documentation/00_quickstart.md) → [10_tutorial_plugin.md](documentation/10_tutorial_plugin.md) → [17_tutorial_ping_aula.md](documentation/17_tutorial_ping_aula.md) → [11_plugins_referencia.md](documentation/11_plugins_referencia.md) |
|
||||
| **Monitor / docência** | [documentation/01_visao_disciplina_e_tp.md](documentation/01_visao_disciplina_e_tp.md) → [15_testes_e_gate.md](documentation/15_testes_e_gate.md) |
|
||||
| **Desenvolvedor** | [documentation/13_guia_dev_e_agente.md](documentation/13_guia_dev_e_agente.md) → [14_arquitetura_e_manutencao.md](documentation/14_arquitetura_e_manutencao.md) |
|
||||
| **Desenvolvedor** | [documentation/13_guia_desenvolvimento.md](documentation/13_guia_desenvolvimento.md) → [14_arquitetura_e_manutencao.md](documentation/14_arquitetura_e_manutencao.md) |
|
||||
| **Professor / banca** | [documentation/16_fundamentacao_redes.md](documentation/16_fundamentacao_redes.md) → [references/README.md](references/README.md) |
|
||||
|
||||
**Idioma:** documentação em PT-BR; pacotes e classes Java como no código.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| 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) |
|
||||
| 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) |
|
||||
| D10 | Erros com `ErrorCode` | Mensagens PT na UI; `PitiupiException` no domínio | [13_guia_desenvolvimento.md](documentation/13_guia_desenvolvimento.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) |
|
||||
|
||||
@@ -16,7 +16,7 @@ Cada equipe/aluno entrega um **módulo Java separado** que:
|
||||
|
||||
1. Compila para JAR e carrega via `pitiupi/plugins/`.
|
||||
2. Define mensagens (`extends Message`) e o fluxo entre peers.
|
||||
3. Integra menus/janelas via `GuiManager`.
|
||||
3. Integra UI na aba central via `PluginWorkspace` e peer actions na lista lateral.
|
||||
4. Inclui README do módulo (build + protocolo).
|
||||
|
||||
Exemplos de tema: chat temático, tabuleiro (xadrez/damas), compartilhamento de arquivo, quiz síncrono.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Sistema de plugins
|
||||
|
||||
O sistema de plugins permite extensão modular sem alterar o núcleo. O contrato
|
||||
está em `pitiupi.models.Plugin` (classe abstrata).
|
||||
O sistema de plugins permite extensão modular sem alterar o núcleo. O contrato está em `pitiupi.models.Plugin` (classe abstrata).
|
||||
|
||||
**Breaking change (pós-v1):** plugins devem implementar apenas `createPlugin(ConnectionManager, PluginWorkspace)`. A API antiga com `GuiManager` e menus por categoria foi removida.
|
||||
|
||||
## Interface Plugin
|
||||
|
||||
@@ -11,8 +12,7 @@ Métodos obrigatórios:
|
||||
| --- | --- |
|
||||
| `getName()`, `getAuthor()`, `getVersion()`, `getIdentifier()` | Identificação (o `identifier` distingue plugins em conexões TCP dedicadas) |
|
||||
| `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 |
|
||||
| `createPlugin(ConnectionManager, PluginWorkspace)` | Inicialização: `bindShellWorkspaceContent`, `installPeerActions`, rede |
|
||||
| `getMessages()` | Lista de classes `Message` que o plugin desserializa |
|
||||
| `receiveMessage(Message)` | Processamento após o `PluginManager` identificar o tipo |
|
||||
|
||||
|
||||
@@ -12,9 +12,24 @@ Este guia complementa [09_sistema_de_plugins.md](09_sistema_de_plugins.md). Use
|
||||
|
||||
```java
|
||||
public class MeuPlugin extends Plugin {
|
||||
private PluginWorkspace workspace;
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager cm, GuiManager gui) {
|
||||
// registrar menus, abrir janelas
|
||||
public void createPlugin(ConnectionManager cm, PluginWorkspace workspace) {
|
||||
this.workspace = workspace;
|
||||
MeuPanel panel = new MeuPanel(cm);
|
||||
bindShellWorkspaceContent(panel);
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
registrar.register(PeerAction.of(
|
||||
"meuplugin.abrir",
|
||||
"Abrir",
|
||||
PeerActionTarget.ALL_PEERS,
|
||||
target -> workspace.openOrFocusTab(getIdentifier(), getDisplayName())
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,25 +64,9 @@ Arquivo `src/META-INF/services/pitiupi.models.Plugin`:
|
||||
pacote.MeuPlugin
|
||||
```
|
||||
|
||||
## 5. Categoria no menu
|
||||
## 5. Registrar no catálogo
|
||||
|
||||
Escolha onde o plugin aparece na barra **Plugin**:
|
||||
|
||||
| `MenuCategory` | Uso |
|
||||
| --- | --- |
|
||||
| `Social` | Chat, presença |
|
||||
| `Jogos` | Jogos entre peers |
|
||||
| `Streaming` | Screen cast, mídia |
|
||||
|
||||
Exemplo:
|
||||
|
||||
```java
|
||||
guiManager.createMenu(MenuCategory.Jogos, new MeuMenu(...));
|
||||
```
|
||||
|
||||
## 6. Registrar no catálogo
|
||||
|
||||
Adicione uma entrada em [`plugins.registry.json`](../../plugins.registry.json) na raiz do repo:
|
||||
Adicione uma entrada em [`plugins.registry.json`](../../plugins.registry.json):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -81,29 +80,15 @@ Adicione uma entrada em [`plugins.registry.json`](../../plugins.registry.json) n
|
||||
|
||||
O `id` é o que você passa para `./scripts/pitiupi.sh run --plugins xadrez`.
|
||||
|
||||
## 7. Build e instalação
|
||||
## 6. Build e instalação
|
||||
|
||||
```bash
|
||||
./scripts/pitiupi.sh build --plugins xadrez
|
||||
# ou, manualmente:
|
||||
cd pitiupi.xadrez && ant jar
|
||||
ln -sf ../../pitiupi.xadrez/dist/pitiupi.xadrez.jar ../pitiupi/plugins/xadrez.jar
|
||||
```
|
||||
|
||||
Reinicie o Pitiupi após trocar JARs.
|
||||
JARs de runtime ficam em `PITIUPI_HOME/plugins` (ver [00_quickstart.md](00_quickstart.md)).
|
||||
|
||||
## 8. Teste
|
||||
## 7. Próximos passos
|
||||
|
||||
Duas instâncias, mesma rede, portas distintas se no mesmo host. Ver [00_quickstart.md](00_quickstart.md).
|
||||
|
||||
## TCP dedicado (avançado)
|
||||
|
||||
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
|
||||
|
||||
| Problema | Causa provável |
|
||||
| --- | --- |
|
||||
| Plugin não aparece | SPI ausente ou JAR fora de `plugins/` |
|
||||
| `ClassNotFoundException` na recepção | Classe da mensagem não no JAR do remetente/destino |
|
||||
| Peers não se veem | Multicast bloqueado — [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md) |
|
||||
- [11_plugins_referencia.md](11_plugins_referencia.md) — chat e screenCast
|
||||
- [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md) — plugin didático Ping (opcional, fora do commit)
|
||||
@@ -1,34 +1,15 @@
|
||||
# Menus, peer actions e transportes
|
||||
# Menus e ações na lista (histórico)
|
||||
|
||||
## Interface atual (shell v1)
|
||||
Na v1 atual, plugins **não** registram menus na barra superior nem submenus legados por categoria.
|
||||
|
||||
Plugins de referência **não** dependem da barra de menus superior para o fluxo principal:
|
||||
## Como expor ações hoje
|
||||
|
||||
- **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.
|
||||
- Dentro da aba do plugin, **`PluginRoutingPanel`** padroniza destino (sala geral vs IP), forma de envio (multicast UDP, UDP unicast, TCP) e texto de ajuda.
|
||||
1. **Galeria Plugins** — abre a aba central do plugin.
|
||||
2. **Lista lateral** — clique em um peer ou em **Todos** → menu com **peer actions** registradas por cada plugin (`PeerActionBroker`).
|
||||
3. **`PluginWorkspace.openOrFocusTab`** — dentro do handler da peer action ou após carregar o painel.
|
||||
|
||||
Ver [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md) e [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
||||
## Catálogo `menuCategory`
|
||||
|
||||
## MenuCategory (legado)
|
||||
O campo `menuCategory` em [`plugins.registry.json`](../../plugins.registry.json) (`Social`, `Jogos`, `Streaming`) serve apenas ao script `./scripts/pitiupi.sh` para filtrar build/run — não cria menus Swing.
|
||||
|
||||
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(...)`.
|
||||
|
||||
Novos plugins devem preferir `PluginWorkspace` + peer actions. O menu da shell expõe apenas **Ajuda → Sobre**.
|
||||
|
||||
## Unicast vs multicast na aplicação
|
||||
|
||||
| Modo | API típica | Semântica |
|
||||
| --- | --- | --- |
|
||||
| Multicast | `MessageDispatcher` / `SocketUDP.sendMulticast` | Todos no grupo na porta configurada |
|
||||
| Unicast UDP | envio para IP de um peer | Um destinatário, sem estabelecer TCP |
|
||||
| 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).
|
||||
|
||||
## Referência de código
|
||||
|
||||
- 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).
|
||||
Referência de plugins: [11_plugins_referencia.md](11_plugins_referencia.md).
|
||||
+5
-7
@@ -1,6 +1,6 @@
|
||||
# Guia para desenvolvedor e agente
|
||||
# Guia para desenvolvimento
|
||||
|
||||
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.
|
||||
Esta documentação descreve a ferramenta Pitiupi neste repositório: build, rede, plugins e convenções de código.
|
||||
|
||||
## Leitura mínima antes de mudar código
|
||||
|
||||
@@ -21,23 +21,21 @@ Esta documentação descreve **apenas** a ferramenta Pitiupi neste repositório:
|
||||
|
||||
## Gate de build
|
||||
|
||||
Compilar núcleo e os dois plugins do catálogo:
|
||||
|
||||
```bash
|
||||
cd pitiupi && ant jar && ant test
|
||||
cd plugins/chat && ant jar
|
||||
cd plugins/screencast && ant jar
|
||||
```
|
||||
|
||||
Ou na raiz do clone: `./scripts/pitiupi.sh build --all`.
|
||||
Ou na raiz: `./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).
|
||||
Testes manuais em rede: [15_testes_e_gate.md](15_testes_e_gate.md).
|
||||
|
||||
## Convenções
|
||||
|
||||
- Código e identificadores em inglês (pacotes `pitiupi.*`).
|
||||
- Documentação em português (BR) em `docs/`.
|
||||
- Evitar alterar o núcleo para necessidades de um único plugin — preferir extensão via `Plugin`.
|
||||
- Evitar alterar o núcleo para um único plugin — preferir extensão via `Plugin` e `PluginWorkspace`.
|
||||
|
||||
## Decisões e débitos
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
| Usuários online, peer actions | `pitiupi.presentation.shell.NetworkUsersPanel`, `PeerActionBroker` |
|
||||
| Facade rede para plugins | `pitiupi.control.ConnectionManager` |
|
||||
| Workspace na aba (SPI) | `pitiupi.api.plugin.PluginWorkspace`, `ShellPluginWorkspace` |
|
||||
| Menus legados (adapter) | `pitiupi.control.GuiManager` — deprecado; preferir peer actions |
|
||||
| UDP | `pitiupi.net.udp.SocketUDP` |
|
||||
| TCP servidor e mapa de conexões | `pitiupi.net.tcp.SocketTCP`, `TcpConnectionManager` |
|
||||
| Conexão TCP por peer | `pitiupi.net.tcp.TcpConnection` |
|
||||
@@ -18,8 +17,6 @@
|
||||
| Heartbeat | `pitiupi.control.HeartbeatManager`, `HeartbeatMessage` |
|
||||
| Carregar JARs, despacho | `pitiupi.control.plugins.PluginManager` |
|
||||
| Contrato plugin | `pitiupi.models.Plugin`, `pitiupi.models.Messages.Message` |
|
||||
| Menus por categoria | `pitiupi.models.Menus.*` |
|
||||
|
||||
## Dependências entre módulos
|
||||
|
||||
```
|
||||
|
||||
@@ -10,6 +10,8 @@ cd pitiupi && ant test
|
||||
|
||||
Falha se `java`, `javac` ou Ant estiverem ausentes.
|
||||
|
||||
Pacotes com testes automatizados (JUnit 5): `platform` (`PitiupiHome`, `ConfigLoader`), `plugins.store`, `application.presence`, `application.peeractions`, `application.invites`, `application.messaging`, `application.activity`, `presentation.shell`, `control` (goodbye), `net.tcp` (fechamento de conexões), `models`.
|
||||
|
||||
## Gate de build (produto)
|
||||
|
||||
Na raiz do clone:
|
||||
|
||||
@@ -20,7 +20,7 @@ Referência para **código Java**, **rede P2P** e **plugins**. Índice geral: [d
|
||||
| 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 |
|
||||
| 13 | [13_guia_dev_e_agente.md](13_guia_dev_e_agente.md) | Leitura para dev/agente |
|
||||
| 13 | [13_guia_desenvolvimento.md](13_guia_desenvolvimento.md) | Guia de desenvolvimento |
|
||||
| 14 | [14_arquitetura_e_manutencao.md](14_arquitetura_e_manutencao.md) | Onde editar cada comportamento |
|
||||
| 15 | [15_testes_e_gate.md](15_testes_e_gate.md) | Build gate e testes manuais |
|
||||
| 16 | [16_fundamentacao_redes.md](16_fundamentacao_redes.md) | Teoria ↔ código |
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -12,14 +11,13 @@ import pitiupi.presentation.shell.TabWorkspacePanel;
|
||||
import javax.swing.JComponent;
|
||||
|
||||
/**
|
||||
* Workspace com abas e barra de status (Onda D).
|
||||
* Workspace com abas e barra de status.
|
||||
*/
|
||||
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;
|
||||
@@ -28,7 +26,6 @@ public final class ShellPluginWorkspace implements PluginWorkspace {
|
||||
String pluginId,
|
||||
Plugin plugin,
|
||||
ConnectionManager connectionManager,
|
||||
GuiManager guiManager,
|
||||
PeerActionBroker peerActionBroker,
|
||||
InviteInboxService inviteInboxService,
|
||||
TabWorkspacePanel tabWorkspace,
|
||||
@@ -37,7 +34,6 @@ public final class ShellPluginWorkspace implements PluginWorkspace {
|
||||
this.pluginId = pluginId;
|
||||
this.plugin = plugin;
|
||||
this.connectionManager = connectionManager;
|
||||
this.guiManager = guiManager;
|
||||
this.tabWorkspace = tabWorkspace;
|
||||
this.shellHost = shellHost;
|
||||
this.registrar = new PluginPeerActionsRegistrar(peerActionBroker, pluginId);
|
||||
@@ -46,10 +42,6 @@ public final class ShellPluginWorkspace implements PluginWorkspace {
|
||||
inviteInboxService.setInviteHandler(pluginId, plugin::onInviteAccepted);
|
||||
}
|
||||
|
||||
public GuiManager guiManager() {
|
||||
return guiManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionManager connectionManager() {
|
||||
return connectionManager;
|
||||
|
||||
@@ -30,12 +30,6 @@ public final class PeerActivityInbox {
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated use {@link #recordCounted} */
|
||||
@Deprecated
|
||||
public void record(String peerInstanceId, String pluginId, String peerActionId) {
|
||||
recordCounted(peerInstanceId, pluginId, peerActionId, null);
|
||||
}
|
||||
|
||||
public void recordBroadcast(String pluginId, String peerActionId) {
|
||||
recordCounted(BROADCAST_PEER_KEY, pluginId, peerActionId, PeerActivityFocusTokens.broadcast());
|
||||
}
|
||||
@@ -74,12 +68,6 @@ public final class PeerActivityInbox {
|
||||
return sum(countedByPeerAndAction.getOrDefault(peerKey(peerInstanceId), Map.of()));
|
||||
}
|
||||
|
||||
/** @deprecated use {@link #totalCountedForPeer} */
|
||||
@Deprecated
|
||||
public int totalForPeer(String peerInstanceId) {
|
||||
return totalCountedForPeer(peerInstanceId);
|
||||
}
|
||||
|
||||
public boolean hasIndicatorForPeer(String peerInstanceId) {
|
||||
Map<String, Integer> map = indicatorByPeerAndAction.get(peerKey(peerInstanceId));
|
||||
return map != null && !map.isEmpty();
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package pitiupi.control;
|
||||
|
||||
import pitiupi.presentation.shell.NetworkUsersPanel;
|
||||
import pitiupi.presentation.shell.ShellHost;
|
||||
import pitiupi.models.Menus.*;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
import pitiupi.models.Menus.UnicastMenus.UnicastActionMenu;
|
||||
import pitiupi.models.Menus.UnicastMenus.UnicastMenu;
|
||||
import pitiupi.models.Menus.UnicastMenus.UnicastSubMenu;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class GuiManager {
|
||||
private final ShellHost shellHost;
|
||||
private final NetworkUsersPanel networkUsersPanel;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
public GuiManager(ShellHost shellHost, javax.swing.JMenuBar menuBar, NetworkUsersPanel networkUsersPanel) {
|
||||
this.shellHost = shellHost;
|
||||
this.networkUsersPanel = networkUsersPanel;
|
||||
this.logger = Logger.getLogger(GuiManager.class.getName());
|
||||
}
|
||||
|
||||
public void createMenu(MenuCategory category, Menu menu) {
|
||||
try {
|
||||
JMenuItem newMenu = buildMenu(menu);
|
||||
if (menu instanceof MulticastMenu) {
|
||||
// Onda D: ações multicast migram para peer actions / abas (Onda E).
|
||||
}
|
||||
if (menu instanceof UnicastMenu) {
|
||||
networkUsersPanel.addUnicastMenu(category, newMenu);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
logger.log(Level.WARNING, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JMenuItem buildMenu(Menu menu) {
|
||||
if (menu instanceof MulticastSubMenu multicastSubMenu) {
|
||||
JMenu newMenu = new JMenu(menu.getText());
|
||||
|
||||
for (Menu child : multicastSubMenu.getMenus()) {
|
||||
newMenu.add(buildMenu(child));
|
||||
}
|
||||
|
||||
return newMenu;
|
||||
}
|
||||
|
||||
if (menu instanceof UnicastSubMenu unicastSubMenu) {
|
||||
JMenu newMenu = new JMenu(menu.getText());
|
||||
|
||||
for (Menu child : unicastSubMenu.getMenus()) {
|
||||
newMenu.add(buildMenu(child));
|
||||
}
|
||||
|
||||
return newMenu;
|
||||
}
|
||||
|
||||
if (menu instanceof MulticastActionMenu multicastActionMenu) {
|
||||
JMenuItem newMenu = new JMenuItem(menu.getText());
|
||||
newMenu.addActionListener(e -> multicastActionMenu.onClick());
|
||||
return newMenu;
|
||||
}
|
||||
|
||||
if (menu instanceof UnicastActionMenu unicastActionMenu) {
|
||||
JMenuItem newMenu = new JMenuItem(menu.getText());
|
||||
|
||||
networkUsersPanel.initUnicastMenu(newMenu, unicastActionMenu::onClick);
|
||||
return newMenu;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid menu type");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import pitiupi.Main;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.errors.ErrorCode;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.platform.PitiupiHome;
|
||||
@@ -112,7 +111,6 @@ public class PluginManager {
|
||||
|
||||
public void loadPlugins(
|
||||
ConnectionManager connectionManager,
|
||||
GuiManager guiManager,
|
||||
Function<Plugin, PluginWorkspace> workspaceFactory
|
||||
) {
|
||||
File pluginDir = PitiupiHome.get().pluginsDirectory().toFile();
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package pitiupi.models.Menus;
|
||||
|
||||
public abstract class Menu {
|
||||
public abstract String getText();
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pitiupi.models.Menus;
|
||||
|
||||
public enum MenuCategory {
|
||||
Jogos,
|
||||
Social,
|
||||
Streaming
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package pitiupi.models.Menus.MulticastMenus;
|
||||
|
||||
public abstract class MulticastActionMenu extends MulticastMenu{
|
||||
public abstract void onClick();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package pitiupi.models.Menus.MulticastMenus;
|
||||
|
||||
import pitiupi.models.Menus.Menu;
|
||||
|
||||
public abstract class MulticastMenu extends Menu {
|
||||
MulticastMenu() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pitiupi.models.Menus.MulticastMenus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MulticastSubMenu extends MulticastMenu{
|
||||
public abstract List<MulticastMenu> getMenus();
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pitiupi.models.Menus.UnicastMenus;
|
||||
|
||||
import pitiupi.models.User;
|
||||
|
||||
public abstract class UnicastActionMenu extends UnicastMenu{
|
||||
public abstract void onClick(User user);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package pitiupi.models.Menus.UnicastMenus;
|
||||
|
||||
import pitiupi.models.Menus.Menu;
|
||||
import pitiupi.models.User;
|
||||
|
||||
public abstract class UnicastMenu extends Menu {
|
||||
UnicastMenu() {}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package pitiupi.models.Menus.UnicastMenus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class UnicastSubMenu extends UnicastMenu{
|
||||
public abstract List<UnicastMenu> getMenus();
|
||||
}
|
||||
@@ -7,9 +7,7 @@ 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.GuiManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
|
||||
@@ -54,24 +52,9 @@ public abstract class Plugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa o plugin.
|
||||
*
|
||||
* <p>Este método é chamado pela aplicação após o carregamento do plugin.</p>
|
||||
*
|
||||
* @param connectionManager interface com o sistema de conexões da aplicação.
|
||||
* @param guiManager interface com a janela principal da aplicação
|
||||
* Inicializa o plugin na aba central (shell) e registra peer actions.
|
||||
*/
|
||||
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 abstract void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace);
|
||||
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
}
|
||||
@@ -87,19 +70,9 @@ public abstract class Plugin {
|
||||
|
||||
/**
|
||||
* Retorna uma mensagem utilizada pelo plugin.
|
||||
*
|
||||
* @return mensagem criada pelo plugin.
|
||||
*/
|
||||
public abstract List<Class<? extends Message>> getMessages();
|
||||
|
||||
/**
|
||||
* Recebe uma mensagem enviada pela aplicação.
|
||||
*
|
||||
* <p>A mensagem é recebida em formato serializado. O plugin deve verificar
|
||||
* se a mensagem pertence ao seu tipo e realizar o processamento necessário.</p>
|
||||
*
|
||||
* @param message mensagem serializada recebida.
|
||||
*/
|
||||
public abstract void receiveMessage(Message message);
|
||||
|
||||
public boolean receiveAllTypes() {return false;}
|
||||
@@ -112,4 +85,4 @@ public abstract class Plugin {
|
||||
public boolean onSocketClosed(TcpConnection connection) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
|
||||
@@ -22,7 +21,6 @@ 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;
|
||||
@@ -49,8 +47,6 @@ public final class MainShellFrame extends JFrame implements ShellHost {
|
||||
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);
|
||||
@@ -90,11 +86,10 @@ public final class MainShellFrame extends JFrame implements ShellHost {
|
||||
}
|
||||
|
||||
private void loadPlugins() {
|
||||
pluginManager.loadPlugins(connectionManager, guiManager, (plugin) -> new ShellPluginWorkspace(
|
||||
pluginManager.loadPlugins(connectionManager, (plugin) -> new ShellPluginWorkspace(
|
||||
plugin.getIdentifier(),
|
||||
plugin,
|
||||
connectionManager,
|
||||
guiManager,
|
||||
connectionManager.getPeerActionBroker(),
|
||||
connectionManager.getInviteInboxService(),
|
||||
tabWorkspace,
|
||||
|
||||
@@ -7,9 +7,7 @@ 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;
|
||||
|
||||
@@ -28,10 +26,7 @@ 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 e peer actions (sem badges de atividade na v1).
|
||||
@@ -40,8 +35,6 @@ 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) {
|
||||
@@ -49,12 +42,6 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
||||
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(
|
||||
@@ -114,40 +101,13 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
||||
}
|
||||
popup.add(pluginMenu);
|
||||
}
|
||||
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) {
|
||||
public void onPeersChanged(List<pitiupi.models.User> peers) {
|
||||
refreshFromPresence();
|
||||
}
|
||||
|
||||
@@ -160,7 +120,7 @@ public final class NetworkUsersPanel extends JPanel implements PeerListener {
|
||||
});
|
||||
}
|
||||
|
||||
record DisplayPeer(UserPeerView view) {
|
||||
private record DisplayPeer(UserPeerView view) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return view.getDisplayName();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package pitiupi.models;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
@@ -36,8 +38,7 @@ class PluginDisplayNameTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(pitiupi.control.ConnectionManager connectionManager,
|
||||
pitiupi.control.GuiManager guiManager) {
|
||||
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,8 +78,7 @@ class PluginDisplayNameTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(pitiupi.control.ConnectionManager connectionManager,
|
||||
pitiupi.control.GuiManager guiManager) {
|
||||
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package pitiupi.net.tcp;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TcpConnectionManagerTest {
|
||||
|
||||
@Test
|
||||
void closeAllConnectionsOnEmptyManagerDoesNotThrow() {
|
||||
TcpConnectionManager manager = new TcpConnectionManager();
|
||||
manager.closeAllConnections();
|
||||
manager.closeAllConnections("reason");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package pitiupi.platform;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ConfigLoaderTest {
|
||||
|
||||
@Test
|
||||
void loadOrCreateDefaultsWritesConfig(@TempDir Path home) throws Exception {
|
||||
ConfigLoader loader = new ConfigLoader(home);
|
||||
loader.loadOrCreateDefaults();
|
||||
assertTrue(Files.isRegularFile(home.resolve("config.properties")));
|
||||
assertTrue(loader.isMigrationCwdPluginsPromptEnabled());
|
||||
assertFalse(loader.isMigrationCwdPluginsDone());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMigrationFlagsPersist(@TempDir Path home) throws Exception {
|
||||
ConfigLoader loader = new ConfigLoader(home);
|
||||
loader.loadOrCreateDefaults();
|
||||
loader.setMigrationCwdPluginsDone(true);
|
||||
ConfigLoader reloaded = new ConfigLoader(home);
|
||||
reloaded.loadOrCreateDefaults();
|
||||
assertTrue(reloaded.isMigrationCwdPluginsDone());
|
||||
}
|
||||
}
|
||||
@@ -1,32 +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.GUI;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ChatMenu extends MulticastSubMenu {
|
||||
ChatMulticastMenu chatMulticastMenu;
|
||||
|
||||
public ChatMenu(ChatWindow chatWindow) {
|
||||
this.chatMulticastMenu = new ChatMulticastMenu(chatWindow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MulticastMenu> getMenus() {
|
||||
return List.of(chatMulticastMenu);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package pitiupi.chat.GUI;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;;
|
||||
|
||||
public class ChatMulticastMenu extends MulticastActionMenu {
|
||||
ChatWindow chatWindow;
|
||||
|
||||
public ChatMulticastMenu(ChatWindow chatWindow) {
|
||||
this.chatWindow = chatWindow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Show Chat Window";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
chatWindow.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import pitiupi.chat.control.PrivateChatHost;
|
||||
import pitiupi.chat.net.ChatMessage;
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.models.User;
|
||||
@@ -44,10 +43,6 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installPeerActions(PeerActionsRegistrar registrar) {
|
||||
registrar.register(PeerAction.of(
|
||||
|
||||
@@ -1,52 +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.GUI;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
|
||||
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author flavio
|
||||
*/
|
||||
public class ScreenCastMenu extends MulticastSubMenu {
|
||||
|
||||
private final ScreenCastShowMenu showMenu;
|
||||
|
||||
public ScreenCastMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.showMenu = new ScreenCastShowMenu(screenCastPanel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "Screen Cast";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MulticastMenu> getMenus() {
|
||||
return List.of(showMenu);
|
||||
}
|
||||
|
||||
private static final class ScreenCastShowMenu extends MulticastActionMenu {
|
||||
private final ScreenCastPanel screenCastPanel;
|
||||
|
||||
ScreenCastShowMenu(ScreenCastPanel screenCastPanel) {
|
||||
this.screenCastPanel = screenCastPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return "ScreenCast Window";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
screenCastPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,11 @@ import pitiupi.application.presence.UserPeerView;
|
||||
import pitiupi.application.peeractions.PeerActionTarget;
|
||||
import pitiupi.application.peeractions.PeerTarget;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Menus.MenuCategory;
|
||||
import pitiupi.application.session.SessionNoticeChannel;
|
||||
import pitiupi.application.session.SessionNoticePhase;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Messages.SessionNoticeMessage;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.screenCast.GUI.ScreenCastMenu;
|
||||
import pitiupi.screenCast.GUI.ScreenCastPanel;
|
||||
import pitiupi.screenCast.GUI.ScreenCastReceiveScope;
|
||||
import pitiupi.screenCast.net.ScreenCastMessage;
|
||||
@@ -62,12 +59,6 @@ public class ScreenCastPlugin extends Plugin {
|
||||
installPeerActions(workspace.peerActions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
this.screenCastPanel = new ScreenCastPanel(connectionManager);
|
||||
guiManager.createMenu(MenuCategory.Streaming, new ScreenCastMenu(screenCastPanel));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShellTabClosed() {
|
||||
if (screenCastPanel != null) {
|
||||
|
||||
Reference in new issue
Block a user