Compare commits
283 Commits
v0.1-beta
...
f/fancy-di
Author | SHA1 | Date | |
---|---|---|---|
6b02fd0f46
|
|||
a062911d55 | |||
241e5def03
|
|||
cd8971b6b4
|
|||
e79f60e95e
|
|||
aaaf5ef7be
|
|||
98f59c1383
|
|||
db28f02505
|
|||
b2c3cf62c8
|
|||
77a75fc37c
![]() |
|||
a0812f193e
![]() |
|||
ebe19c00c9
![]() |
|||
dd477b6cbc
![]() |
|||
571a953c40
![]() |
|||
a515ec961a
![]() |
|||
12184848b6
|
|||
2e17caea4d
|
|||
44f4d8f1e0
|
|||
5b85c1bf54
|
|||
f4f34ff829
|
|||
ab2e9aa114
![]() |
|||
75f0a65517
![]() |
|||
08bd915f04
|
|||
fa2a5d0b24
|
|||
1d191858fe
|
|||
3c8c544cbd
|
|||
e8202e0c94
|
|||
3810fdef02
|
|||
637ad9f61f
|
|||
f2eb89d469
![]() |
|||
6f9982bbc3
|
|||
5e1b9a9d5b
|
|||
fb1147f939
![]() |
|||
7ca770cbc3
![]() |
|||
da6bdafc68
![]() |
|||
99867eb23a
![]() |
|||
994cbbcd72 | |||
51b189e8f5
|
|||
3d987985ff
|
|||
5f0910635a
|
|||
ab77c98a36
|
|||
434d577c15
|
|||
8c0add517a
![]() |
|||
9934eefd41
![]() |
|||
8543e94040
|
|||
8592839156
|
|||
7fffa0da83
|
|||
85d0aa37d2 | |||
80795a3fc2 | |||
f5bfb73abe | |||
2779971e99 | |||
a4e9474b97 | |||
3f0267624c | |||
837ed0106f
|
|||
4a0bcf9762 | |||
829e94fa5f | |||
c7ee545ee2 | |||
d70a848ef3
|
|||
d1d52468bc | |||
ede50ed3e5
|
|||
61fbeda05e
|
|||
5daff3620e
|
|||
618a4aa3cf
|
|||
108db1ae11
|
|||
6d7afbaa8f
|
|||
86e189a40a
|
|||
0efd1e5594
|
|||
f6eeeee79b
|
|||
8eb7743057
|
|||
f0e645c0ae | |||
af219274f5
|
|||
05d4917bb2
|
|||
f02b01291b | |||
84d80982e5
|
|||
2d9283551a
|
|||
758e52e030 | |||
b9e19d69b9 | |||
c6819e637b
|
|||
41f07dc452 | |||
9419ba2ee8 | |||
f36f330c81
|
|||
5b4f2762e5
|
|||
1b60ab3f0d | |||
8ed6faca96 | |||
52d6282e13
|
|||
0dbd15e958 | |||
d8ae8a65b8 | |||
a12d765494 | |||
3cd9d76d2c
|
|||
d394c2d058
|
|||
7cc4928826
|
|||
4959bc9634
|
|||
16a0786d54
|
|||
40447f3f42
|
|||
be945fe3ee
|
|||
a8aa1c9ea7
|
|||
fd21c5789f
|
|||
1ccf4354aa | |||
cb2a3a6540
|
|||
3e594c1fbd
|
|||
f21d077522
|
|||
31cb22035b
|
|||
ec6b67099f
|
|||
89b9afb3db
|
|||
f98811c899 | |||
920dcb53fc | |||
4ba85f68ef | |||
e06dd7dd57 | |||
c21da25789
|
|||
8a01229855 | |||
763830c727
|
|||
8829f267ec | |||
465ed20efa
|
|||
69ea737361 | |||
74a1f8232b
|
|||
9b6d0f3c97
|
|||
ff1891108e
|
|||
78573399e9
|
|||
beb0f3e469 | |||
dd2e09b6dc | |||
cf401d201c | |||
aa992e2bcf | |||
63ed1c480d | |||
3f3c561e25 | |||
fcd5767c4b | |||
d97af36ae1 | |||
d0c8c685ab | |||
8b204b3715 | |||
efbca9cbc9 | |||
661823219c | |||
9f517cfc6b | |||
ee0d70647c | |||
88f28e60f1 | |||
9bd06336eb | |||
dc114e5b3c | |||
f6c62f9073 | |||
4137bf393a | |||
dc58290f22 | |||
74025c6111 | |||
6c32cf650e | |||
f86f3ec200 | |||
f581b83359 | |||
b7ea7f0e85 | |||
e7d85bd968 | |||
15265d2b7c | |||
78ade078d4 | |||
5f3e615641 | |||
572541e381 | |||
f6c3da394d | |||
da309098b7 | |||
1983cebde1 | |||
46a883dda9 | |||
a6e5b3d77d | |||
ddbf9acd07 | |||
1d03128744 | |||
72ffa71d6b | |||
14ccf4ce58 | |||
bd75da1ab9 | |||
f77795edb1 | |||
dbf69c7cc1 | |||
d0f125f058 | |||
b4397fe2f2 | |||
1fe83dbcc0 | |||
c784ebb787 | |||
eb4e421974 | |||
4bbc4189ec | |||
19dcb2bea8 | |||
2cb124505d | |||
cb95c40ad6 | |||
b081960a31 | |||
f4a3bfed97 | |||
ecede45360 | |||
5acbd3b6e1 | |||
33aa851090 | |||
2491812ba0 | |||
71bb329857 | |||
dee317c27d | |||
c3dfedc642 | |||
a1d09d6550 | |||
0901f900e7 | |||
56bb00cd32 | |||
fe4f9bf219 | |||
209262b4c9 | |||
3fdbbfd756 | |||
0d77fbf831 | |||
59188711b8 | |||
74ebd158f2 | |||
719aa4cd4f | |||
498f3ef43d | |||
b02c2fdc65 | |||
b678ae295b | |||
3cbe3b5045 | |||
268e4439d7 | |||
98ebb321ce | |||
9234e23fae | |||
3e7a949be5 | |||
0167af54b0 | |||
517c840487 | |||
e216152e6b | |||
63f42ab8d9 | |||
1cdad2df0b | |||
5a5e6e2086 | |||
e382a86623 | |||
6f8859c3fd | |||
72d1e074f4 | |||
cd2e739529 | |||
8d81b76bad | |||
c34457730f | |||
00fc160550 | |||
9d7f85c58d | |||
4d4de3a27f | |||
8718596be2 | |||
9a947739a6 | |||
2ffcad9d35 | |||
59354c403d | |||
07fbe3438a | |||
2ed30c56cd | |||
e49d390089 | |||
d3c2eb4ff7 | |||
42184c47f7 | |||
6a1a9ecdbb | |||
f1856534c6 | |||
9ea8d24ab6 | |||
38c57c997f | |||
7bf35977f0 | |||
5d2a3b83d2 | |||
9e427e1ec3 | |||
ebfe603bc7 | |||
60791f2913 | |||
5d03d0f0eb | |||
79a121b6b5 | |||
e00fa592d6 | |||
a283217308 | |||
145ec06f57 | |||
01f81fadac | |||
e51d2946d0 | |||
1a17448724 | |||
0674035183 | |||
fdbec3d652 | |||
5ce62c10ca | |||
fa7be8c343 | |||
282db47153 | |||
381740e087 | |||
da77afdc32 | |||
2e42da87ec | |||
2e45e375b1 | |||
2e4a17c6c5 | |||
b4225b0d80 | |||
f135a99fdd | |||
698e260746 | |||
47ab5d1e0c | |||
71145bbb24 | |||
62d9df7ae8 | |||
b88f260efc | |||
e104a1f9b4 | |||
7b693e0328 | |||
afcf1e48a4 | |||
a21a5c8588 | |||
00603bedf6 | |||
96bfe489da | |||
698b57d99d | |||
c71c038317 | |||
43c1edae39 | |||
176f6c6463 | |||
bf499da97d | |||
c0f4a8e212 | |||
fb4fd85fe4 | |||
bc355f190f | |||
a76c2a347e | |||
07c4ccf3c8 | |||
e7e4c5af42 | |||
1e63c1a7d1 | |||
c5094e52cd | |||
9a9a475c0e | |||
f608b2d6ec | |||
abd0113588 | |||
ba336908d1 | |||
4bc393b055 | |||
bdd1b40107 | |||
0267a7bbab | |||
a437fb25da | |||
659a468049 | |||
062c9f418d |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @CyB3RC0nN0R
|
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,32 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333
|
||||
projects: Envoy
|
||||
milestone: Envoy v0.2-beta
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Debian GNU/Linux, Microsoft Windows 10]
|
||||
- Version [e.g. 0.1-beta]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement, feature
|
||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
||||
project: Envoy
|
||||
milestones: Envoy v0.2-beta
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
10
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
10
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Bug fix
|
||||
title: Fixed Bug
|
||||
labels: bug
|
||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
||||
reviewers: CyB3RC0nN0R, delvh
|
||||
projects: Envoy
|
||||
milestone: Envoy v0.1-beta
|
||||
---
|
||||
Fixes #{issue}
|
@ -1,9 +0,0 @@
|
||||
---
|
||||
name: Feature integration
|
||||
title: Added feature
|
||||
labels: feature
|
||||
assignees: CyB3RC0nN0R, delvh, DieGurke
|
||||
reviewers: CyB3RC0nN0R, delvh
|
||||
projects: Envoy
|
||||
milestone: Envoy v0.1-beta
|
||||
---
|
10
.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
vendored
10
.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Updated Javadoc
|
||||
title: Updated Javadoc
|
||||
labels: documentation
|
||||
assignees: CyB3RC0nN0R, delvh
|
||||
reviewers: CyB3RC0nN0R, delvh
|
||||
projects: Envoy
|
||||
milestone: Envoy v0.1-beta
|
||||
|
||||
---
|
32
.github/workflows/maven.yml
vendored
32
.github/workflows/maven.yml
vendored
@ -1,32 +0,0 @@
|
||||
name: Java CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Package
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-m2
|
||||
- name: Build with Maven
|
||||
run: mvn -B package
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: envoy-${{ matrix.os }}
|
||||
path: |
|
||||
server/target/envoy-server-jar-with-dependencies.jar
|
||||
client/target/envoy-client*shaded.jar
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1 +1,8 @@
|
||||
# build folders
|
||||
target/
|
||||
|
||||
# Eclipse settings
|
||||
/.settings
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*.log
|
||||
|
@ -1,2 +0,0 @@
|
||||
eclipse.preferences.version=1
|
||||
encoding/<project>=UTF-8
|
69
README.md
Normal file
69
README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Envoy
|
||||
|
||||
<img src="https://git.kske.dev/repo-avatars/33-31e14133097b01b748ab361e3c3adb47" style="display: block" width="150" height="150">
|
||||
|
||||
Envoy is a messenger written in Java.
|
||||
On this page, the project is explained for different user groups.
|
||||
|
||||
## Regular User
|
||||
|
||||
To use Envoy to join an existing server, download the client from the [release page](https://git.kske.dev/zdm/envoy/releases).
|
||||
|
||||
When starting it for the first time, you can register yourself at a server of your choice.
|
||||
After connecting to the server, you can add other users to your contact list and send them messages.
|
||||
|
||||
To chat with multiple users at once, you can create a group.
|
||||
If you want to transfer a file to another user, you can attach it to a message.
|
||||
|
||||
On the settings page some convenience features can be configured, as well as the color theme.
|
||||
|
||||
### System requirements
|
||||
|
||||
To run Envoy, you have to install a Java Runtime Environment (JRE) of at least version 11.
|
||||
You can download an open source implementation from [here](https://jdk.java.net/15/).
|
||||
|
||||
If you are running a Linux distribution, make sure that an emoji font like [Noto emoji](https://github.com/googlefonts/noto-emoji) is installed.
|
||||
Most major Linux distributions like Debian, Arch and Gentoo have a Noto emoji package available inside their package repositories.
|
||||
|
||||
## Server Administrator
|
||||
|
||||
To set up an Envoy server, download the package from the release page.
|
||||
|
||||
Because the project lacks external documentation for the moment, please refer to the Javadoc inside the source code to configure your Envoy instance.
|
||||
|
||||
### System requirements
|
||||
|
||||
To run Envoy server, you have to install a JRE as mentioned above, as well as a database.
|
||||
In development, PostgreSQL is used, which you can download from [here](https://www.postgresql.org/download/).
|
||||
|
||||
Look at the file `META-INF/persistence.xml` inside `envoy-server.jar` for the database configuration.
|
||||
|
||||
After creating a database and configuring the credentials, the server will initialize the necessary tables automatically.
|
||||
|
||||
## Programmer
|
||||
|
||||
Envoy is organized as a Maven project that is split into three modules.
|
||||
|
||||
### Client
|
||||
|
||||
* Sending and receiving of messages, groups, sending images and voice messages
|
||||
* User interface (UI)
|
||||
* Client configuration
|
||||
* Advanced logging possibilities
|
||||
* Tons of Events to interact with
|
||||
* Detailed Javadoc to improve readability of code
|
||||
|
||||
### Common
|
||||
* Basic datatypes
|
||||
* Events sent between client and server
|
||||
* Configuration API
|
||||
* Logging API based on `java.util.logging`
|
||||
* Envoy-specific Exception
|
||||
* Useful utility classes
|
||||
|
||||
### Server
|
||||
* Non-blocking connectivity infrastructure based on `java.nio`
|
||||
* Processors to handle incoming events
|
||||
* Database connectivity
|
||||
* Database entities
|
||||
* Utility classes to check client version compatability and password validity
|
@ -13,14 +13,15 @@
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="module" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
|
4
client/.gitignore
vendored
4
client/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
/target/
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<projectDescription>
|
||||
<name>envoy-client</name>
|
||||
<name>client</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
|
@ -18,6 +18,7 @@ org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||
org.eclipse.jdt.core.compiler.doc.comment.support=enabled
|
||||
org.eclipse.jdt.core.compiler.problem.APILeak=warning
|
||||
org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info
|
||||
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
|
||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
|
||||
@ -128,364 +129,4 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
|
||||
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
|
||||
org.eclipse.jdt.core.compiler.release=disabled
|
||||
org.eclipse.jdt.core.compiler.source=11
|
||||
org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=true
|
||||
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=1
|
||||
org.eclipse.jdt.core.formatter.align_type_members_on_columns=true
|
||||
org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=true
|
||||
org.eclipse.jdt.core.formatter.align_with_spaces=false
|
||||
org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=84
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=80
|
||||
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=20
|
||||
org.eclipse.jdt.core.formatter.alignment_for_assignment=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
|
||||
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_module_statements=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
|
||||
org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=84
|
||||
org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
|
||||
org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0
|
||||
org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
|
||||
org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0
|
||||
org.eclipse.jdt.core.formatter.blank_lines_after_package=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_field=0
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_method=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_before_package=0
|
||||
org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
|
||||
org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0
|
||||
org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
|
||||
org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
|
||||
org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=true
|
||||
org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false
|
||||
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=true
|
||||
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
|
||||
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_block_comments=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_header=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_html=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_line_comments=true
|
||||
org.eclipse.jdt.core.formatter.comment.format_source_code=true
|
||||
org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
|
||||
org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
|
||||
org.eclipse.jdt.core.formatter.comment.indent_tag_description=false
|
||||
org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
|
||||
org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert
|
||||
org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
|
||||
org.eclipse.jdt.core.formatter.comment.line_length=80
|
||||
org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
|
||||
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
|
||||
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
|
||||
org.eclipse.jdt.core.formatter.compact_else_if=true
|
||||
org.eclipse.jdt.core.formatter.continuation_indentation=2
|
||||
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
|
||||
org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
|
||||
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
|
||||
org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true
|
||||
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
|
||||
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
|
||||
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
|
||||
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
|
||||
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
|
||||
org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
|
||||
org.eclipse.jdt.core.formatter.indent_empty_lines=false
|
||||
org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
|
||||
org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
|
||||
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
|
||||
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
|
||||
org.eclipse.jdt.core.formatter.indentation.size=4
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
|
||||
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
|
||||
org.eclipse.jdt.core.formatter.join_lines_in_comments=false
|
||||
org.eclipse.jdt.core.formatter.join_wrapped_lines=true
|
||||
org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_if_single_item
|
||||
org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never
|
||||
org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
|
||||
org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item
|
||||
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
|
||||
org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_if_single_item
|
||||
org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
|
||||
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true
|
||||
org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty
|
||||
org.eclipse.jdt.core.formatter.lineSplit=150
|
||||
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
|
||||
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0
|
||||
org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0
|
||||
org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=separate_lines_if_wrapped
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
|
||||
org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines
|
||||
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
|
||||
org.eclipse.jdt.core.formatter.tabulation.char=tab
|
||||
org.eclipse.jdt.core.formatter.tabulation.size=4
|
||||
org.eclipse.jdt.core.formatter.text_block_indentation=0
|
||||
org.eclipse.jdt.core.formatter.use_on_off_tags=false
|
||||
org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
|
||||
org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false
|
||||
org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true
|
||||
org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true
|
||||
org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
|
||||
org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
||||
default.configuration=
|
||||
eclipse.preferences.version=1
|
||||
hibernate3.enabled=false
|
@ -9,24 +9,24 @@
|
||||
<parent>
|
||||
<groupId>informatik-ag-ngl</groupId>
|
||||
<artifactId>envoy</artifactId>
|
||||
<version>0.1-beta</version>
|
||||
<version>0.2-beta</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>informatik-ag-ngl</groupId>
|
||||
<artifactId>envoy-common</artifactId>
|
||||
<version>0.1-beta</version>
|
||||
<version>0.2-beta</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>11.0.2</version>
|
||||
<version>15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>11.0.2</version>
|
||||
<version>15</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@ -37,15 +37,6 @@
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -7,24 +7,33 @@ import envoy.client.ui.Startup;
|
||||
/**
|
||||
* Triggers application startup.
|
||||
* <p>
|
||||
* To allow Maven shading, the main method has to be separated from the
|
||||
* {@link Startup} class which extends {@link Application}.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Main.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
* To allow Maven shading, the main method has to be separated from the {@link Startup} class which
|
||||
* extends {@link Application}.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class Main {
|
||||
public final class Main {
|
||||
|
||||
/**
|
||||
* A funny debug switch put in by {@code delvh} to enable easy debugging.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private static final boolean debug = false;
|
||||
|
||||
/**
|
||||
* Starts the application.
|
||||
*
|
||||
* @param args the command line arguments are processed by the
|
||||
* client configuration
|
||||
* @param args the command line arguments are processed by the client configuration
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static void main(String[] args) { Application.launch(Startup.class, args); }
|
||||
public static void main(String[] args) {
|
||||
if (debug) {
|
||||
// Put testing code here
|
||||
System.out.println();
|
||||
System.exit(0);
|
||||
}
|
||||
Application.launch(Startup.class, args);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,14 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.*;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Stores elements in a queue to process them later.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Cache.java</strong><br>
|
||||
* Created: <strong>6 Feb 2020</strong><br>
|
||||
*
|
||||
* @param <T> the type of cached elements
|
||||
* @author Kai S. K. Engelbart
|
||||
@ -41,7 +35,9 @@ public final class Cache<T> implements Consumer<T>, Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("Cache[elements=" + elements + "]"); }
|
||||
public String toString() {
|
||||
return String.format("Cache[elements=" + elements + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the processor to which cached elements are relayed.
|
||||
@ -58,8 +54,18 @@ public final class Cache<T> implements Consumer<T>, Serializable {
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void relay() {
|
||||
if (processor == null) throw new IllegalStateException("Processor is not defined");
|
||||
if (processor == null)
|
||||
throw new IllegalStateException("Processor is not defined");
|
||||
elements.forEach(processor::accept);
|
||||
elements.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears this cache of all stored elements.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void clear() {
|
||||
elements.clear();
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,11 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Stores a heterogeneous map of {@link Cache} objects with different type
|
||||
* parameters.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>CacheMap.java</strong><br>
|
||||
* Created: <strong>09.07.2020</strong><br>
|
||||
*
|
||||
* Stores a heterogeneous map of {@link Cache} objects with different type parameters.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -23,27 +17,31 @@ public final class CacheMap implements Serializable {
|
||||
|
||||
/**
|
||||
* Adds a cache to the map.
|
||||
*
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @param cache the cache to store
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
|
||||
public <T> void put(Class<T> key, Cache<T> cache) {
|
||||
map.put(key, cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cache mapped by a class.
|
||||
*
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @return the cache
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); }
|
||||
public <T> Cache<T> get(Class<T> key) {
|
||||
return (Cache<T>) map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cache mapped by a class or any of its subclasses.
|
||||
*
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @return the cache
|
||||
@ -52,7 +50,7 @@ public final class CacheMap implements Serializable {
|
||||
public <T> Cache<? super T> getApplicable(Class<T> key) {
|
||||
Cache<? super T> cache = get(key);
|
||||
if (cache == null)
|
||||
for (var e : map.entrySet())
|
||||
for (final var e : map.entrySet())
|
||||
if (e.getKey().isAssignableFrom(key))
|
||||
cache = (Cache<? super T>) e.getValue();
|
||||
return cache;
|
||||
@ -63,4 +61,13 @@ public final class CacheMap implements Serializable {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Map<Class<?>, Cache<?>> getMap() { return map; }
|
||||
|
||||
/**
|
||||
* Clears the caches of this map of any values.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void clear() {
|
||||
map.values().forEach(Cache::clear);
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,19 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.*;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.MessageStatusChange;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
|
||||
/**
|
||||
* Represents a chat between two {@link User}s
|
||||
* as a list of {@link Message} objects.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Chat.java</strong><br>
|
||||
* Created: <strong>19 Oct 2019</strong><br>
|
||||
* Represents a chat between two {@link User}s as a list of {@link Message} objects.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @author Leon Hofmeister
|
||||
@ -26,12 +22,21 @@ import envoy.event.MessageStatusChange;
|
||||
*/
|
||||
public class Chat implements Serializable {
|
||||
|
||||
protected final Contact recipient;
|
||||
protected final List<Message> messages = new ArrayList<>();
|
||||
protected boolean disabled;
|
||||
|
||||
protected int unreadAmount;
|
||||
/**
|
||||
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
||||
*/
|
||||
protected transient long lastWritingEvent;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
|
||||
|
||||
protected int unreadAmount;
|
||||
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
|
||||
|
||||
protected final Contact recipient;
|
||||
|
||||
private static final long serialVersionUID = 2L;
|
||||
|
||||
/**
|
||||
* Provides the list of messages that the recipient receives.
|
||||
@ -42,66 +47,91 @@ public class Chat implements Serializable {
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public Chat(Contact recipient) {
|
||||
this.recipient = recipient;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
|
||||
stream.defaultReadObject();
|
||||
messages = FXCollections.observableList((List<Message>) stream.readObject());
|
||||
totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount);
|
||||
}
|
||||
|
||||
private void writeObject(ObjectOutputStream stream) throws IOException {
|
||||
stream.defaultWriteObject();
|
||||
stream.writeObject(new ArrayList<>(messages));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s[recipient=%s,messages=%d,disabled=%b]",
|
||||
getClass().getSimpleName(),
|
||||
recipient,
|
||||
messages.size(),
|
||||
disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a hash code based on the recipient.
|
||||
*
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() { return Objects.hash(recipient); }
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests equality to another object based on the recipient.
|
||||
*
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Chat)) return false;
|
||||
Chat other = (Chat) obj;
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof Chat))
|
||||
return false;
|
||||
final var other = (Chat) obj;
|
||||
return Objects.equals(recipient, other.recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of all chat messages received from the recipient to
|
||||
* {@code READ} starting from the bottom and stopping once a read message is
|
||||
* found.
|
||||
* Sets the status of all chat messages received from the recipient to {@code READ} starting
|
||||
* from the bottom and stopping once a read message is found.
|
||||
*
|
||||
* @param writeProxy the write proxy instance used to notify the server about
|
||||
* the message status changes
|
||||
* @throws IOException if a {@link MessageStatusChange} could not be
|
||||
* delivered to the server
|
||||
* @param writeProxy the write proxy instance used to notify the server about the message status
|
||||
* changes
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void read(WriteProxy writeProxy) throws IOException {
|
||||
public void read(WriteProxy writeProxy) {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
final Message m = messages.get(i);
|
||||
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
||||
else {
|
||||
m.setStatus(MessageStatus.READ);
|
||||
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
||||
}
|
||||
final var m = messages.get(i);
|
||||
if (m.getSenderID() == recipient.getID())
|
||||
if (m.getStatus() == MessageStatus.READ)
|
||||
break;
|
||||
else {
|
||||
m.setStatus(MessageStatus.READ);
|
||||
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
||||
}
|
||||
}
|
||||
totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount);
|
||||
unreadAmount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the newest message received in the chat doesn't have
|
||||
* the status {@code READ}
|
||||
* @return {@code true} if the newest message received in the chat doesn't have the status
|
||||
* {@code READ}
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
|
||||
public boolean isUnread() {
|
||||
return !messages.isEmpty()
|
||||
&& messages.get(messages.size() - 1).getStatus() != MessageStatus.READ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a message at the correct place according to its creation date.
|
||||
*
|
||||
*
|
||||
* @param message the message to insert
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@ -115,14 +145,34 @@ public class Chat implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the amount of unread messages.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
* Removes the message with the given ID.
|
||||
*
|
||||
* @param messageID the ID of the message to remove
|
||||
* @return whether the message has been found and removed
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void incrementUnreadAmount() { unreadAmount++; }
|
||||
public boolean remove(long messageID) {
|
||||
return messages.removeIf(m -> m.getID() == messageID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the amount of unread mesages in this chat
|
||||
* @return an integer property storing the total amount of unread messages
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public static IntegerProperty getTotalUnreadAmount() { return totalUnreadAmount; }
|
||||
|
||||
/**
|
||||
* Increments the amount of unread messages.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void incrementUnreadAmount() {
|
||||
++unreadAmount;
|
||||
totalUnreadAmount.set(totalUnreadAmount.get() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the amount of unread messages in this chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public int getUnreadAmount() { return unreadAmount; }
|
||||
@ -131,7 +181,7 @@ public class Chat implements Serializable {
|
||||
* @return all messages in the current chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public List<Message> getMessages() { return messages; }
|
||||
public ObservableList<Message> getMessages() { return messages; }
|
||||
|
||||
/**
|
||||
* @return the recipient of a message
|
||||
@ -140,14 +190,35 @@ public class Chat implements Serializable {
|
||||
public Contact getRecipient() { return recipient; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link User}
|
||||
* @since Envoy Client v0.1-beta
|
||||
* @return the last known time a {@link envoy.event.IsTyping} event has been sent
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isUserChat() { return recipient instanceof User; }
|
||||
public long getLastWritingEvent() { return lastWritingEvent; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link Group}
|
||||
* @since Envoy Client v0.1-beta
|
||||
* Sets the {@code lastWritingEvent} to {@code System#currentTimeMillis()}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isGroupChat() { return recipient instanceof Group; }
|
||||
public void lastWritingEventWasNow() {
|
||||
lastWritingEvent = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether messages can be sent in this chat. Should be {@code true} i.e. for chats
|
||||
* whose recipient deleted this client as a contact.
|
||||
*
|
||||
* @return whether this chat has been disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public boolean isDisabled() { return disabled; }
|
||||
|
||||
/**
|
||||
* Determines whether messages can be sent in this chat. Should be true i.e. for chats whose
|
||||
* recipient deleted this client as a contact.
|
||||
*
|
||||
* @param disabled whether this chat should be disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void setDisabled(boolean disabled) { this.disabled = disabled; }
|
||||
}
|
||||
|
@ -2,26 +2,16 @@ package envoy.client.data;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.data.Config;
|
||||
import envoy.data.ConfigItem;
|
||||
import envoy.data.LoginCredentials;
|
||||
|
||||
/**
|
||||
* Implements a configuration specific to the Envoy Client with default values
|
||||
* and convenience methods.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ClientConfig.java</strong><br>
|
||||
* Created: <strong>01.03.2020</strong><br>
|
||||
* Implements a configuration specific to the Envoy Client with default values and convenience
|
||||
* methods.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ClientConfig extends Config {
|
||||
public final class ClientConfig extends Config {
|
||||
|
||||
private static ClientConfig config;
|
||||
|
||||
@ -30,20 +20,16 @@ public class ClientConfig extends Config {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static ClientConfig getInstance() {
|
||||
if (config == null) config = new ClientConfig();
|
||||
if (config == null)
|
||||
config = new ClientConfig();
|
||||
return config;
|
||||
}
|
||||
|
||||
private ClientConfig() {
|
||||
items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
|
||||
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
|
||||
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
|
||||
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
|
||||
items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
|
||||
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
|
||||
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
|
||||
items.put("user", new ConfigItem<>("user", "u", identity()));
|
||||
items.put("password", new ConfigItem<>("password", "pw", identity()));
|
||||
super(".envoy");
|
||||
put("server", "s", identity());
|
||||
put("port", "p", Integer::parseInt);
|
||||
put("localDBSaveInterval", "db-si", Integer::parseInt);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,57 +45,10 @@ public class ClientConfig extends Config {
|
||||
public Integer getPort() { return (Integer) items.get("port").get(); }
|
||||
|
||||
/**
|
||||
* @return the local database specific to the client user
|
||||
* @since Envoy Client v0.1-alpha
|
||||
* @return the amount of minutes after which the local database should be saved
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public File getLocalDB() { return (File) items.get("localDB").get(); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the local database is to be ignored
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
|
||||
|
||||
/**
|
||||
* @return the directory in which all local files are saves
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
|
||||
|
||||
/**
|
||||
* @return the minimal {@link Level} to log inside the log file
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
|
||||
|
||||
/**
|
||||
* @return the minimal {@link Level} to log inside the console
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
|
||||
|
||||
/**
|
||||
* @return the user name
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getUser() { return (String) items.get("user").get(); }
|
||||
|
||||
/**
|
||||
* @return the password
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getPassword() { return (String) items.get("password").get(); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if user name and password are set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
|
||||
|
||||
/**
|
||||
* @return login credentials for the specified user name and password, without
|
||||
* the registration option
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
|
||||
public Integer getLocalDBSaveInterval() {
|
||||
return (Integer) items.get("localDBSaveInterval").get();
|
||||
}
|
||||
}
|
||||
|
94
client/src/main/java/envoy/client/data/Context.java
Normal file
94
client/src/main/java/envoy/client/data/Context.java
Normal file
@ -0,0 +1,94 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import envoy.client.net.*;
|
||||
import envoy.client.ui.SceneContext;
|
||||
|
||||
/**
|
||||
* Provides access to commonly used objects.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class Context {
|
||||
|
||||
private WriteProxy writeProxy;
|
||||
private LocalDB localDB;
|
||||
private Stage stage;
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private final Client client = new Client();
|
||||
|
||||
private static final Context instance = new Context();
|
||||
|
||||
private Context() {}
|
||||
|
||||
/**
|
||||
* @return the instance of {@code Context} used throughout Envoy
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static Context getInstance() { return instance; }
|
||||
|
||||
/**
|
||||
* Initializes the write proxy given that {@code localDB} is initialized.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void initWriteProxy() {
|
||||
if (localDB == null)
|
||||
throw new IllegalStateException("The LocalDB has to be initialized!");
|
||||
writeProxy = new WriteProxy(client, localDB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the localDB
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public LocalDB getLocalDB() { return localDB; }
|
||||
|
||||
/**
|
||||
* @param localDB the localDB to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setLocalDB(LocalDB localDB) { this.localDB = localDB; }
|
||||
|
||||
/**
|
||||
* @return the sceneContext
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SceneContext getSceneContext() { return sceneContext; }
|
||||
|
||||
/**
|
||||
* @param sceneContext the sceneContext to set. Additionally sets the stage.
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setSceneContext(SceneContext sceneContext) {
|
||||
this.sceneContext = sceneContext;
|
||||
stage = sceneContext.getStage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the client
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Client getClient() { return client; }
|
||||
|
||||
/**
|
||||
* @return the writeProxy
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public WriteProxy getWriteProxy() { return writeProxy; }
|
||||
|
||||
/**
|
||||
* @return the stage
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Stage getStage() { return stage; }
|
||||
|
||||
/**
|
||||
* @param stage the stage to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setStage(Stage stage) { this.stage = stage; }
|
||||
}
|
@ -1,27 +1,20 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Instant;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.data.Contact;
|
||||
import envoy.data.GroupMessage;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.data.User;
|
||||
import envoy.event.GroupMessageStatusChange;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
|
||||
/**
|
||||
* Represents a chat between a user and a group
|
||||
* as a list of messages.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>GroupChat.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* Represents a chat between a user and a group as a list of messages.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupChat extends Chat {
|
||||
public final class GroupChat extends Chat {
|
||||
|
||||
private final User sender;
|
||||
|
||||
@ -32,23 +25,23 @@ public class GroupChat extends Chat {
|
||||
* @param recipient the group whose members receive the messages
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public GroupChat(User sender, Contact recipient) {
|
||||
public GroupChat(User sender, Group recipient) {
|
||||
super(recipient);
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(WriteProxy writeProxy) throws IOException {
|
||||
public void read(WriteProxy writeProxy) {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
final GroupMessage gmsg = (GroupMessage) messages.get(i);
|
||||
if (gmsg.getSenderID() != sender.getID()) {
|
||||
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
|
||||
if (gmsg.getSenderID() != sender.getID())
|
||||
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ)
|
||||
break;
|
||||
else {
|
||||
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
|
||||
writeProxy
|
||||
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
|
||||
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(),
|
||||
MessageStatus.READ, Instant.now(), sender.getID()));
|
||||
}
|
||||
}
|
||||
}
|
||||
unreadAmount = 0;
|
||||
}
|
||||
|
@ -1,116 +1,443 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.logging.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.*;
|
||||
|
||||
import dev.kske.eventbus.Event;
|
||||
import dev.kske.eventbus.EventBus;
|
||||
import dev.kske.eventbus.EventListener;
|
||||
|
||||
import envoy.data.*;
|
||||
import envoy.event.GroupResize;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.event.NameChange;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.*;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.*;
|
||||
|
||||
import envoy.client.event.*;
|
||||
|
||||
/**
|
||||
* Stores information about the current {@link User} and their {@link Chat}s.
|
||||
* For message ID generation a {@link IDGenerator} is stored as well.
|
||||
* Stores information about the current {@link User} and their {@link Chat}s. For message ID
|
||||
* generation a {@link IDGenerator} is stored as well.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>LocalDB.java</strong><br>
|
||||
* Created: <strong>3 Feb 2020</strong><br>
|
||||
* The managed objects are stored inside a folder in the local file system.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public abstract class LocalDB {
|
||||
public final class LocalDB implements EventListener {
|
||||
|
||||
protected User user;
|
||||
protected Map<String, Contact> users = new HashMap<>();
|
||||
protected List<Chat> chats = new ArrayList<>();
|
||||
protected IDGenerator idGenerator;
|
||||
protected CacheMap cacheMap = new CacheMap();
|
||||
// Data
|
||||
private User user;
|
||||
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||
private ObservableList<Chat> chats = FXCollections.observableArrayList();
|
||||
private IDGenerator idGenerator;
|
||||
private CacheMap cacheMap = new CacheMap();
|
||||
private String authToken;
|
||||
private boolean contactsChanged;
|
||||
|
||||
{
|
||||
// Auto save timer
|
||||
private Timer autoSaver;
|
||||
private boolean autoSaveRestart = true;
|
||||
|
||||
// State management
|
||||
private Instant lastSync = Instant.EPOCH;
|
||||
|
||||
// Persistence
|
||||
private File userFile;
|
||||
private FileLock instanceLock;
|
||||
|
||||
private final File dbDir, idGeneratorFile, lastLoginFile, usersFile;
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(LocalDB.class);
|
||||
|
||||
/**
|
||||
* Constructs an empty local database.
|
||||
*
|
||||
* @param dbDir the directory in which to persist data
|
||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public LocalDB(File dbDir) throws IOException, EnvoyException {
|
||||
this.dbDir = dbDir;
|
||||
EventBus.getInstance().registerListener(this);
|
||||
|
||||
// Ensure that the database directory exists
|
||||
if (!dbDir.exists())
|
||||
dbDir.mkdirs();
|
||||
else if (!dbDir.isDirectory())
|
||||
throw new IOException(
|
||||
String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||
|
||||
// Lock the directory
|
||||
lock();
|
||||
|
||||
// Initialize global files
|
||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||
lastLoginFile = new File(dbDir, "last_login.db");
|
||||
usersFile = new File(dbDir, "users.db");
|
||||
|
||||
// Load global files
|
||||
loadGlobalData();
|
||||
|
||||
// Initialize offline caches
|
||||
cacheMap.put(Message.class, new Cache<>());
|
||||
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a storage space for a user-specific list of chats.
|
||||
* Ensured that only one Envoy instance is using this local database by creating a lock file.
|
||||
* The lock file is deleted on application exit.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
* @throws EnvoyException if the lock cannot by acquired
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void initializeUserStorage() {}
|
||||
private synchronized void lock() throws EnvoyException {
|
||||
final var file = new File(dbDir, "instance.lock");
|
||||
try {
|
||||
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.WRITE);
|
||||
instanceLock = fc.tryLock();
|
||||
if (instanceLock == null)
|
||||
throw new EnvoyException("Another Envoy instance is using this local database!");
|
||||
} catch (final IOException e) {
|
||||
throw new EnvoyException("Could not create lock file!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all users. If the client user is specified, their chats will be stored
|
||||
* as well. The message id generator will also be saved if present.
|
||||
* Loads the local user registry {@code users.db}, the id generator {@code id_gen.db} and last
|
||||
* login file {@code last_login.db}.
|
||||
*
|
||||
* @throws Exception if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void save() throws Exception {}
|
||||
|
||||
/**
|
||||
* Loads all user data.
|
||||
*
|
||||
* @throws Exception if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUsers() throws Exception {}
|
||||
private synchronized void loadGlobalData() {
|
||||
try {
|
||||
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
|
||||
users = (Map<String, User>) in.readObject();
|
||||
}
|
||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||
user = (User) in.readObject();
|
||||
authToken = (String) in.readObject();
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all data of the client user.
|
||||
*
|
||||
* @throws Exception if the loading process failed
|
||||
* @throws ClassNotFoundException if the loading process failed
|
||||
* @throws IOException if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUserData() throws Exception {}
|
||||
public synchronized void loadUserData() throws ClassNotFoundException, IOException {
|
||||
if (user == null)
|
||||
throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||
Chat.getTotalUnreadAmount().set(0);
|
||||
chats = FXCollections.observableList((List<Chat>) in.readObject());
|
||||
|
||||
// Some chats have changed and should not be overwritten by the saved values
|
||||
if (contactsChanged) {
|
||||
final var contacts = user.getContacts();
|
||||
|
||||
// Mark chats as disabled if a contact is no longer in this users contact list
|
||||
final var changedUserChats = chats.stream()
|
||||
.filter(not(chat -> contacts.contains(chat.getRecipient())))
|
||||
.peek(chat -> {
|
||||
chat.setDisabled(true);
|
||||
logger.log(Level.INFO,
|
||||
String.format("Deleted chat with %s.", chat.getRecipient()));
|
||||
});
|
||||
|
||||
// Also update groups with a different member count
|
||||
final var changedGroupChats =
|
||||
contacts.stream().filter(Group.class::isInstance).flatMap(group -> {
|
||||
final var potentialChat = getChat(group.getID());
|
||||
if (potentialChat.isEmpty())
|
||||
return Stream.empty();
|
||||
final var chat = potentialChat.get();
|
||||
if (group.getContacts().size() != chat.getRecipient().getContacts()
|
||||
.size()) {
|
||||
logger.log(Level.INFO, "Removed one (or more) members from " + group);
|
||||
return Stream.of(chat);
|
||||
} else
|
||||
return Stream.empty();
|
||||
});
|
||||
Stream.concat(changedUserChats, changedGroupChats)
|
||||
.forEach(chat -> chats.set(chats.indexOf(chat), chat));
|
||||
|
||||
// loadUserData can get called two (or more?) times during application lifecycle
|
||||
contactsChanged = false;
|
||||
}
|
||||
cacheMap = (CacheMap) in.readObject();
|
||||
lastSync = (Instant) in.readObject();
|
||||
} finally {
|
||||
synchronize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the ID generator. Any exception thrown during this process is ignored.
|
||||
* Synchronizes the contact list of the client user with the chat and user storage.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadIDGenerator() {}
|
||||
|
||||
/**
|
||||
* Synchronizes the contact list of the client user with the chat and user
|
||||
* storage.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void synchronize() {
|
||||
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
|
||||
private void synchronize() {
|
||||
user.getContacts().stream()
|
||||
.filter(u -> u instanceof User && !users.containsKey(u.getName()))
|
||||
.forEach(u -> users.put(u.getName(), (User) u));
|
||||
users.put(user.getName(), user);
|
||||
|
||||
// Synchronize user status data
|
||||
for (Contact contact : users.values())
|
||||
for (final var contact : user.getContacts())
|
||||
if (contact instanceof User)
|
||||
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
|
||||
getChat(contact.getID()).ifPresent(chat -> {
|
||||
((User) chat.getRecipient()).setStatus(((User) contact).getStatus());
|
||||
});
|
||||
|
||||
// Create missing chats
|
||||
user.getContacts()
|
||||
.stream()
|
||||
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
|
||||
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
|
||||
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, (Group) c))
|
||||
.forEach(chats::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||
* user names as keys
|
||||
* Initializes a timer that automatically saves this local database after a period of time
|
||||
* specified in the settings.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void initAutoSave() {
|
||||
|
||||
// A logout happened so the timer should be restarted
|
||||
if (autoSaveRestart) {
|
||||
autoSaver = new Timer("LocalDB Autosave", true);
|
||||
autoSaveRestart = false;
|
||||
}
|
||||
|
||||
autoSaver.schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
save();
|
||||
}
|
||||
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all users. If the client user is specified, their chats will be stored as well. The
|
||||
* message id generator will also be saved if present.
|
||||
*
|
||||
* @throws IOException if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 500)
|
||||
private synchronized void save() {
|
||||
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
|
||||
|
||||
// Save users
|
||||
try {
|
||||
SerializationUtils.write(usersFile, users);
|
||||
|
||||
// Save user data and last sync time stamp
|
||||
if (user != null)
|
||||
SerializationUtils
|
||||
.write(userFile, new ArrayList<>(chats), cacheMap,
|
||||
Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
|
||||
|
||||
// Save last login information
|
||||
if (authToken != null)
|
||||
SerializationUtils.write(lastLoginFile, user, authToken);
|
||||
|
||||
// Save ID generator
|
||||
if (hasIDGenerator())
|
||||
SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||
} catch (final IOException e) {
|
||||
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onMessage(Message msg) {
|
||||
if (msg.getStatus() == MessageStatus.SENT)
|
||||
msg.nextStatus();
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onGroupMessage(GroupMessage msg) {
|
||||
// TODO: Cancel event once EventBus is updated
|
||||
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
|
||||
logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onMessageStatusChange(MessageStatusChange evt) {
|
||||
getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get()));
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
|
||||
this.<GroupMessage>getMessage(evt.getID())
|
||||
.ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onUserStatusChange(UserStatusChange evt) {
|
||||
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast)
|
||||
.ifPresent(u -> u.setStatus(evt.get()));
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
final var eventUser = operation.get();
|
||||
switch (operation.getOperationType()) {
|
||||
case ADD:
|
||||
Platform.runLater(() -> chats.add(0, new Chat(eventUser)));
|
||||
break;
|
||||
case REMOVE:
|
||||
getChat(eventUser.getID()).ifPresent(chat -> chat.setDisabled(true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onGroupCreationResult(GroupCreationResult evt) {
|
||||
final var newGroup = evt.get();
|
||||
|
||||
// The group creation was not successful
|
||||
if (newGroup == null)
|
||||
return;
|
||||
|
||||
// The group was successfully created
|
||||
else
|
||||
Platform.runLater(() -> chats.add(new GroupChat(user, newGroup)));
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onGroupResize(GroupResize evt) {
|
||||
getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast)
|
||||
.ifPresent(evt::apply);
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onNameChange(NameChange evt) {
|
||||
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny()
|
||||
.ifPresent(c -> c.setName(evt.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new authentication token.
|
||||
*
|
||||
* @param evt the event containing the authentication token
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
@Event
|
||||
private void onNewAuthToken(NewAuthToken evt) {
|
||||
authToken = evt.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all associations to the current user.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
@Event(eventType = Logout.class, priority = 50)
|
||||
private void onLogout() {
|
||||
autoSaver.cancel();
|
||||
autoSaveRestart = true;
|
||||
lastLoginFile.delete();
|
||||
userFile = null;
|
||||
user = null;
|
||||
authToken = null;
|
||||
chats.clear();
|
||||
lastSync = Instant.EPOCH;
|
||||
cacheMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the message with the given ID, if present.
|
||||
*
|
||||
* @param message the event that was
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
@Event
|
||||
private void onMessageDeletion(MessageDeletion message) {
|
||||
Platform.runLater(() -> {
|
||||
|
||||
// We suppose that messages have unique IDs, hence the search can be stopped
|
||||
// once a message was removed
|
||||
final var messageID = message.get();
|
||||
for (final var chat : chats)
|
||||
if (chat.remove(messageID))
|
||||
break;
|
||||
});
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onOwnStatusChange(OwnStatusChange statusChange) {
|
||||
user.setStatus(statusChange.get());
|
||||
}
|
||||
|
||||
@Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500)
|
||||
private void onContactsChangedSinceLastLogin() {
|
||||
contactsChanged = true;
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onContactDisabled(ContactDisabled event) {
|
||||
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@code Map<String, User>} of all users stored locally with their user names as keys
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Map<String, Contact> getUsers() { return users; }
|
||||
public Map<String, User> getUsers() { return users; }
|
||||
|
||||
/**
|
||||
* @return all saved {@link Chat} objects that list the client user as the
|
||||
* sender
|
||||
* Searches for a message by ID.
|
||||
*
|
||||
* @param id the ID of the message to search for
|
||||
* @return an optional containing the message
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public <T extends Message> Optional<T> getMessage(long id) {
|
||||
return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream)
|
||||
.filter(m -> m.getID() == id).findAny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a chat by recipient ID.
|
||||
*
|
||||
* @param recipientID the ID of the chat's recipient
|
||||
* @return an optional containing the chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Optional<Chat> getChat(long recipientID) {
|
||||
return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all saved {@link Chat} objects that list the client user as the sender
|
||||
* @since Envoy Client v0.1-alpha
|
||||
**/
|
||||
public List<Chat> getChats() { return chats; }
|
||||
|
||||
/**
|
||||
* @param chats the chats to set
|
||||
*/
|
||||
public void setChats(List<Chat> chats) { this.chats = chats; }
|
||||
public ObservableList<Chat> getChats() { return chats; }
|
||||
|
||||
/**
|
||||
* @return the {@link User} who initialized the local database
|
||||
@ -134,13 +461,16 @@ public abstract class LocalDB {
|
||||
* @param idGenerator the message ID generator to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Event(priority = 150)
|
||||
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
|
||||
|
||||
/**
|
||||
* @return {@code true} if an {@link IDGenerator} is present
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public boolean hasIDGenerator() { return idGenerator != null; }
|
||||
public boolean hasIDGenerator() {
|
||||
return idGenerator != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the cache map for messages and message status changes
|
||||
@ -149,57 +479,14 @@ public abstract class LocalDB {
|
||||
public CacheMap getCacheMap() { return cacheMap; }
|
||||
|
||||
/**
|
||||
* Searches for a message by ID.
|
||||
*
|
||||
* @param id the ID of the message to search for
|
||||
* @return an optional containing the message
|
||||
* @since Envoy Client v0.1-beta
|
||||
* @return the time stamp when the database was last saved
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Optional<Message> getMessage(long id) {
|
||||
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
|
||||
}
|
||||
public Instant getLastSync() { return lastSync; }
|
||||
|
||||
/**
|
||||
* Searches for a chat by recipient ID.
|
||||
*
|
||||
* @param recipientID the ID of the chat's recipient
|
||||
* @return an optional containing the chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
* @return the authentication token of the user
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
|
||||
|
||||
/**
|
||||
* Performs a contact name change if the corresponding contact is present.
|
||||
*
|
||||
* @param event the {@link NameChange} to process
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void replaceContactName(NameChange event) {
|
||||
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a group resize operation if the corresponding group is present.
|
||||
*
|
||||
* @param event the {@link GroupResize} to process
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void updateGroup(GroupResize event) {
|
||||
chats.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(Group.class::isInstance)
|
||||
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
|
||||
.map(Group.class::cast)
|
||||
.findAny()
|
||||
.ifPresent(group -> {
|
||||
switch (event.getOperation()) {
|
||||
case ADD:
|
||||
group.getContacts().add(event.get());
|
||||
break;
|
||||
case REMOVE:
|
||||
group.getContacts().remove(event.get());
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
public String getAuthToken() { return authToken; }
|
||||
}
|
||||
|
@ -1,88 +0,0 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import envoy.data.IDGenerator;
|
||||
import envoy.util.SerializationUtils;
|
||||
|
||||
/**
|
||||
* Implements a {@link LocalDB} in a way that stores all information inside a
|
||||
* folder on the local file system.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>PersistentLocalDB.java</strong><br>
|
||||
* Created: <strong>27.10.2019</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public final class PersistentLocalDB extends LocalDB {
|
||||
|
||||
private File dbDir, userFile, idGeneratorFile, usersFile;
|
||||
|
||||
/**
|
||||
* Constructs an empty local database. To serialize any user-specific data to
|
||||
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
|
||||
* and then {@link PersistentLocalDB#save()}.
|
||||
*
|
||||
* @param dbDir the directory in which to persist data
|
||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public PersistentLocalDB(File dbDir) throws IOException {
|
||||
this.dbDir = dbDir;
|
||||
|
||||
// Test if the database directory is actually a directory
|
||||
if (dbDir.exists() && !dbDir.isDirectory())
|
||||
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||
|
||||
// Initialize global files
|
||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||
usersFile = new File(dbDir, "users.db");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database file for a user-specific list of chats.
|
||||
*
|
||||
* @throws IllegalStateException if the client user is not specified
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
@Override
|
||||
public void initializeUserStorage() {
|
||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() throws IOException {
|
||||
// Save users
|
||||
SerializationUtils.write(usersFile, users);
|
||||
|
||||
// Save user data
|
||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
|
||||
|
||||
// Save id generator
|
||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
|
||||
|
||||
@Override
|
||||
public void loadUserData() throws ClassNotFoundException, IOException {
|
||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||
chats = (ArrayList<Chat>) in.readObject();
|
||||
cacheMap = (CacheMap) in.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadIDGenerator() {
|
||||
try {
|
||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||
} catch (ClassNotFoundException | IOException e) {}
|
||||
}
|
||||
}
|
@ -1,28 +1,27 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import envoy.util.SerializationUtils;
|
||||
import dev.kske.eventbus.*;
|
||||
import dev.kske.eventbus.EventListener;
|
||||
|
||||
import envoy.util.*;
|
||||
|
||||
import envoy.client.event.EnvoyCloseEvent;
|
||||
|
||||
/**
|
||||
* Manages all application settings, which are different objects that can be
|
||||
* changed during runtime and serialized them by using either the file system or
|
||||
* the {@link Preferences} API.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Settings.java</strong><br>
|
||||
* Created: <strong>11 Nov 2019</strong><br>
|
||||
* Manages all application settings, which are different objects that can be changed during runtime
|
||||
* and serialized them by using either the file system or the {@link Preferences} API.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class Settings {
|
||||
public final class Settings implements EventListener {
|
||||
|
||||
// Actual settings accessible by the rest of the application
|
||||
private Map<String, SettingsItem<?>> items;
|
||||
@ -30,7 +29,8 @@ public class Settings {
|
||||
/**
|
||||
* Settings are stored in this file.
|
||||
*/
|
||||
private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
|
||||
private static final File settingsFile =
|
||||
new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
|
||||
|
||||
/**
|
||||
* Singleton instance of this class.
|
||||
@ -38,12 +38,14 @@ public class Settings {
|
||||
private static Settings settings = new Settings();
|
||||
|
||||
/**
|
||||
* The way to instantiate the settings. Is set to private to deny other
|
||||
* instances of that object.
|
||||
* The way to instantiate the settings. Is set to private to deny other instances of that
|
||||
* object.
|
||||
*
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
private Settings() {
|
||||
EventBus.getInstance().registerListener(this);
|
||||
|
||||
// Load settings from settings file
|
||||
try {
|
||||
items = SerializationUtils.read(settingsFile, HashMap.class);
|
||||
@ -67,16 +69,35 @@ public class Settings {
|
||||
* @throws IOException if an error occurs while saving the themes
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void save() throws IOException {
|
||||
@Event(eventType = EnvoyCloseEvent.class)
|
||||
private void save() {
|
||||
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
|
||||
|
||||
// Save settings to settings file
|
||||
SerializationUtils.write(settingsFile, items);
|
||||
try {
|
||||
SerializationUtils.write(settingsFile, items);
|
||||
} catch (final IOException e) {
|
||||
EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void supplementDefaults() {
|
||||
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
|
||||
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
|
||||
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
|
||||
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send",
|
||||
"Sends a message by pressing the enter key."));
|
||||
items.putIfAbsent("hideOnClose",
|
||||
new SettingsItem<>(false, "Hide on close", "Hides the chat window when it is closed."));
|
||||
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name",
|
||||
"The name of the currently selected theme."));
|
||||
items.putIfAbsent("downloadLocation",
|
||||
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"),
|
||||
"Download location",
|
||||
"The location where files will be saved to"));
|
||||
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?",
|
||||
"Should downloads be saved without asking?"));
|
||||
items.putIfAbsent("askForConfirmation",
|
||||
new SettingsItem<>(true, "Ask for confirmation",
|
||||
"Will ask for confirmation before doing certain things"));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +112,9 @@ public class Settings {
|
||||
* @param themeName the name to set
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
|
||||
public void setCurrentTheme(String themeName) {
|
||||
((SettingsItem<String>) items.get("currentTheme")).set(themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the currently used theme is one of the default themes
|
||||
@ -103,9 +126,8 @@ public class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true}, if pressing the {@code Enter} key suffices to send a
|
||||
* message. Otherwise it has to be pressed in conjunction with the
|
||||
* {@code Control} key.
|
||||
* @return {@code true}, if pressing the {@code Enter} key suffices to send a message. Otherwise
|
||||
* it has to be pressed in conjunction with the {@code Control} key.
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
|
||||
@ -113,26 +135,84 @@ public class Settings {
|
||||
/**
|
||||
* Changes the keystrokes performed by the user to send a message.
|
||||
*
|
||||
* @param enterToSend If set to {@code true} a message can be sent by pressing
|
||||
* the {@code Enter} key. Otherwise it has to be pressed in
|
||||
* conjunction with the {@code Control} key.
|
||||
* @param enterToSend If set to {@code true} a message can be sent by pressing the {@code Enter}
|
||||
* key. Otherwise it has to be pressed in conjunction with the
|
||||
* {@code Control} key.
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
|
||||
public void setEnterToSend(boolean enterToSend) {
|
||||
((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Boolean isDownloadSavedWithoutAsking() {
|
||||
return (Boolean) items.get("autoSaveDownloads").get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment}.
|
||||
*
|
||||
* @param autosaveDownload whether a download should be saved without asking before
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) {
|
||||
((SettingsItem<Boolean>) items.get("autoSaveDownloads")).set(autosaveDownload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the path where downloads should be saved
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public File getDownloadLocation() { return (File) items.get("downloadLocation").get(); }
|
||||
|
||||
/**
|
||||
* Sets the path where downloads should be saved.
|
||||
*
|
||||
* @param downloadLocation the path to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setDownloadLocation(File downloadLocation) {
|
||||
((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current on close mode.
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
|
||||
public Boolean isHideOnClose() { return (Boolean) items.get("hideOnClose").get(); }
|
||||
|
||||
/**
|
||||
* Sets the current on close mode.
|
||||
*
|
||||
* @param currentOnCloseMode the on close mode that should be set.
|
||||
* @param hideOnClose whether the application should be minimized on close
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
|
||||
public void setHideOnClose(boolean hideOnClose) {
|
||||
((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether a confirmation dialog should be displayed before certain actions
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Boolean isAskForConfirmation() {
|
||||
return (Boolean) items.get("askForConfirmation").get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the behavior of calling certain functionality by displaying a confirmation dialog
|
||||
* before executing it.
|
||||
*
|
||||
* @param askForConfirmation whether confirmation dialogs should be displayed before certain
|
||||
* actions
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void setAskForConfirmation(boolean askForConfirmation) {
|
||||
((SettingsItem<Boolean>) items.get("askForConfirmation")).set(askForConfirmation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the items
|
||||
|
@ -1,35 +1,26 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
/**
|
||||
* Encapsulates a persistent value that is directly or indirectly mutable by the
|
||||
* user.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SettingsItem.java</strong><br>
|
||||
* Created: <strong>23.12.2019</strong><br>
|
||||
* Encapsulates a persistent value that is directly or indirectly mutable by the user.
|
||||
*
|
||||
* @param <T> the type of this {@link SettingsItem}'s value
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class SettingsItem<T> implements Serializable {
|
||||
public final class SettingsItem<T> implements Serializable {
|
||||
|
||||
private T value;
|
||||
private String userFriendlyName, description;
|
||||
|
||||
private transient Consumer<T> changeHandler;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Initializes a {@link SettingsItem}. The default value's class will be mapped
|
||||
* to a {@link JComponent} that can be used to display this {@link SettingsItem}
|
||||
* to the user.
|
||||
* Initializes a {@link SettingsItem}. The default value's class will be mapped to a
|
||||
* {@link JComponent} that can be used to display this {@link SettingsItem} to the user.
|
||||
*
|
||||
* @param value the default value
|
||||
* @param userFriendlyName the user friendly name (short)
|
||||
@ -46,17 +37,18 @@ public class SettingsItem<T> implements Serializable {
|
||||
* @return the value
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public T get() { return value; }
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
|
||||
* defined, it will be invoked with this value.
|
||||
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if defined, it
|
||||
* will be invoked with this value.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void set(T value) {
|
||||
if (changeHandler != null && value != this.value) changeHandler.accept(value);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@ -70,7 +62,9 @@ public class SettingsItem<T> implements Serializable {
|
||||
* @param userFriendlyName the userFriendlyName to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
|
||||
public void setUserFriendlyName(String userFriendlyName) {
|
||||
this.userFriendlyName = userFriendlyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the description
|
||||
@ -83,17 +77,4 @@ public class SettingsItem<T> implements Serializable {
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
/**
|
||||
* Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
|
||||
* invoked with the current value once during the registration and every time
|
||||
* when the value changes.
|
||||
*
|
||||
* @param changeHandler the changeHandler to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setChangeHandler(Consumer<T> changeHandler) {
|
||||
this.changeHandler = changeHandler;
|
||||
changeHandler.accept(value);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package envoy.client.data;
|
||||
|
||||
/**
|
||||
* Implements a {@link LocalDB} in a way that does not persist any information
|
||||
* after application shutdown.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>TransientLocalDB.java</strong><br>
|
||||
* Created: <strong>3 Feb 2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public final class TransientLocalDB extends LocalDB {
|
||||
}
|
@ -6,10 +6,6 @@ import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Plays back audio from a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioPlayer.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
@ -26,7 +22,9 @@ public final class AudioPlayer {
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
|
||||
public AudioPlayer() {
|
||||
this(AudioRecorder.DEFAULT_AUDIO_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the player with a given audio format.
|
||||
|
@ -1,8 +1,7 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.*;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
@ -10,10 +9,6 @@ import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Records audio and exports it as a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioRecorder.java</strong><br>
|
||||
* Created: <strong>02.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
@ -25,7 +20,13 @@ public final class AudioRecorder {
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT =
|
||||
new AudioFormat(16000, 16, 1, true, false);
|
||||
|
||||
/**
|
||||
* The format in which audio files will be saved.
|
||||
*/
|
||||
public static final String FILE_FORMAT = "wav";
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
@ -38,7 +39,9 @@ public final class AudioRecorder {
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
|
||||
public AudioRecorder() {
|
||||
this(DEFAULT_AUDIO_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the recorder with a given audio format.
|
||||
@ -78,7 +81,7 @@ public final class AudioRecorder {
|
||||
line.start();
|
||||
|
||||
// Prepare temp file
|
||||
tempFile = Files.createTempFile("recording", "wav");
|
||||
tempFile = Files.createTempFile("recording", FILE_FORMAT);
|
||||
|
||||
// Start the recording
|
||||
final var ais = new AudioInputStream(line);
|
||||
@ -117,6 +120,6 @@ public final class AudioRecorder {
|
||||
line.close();
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {}
|
||||
} catch (final IOException e) {}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
/**
|
||||
* Contains classes related to recording and playing back audio clips.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
|
@ -0,0 +1,20 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This interface defines an action that should be performed when a system command gets called.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public interface Callable {
|
||||
|
||||
/**
|
||||
* Performs the instance specific action when a {@link SystemCommand} has been called.
|
||||
*
|
||||
* @param arguments the arguments that should be passed to the {@link SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
void call(List<String> arguments);
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This class is the base class of all {@code SystemCommands} and contains an action and a number of
|
||||
* arguments that should be used as input for this function. No {@code SystemCommand} can return
|
||||
* anything. Every {@code SystemCommand} must have as argument type {@code List<String>} so that the
|
||||
* words following the indicator String can be used as input of the function. This approach has one
|
||||
* limitation:<br>
|
||||
* <b>Order matters!</b> Changing the order of arguments will likely result in unexpected behavior.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class SystemCommand implements Callable {
|
||||
|
||||
protected int relevance;
|
||||
|
||||
/**
|
||||
* The argument count of the command.
|
||||
*/
|
||||
protected final int numberOfArguments;
|
||||
|
||||
/**
|
||||
* This function takes a {@code List<String>} as argument because automatically
|
||||
* {@code SystemCommand#numberOfArguments} words following the necessary command will be put
|
||||
* into this list.
|
||||
*
|
||||
* @see String#split(String)
|
||||
*/
|
||||
protected final Consumer<List<String>> action;
|
||||
|
||||
protected final String description;
|
||||
|
||||
protected final List<String> defaults;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code SystemCommand}.
|
||||
*
|
||||
* @param action the action performed by the command
|
||||
* @param numberOfArguments the argument count accepted by the action
|
||||
* @param defaults the default values for the corresponding arguments
|
||||
* @param description the description of this {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand(Consumer<List<String>> action, int numberOfArguments,
|
||||
List<String> defaults, String description) {
|
||||
this.numberOfArguments = numberOfArguments;
|
||||
this.action = action;
|
||||
this.defaults = defaults == null ? new ArrayList<>() : defaults;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the argument count of the command
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public int getNumberOfArguments() { return numberOfArguments; }
|
||||
|
||||
/**
|
||||
* @return the description
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public String getDescription() { return description; }
|
||||
|
||||
/**
|
||||
* @return the relevance
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public int getRelevance() { return relevance; }
|
||||
|
||||
/**
|
||||
* @param relevance the relevance to set
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void setRelevance(int relevance) { this.relevance = relevance; }
|
||||
|
||||
@Override
|
||||
public void call(List<String> arguments) {
|
||||
action.accept(arguments);
|
||||
++relevance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the defaults
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public List<String> getDefaults() { return defaults; }
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final var other = (SystemCommand) obj;
|
||||
return Objects.equals(action, other.action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments
|
||||
+ ", "
|
||||
+ (description != null ? "description=" + description + ", " : "")
|
||||
+ (defaults != null ? "defaults=" + defaults : "") + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,232 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* This class acts as a builder for {@link SystemCommand}s.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class SystemCommandBuilder {
|
||||
|
||||
private int numberOfArguments;
|
||||
private Consumer<List<String>> action;
|
||||
private List<String> defaults;
|
||||
private String description;
|
||||
private int relevance;
|
||||
|
||||
private final SystemCommandMap commandsMap;
|
||||
|
||||
/**
|
||||
* Creates a new {@code SystemCommandsBuilder} without underlying {@link SystemCommandMap}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param commandsMap the map to use when calling build (optional)
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder(SystemCommandMap commandsMap) {
|
||||
this.commandsMap = commandsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numberOfArguments the numberOfArguments to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setNumberOfArguments(int numberOfArguments) {
|
||||
this.numberOfArguments = numberOfArguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param action the action to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setAction(Consumer<List<String>> action) {
|
||||
this.action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param description the description to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param relevance the relevance to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setRelevance(int relevance) {
|
||||
this.relevance = relevance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param defaults the defaults to set
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder setDefaults(String... defaults) {
|
||||
this.defaults = List.of(defaults);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all values stored.
|
||||
*
|
||||
* @return this {@code SystemCommandBuilder}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommandBuilder reset() {
|
||||
numberOfArguments = 0;
|
||||
action = null;
|
||||
defaults = new ArrayList<>();
|
||||
description = "";
|
||||
relevance = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build() {
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the previous
|
||||
* value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildNoArg() {
|
||||
numberOfArguments = 0;
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the string as
|
||||
* argument, regardless of the previous value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildRemainingArg() {
|
||||
numberOfArguments = -1;
|
||||
return build(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* Automatically adds the built object to the given map. At the end, this
|
||||
* {@code SystemCommandBuilder} <b>can</b> be reset but must not be.
|
||||
*
|
||||
* @param reset whether this {@code SystemCommandBuilder} should be reset afterwards.<br>
|
||||
* This can be useful if another command wants to execute something similar
|
||||
* @return the built {@code SystemCommand}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build(boolean reset) {
|
||||
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
|
||||
sc.setRelevance(relevance);
|
||||
if (reset)
|
||||
reset();
|
||||
return sc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data. Automatically adds the
|
||||
* built object to the given map.
|
||||
*
|
||||
* @param command the command under which to store the SystemCommand in the
|
||||
* {@link SystemCommandMap}
|
||||
* @return the built {@code SystemCommand}
|
||||
* @throws NullPointerException if no map has been assigned to this builder
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build(String command) {
|
||||
return build(command, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments}
|
||||
* will be set to 0, regardless of the previous value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @param command the command under which to store the SystemCommand in the
|
||||
* {@link SystemCommandMap}
|
||||
* @return the built {@code SystemCommand}
|
||||
* @throws NullPointerException if no map has been assigned to this builder
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildNoArg(String command) {
|
||||
numberOfArguments = 0;
|
||||
return build(command, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments}
|
||||
* will be set to use the rest of the string as argument, regardless of the previous value.<br>
|
||||
* At the end, this {@code SystemCommandBuilder} will be reset.
|
||||
*
|
||||
* @param command the command under which to store the SystemCommand in the
|
||||
* {@link SystemCommandMap}
|
||||
* @return the built {@code SystemCommand}
|
||||
* @throws NullPointerException if no map has been assigned to this builder
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand buildRemainingArg(String command) {
|
||||
numberOfArguments = -1;
|
||||
return build(command, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
|
||||
* Automatically adds the built object to the given map. At the end, this
|
||||
* {@code SystemCommandBuilder} <b>can</b> be reset but must not be.
|
||||
*
|
||||
* @param command the command under which to store the SystemCommand in the
|
||||
* {@link SystemCommandMap}
|
||||
* @param reset whether this {@code SystemCommandBuilder} should be reset afterwards.<br>
|
||||
* This can be useful if another command wants to execute something similar
|
||||
* @return the built {@code SystemCommand}
|
||||
* @throws NullPointerException if no map has been assigned to this builder
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public SystemCommand build(String command, boolean reset) {
|
||||
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
|
||||
sc.setRelevance(relevance);
|
||||
if (commandsMap != null)
|
||||
commandsMap.add(command, sc);
|
||||
else
|
||||
throw new NullPointerException("No map in SystemCommandsBuilder present");
|
||||
if (reset)
|
||||
reset();
|
||||
return sc;
|
||||
}
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
package envoy.client.data.commands;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Stores all {@link SystemCommand}s used. SystemCommands can be called using an activator char and
|
||||
* the text that needs to be present behind the activator. Additionally offers the option to request
|
||||
* recommendations for a partial input String.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class SystemCommandMap {
|
||||
|
||||
private final Character activator;
|
||||
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
|
||||
private final Pattern commandPattern =
|
||||
Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$");
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
|
||||
|
||||
/**
|
||||
* Creates a new {@code SystemCommandMap} with the given char as activator. If this Character is
|
||||
* null, any text used as input will be treated as a system command.
|
||||
*
|
||||
* @param activator the char to use as activator for commands
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public SystemCommandMap(Character activator) {
|
||||
this.activator = activator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code SystemCommandMap} with '/' as activator.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public SystemCommandMap() {
|
||||
activator = '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new command to the map if the command name is valid.
|
||||
*
|
||||
* @param command the input string to execute the given action
|
||||
* @param systemCommand the command to add - can be built using {@link SystemCommandBuilder}
|
||||
* @see SystemCommandMap#isValidKey(String)
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void add(String command, SystemCommand systemCommand) {
|
||||
if (isValidKey(command))
|
||||
systemCommands.put(command.toLowerCase(), systemCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if the input String is a key in the map and returns the wrapped System
|
||||
* command if present.
|
||||
* <p>
|
||||
* Usage example:<br>
|
||||
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
|
||||
* {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, ""));}<br>
|
||||
* {@code ....}<br>
|
||||
* user input: {@code "*example xyz ..."}<br>
|
||||
* {@code systemCommands.get("example xyz ...")} or
|
||||
* {@code systemCommands.get("*example xyz ...")} result:
|
||||
* {@code Optional<SystemCommand>.get() != null}
|
||||
*
|
||||
* @param input the input string given by the user
|
||||
* @return the wrapped system command, if present
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Optional<SystemCommand> get(String input) {
|
||||
return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method ensures that the activator of a {@link SystemCommand} is stripped.<br>
|
||||
* It only checks the word beginning from the first non-blank position in the input. It returns
|
||||
* the command as (most likely) entered as key in the map for the first word of the text.<br>
|
||||
* Activators in the middle of the word will be disregarded.
|
||||
*
|
||||
* @param raw the input
|
||||
* @return the command as entered in the map
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote this method will (most likely) not return anything useful if whatever is entered
|
||||
* after the activator is not a system command. Only exception: for recommendation
|
||||
* purposes.
|
||||
*/
|
||||
public String getCommand(String raw) {
|
||||
final var trimmed = raw.stripLeading();
|
||||
|
||||
// Entering only the activator should not throw an error
|
||||
if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0)))
|
||||
return "";
|
||||
else {
|
||||
final var index = trimmed.indexOf(' ');
|
||||
return trimmed.substring(
|
||||
activator != null && activator.equals(trimmed.charAt(0)) ? 1 : 0,
|
||||
index < 1 ? trimmed.length() : index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines whether a key can be put in the map and logs it with {@code Level.WARNING} if that
|
||||
* key violates API constrictions.<br>
|
||||
* (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>)
|
||||
* <p>
|
||||
* The approach to not throw an exception was taken so that an ugly try-catch block for every
|
||||
* addition to the system commands map could be avoided, an error that should only occur during
|
||||
* implementation and not in production.
|
||||
*
|
||||
* @param command the key to examine
|
||||
* @return whether this key can be used in the map
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isValidKey(String command) {
|
||||
final var valid = commandPattern.matcher(command).matches();
|
||||
if (!valid)
|
||||
logger.log(Level.WARNING,
|
||||
"The command \"" + command
|
||||
+ "\" is not valid. As it might cause problems when executed, it will not be entered into the map. Only the characters "
|
||||
+ commandPattern + "are allowed");
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a 'raw' string (the whole input) and checks if the activator is the first visible
|
||||
* character and then checks if a command is present after that activator. If that is the case,
|
||||
* it will be executed.
|
||||
*
|
||||
* @param raw the raw input string
|
||||
* @return whether a command could be found and successfully executed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean executeIfPresent(String raw) {
|
||||
|
||||
// possibly a command was detected and could be executed
|
||||
final var raw2 = raw.stripLeading();
|
||||
final var commandFound = activator == null || raw2.startsWith(activator.toString())
|
||||
? executeAvailableCommand(raw2)
|
||||
: false;
|
||||
|
||||
// the command was executed successfully - no further checking needed
|
||||
if (commandFound)
|
||||
logger.log(Level.FINE, "executed system command " + getCommand(raw2));
|
||||
return commandFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the recommendations based on the current input entered.<br>
|
||||
* The first word is used for the recommendations and it does not matter if the activator is at
|
||||
* its beginning or not.<br>
|
||||
* If recommendations are present, the given function will be executed on the
|
||||
* recommendations.<br>
|
||||
* Otherwise nothing will be done.<br>
|
||||
*
|
||||
* @param input the input string
|
||||
* @param action the action that should be taken for the recommendations, if any are present
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void requestRecommendations(String input, Consumer<Set<String>> action) {
|
||||
final var partialCommand = getCommand(input);
|
||||
|
||||
// Get the expected commands
|
||||
final var recommendations = recommendCommands(partialCommand);
|
||||
if (recommendations.isEmpty())
|
||||
return;
|
||||
|
||||
// Execute the given action
|
||||
else
|
||||
action.accept(recommendations);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if the input String is a key in the map and executes the wrapped System
|
||||
* command if present.
|
||||
* <p>
|
||||
* Usage example:<br>
|
||||
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
|
||||
* {@code Button button = new Button();}<br>
|
||||
* {@code systemCommands.add("example", new SystemCommand(text -> {button.setText(text.get(0))},
|
||||
* 1, null, ""));}<br>
|
||||
* {@code ....}<br>
|
||||
* user input: {@code "*example xyz ..."}<br>
|
||||
* {@code systemCommands.executeIfPresent("example xyz ...")} or
|
||||
* {@code systemCommands.executeIfPresent("*example xyz ...")} result:
|
||||
* {@code button.getText()=="xyz"}
|
||||
*
|
||||
* @param input the input string given by the user
|
||||
* @return whether a command could be found and successfully executed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private boolean executeAvailableCommand(String input) {
|
||||
final var command = getCommand(input);
|
||||
final var value = get(command);
|
||||
final var commandExecuted = new AtomicBoolean(value.isPresent());
|
||||
value.ifPresent(systemCommand -> {
|
||||
|
||||
// Splitting the String so that the leading command including the first " " is
|
||||
// removed and only as many following words as allowed by the system command
|
||||
// persist
|
||||
final var arguments = extractArguments(input, systemCommand);
|
||||
|
||||
// Executing the function
|
||||
try {
|
||||
systemCommand.call(arguments);
|
||||
} catch (final NumberFormatException e) {
|
||||
logger.log(Level.INFO,
|
||||
String.format(
|
||||
"System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.",
|
||||
command));
|
||||
Platform.runLater(() -> {
|
||||
final var alert = new Alert(AlertType.ERROR);
|
||||
alert.setContentText("Please enter a readable number as argument.");
|
||||
alert.showAndWait();
|
||||
});
|
||||
commandExecuted.set(false);
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e);
|
||||
Platform.runLater(() -> {
|
||||
final var alert = new Alert(AlertType.ERROR);
|
||||
alert.setContentText(
|
||||
"Could not execute system command: Internal error. Please insult the responsible programmer.");
|
||||
alert.showAndWait();
|
||||
});
|
||||
commandExecuted.set(false);
|
||||
}
|
||||
});
|
||||
return commandExecuted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies missing values with default values.
|
||||
*
|
||||
* @param input the input String
|
||||
* @param systemCommand the command that is expected
|
||||
* @return the list of arguments that can be used to parse the systemCommand
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private List<String> extractArguments(String input, SystemCommand systemCommand) {
|
||||
|
||||
// no more arguments follow after the command (e.g. text = "/DABR")
|
||||
final var indexOfSpace = input.indexOf(" ");
|
||||
if (indexOfSpace < 0)
|
||||
return supplementDefaults(new String[] {}, systemCommand);
|
||||
|
||||
// the arguments behind a system command
|
||||
final var remainingString = input.substring(indexOfSpace + 1);
|
||||
final var numberOfArguments = systemCommand.getNumberOfArguments();
|
||||
|
||||
// splitting those arguments and supplying default values
|
||||
final var textArguments = remainingString.split(" ", -1);
|
||||
final var originalArguments =
|
||||
numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments)
|
||||
: textArguments;
|
||||
final var arguments = supplementDefaults(originalArguments, systemCommand);
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommends commands based upon the currently entered input.<br>
|
||||
* In the current implementation, all that gets checked is whether a key contains this input.
|
||||
* This might be updated later on.
|
||||
*
|
||||
* @param partialCommand the partially entered command
|
||||
* @return a set of all commands that match this input
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
private Set<String> recommendCommands(String partialCommand) {
|
||||
|
||||
// current implementation only looks if input is contained within a command,
|
||||
// might be updated
|
||||
return systemCommands.keySet()
|
||||
.stream()
|
||||
.filter(command -> command.contains(partialCommand))
|
||||
.sorted(
|
||||
(command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(),
|
||||
systemCommands.get(command2).getRelevance()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies the default values for arguments if none are present in the text for any argument.
|
||||
* <br>
|
||||
*
|
||||
* @param textArguments the arguments that were parsed from the text
|
||||
* @param toEvaluate the system command whose default values should be used
|
||||
* @return the final argument list
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote this method will insert an empty String if the size of the list given to the
|
||||
* {@code SystemCommand} is smaller than its argument counter and no more text
|
||||
* arguments could be found.
|
||||
*/
|
||||
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
|
||||
final var defaults = toEvaluate.getDefaults();
|
||||
final var numberOfArguments = toEvaluate.getNumberOfArguments();
|
||||
final List<String> result = new ArrayList<>();
|
||||
|
||||
if (toEvaluate.getNumberOfArguments() > 0)
|
||||
for (var index = 0; index < numberOfArguments; index++) {
|
||||
String textArg = null;
|
||||
if (index < textArguments.length)
|
||||
textArg = textArguments[index];
|
||||
|
||||
// Set the argument at position index to the current argument of the text, if it
|
||||
// is present. Otherwise the default for that argument will be taken if present.
|
||||
// In the worst case, an empty String will be used.
|
||||
result.add(!(textArg == null) && !textArg.isBlank() ? textArg
|
||||
: index < defaults.size() ? defaults.get(index) : "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all {@link SystemCommand}s used with the underlying command as key
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; }
|
||||
|
||||
/**
|
||||
* @return the activator of any command in this map. Can be null.
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public Character getActivator() { return activator; }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This package contains all classes that can be used as system commands.<br>
|
||||
* Every system command can be called using a specific syntax:"/<command>"
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
package envoy.client.data.commands;
|
@ -0,0 +1,77 @@
|
||||
package envoy.client.data.shortcuts;
|
||||
|
||||
import javafx.scene.input.*;
|
||||
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.util.UserUtil;
|
||||
|
||||
/**
|
||||
* Envoy-specific implementation of the keyboard-shortcut interaction offered by
|
||||
* {@link GlobalKeyShortcuts}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class EnvoyShortcutConfig {
|
||||
|
||||
private EnvoyShortcutConfig() {}
|
||||
|
||||
/**
|
||||
* Supplies the default shortcuts for {@link GlobalKeyShortcuts}.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public static void initializeEnvoyShortcuts() {
|
||||
final var instance = GlobalKeyShortcuts.getInstance();
|
||||
|
||||
// Add the option to exit with "Control" + "Q" or "Alt" + "F4" as offered by
|
||||
// some desktop environments
|
||||
instance.add(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN),
|
||||
ShutdownHelper::exit);
|
||||
|
||||
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene
|
||||
instance.addForNotExcluded(
|
||||
new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN,
|
||||
KeyCombination.SHIFT_DOWN),
|
||||
UserUtil::logout,
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to open settings scene with "Control"+"S", if not in login scene
|
||||
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN),
|
||||
() -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE),
|
||||
SceneInfo.SETTINGS_SCENE,
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status away
|
||||
instance.addForNotExcluded(
|
||||
new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN,
|
||||
KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.AWAY),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status busy
|
||||
instance.addForNotExcluded(
|
||||
new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN,
|
||||
KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.BUSY),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status offline
|
||||
instance.addForNotExcluded(
|
||||
new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN,
|
||||
KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.OFFLINE),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status online
|
||||
instance.addForNotExcluded(
|
||||
new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN,
|
||||
KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.ONLINE),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package envoy.client.data.shortcuts;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
|
||||
/**
|
||||
* Contains all keyboard shortcuts used throughout the application.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public final class GlobalKeyShortcuts {
|
||||
|
||||
private final EnumMap<SceneInfo, Map<KeyCombination, Runnable>> shortcuts =
|
||||
new EnumMap<>(SceneInfo.class);
|
||||
|
||||
private static GlobalKeyShortcuts instance = new GlobalKeyShortcuts();
|
||||
|
||||
private GlobalKeyShortcuts() {
|
||||
for (final var sceneInfo : SceneInfo.values())
|
||||
shortcuts.put(sceneInfo, new HashMap<KeyCombination, Runnable>());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the instance of global keyboard shortcuts.
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public static GlobalKeyShortcuts getInstance() { return instance; }
|
||||
|
||||
/**
|
||||
* Adds the given keyboard shortcut and its action to all scenes.
|
||||
*
|
||||
* @param keys the keys to press to perform the given action
|
||||
* @param action the action to perform
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void add(KeyCombination keys, Runnable action) {
|
||||
shortcuts.values().forEach(collection -> collection.put(keys, action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given keyboard shortcut and its action to all scenes that are not part of exclude.
|
||||
*
|
||||
* @param keys the keys to press to perform the given action
|
||||
* @param action the action to perform
|
||||
* @param exclude the scenes that should be excluded from receiving this keyboard shortcut
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void addForNotExcluded(KeyCombination keys, Runnable action, SceneInfo... exclude) {
|
||||
|
||||
// Computing the remaining sceneInfos
|
||||
final var include = new SceneInfo[SceneInfo.values().length - exclude.length];
|
||||
int index = 0;
|
||||
outer: for (final var sceneInfo : SceneInfo.values()) {
|
||||
for (final var excluded : exclude)
|
||||
if (sceneInfo.equals(excluded))
|
||||
continue outer;
|
||||
include[index++] = sceneInfo;
|
||||
}
|
||||
|
||||
// Adding the action to the remaining sceneInfos
|
||||
for (final var sceneInfo : include)
|
||||
shortcuts.get(sceneInfo).put(keys, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all stored keyboard shortcuts for the given scene constant.
|
||||
*
|
||||
* @param sceneInfo the currently loading scene
|
||||
* @return all stored keyboard shortcuts for this scene
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public Map<KeyCombination, Runnable> getKeyboardShortcuts(SceneInfo sceneInfo) {
|
||||
return shortcuts.get(sceneInfo);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package envoy.client.data.shortcuts;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
||||
import envoy.client.ui.SceneContext;
|
||||
|
||||
/**
|
||||
* Provides methods to set the keyboard shortcuts for a specific scene. Should only be implemented
|
||||
* by controllers of scenes so that these methods can automatically be called inside
|
||||
* {@link SceneContext} as soon as the underlying FXML file has been loaded.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public interface KeyboardMapping {
|
||||
|
||||
/**
|
||||
* @return all keyboard shortcuts of a scene
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
Map<KeyCombination, Runnable> getKeyboardShortcuts();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Contains the necessary classes to enable using keyboard shortcuts in Envoy.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
package envoy.client.data.shortcuts;
|
15
client/src/main/java/envoy/client/event/BackEvent.java
Normal file
15
client/src/main/java/envoy/client/event/BackEvent.java
Normal file
@ -0,0 +1,15 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.event.Event.Valueless;
|
||||
|
||||
/**
|
||||
* This event serves the purpose of triggering the tab change to tab 0 in
|
||||
* {@link envoy.client.ui.controller.ChatScene}.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class BackEvent extends Valueless {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
}
|
23
client/src/main/java/envoy/client/event/ContactDisabled.java
Normal file
23
client/src/main/java/envoy/client/event/ContactDisabled.java
Normal file
@ -0,0 +1,23 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.data.Contact;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Signifies that the chat of a contact should be disabled.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class ContactDisabled extends Event<Contact> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param contact the contact that should be disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public ContactDisabled(Contact contact) {
|
||||
super(contact);
|
||||
}
|
||||
}
|
15
client/src/main/java/envoy/client/event/EnvoyCloseEvent.java
Normal file
15
client/src/main/java/envoy/client/event/EnvoyCloseEvent.java
Normal file
@ -0,0 +1,15 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.event.Event.Valueless;
|
||||
|
||||
/**
|
||||
* This event notifies various Envoy components of the application being about to shut down. This
|
||||
* allows the graceful closing of connections, persisting local data etc.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public class EnvoyCloseEvent extends Valueless {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
14
client/src/main/java/envoy/client/event/Logout.java
Normal file
14
client/src/main/java/envoy/client/event/Logout.java
Normal file
@ -0,0 +1,14 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.event.Event.Valueless;
|
||||
|
||||
/**
|
||||
* Indicates that a logout has been requested.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class Logout extends Valueless {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.data.Message;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>MessageCreationEvent.java</strong><br>
|
||||
* Created: <strong>4 Dec 2019</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class MessageCreationEvent extends Event<Message> {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* @param message the {@link Message} that has been created
|
||||
*/
|
||||
public MessageCreationEvent(Message message) { super(message); }
|
||||
}
|
22
client/src/main/java/envoy/client/event/MessageDeletion.java
Normal file
22
client/src/main/java/envoy/client/event/MessageDeletion.java
Normal file
@ -0,0 +1,22 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Conveys the deletion of a message.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public class MessageDeletion extends Event<Long> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param messageID the ID of the deleted message
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public MessageDeletion(long messageID) {
|
||||
super(messageID);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.data.Message;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>MessageModificationEvent.java</strong><br>
|
||||
* Created: <strong>4 Dec 2019</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class MessageModificationEvent extends Event<Message> {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* @param message the {@link Message} that has been modified
|
||||
*/
|
||||
public MessageModificationEvent(Message message) { super(message); }
|
||||
}
|
23
client/src/main/java/envoy/client/event/OwnStatusChange.java
Normal file
23
client/src/main/java/envoy/client/event/OwnStatusChange.java
Normal file
@ -0,0 +1,23 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Signifies a manual status change of the client user.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class OwnStatusChange extends Event<UserStatus> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param value the new user status of the client user
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public OwnStatusChange(UserStatus value) {
|
||||
super(value);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SendEvent.java</strong><br>
|
||||
* Created: <strong>11.02.2020</strong><br>
|
||||
*
|
||||
* @author: Maximilian Käfer
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class SendEvent extends Event<Event<?>> {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* @param value the event to send to the server
|
||||
*/
|
||||
public SendEvent(Event<?> value) { super(value); }
|
||||
|
||||
}
|
@ -3,23 +3,12 @@ package envoy.client.event;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ThemeChangeEvent.java</strong><br>
|
||||
* Created: <strong>15 Dec 2019</strong><br>
|
||||
* Notifies UI components of a theme change.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class ThemeChangeEvent extends Event<String> {
|
||||
public final class ThemeChangeEvent extends Event.Valueless {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* Initializes a {@link ThemeChangeEvent} conveying information about the change
|
||||
* of the theme currently in use.
|
||||
*
|
||||
* @param theme the name of the new theme
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public ThemeChangeEvent(String theme) { super(theme); }
|
||||
}
|
||||
|
33
client/src/main/java/envoy/client/helper/AlertHelper.java
Normal file
33
client/src/main/java/envoy/client/helper/AlertHelper.java
Normal file
@ -0,0 +1,33 @@
|
||||
package envoy.client.helper;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
|
||||
/**
|
||||
* Provides methods that are commonly used for alerts.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class AlertHelper {
|
||||
|
||||
private AlertHelper() {}
|
||||
|
||||
/**
|
||||
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()} returns
|
||||
* {@code true}. Immediately executes the action if no dialog was requested or the dialog was
|
||||
* exited with a confirmation. Does nothing if the dialog was closed without clicking on OK.
|
||||
*
|
||||
* @param alert the (customized) alert to show. <strong>Should not be shown already</strong>
|
||||
* @param action the action to perform in case of success
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static void confirmAction(Alert alert, Runnable action) {
|
||||
alert.setHeaderText("");
|
||||
if (Settings.getInstance().isAskForConfirmation())
|
||||
alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
|
||||
else
|
||||
action.run();
|
||||
}
|
||||
}
|
45
client/src/main/java/envoy/client/helper/ShutdownHelper.java
Normal file
45
client/src/main/java/envoy/client/helper/ShutdownHelper.java
Normal file
@ -0,0 +1,45 @@
|
||||
package envoy.client.helper;
|
||||
|
||||
import dev.kske.eventbus.EventBus;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.event.EnvoyCloseEvent;
|
||||
import envoy.client.ui.StatusTrayIcon;
|
||||
|
||||
/**
|
||||
* Simplifies shutdown actions.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class ShutdownHelper {
|
||||
|
||||
private ShutdownHelper() {}
|
||||
|
||||
/**
|
||||
* Exits Envoy or minimizes it, depending on the current state of
|
||||
* {@link Settings#isHideOnClose()} and {@link StatusTrayIcon#isSupported()}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static void exit() {
|
||||
exit(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits Envoy immediately if {@code force = true}, else it can exit or minimize Envoy,
|
||||
* depending on the current state of {@link Settings#isHideOnClose()} and
|
||||
* {@link StatusTrayIcon#isSupported()}.
|
||||
*
|
||||
* @param force whether to close in any case.
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static void exit(boolean force) {
|
||||
if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
|
||||
Context.getInstance().getStage().setIconified(true);
|
||||
else {
|
||||
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Provides helper methods that reduce boilerplate code.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Kai S. K. Engelbert
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
package envoy.client.helper;
|
@ -1,35 +1,30 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.*;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
import dev.kske.eventbus.Event;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.data.*;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.event.contact.ContactSearchResult;
|
||||
import envoy.util.EnvoyLog;
|
||||
import envoy.util.SerializationUtils;
|
||||
import envoy.util.*;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.event.EnvoyCloseEvent;
|
||||
|
||||
/**
|
||||
* Establishes a connection to the server, performs a handshake and delivers
|
||||
* certain objects to the server.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Client.java</strong><br>
|
||||
* Created: <strong>28 Sep 2019</strong><br>
|
||||
* Establishes a connection to the server, performs a handshake and delivers certain objects to the
|
||||
* server.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public class Client implements Closeable {
|
||||
public final class Client implements EventListener, Closeable {
|
||||
|
||||
// Connection handling
|
||||
private Socket socket;
|
||||
@ -46,35 +41,45 @@ public class Client implements Closeable {
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
|
||||
/**
|
||||
* Enters the online mode by acquiring a user ID from the server. As a
|
||||
* connection has to be established and a handshake has to be made, this method
|
||||
* will block for up to 5 seconds. If the handshake does exceed this time limit,
|
||||
* an exception is thrown.
|
||||
* Constructs a client and registers it as an event listener.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public Client() {
|
||||
eventBus.registerListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the online mode by acquiring a user ID from the server. As a connection has to be
|
||||
* established and a handshake has to be made, this method will block for up to 5 seconds. If
|
||||
* the handshake does exceed this time limit, an exception is thrown.
|
||||
*
|
||||
* @param credentials the login credentials of the user
|
||||
* @param cacheMap the map of all caches needed
|
||||
* @throws TimeoutException if the server could not be reached
|
||||
* @throws IOException if the login credentials could not be written
|
||||
* @throws InterruptedException if the current thread is interrupted while
|
||||
* waiting for the handshake response
|
||||
* @throws InterruptedException if the current thread is interrupted while waiting for the
|
||||
* handshake response
|
||||
*/
|
||||
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
|
||||
if (online) throw new IllegalStateException("Handshake has already been performed successfully");
|
||||
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap)
|
||||
throws TimeoutException, IOException, InterruptedException {
|
||||
if (online)
|
||||
throw new IllegalStateException("Handshake has already been performed successfully");
|
||||
rejected = false;
|
||||
|
||||
// Establish TCP connection
|
||||
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
|
||||
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...",
|
||||
config.getServer(), config.getPort()));
|
||||
socket = new Socket(config.getServer(), config.getPort());
|
||||
logger.log(Level.FINE, "Successfully established TCP connection to server");
|
||||
|
||||
// Create object receiver
|
||||
receiver = new Receiver(socket.getInputStream());
|
||||
|
||||
// Register user creation processor, contact list processor and message cache
|
||||
// Register user creation processor, contact list processor, message cache and
|
||||
// authentication token
|
||||
receiver.registerProcessor(User.class, sender -> this.sender = sender);
|
||||
receiver.registerProcessors(cacheMap.getMap());
|
||||
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
|
||||
|
||||
rejected = false;
|
||||
|
||||
// Start receiver
|
||||
receiver.start();
|
||||
@ -94,24 +99,24 @@ public class Client implements Closeable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
|
||||
if (System.currentTimeMillis() - start > 5000) {
|
||||
rejected = true;
|
||||
throw new TimeoutException("Did not log in after 5 seconds");
|
||||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
online = true;
|
||||
|
||||
logger.log(Level.INFO, "Handshake completed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the {@link Receiver} used to process data sent from the server to
|
||||
* this client.
|
||||
* Initializes the {@link Receiver} used to process data sent from the server to this client.
|
||||
*
|
||||
* @param localDB the local database used to persist the current
|
||||
* {@link IDGenerator}
|
||||
* @param localDB the local database used to persist the current {@link IDGenerator}
|
||||
* @param cacheMap the map of all caches needed
|
||||
* @throws IOException if no {@link IDGenerator} is present and none could be
|
||||
* requested from the server
|
||||
* @throws IOException if no {@link IDGenerator} is present and none could be requested from the
|
||||
* server
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
|
||||
@ -120,99 +125,92 @@ public class Client implements Closeable {
|
||||
// Remove all processors as they are only used during the handshake
|
||||
receiver.removeAllProcessors();
|
||||
|
||||
// Process incoming messages
|
||||
final var receivedMessageProcessor = new ReceivedMessageProcessor();
|
||||
final var receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor();
|
||||
final var messageStatusChangeProcessor = new MessageStatusChangeProcessor();
|
||||
final var groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor();
|
||||
|
||||
receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
|
||||
receiver.registerProcessor(Message.class, receivedMessageProcessor);
|
||||
receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
|
||||
receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
|
||||
|
||||
// Relay cached messages and message status changes
|
||||
cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
|
||||
cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
|
||||
cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
|
||||
cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
|
||||
|
||||
// Process user status changes
|
||||
receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
|
||||
|
||||
// Process message ID generation
|
||||
receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
|
||||
|
||||
// Process name changes
|
||||
receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
|
||||
|
||||
// Process contact searches
|
||||
receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
|
||||
|
||||
// Process contact operations
|
||||
receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
|
||||
|
||||
// Process group size changes
|
||||
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
|
||||
|
||||
// Send event
|
||||
eventBus.register(SendEvent.class, evt -> {
|
||||
try {
|
||||
sendEvent(evt.get());
|
||||
} catch (final IOException e) {
|
||||
logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
|
||||
}
|
||||
});
|
||||
cacheMap.get(Message.class).setProcessor(eventBus::dispatch);
|
||||
cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch);
|
||||
cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch);
|
||||
cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
|
||||
|
||||
// Request a generator if none is present or the existing one is consumed
|
||||
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator();
|
||||
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext())
|
||||
requestIDGenerator();
|
||||
|
||||
// Relay caches
|
||||
cacheMap.getMap().values().forEach(Cache::relay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the server. The message's status will be incremented once
|
||||
* it was delivered successfully.
|
||||
* Sends an object to the server.
|
||||
*
|
||||
* @param obj the object to send
|
||||
* @throws IllegalStateException if the client is not online
|
||||
* @throws RuntimeException if the object serialization failed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void send(Serializable obj) throws IllegalStateException, RuntimeException {
|
||||
checkOnline();
|
||||
logger.log(Level.FINE, "Sending " + obj);
|
||||
try {
|
||||
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the server. The message's status will be incremented once it was delivered
|
||||
* successfully.
|
||||
*
|
||||
* @param message the message to send
|
||||
* @throws IOException if the message does not reach the server
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void sendMessage(Message message) throws IOException {
|
||||
writeObject(message);
|
||||
public void sendMessage(Message message) {
|
||||
send(message);
|
||||
message.nextStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to the server.
|
||||
*
|
||||
* @param evt the event to send
|
||||
* @throws IOException if the event did not reach the server
|
||||
*/
|
||||
public void sendEvent(Event<?> evt) throws IOException { writeObject(evt); }
|
||||
|
||||
/**
|
||||
* Requests a new {@link IDGenerator} from the server.
|
||||
*
|
||||
* @throws IOException if the request does not reach the server
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void requestIdGenerator() throws IOException {
|
||||
public void requestIDGenerator() {
|
||||
logger.log(Level.INFO, "Requesting new id generator...");
|
||||
writeObject(new IDGeneratorRequest());
|
||||
send(new IDGeneratorRequest());
|
||||
}
|
||||
|
||||
@Event(eventType = HandshakeRejection.class, priority = 1000)
|
||||
private void onHandshakeRejection() {
|
||||
rejected = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException { if (online) socket.close(); }
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 50)
|
||||
public void close() {
|
||||
if (online) {
|
||||
logger.log(Level.INFO, "Closing connection...");
|
||||
try {
|
||||
|
||||
private void writeObject(Object obj) throws IOException {
|
||||
checkOnline();
|
||||
logger.log(Level.FINE, "Sending " + obj);
|
||||
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
|
||||
// The sender must be reset as otherwise the handshake is immediately closed
|
||||
sender = null;
|
||||
online = false;
|
||||
socket.close();
|
||||
} catch (final IOException e) {
|
||||
logger.log(Level.WARNING, "Failed to close socket: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
|
||||
/**
|
||||
* Ensured that the client is online.
|
||||
*
|
||||
* @throws IllegalStateException if the client is not online
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
private void checkOnline() throws IllegalStateException {
|
||||
if (!online)
|
||||
throw new IllegalStateException("Client is not online");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link User} as which this client is logged in
|
||||
@ -230,6 +228,7 @@ public class Client implements Closeable {
|
||||
|
||||
/**
|
||||
* @return the {@link Receiver} used by this {@link Client}
|
||||
* @since v0.2-alpha
|
||||
*/
|
||||
public Receiver getReceiver() { return receiver; }
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.GroupMessageStatusChange;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>GroupMessageStatusChangePocessor.java</strong><br>
|
||||
* Created: <strong>03.07.2020</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupMessageStatusChangeProcessor implements Consumer<GroupMessageStatusChange> {
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
|
||||
|
||||
@Override
|
||||
public void accept(GroupMessageStatusChange evt) {
|
||||
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
|
||||
else EventBus.getInstance().dispatch(evt);
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>MessageStatusChangeProcessor.java</strong><br>
|
||||
* Created: <strong>4 Feb 2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class MessageStatusChangeProcessor implements Consumer<MessageStatusChange> {
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
|
||||
|
||||
/**
|
||||
* Dispatches a {@link MessageStatusChange} if the status is
|
||||
* {@code RECEIVED} or {@code READ}.
|
||||
*
|
||||
* @param evt the status change event
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Override
|
||||
public void accept(MessageStatusChange evt) {
|
||||
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
|
||||
else EventBus.getInstance().dispatch(evt);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.data.GroupMessage;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ReceivedGroupMessageProcessor.java</strong><br>
|
||||
* Created: <strong>13.06.2020</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ReceivedGroupMessageProcessor implements Consumer<GroupMessage> {
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
|
||||
|
||||
@Override
|
||||
public void accept(GroupMessage groupMessage) {
|
||||
if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
|
||||
logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
|
||||
|
||||
// Dispatch event
|
||||
EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ReceivedMessageProcessor.java</strong><br>
|
||||
* Created: <strong>31.12.2019</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class ReceivedMessageProcessor implements Consumer<Message> {
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
|
||||
|
||||
@Override
|
||||
public void accept(Message message) {
|
||||
if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
|
||||
else {
|
||||
// Update status to RECEIVED
|
||||
message.nextStatus();
|
||||
|
||||
// Dispatch event
|
||||
EventBus.getInstance().dispatch(new MessageCreationEvent(message));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +1,30 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.*;
|
||||
import java.net.SocketException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.*;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
import envoy.util.SerializationUtils;
|
||||
import dev.kske.eventbus.*;
|
||||
|
||||
import envoy.util.*;
|
||||
|
||||
/**
|
||||
* Receives objects from the server and passes them to processor objects based
|
||||
* on their class.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Receiver.java</strong><br>
|
||||
* Created: <strong>30.12.2019</strong><br>
|
||||
* Receives objects from the server and passes them to processor objects based on their class.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class Receiver extends Thread {
|
||||
public final class Receiver extends Thread {
|
||||
|
||||
private boolean isAlive = true;
|
||||
|
||||
private final InputStream in;
|
||||
private final Map<Class<?>, Consumer<?>> processors = new HashMap<>();
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
|
||||
|
||||
/**
|
||||
* Creates an instance of {@link Receiver}.
|
||||
@ -40,18 +35,18 @@ public class Receiver extends Thread {
|
||||
public Receiver(InputStream in) {
|
||||
super("Receiver");
|
||||
this.in = in;
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the receiver loop. When an object is read, it is passed to the
|
||||
* appropriate processor.
|
||||
* Starts the receiver loop. When an object is read, it is passed to the appropriate processor.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while (true) {
|
||||
while (isAlive)
|
||||
try {
|
||||
// Read object length
|
||||
final byte[] lenBytes = new byte[4];
|
||||
@ -66,53 +61,79 @@ public class Receiver extends Thread {
|
||||
|
||||
// Catch LV encoding errors
|
||||
if (len != bytesRead) {
|
||||
// Server has stopped sending, i.e. because he went offline
|
||||
if (bytesRead == -1) {
|
||||
isAlive = false;
|
||||
logger.log(Level.INFO,
|
||||
"Lost connection to the server. Exiting receiver...");
|
||||
continue;
|
||||
}
|
||||
logger.log(Level.WARNING,
|
||||
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
|
||||
String.format(
|
||||
"LV encoding violated: expected %d bytes, received %d bytes. Discarding object...",
|
||||
len, bytesRead));
|
||||
continue;
|
||||
}
|
||||
|
||||
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
|
||||
try (ObjectInputStream oin =
|
||||
new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
|
||||
final Object obj = oin.readObject();
|
||||
logger.log(Level.FINE, "Received " + obj);
|
||||
|
||||
// Get appropriate processor
|
||||
@SuppressWarnings("rawtypes")
|
||||
final Consumer processor = processors.get(obj.getClass());
|
||||
if (processor == null)
|
||||
logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
|
||||
else processor.accept(obj);
|
||||
|
||||
// Dispatch to the processor if present
|
||||
if (processor != null)
|
||||
processor.accept(obj);
|
||||
// Dispatch to the event bus if the object is an event without a processor
|
||||
else if (obj instanceof IEvent)
|
||||
eventBus.dispatch((IEvent) obj);
|
||||
// Notify if no processor could be located
|
||||
else
|
||||
logger.log(Level.WARNING,
|
||||
String.format(
|
||||
"The received object has the %s for which no processor is defined.",
|
||||
obj.getClass()));
|
||||
}
|
||||
} catch (final SocketException e) {
|
||||
} catch (final SocketException | EOFException e) {
|
||||
// Connection probably closed by client.
|
||||
logger.log(Level.INFO, "Exiting receiver...");
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Error on receiver thread", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an object processor to this {@link Receiver}. It will be called once an
|
||||
* object of the accepted class has been received.
|
||||
* Adds an object processor to this {@link Receiver}. It will be called once an object of the
|
||||
* accepted class has been received.
|
||||
*
|
||||
* @param processorClass the object class accepted by the processor
|
||||
* @param processor the object processor
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); }
|
||||
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) {
|
||||
processors.put(processorClass, processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a map of object processors to this {@link Receiver}.
|
||||
*
|
||||
*
|
||||
* @param processors the processors to add the processors to add
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); }
|
||||
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) {
|
||||
this.processors.putAll(processors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all object processors registered at this {@link Receiver}.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void removeAllProcessors() { processors.clear(); }
|
||||
public void removeAllProcessors() {
|
||||
processors.clear();
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,21 @@
|
||||
package envoy.client.net;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.*;
|
||||
|
||||
import envoy.client.data.Cache;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.data.Message;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.*;
|
||||
|
||||
/**
|
||||
* Implements methods to send {@link Message}s and
|
||||
* {@link MessageStatusChange}s to the server or cache them inside a
|
||||
* {@link LocalDB} depending on the online status.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>WriteProxy.java</strong><br>
|
||||
* Created: <strong>6 Feb 2020</strong><br>
|
||||
* Implements methods to send {@link Message}s and {@link MessageStatusChange}s to the server or
|
||||
* cache them inside a {@link LocalDB} depending on the online status.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class WriteProxy {
|
||||
public final class WriteProxy {
|
||||
|
||||
private final Client client;
|
||||
private final LocalDB localDB;
|
||||
@ -30,13 +23,11 @@ public class WriteProxy {
|
||||
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
|
||||
|
||||
/**
|
||||
* Initializes a write proxy using a client and a local database. The
|
||||
* corresponding cache processors are injected into the caches.
|
||||
* Initializes a write proxy using a client and a local database. The corresponding cache
|
||||
* processors are injected into the caches.
|
||||
*
|
||||
* @param client the client used to send messages and message status change
|
||||
* events
|
||||
* @param localDB the local database used to cache messages and message status
|
||||
* change events
|
||||
* @param client the client instance used to send messages and events if online
|
||||
* @param localDB the local database used to cache messages and events if offline
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public WriteProxy(Client client, LocalDB localDB) {
|
||||
@ -45,26 +36,17 @@ public class WriteProxy {
|
||||
|
||||
// Initialize cache processors for messages and message status change events
|
||||
localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
|
||||
try {
|
||||
logger.log(Level.FINER, "Sending cached " + msg);
|
||||
client.sendMessage(msg);
|
||||
} catch (final IOException e) {
|
||||
logger.log(Level.SEVERE, "Could not send cached message: ", e);
|
||||
}
|
||||
logger.log(Level.FINER, "Sending cached " + msg);
|
||||
client.sendMessage(msg);
|
||||
});
|
||||
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
|
||||
logger.log(Level.FINER, "Sending cached " + evt);
|
||||
try {
|
||||
client.sendEvent(evt);
|
||||
} catch (final IOException e) {
|
||||
logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
|
||||
}
|
||||
client.send(evt);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the
|
||||
* server.
|
||||
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the server.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@ -73,28 +55,30 @@ public class WriteProxy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers a message to the server if online. Otherwise the message is cached
|
||||
* inside the local database.
|
||||
* Delivers a message to the server if online. Otherwise the message is cached inside the local
|
||||
* database.
|
||||
*
|
||||
* @param message the message to send
|
||||
* @throws IOException if the message could not be sent
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void writeMessage(Message message) throws IOException {
|
||||
if (client.isOnline()) client.sendMessage(message);
|
||||
else localDB.getCacheMap().getApplicable(Message.class).accept(message);
|
||||
public void writeMessage(Message message) {
|
||||
if (client.isOnline())
|
||||
client.sendMessage(message);
|
||||
else
|
||||
localDB.getCacheMap().getApplicable(Message.class).accept(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers a message status change event to the server if online. Otherwise the
|
||||
* event is cached inside the local database.
|
||||
* Delivers a message status change event to the server if online. Otherwise the event is cached
|
||||
* inside the local database.
|
||||
*
|
||||
* @param evt the event to send
|
||||
* @throws IOException if the event could not be sent
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
|
||||
if (client.isOnline()) client.sendEvent(evt);
|
||||
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
|
||||
public void writeMessageStatusChange(MessageStatusChange evt) {
|
||||
if (client.isOnline())
|
||||
client.send(evt);
|
||||
else
|
||||
localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
|
||||
}
|
||||
}
|
||||
|
@ -1,169 +0,0 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
|
||||
/**
|
||||
* This class offers a text field that is automatically equipped with a clear
|
||||
* button.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ClearableTextField.java</strong><br>
|
||||
* Created: <strong>25.06.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ClearableTextField extends GridPane {
|
||||
|
||||
private final TextField textField;
|
||||
|
||||
private final Button clearButton;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ClearableTextField} with no initial text and icon
|
||||
* size 16.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ClearableTextField() { this("", 16); }
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ClearableTextField} with initial text and a
|
||||
* predetermined icon size.
|
||||
*
|
||||
* @param text the text that should be displayed by default
|
||||
* @param size the size of the icon
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ClearableTextField(String text, int size) {
|
||||
// initializing the textField and the button
|
||||
textField = new TextField(text);
|
||||
clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
|
||||
clearButton.setOnAction(e -> textField.clear());
|
||||
clearButton.setFocusTraversable(false);
|
||||
clearButton.getStyleClass().clear();
|
||||
clearButton.setBackground(Background.EMPTY);
|
||||
// Adding the two elements to the GridPane
|
||||
add(textField, 0, 0, 2, 1);
|
||||
add(clearButton, 1, 0, 1, 1);
|
||||
// Setting the percent - widths of the two columns.
|
||||
// Used to locate the button on the right.
|
||||
final var columnConstraints = new ColumnConstraints();
|
||||
columnConstraints.setPercentWidth(90);
|
||||
getColumnConstraints().add(columnConstraints);
|
||||
final var columnConstraints2 = new ColumnConstraints();
|
||||
columnConstraints2.setPercentWidth(10);
|
||||
getColumnConstraints().add(columnConstraints2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the underlying {@code textField}
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public TextField getTextField() { return textField; }
|
||||
|
||||
/**
|
||||
* This method offers the freedom to perform custom actions when the
|
||||
* {@code clearButton} has been pressed.
|
||||
* <p>
|
||||
* The default is
|
||||
* <b><code> e -> {clearableTextField.getTextField().clear();}</code></b>
|
||||
*
|
||||
* @param onClearButtonAction the action that should be performed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void setClearButtonListener(EventHandler<ActionEvent> onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
|
||||
|
||||
/**
|
||||
* @return the current property of the prompt text
|
||||
* @see javafx.scene.control.TextInputControl#promptTextProperty()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
|
||||
|
||||
/**
|
||||
* @return the current prompt text
|
||||
* @see javafx.scene.control.TextInputControl#getPromptText()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final String getPromptText() { return textField.getPromptText(); }
|
||||
|
||||
/**
|
||||
* @param value the prompt text to display
|
||||
* @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final void setPromptText(String value) { textField.setPromptText(value); }
|
||||
|
||||
/**
|
||||
* @return the current property of the tooltip
|
||||
* @see javafx.scene.control.Control#tooltipProperty()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final ObjectProperty<Tooltip> tooltipProperty() { return textField.tooltipProperty(); }
|
||||
|
||||
/**
|
||||
* @param value the new tooltip
|
||||
* @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
|
||||
|
||||
/**
|
||||
* @return the current tooltip
|
||||
* @see javafx.scene.control.Control#getTooltip()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final Tooltip getTooltip() { return textField.getTooltip(); }
|
||||
|
||||
/**
|
||||
* @return the current property of the context menu
|
||||
* @see javafx.scene.control.Control#contextMenuProperty()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final ObjectProperty<ContextMenu> contextMenuProperty() { return textField.contextMenuProperty(); }
|
||||
|
||||
/**
|
||||
* @param value the new context menu
|
||||
* @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
|
||||
|
||||
/**
|
||||
* @return the current context menu
|
||||
* @see javafx.scene.control.Control#getContextMenu()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
|
||||
|
||||
/**
|
||||
* @param value whether this ClearableTextField should be editable
|
||||
* @see javafx.scene.control.TextInputControl#setEditable(boolean)
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final void setEditable(boolean value) { textField.setEditable(value); }
|
||||
|
||||
/**
|
||||
* @return the current property whether this ClearableTextField is editable
|
||||
* @see javafx.scene.control.TextInputControl#editableProperty()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final BooleanProperty editableProperty() { return textField.editableProperty(); }
|
||||
|
||||
/**
|
||||
* @return whether this {@code ClearableTextField} is editable
|
||||
* @see javafx.scene.control.TextInputControl#isEditable()
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final boolean isEditable() { return textField.isEditable(); }
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
/**
|
||||
* This interface defines an action that should be performed when a scene gets
|
||||
* restored from the scene stack in {@link SceneContext}.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Restorable.java</strong><br>
|
||||
* Created: <strong>03.07.2020</strong><br>
|
||||
* This interface defines an action that should be performed when a scene gets restored from the
|
||||
* scene stack in {@link SceneContext}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
@ -16,8 +12,7 @@ public interface Restorable {
|
||||
|
||||
/**
|
||||
* This method is getting called when a scene gets restored.<br>
|
||||
* Hence, it can contain anything that should be done when the underlying scene
|
||||
* gets restored.
|
||||
* Hence, it can contain anything that should be done when the underlying scene gets restored.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
|
@ -4,32 +4,29 @@ import java.io.IOException;
|
||||
import java.util.Stack;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.*;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.event.ThemeChangeEvent;
|
||||
import envoy.event.EventBus;
|
||||
import dev.kske.eventbus.*;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.data.shortcuts.*;
|
||||
import envoy.client.event.*;
|
||||
|
||||
/**
|
||||
* Manages a stack of scenes. The most recently added scene is displayed inside
|
||||
* a stage. When a scene is removed from the stack, its predecessor is
|
||||
* displayed.
|
||||
* Manages a stack of scenes. The most recently added scene is displayed inside a stage. When a
|
||||
* scene is removed from the stack, its predecessor is displayed.
|
||||
* <p>
|
||||
* When a scene is loaded, the style sheet for the current theme is applied to
|
||||
* it.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SceneContext.java</strong><br>
|
||||
* Created: <strong>06.06.2020</strong><br>
|
||||
* When a scene is loaded, the style sheet for the current theme is applied to it.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class SceneContext {
|
||||
public final class SceneContext implements EventListener {
|
||||
|
||||
/**
|
||||
* Contains information about different scenes and their FXML resource files.
|
||||
@ -53,40 +50,21 @@ public final class SceneContext {
|
||||
*/
|
||||
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
|
||||
|
||||
/**
|
||||
* The scene in which the contact search screen is displayed.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
|
||||
|
||||
/**
|
||||
* The scene in which the group creation screen is displayed.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
|
||||
|
||||
/**
|
||||
* The scene in which the login screen is displayed.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
LOGIN_SCENE("/fxml/LoginScene.fxml"),
|
||||
|
||||
/**
|
||||
* The scene in which the info screen is displayed.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
|
||||
LOGIN_SCENE("/fxml/LoginScene.fxml");
|
||||
|
||||
/**
|
||||
* The path to the FXML resource.
|
||||
*/
|
||||
public final String path;
|
||||
|
||||
SceneInfo(String path) { this.path = path; }
|
||||
SceneInfo(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
private final Stage stage;
|
||||
@ -104,7 +82,7 @@ public final class SceneContext {
|
||||
*/
|
||||
public SceneContext(Stage stage) {
|
||||
this.stage = stage;
|
||||
EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS());
|
||||
EventBus.getInstance().registerListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,21 +93,39 @@ public final class SceneContext {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void load(SceneInfo sceneInfo) {
|
||||
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
|
||||
loader.setRoot(null);
|
||||
loader.setController(null);
|
||||
|
||||
try {
|
||||
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
|
||||
final var rootNode =
|
||||
(Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
|
||||
final var scene = new Scene(rootNode);
|
||||
controllerStack.push(loader.getController());
|
||||
final var controller = loader.getController();
|
||||
controllerStack.push(controller);
|
||||
|
||||
sceneStack.push(scene);
|
||||
stage.setScene(scene);
|
||||
applyCSS();
|
||||
|
||||
// Supply the global custom keyboard shortcuts for that scene
|
||||
scene.getAccelerators()
|
||||
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo));
|
||||
|
||||
// Supply the scene specific keyboard shortcuts
|
||||
if (controller instanceof KeyboardMapping)
|
||||
scene.getAccelerators()
|
||||
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
|
||||
|
||||
// The LoginScene is the only scene not intended to be resized
|
||||
// As strange as it seems, this is needed as otherwise the LoginScene won't be
|
||||
// displayed on some OS (...Debian...)
|
||||
stage.sizeToScene();
|
||||
Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
|
||||
applyCSS();
|
||||
stage.show();
|
||||
} catch (final IOException e) {
|
||||
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
|
||||
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE,
|
||||
String.format("Could not load scene for %s: ", sceneInfo), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@ -140,8 +136,12 @@ public final class SceneContext {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void pop() {
|
||||
|
||||
// Pop scene and controller
|
||||
sceneStack.pop();
|
||||
controllerStack.pop();
|
||||
|
||||
// Apply new scene if present
|
||||
if (!sceneStack.isEmpty()) {
|
||||
final var newScene = sceneStack.peek();
|
||||
stage.setScene(newScene);
|
||||
@ -150,7 +150,8 @@ public final class SceneContext {
|
||||
// If the controller implements the Restorable interface,
|
||||
// the actions to perform on restoration will be executed here
|
||||
final var controller = controllerStack.peek();
|
||||
if (controller instanceof Restorable) ((Restorable) controller).onRestore();
|
||||
if (controller instanceof Restorable)
|
||||
((Restorable) controller).onRestore();
|
||||
}
|
||||
stage.show();
|
||||
}
|
||||
@ -160,10 +161,22 @@ public final class SceneContext {
|
||||
final var styleSheets = stage.getScene().getStylesheets();
|
||||
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
|
||||
styleSheets.clear();
|
||||
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm());
|
||||
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(),
|
||||
getClass().getResource(themeCSS).toExternalForm());
|
||||
}
|
||||
}
|
||||
|
||||
@Event(eventType = Logout.class, priority = 150)
|
||||
private void onLogout() {
|
||||
sceneStack.clear();
|
||||
controllerStack.clear();
|
||||
}
|
||||
|
||||
@Event(priority = 150, eventType = ThemeChangeEvent.class)
|
||||
private void onThemeChange() {
|
||||
applyCSS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <T> the type of the controller
|
||||
* @return the controller used by the current scene
|
||||
@ -176,4 +189,10 @@ public final class SceneContext {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Stage getStage() { return stage; }
|
||||
|
||||
/**
|
||||
* @return whether the scene stack is empty
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public boolean isEmpty() { return sceneStack.isEmpty(); }
|
||||
}
|
||||
|
@ -1,33 +1,31 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.io.*;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.ui.controller.LoginScene;
|
||||
import envoy.data.GroupMessage;
|
||||
import envoy.data.Message;
|
||||
import envoy.event.GroupMessageStatusChange;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.data.*;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.*;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.data.shortcuts.EnvoyShortcutConfig;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.ui.controller.LoginScene;
|
||||
import envoy.client.util.IconUtil;
|
||||
|
||||
/**
|
||||
* Handles application startup and shutdown.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Startup.java</strong><br>
|
||||
* Created: <strong>26.03.2020</strong><br>
|
||||
* Handles application startup.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
@ -40,96 +38,210 @@ public final class Startup extends Application {
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static final String VERSION = "0.1-beta";
|
||||
public static final String VERSION = "0.2-beta";
|
||||
|
||||
private LocalDB localDB;
|
||||
private Client client;
|
||||
private static LocalDB localDB;
|
||||
|
||||
private static final Context context = Context.getInstance();
|
||||
private static final Client client = context.getClient();
|
||||
private static final ClientConfig config = ClientConfig.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(Startup.class);
|
||||
|
||||
/**
|
||||
* Loads the configuration, initializes the client and the local database and
|
||||
* delegates the rest of the startup process to {@link LoginScene}.
|
||||
* Loads the configuration, initializes the client and the local database and delegates the rest
|
||||
* of the startup process to {@link LoginScene}.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
|
||||
// Initialize config and logger
|
||||
try {
|
||||
// Load the configuration from client.properties first
|
||||
final Properties properties = new Properties();
|
||||
properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties"));
|
||||
config.load(properties);
|
||||
|
||||
// Override configuration values with command line arguments
|
||||
final String[] args = getParameters().getRaw().toArray(new String[0]);
|
||||
if (args.length > 0) config.load(args);
|
||||
|
||||
// Check if all mandatory configuration values have been initialized
|
||||
if (!config.isInitialized()) throw new EnvoyException("Configuration is not fully initialized");
|
||||
} catch (final Exception e) {
|
||||
config.loadAll(Startup.class, "client.properties",
|
||||
getParameters().getRaw().toArray(new String[0]));
|
||||
EnvoyLog.initialize(config);
|
||||
} catch (final IllegalStateException e) {
|
||||
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
|
||||
logger.log(Level.SEVERE, "Error loading configuration values: ", e);
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// Setup logger for the envoy package
|
||||
EnvoyLog.initialize(config);
|
||||
EnvoyLog.attach("envoy");
|
||||
EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
|
||||
EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
|
||||
|
||||
logger.log(Level.INFO, "Envoy starting...");
|
||||
|
||||
// Initialize the local database
|
||||
if (config.isIgnoreLocalDB()) {
|
||||
localDB = new TransientLocalDB();
|
||||
new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait();
|
||||
} else try {
|
||||
localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
|
||||
} catch (final IOException e3) {
|
||||
logger.log(Level.SEVERE, "Could not initialize local database: ", e3);
|
||||
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
|
||||
try {
|
||||
final var localDBFile = new File(config.getHomeDirectory(), config.getServer());
|
||||
logger.info("Initializing LocalDB at " + localDBFile);
|
||||
localDB = new LocalDB(localDBFile);
|
||||
} catch (IOException | EnvoyException e) {
|
||||
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
|
||||
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize client and unread message cache
|
||||
client = new Client();
|
||||
// Prepare handshake
|
||||
context.setLocalDB(localDB);
|
||||
|
||||
// Configure stage
|
||||
stage.setTitle("Envoy");
|
||||
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
|
||||
|
||||
// Configure global shortcuts
|
||||
EnvoyShortcutConfig.initializeEnvoyShortcuts();
|
||||
|
||||
// Create scene context
|
||||
final var sceneContext = new SceneContext(stage);
|
||||
context.setSceneContext(sceneContext);
|
||||
|
||||
// Authenticate with token if present
|
||||
if (localDB.getAuthToken() != null) {
|
||||
logger.info("Attempting authentication with token...");
|
||||
localDB.loadUserData();
|
||||
if (!performHandshake(
|
||||
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(),
|
||||
VERSION, localDB.getLastSync())))
|
||||
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
||||
} else
|
||||
// Load login scene
|
||||
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to perform a Handshake with the server.
|
||||
*
|
||||
* @param credentials the credentials to use for the handshake
|
||||
* @return whether the handshake was successful or offline mode could be entered
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static boolean performHandshake(LoginCredentials credentials) {
|
||||
final var cacheMap = new CacheMap();
|
||||
cacheMap.put(Message.class, new Cache<Message>());
|
||||
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
|
||||
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
|
||||
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
|
||||
final var originalStatus =
|
||||
localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus();
|
||||
try {
|
||||
client.performHandshake(credentials, cacheMap);
|
||||
if (client.isOnline()) {
|
||||
|
||||
stage.setTitle("Envoy");
|
||||
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
|
||||
|
||||
final var sceneContext = new SceneContext(stage);
|
||||
sceneContext.load(SceneInfo.LOGIN_SCENE);
|
||||
sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext);
|
||||
// Restore the original status as the server automatically returns status ONLINE
|
||||
client.getSender().setStatus(originalStatus);
|
||||
loadChatScene();
|
||||
client.initReceiver(localDB, cacheMap);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
} catch (IOException | InterruptedException | TimeoutException e) {
|
||||
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
|
||||
return attemptOfflineMode(credentials.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the client connection and saves the local database and settings.
|
||||
* Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode for a given
|
||||
* user.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
* @param identifier the identifier of the user - currently his username
|
||||
* @return whether the offline mode could be entered
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
public static boolean attemptOfflineMode(String identifier) {
|
||||
try {
|
||||
logger.log(Level.INFO, "Closing connection...");
|
||||
client.close();
|
||||
|
||||
logger.log(Level.INFO, "Saving local database and settings...");
|
||||
localDB.save();
|
||||
Settings.getInstance().save();
|
||||
logger.log(Level.INFO, "Envoy was terminated by its user");
|
||||
// Try entering offline mode
|
||||
final User clientUser = localDB.getUsers().get(identifier);
|
||||
if (clientUser == null)
|
||||
throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||
client.setSender(clientUser);
|
||||
loadChatScene();
|
||||
return true;
|
||||
} catch (final Exception e) {
|
||||
logger.log(Level.SEVERE, "Unable to save local files: ", e);
|
||||
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
|
||||
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
|
||||
System.exit(1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the last known time a user has been online.
|
||||
*
|
||||
* @param identifier the identifier of this user - currently his name
|
||||
* @return the last {@code Instant} at which he has been online
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static Instant loadLastSync(String identifier) {
|
||||
try {
|
||||
localDB.setUser(localDB.getUsers().get(identifier));
|
||||
localDB.loadUserData();
|
||||
} catch (final Exception e) {
|
||||
// User storage empty, wrong user name etc. -> default lastSync
|
||||
}
|
||||
return localDB.getLastSync();
|
||||
}
|
||||
|
||||
private static void loadChatScene() {
|
||||
|
||||
// Set client user in local database
|
||||
final var user = client.getSender();
|
||||
localDB.setUser(user);
|
||||
|
||||
// Initialize chats in local database
|
||||
try {
|
||||
localDB.loadUserData();
|
||||
} catch (final FileNotFoundException e) {
|
||||
// The local database file has not yet been created, probably first login
|
||||
} catch (final Exception e) {
|
||||
new Alert(AlertType.ERROR,
|
||||
"Error while loading local database: " + e + "\nChats will not be stored locally.")
|
||||
.showAndWait();
|
||||
logger.log(Level.WARNING, "Could not load local database: ", e);
|
||||
}
|
||||
|
||||
context.initWriteProxy();
|
||||
|
||||
if (client.isOnline()) {
|
||||
context.getWriteProxy().flushCache();
|
||||
|
||||
// Inform the server that this user has a different user status than expected
|
||||
if (!user.getStatus().equals(UserStatus.ONLINE))
|
||||
client.send(new UserStatusChange(user));
|
||||
} else
|
||||
|
||||
// Set all contacts to offline mode
|
||||
localDB.getChats()
|
||||
.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(User.class::isInstance)
|
||||
.map(User.class::cast)
|
||||
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
|
||||
|
||||
final var stage = context.getStage();
|
||||
|
||||
// Pop LoginScene if present
|
||||
if (!context.getSceneContext().isEmpty())
|
||||
context.getSceneContext().pop();
|
||||
|
||||
// Load ChatScene
|
||||
stage.setMinHeight(400);
|
||||
stage.setMinWidth(843);
|
||||
context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE);
|
||||
stage.centerOnScreen();
|
||||
|
||||
// Exit or minimize the stage when a close request occurs
|
||||
stage.setOnCloseRequest(
|
||||
e -> {
|
||||
ShutdownHelper.exit();
|
||||
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
|
||||
e.consume();
|
||||
});
|
||||
|
||||
// Initialize status tray icon
|
||||
if (StatusTrayIcon.isSupported())
|
||||
new StatusTrayIcon(stage).show();
|
||||
|
||||
// Start auto save thread
|
||||
localDB.initAutoSave();
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +1,224 @@
|
||||
package envoy.client.ui;
|
||||
|
||||
import static java.awt.Image.SCALE_SMOOTH;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.logging.Level;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
import dev.kske.eventbus.Event;
|
||||
|
||||
import envoy.client.event.MessageCreationEvent;
|
||||
import envoy.data.Message;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.event.*;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.util.*;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>StatusTrayIcon.java</strong><br>
|
||||
* Created: <strong>3 Dec 2019</strong><br>
|
||||
* A tray icon with the Envoy logo, an "Envoy" tool tip and a pop-up menu with menu items for
|
||||
* <ul>
|
||||
* <li>Changing the user status</li>
|
||||
* <li>Logging out</li>
|
||||
* <li>Quitting Envoy</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class StatusTrayIcon {
|
||||
public final class StatusTrayIcon implements EventListener {
|
||||
|
||||
/**
|
||||
* The {@link TrayIcon} provided by the System Tray API for controlling the
|
||||
* system tray. This includes displaying the icon, but also creating
|
||||
* notifications when new messages are received.
|
||||
* The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This
|
||||
* includes displaying the icon, but also creating notifications when new messages are received.
|
||||
*/
|
||||
private final TrayIcon trayIcon;
|
||||
|
||||
/**
|
||||
* A received {@link Message} is only displayed as a system tray notification if
|
||||
* this variable is set to {@code true}.
|
||||
* A received {@link Message} is only displayed as a system tray notification if this variable
|
||||
* is set to {@code true}.
|
||||
*/
|
||||
private boolean displayMessages = false;
|
||||
private boolean displayMessageNotification;
|
||||
|
||||
/**
|
||||
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
|
||||
* menu.
|
||||
*
|
||||
* @param focusTarget the {@link Window} which focus determines if message
|
||||
* notifications are displayed
|
||||
* @throws EnvoyException if the currently used OS does not support the System
|
||||
* Tray API
|
||||
* @since Envoy Client v0.2-alpha
|
||||
* The size of the tray icon's image.
|
||||
*/
|
||||
public StatusTrayIcon(Window focusTarget) throws EnvoyException {
|
||||
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
|
||||
private final Dimension size;
|
||||
|
||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
|
||||
trayIcon = new TrayIcon(img, "Envoy Client");
|
||||
trayIcon.setImageAutoSize(true);
|
||||
trayIcon.setToolTip("You are notified if you have unread messages.");
|
||||
/**
|
||||
* The Envoy logo on which the current user status and unread message count will be drawn to
|
||||
* compose the tray icon.
|
||||
*/
|
||||
private final Image logo;
|
||||
|
||||
final PopupMenu popup = new PopupMenu();
|
||||
private static final Font unreadMessageFont = new Font("sans-serif", Font.PLAIN, 8);
|
||||
|
||||
final MenuItem exitMenuItem = new MenuItem("Exit");
|
||||
exitMenuItem.addActionListener(evt -> System.exit(0));
|
||||
/**
|
||||
* @return {@code true} if the status tray icon is supported on this platform
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public static boolean isSupported() { return SystemTray.isSupported(); }
|
||||
|
||||
/**
|
||||
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up menu.
|
||||
*
|
||||
* @param stage the stage whose focus determines if message notifications are displayed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public StatusTrayIcon(Stage stage) {
|
||||
size = SystemTray.getSystemTray().getTrayIconSize();
|
||||
logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width,
|
||||
size.height, SCALE_SMOOTH);
|
||||
|
||||
final var popup = new PopupMenu();
|
||||
|
||||
// Adding the exit menu item
|
||||
final var exitMenuItem = new MenuItem("Exit");
|
||||
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true));
|
||||
popup.add(exitMenuItem);
|
||||
|
||||
trayIcon.setPopupMenu(popup);
|
||||
// Adding the logout menu item
|
||||
final var logoutMenuItem = new MenuItem("Logout");
|
||||
logoutMenuItem.addActionListener(evt -> Platform.runLater(UserUtil::logout));
|
||||
popup.add(logoutMenuItem);
|
||||
|
||||
// Only display messages if the chat window is not focused
|
||||
focusTarget.addWindowFocusListener(new WindowAdapter() {
|
||||
// Adding the status change items
|
||||
final var statusSubMenu = new Menu("Change status");
|
||||
for (final var status : UserStatus.values()) {
|
||||
final var statusMenuItem = new MenuItem(status.toString().toLowerCase());
|
||||
statusMenuItem
|
||||
.addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status)));
|
||||
statusSubMenu.add(statusMenuItem);
|
||||
}
|
||||
popup.add(statusSubMenu);
|
||||
|
||||
@Override
|
||||
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
|
||||
// Initialize the icon
|
||||
trayIcon = new TrayIcon(createImage(), "Envoy", popup);
|
||||
|
||||
@Override
|
||||
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
|
||||
});
|
||||
// Only display messages if the stage is not focused and the current user status
|
||||
// is not BUSY (if BUSY, displayMessageNotification will be false)
|
||||
stage.focusedProperty()
|
||||
.addListener((ov, wasFocused, isFocused) -> displayMessageNotification =
|
||||
!displayMessageNotification && wasFocused ? false : !isFocused);
|
||||
|
||||
// Listen to changes in the total unread message amount
|
||||
Chat.getTotalUnreadAmount().addListener((ov, oldValue, newValue) -> updateImage());
|
||||
|
||||
// Show the window if the user clicks on the icon
|
||||
trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); });
|
||||
trayIcon.addActionListener(evt -> Platform.runLater(() -> {
|
||||
stage.setIconified(false);
|
||||
stage.toFront();
|
||||
stage.requestFocus();
|
||||
}));
|
||||
|
||||
// Start processing message events
|
||||
// TODO: Handle other message types
|
||||
EventBus.getInstance()
|
||||
.register(MessageCreationEvent.class,
|
||||
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
|
||||
EventBus.getInstance().registerListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this {@link StatusTrayIcon} appear in the system tray.
|
||||
* Makes the icon appear in the system tray.
|
||||
*
|
||||
* @throws EnvoyException if the status icon could not be attaches to the system
|
||||
* tray for system-internal reasons
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void show() throws EnvoyException {
|
||||
public void show() {
|
||||
try {
|
||||
SystemTray.getSystemTray().add(trayIcon);
|
||||
} catch (final AWTException e) {
|
||||
EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e);
|
||||
throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
|
||||
} catch (AWTException e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the icon from the system tray.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
@Event(eventType = Logout.class)
|
||||
public void hide() {
|
||||
SystemTray.getSystemTray().remove(trayIcon);
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onOwnStatusChange(OwnStatusChange statusChange) {
|
||||
displayMessageNotification = !statusChange.get().equals(UserStatus.BUSY);
|
||||
trayIcon.getImage().flush();
|
||||
trayIcon.setImage(createImage());
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onMessage(Message message) {
|
||||
if (displayMessageNotification)
|
||||
trayIcon
|
||||
.displayMessage(message.hasAttachment()
|
||||
? "New " + message.getAttachment().getType().toString().toLowerCase()
|
||||
+ " message received"
|
||||
: "New message received", message.getText(), MessageType.INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tray icon's image by first releasing the resources held by the current image and
|
||||
* then setting a new one generated by the {@link StatusTrayIcon#createImage()} method.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
private void updateImage() {
|
||||
trayIcon.getImage().flush();
|
||||
trayIcon.setImage(createImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes an icon that displays the current user status and the amount of unread messages, if
|
||||
* any are present.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
private BufferedImage createImage() {
|
||||
|
||||
// Create a new image with the dimensions of the logo
|
||||
var img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
// Obtain the draw graphics of the image and copy the logo
|
||||
var g = img.createGraphics();
|
||||
g.drawImage(logo, 0, 0, null);
|
||||
|
||||
// Draw the current user status
|
||||
switch (Context.getInstance().getLocalDB().getUser().getStatus()) {
|
||||
case ONLINE:
|
||||
g.setColor(Color.GREEN);
|
||||
break;
|
||||
case AWAY:
|
||||
g.setColor(Color.ORANGE);
|
||||
break;
|
||||
case BUSY:
|
||||
g.setColor(Color.RED);
|
||||
break;
|
||||
case OFFLINE:
|
||||
g.setColor(Color.GRAY);
|
||||
}
|
||||
g.fillOval(size.width / 2, size.height / 2, size.width / 2, size.height / 2);
|
||||
|
||||
// Draw total amount of unread messages, if any are present
|
||||
if (Chat.getTotalUnreadAmount().get() > 0) {
|
||||
|
||||
// Draw black background circle
|
||||
g.setColor(Color.BLACK);
|
||||
g.fillOval(size.width / 2, 0, size.width / 2, size.height / 2);
|
||||
|
||||
// Unread amount in white
|
||||
String unreadAmount = Chat.getTotalUnreadAmount().get() > 9 ? "9+"
|
||||
: String.valueOf(Chat.getTotalUnreadAmount().get());
|
||||
g.setColor(Color.WHITE);
|
||||
g.setFont(unreadMessageFont);
|
||||
g.drawString(unreadAmount,
|
||||
3 * size.width / 4 - g.getFontMetrics().stringWidth(unreadAmount) / 2,
|
||||
size.height / 2);
|
||||
}
|
||||
|
||||
// Finish drawing
|
||||
g.dispose();
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,194 @@
|
||||
package envoy.client.ui.chatscene;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.function.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
|
||||
import envoy.data.Message;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.data.commands.*;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.ui.controller.ChatScene;
|
||||
import envoy.client.util.*;
|
||||
|
||||
/**
|
||||
* Contains all {@link SystemCommand}s used for {@link envoy.client.ui.controller.ChatScene}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public final class ChatSceneCommands {
|
||||
|
||||
private final ListView<Message> messageList;
|
||||
private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
|
||||
private final SystemCommandBuilder builder =
|
||||
new SystemCommandBuilder(messageTextAreaCommands);
|
||||
|
||||
private static final String messageDependantCommandDescription =
|
||||
" the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message.";
|
||||
|
||||
/**
|
||||
* @param messageList the message list to use for some commands
|
||||
* @param chatScene the instance of {@code ChatScene} that uses this object
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public ChatSceneCommands(ListView<Message> messageList, ChatScene chatScene) {
|
||||
this.messageList = messageList;
|
||||
|
||||
// Error message initialization
|
||||
builder.setAction(text -> { throw new RuntimeException(); })
|
||||
.setDescription("Shows an error message.").buildNoArg("error");
|
||||
|
||||
// Do A Barrel roll initialization
|
||||
final var random = new Random();
|
||||
builder
|
||||
.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)),
|
||||
Double.parseDouble(text.get(1))))
|
||||
.setDefaults(Integer.toString(random.nextInt(3) + 1),
|
||||
Double.toString(random.nextDouble() * 3 + 1))
|
||||
.setDescription("See for yourself :)")
|
||||
.setNumberOfArguments(2)
|
||||
.build("dabr");
|
||||
|
||||
// Logout initialization
|
||||
builder.setAction(text -> UserUtil.logout()).setDescription("Logs you out.")
|
||||
.buildNoArg("logout");
|
||||
|
||||
// Exit initialization
|
||||
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0)
|
||||
.setDescription("Exits the program.").build("exit", false);
|
||||
builder.build("q");
|
||||
|
||||
// Open settings scene initialization
|
||||
builder
|
||||
.setAction(
|
||||
text -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE))
|
||||
.setDescription("Opens the settings screen")
|
||||
.buildNoArg("settings");
|
||||
|
||||
// Status change initialization
|
||||
builder.setAction(text -> {
|
||||
try {
|
||||
UserUtil.changeStatus(Enum.valueOf(UserStatus.class, text.get(0).toUpperCase()));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
final var alert = new Alert(AlertType.ERROR);
|
||||
alert.setContentText("Please provide an existing status");
|
||||
alert.showAndWait();
|
||||
}
|
||||
}).setDescription("Changes your status to the given status.").setNumberOfArguments(1)
|
||||
.setDefaults("").build("status");
|
||||
|
||||
// Selection of a new message initialization
|
||||
messageDependantAction("s",
|
||||
m -> {
|
||||
messageList.getSelectionModel().clearSelection();
|
||||
messageList.getSelectionModel().select(m);
|
||||
},
|
||||
m -> true,
|
||||
"Selects");
|
||||
|
||||
// Copy text of selection initialization
|
||||
messageDependantAction("cp", MessageUtil::copyMessageText, m -> !m.getText().isEmpty(),
|
||||
"Copies the text of");
|
||||
|
||||
// Delete selection initialization
|
||||
messageDependantAction("del", MessageUtil::deleteMessage, m -> true, "Deletes");
|
||||
|
||||
// Save attachment of selection initialization
|
||||
messageDependantAction("save-att", MessageUtil::saveAttachment, Message::hasAttachment,
|
||||
"Saves the attachment of");
|
||||
}
|
||||
|
||||
private void messageDependantAction(String command, Consumer<Message> action,
|
||||
Predicate<Message> additionalCheck, String description) {
|
||||
builder.setAction(text -> {
|
||||
final var positionalArgument = text.get(0).toLowerCase();
|
||||
|
||||
// the currently selected message was requested
|
||||
if (positionalArgument.startsWith("s")) {
|
||||
final var relativeString =
|
||||
positionalArgument.length() == 1 ? "" : positionalArgument.substring(1);
|
||||
|
||||
// Only s has been used as input
|
||||
if (positionalArgument.length() == 1) {
|
||||
final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
|
||||
if (selectedMessage != null && additionalCheck.test(selectedMessage))
|
||||
action.accept(selectedMessage);
|
||||
return;
|
||||
|
||||
// Either s++ or s-- has been requested
|
||||
} else if (relativeString.equals("++") || relativeString.equals("--"))
|
||||
selectionNeighbor(action, additionalCheck, positionalArgument);
|
||||
|
||||
// A message relative to the currently selected message should be used (i.e.
|
||||
// s+4)
|
||||
else
|
||||
useRelativeMessage(command, action, additionalCheck, relativeString, true);
|
||||
|
||||
// Either ++s or --s has been requested
|
||||
} else if (positionalArgument.equals("--s") || positionalArgument.equals("++s"))
|
||||
selectionNeighbor(action, additionalCheck, positionalArgument);
|
||||
|
||||
// Just a number is expected: ((+)4)
|
||||
else
|
||||
useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
|
||||
}).setDefaults("s").setNumberOfArguments(1)
|
||||
.setDescription(description.concat(messageDependantCommandDescription)).build(command);
|
||||
}
|
||||
|
||||
private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck,
|
||||
final String positionalArgument) {
|
||||
final var wantedIndex = messageList.getSelectionModel().getSelectedIndex()
|
||||
+ (positionalArgument.contains("+") ? 1 : -1);
|
||||
messageList.getSelectionModel().clearAndSelect(wantedIndex);
|
||||
final var selectedMessage = messageList.getItems().get(wantedIndex);
|
||||
if (selectedMessage != null && additionalCheck.test(selectedMessage))
|
||||
action.accept(selectedMessage);
|
||||
}
|
||||
|
||||
private void useRelativeMessage(String command, Consumer<Message> action,
|
||||
Predicate<Message> additionalCheck, final String positionalArgument,
|
||||
boolean useSelectedMessage) throws NumberFormatException {
|
||||
final var stripPlus =
|
||||
positionalArgument.startsWith("+") ? positionalArgument.substring(1)
|
||||
: positionalArgument;
|
||||
final var incDec = Integer.valueOf(stripPlus);
|
||||
try {
|
||||
|
||||
// The currently selected message is the base message
|
||||
if (useSelectedMessage) {
|
||||
final var messageToUse = messageList.getItems()
|
||||
.get(messageList.getSelectionModel().getSelectedIndex() + incDec);
|
||||
if (messageToUse != null && additionalCheck.test(messageToUse))
|
||||
action.accept(messageToUse);
|
||||
|
||||
// The currently upmost completely visible message is the base message
|
||||
} else {
|
||||
final var messageToUse = messageList.getItems()
|
||||
.get(((VirtualFlow<?>) messageList.lookup(".virtual-flow"))
|
||||
.getFirstVisibleCell().getIndex() + 1 + incDec);
|
||||
if (messageToUse != null && additionalCheck.test(messageToUse))
|
||||
action.accept(messageToUse);
|
||||
}
|
||||
} catch (final IndexOutOfBoundsException e) {
|
||||
EnvoyLog.getLogger(ChatSceneCommands.class)
|
||||
.log(Level.INFO,
|
||||
" A non-existing message was requested by the user for System command "
|
||||
+ command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the map used by this {@code ChatSceneCommands}
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public SystemCommandMap getChatSceneCommands() { return messageTextAreaCommands; }
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package envoy.client.ui.chatscene;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.event.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
|
||||
/**
|
||||
* Displays a context menu that offers an additional option when one of its menu items has been
|
||||
* clicked.
|
||||
* <p>
|
||||
* Current options are:
|
||||
* <ul>
|
||||
* <li>undo</li>
|
||||
* <li>redo</li>
|
||||
* <li>cut</li>
|
||||
* <li>copy</li>
|
||||
* <li>paste</li>
|
||||
* <li>delete</li>
|
||||
* <li>clear</li>
|
||||
* <li>Select all</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is
|
||||
* already used by this component
|
||||
*/
|
||||
public class TextInputContextMenu extends ContextMenu {
|
||||
|
||||
private final MenuItem undoMI = new MenuItem("Undo");
|
||||
private final MenuItem redoMI = new MenuItem("Redo");
|
||||
private final MenuItem cutMI = new MenuItem("Cut");
|
||||
private final MenuItem copyMI = new MenuItem("Copy");
|
||||
private final MenuItem pasteMI = new MenuItem("Paste");
|
||||
private final MenuItem deleteMI = new MenuItem("Delete selection");
|
||||
private final MenuItem clearMI = new MenuItem("Clear");
|
||||
private final MenuItem selectAllMI = new MenuItem("Select all");
|
||||
|
||||
/**
|
||||
* Creates a new {@code TextInputContextMenu} with an optional action when this menu was
|
||||
* clicked. Currently shows:
|
||||
* <ul>
|
||||
* <li>undo</li>
|
||||
* <li>redo</li>
|
||||
* <li>cut</li>
|
||||
* <li>copy</li>
|
||||
* <li>paste</li>
|
||||
* <li>delete</li>
|
||||
* <li>clear</li>
|
||||
* <li>Select all</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param control the text input component to display this {@code ContextMenu}
|
||||
* @param menuItemClicked the second action to perform when a menu item of this context menu has
|
||||
* been clicked
|
||||
* @since Envoy Client v0.2-beta
|
||||
* @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is
|
||||
* already used by this component
|
||||
*/
|
||||
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
|
||||
|
||||
// Define the actions when clicked
|
||||
undoMI.setOnAction(addAction(e -> control.undo(), menuItemClicked));
|
||||
redoMI.setOnAction(addAction(e -> control.redo(), menuItemClicked));
|
||||
cutMI.setOnAction(addAction(e -> control.cut(), menuItemClicked));
|
||||
copyMI.setOnAction(addAction(e -> control.copy(), menuItemClicked));
|
||||
pasteMI.setOnAction(addAction(e -> control.paste(), menuItemClicked));
|
||||
deleteMI.setOnAction(addAction(e -> control.replaceSelection(""), menuItemClicked));
|
||||
clearMI.setOnAction(addAction(e -> control.setText(""), menuItemClicked));
|
||||
selectAllMI.setOnAction(addAction(e -> control.selectAll(), menuItemClicked));
|
||||
|
||||
// Define the times it will be disabled
|
||||
undoMI.disableProperty().bind(control.undoableProperty().not());
|
||||
redoMI.disableProperty().bind(control.redoableProperty().not());
|
||||
cutMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||
copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||
deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
|
||||
clearMI.disableProperty().bind(control.textProperty().isEmpty());
|
||||
selectAllMI.disableProperty().bind(control.textProperty().isEmpty());
|
||||
setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()));
|
||||
|
||||
selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
|
||||
|
||||
// Add all items to the ContextMenu
|
||||
getItems().add(undoMI);
|
||||
getItems().add(redoMI);
|
||||
getItems().add(new SeparatorMenuItem());
|
||||
getItems().add(cutMI);
|
||||
getItems().add(copyMI);
|
||||
getItems().add(pasteMI);
|
||||
getItems().add(new SeparatorMenuItem());
|
||||
getItems().add(deleteMI);
|
||||
getItems().add(clearMI);
|
||||
getItems().add(new SeparatorMenuItem());
|
||||
getItems().add(selectAllMI);
|
||||
}
|
||||
|
||||
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction,
|
||||
Consumer<ActionEvent> additionalAction) {
|
||||
return e -> {
|
||||
originalAction.accept(e);
|
||||
additionalAction.accept(e);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Contains classes that influence the appearance and behavior of ChatScene.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
package envoy.client.ui.chatscene;
|
@ -1,30 +1,25 @@
|
||||
package envoy.client.ui;
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
import envoy.client.data.audio.AudioPlayer;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.audio.AudioPlayer;
|
||||
|
||||
/**
|
||||
* Enables the play back of audio clips through a button.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioControl.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioControl extends HBox {
|
||||
|
||||
private AudioPlayer player = new AudioPlayer();
|
||||
private final AudioPlayer player = new AudioPlayer();
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
|
||||
|
@ -0,0 +1,63 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.util.IconUtil;
|
||||
|
||||
/**
|
||||
* Displays a chat using a contact control for the recipient and a label for the unread message
|
||||
* count.
|
||||
*
|
||||
* @see ContactControl
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class ChatControl extends HBox {
|
||||
|
||||
private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
|
||||
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
|
||||
|
||||
/**
|
||||
* Creates a new {@code ChatControl}.
|
||||
*
|
||||
* @param chat the chat to display
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ChatControl(Chat chat) {
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setPadding(new Insets(0, 0, 3, 0));
|
||||
|
||||
// Profile picture
|
||||
var contactProfilePic =
|
||||
new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32);
|
||||
getChildren().add(contactProfilePic);
|
||||
|
||||
// Spacing
|
||||
var leftSpacing = new Region();
|
||||
leftSpacing.setPrefSize(8, 0);
|
||||
leftSpacing.setMinSize(8, 0);
|
||||
leftSpacing.setMaxSize(8, 0);
|
||||
getChildren().add(leftSpacing);
|
||||
|
||||
// Contact control
|
||||
getChildren().add(new ContactControl(chat.getRecipient()));
|
||||
|
||||
// Unread messages
|
||||
if (chat.getUnreadAmount() != 0) {
|
||||
var spacing = new Region();
|
||||
setHgrow(spacing, Priority.ALWAYS);
|
||||
getChildren().add(spacing);
|
||||
var unreadMessagesLabel = new Label(
|
||||
chat.getUnreadAmount() > 99 ? "99+" : String.valueOf(chat.getUnreadAmount()));
|
||||
unreadMessagesLabel.setMinSize(15, 15);
|
||||
unreadMessagesLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||
unreadMessagesLabel.getStyleClass().add("unread-messages-amount");
|
||||
getChildren().add(unreadMessagesLabel);
|
||||
}
|
||||
getStyleClass().add("list-element");
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.data.*;
|
||||
|
||||
/**
|
||||
* Displays information about a contact in two rows. The first row contains the name. The second row
|
||||
* contains the online status (user) or the member count (group).
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class ContactControl extends VBox {
|
||||
|
||||
private final Contact contact;
|
||||
|
||||
/**
|
||||
* @param contact the contact to display
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ContactControl(Contact contact) {
|
||||
this.contact = contact;
|
||||
|
||||
// Name label
|
||||
final var nameLabel = new Label(contact.getName());
|
||||
getChildren().add(nameLabel);
|
||||
|
||||
// Online status (user) or member count (group)
|
||||
getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact)
|
||||
: new GroupSizeLabel((Group) contact));
|
||||
|
||||
getStyleClass().add("list-element");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the info label of this {@code ContactControl} with an updated version.
|
||||
* <p>
|
||||
* This method should be called when the status of the underlying user or the size of the
|
||||
* underlying group has changed.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
* @apiNote will produce buggy results if contact control gets updated so that the info label is
|
||||
* no longer on index 1.
|
||||
*/
|
||||
public void replaceInfoLabel() {
|
||||
getChildren().set(1, contact instanceof User ? new UserStatusLabel((User) contact)
|
||||
: new GroupSizeLabel((Group) contact));
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
import envoy.data.Group;
|
||||
|
||||
/**
|
||||
* Displays the amount of members in a {@link Group}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public final class GroupSizeLabel extends Label {
|
||||
|
||||
/**
|
||||
* @param recipient the group whose members to show
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public GroupSizeLabel(Group recipient) {
|
||||
super(recipient.getContacts().size() + " member"
|
||||
+ (recipient.getContacts().size() != 1 ? "s" : ""));
|
||||
}
|
||||
}
|
165
client/src/main/java/envoy/client/ui/control/MessageControl.java
Normal file
165
client/src/main/java/envoy/client/ui/control/MessageControl.java
Normal file
@ -0,0 +1,165 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import envoy.data.*;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.util.*;
|
||||
|
||||
/**
|
||||
* This class transforms a single {@link Message} into a UI component.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class MessageControl extends Label {
|
||||
|
||||
private final boolean ownMessage;
|
||||
|
||||
private final LocalDB localDB = context.getLocalDB();
|
||||
private final Client client = context.getClient();
|
||||
|
||||
private static final Context context = Context.getInstance();
|
||||
private static final DateTimeFormatter dateFormat =
|
||||
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
private static final Map<MessageStatus, Image> statusImages =
|
||||
IconUtil.loadByEnum(MessageStatus.class, 16);
|
||||
private static final Logger logger =
|
||||
EnvoyLog.getLogger(MessageControl.class);
|
||||
|
||||
/**
|
||||
* @param message the message that should be formatted
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public MessageControl(Message message) {
|
||||
ownMessage = message.getSenderID() == localDB.getUser().getID();
|
||||
|
||||
// Creating the underlying VBox and the dateLabel
|
||||
final var hbox = new HBox();
|
||||
if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) {
|
||||
// Displaying the name of the sender in a group
|
||||
final var label = new Label();
|
||||
label.getStyleClass().add("group-member-names");
|
||||
label.setText(localDB.getUsers()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(c -> c.getID() == message.getSenderID())
|
||||
.findFirst()
|
||||
.map(User::getName)
|
||||
.orElse("Unknown User"));
|
||||
label.setPadding(new Insets(0, 5, 0, 0));
|
||||
hbox.getChildren().add(label);
|
||||
}
|
||||
hbox.getChildren().add(new Label(dateFormat.format(message.getCreationDate())));
|
||||
final var vbox = new VBox(hbox);
|
||||
|
||||
// Creating the actions for the MenuItems
|
||||
final var contextMenu = new ContextMenu();
|
||||
final var items = contextMenu.getItems();
|
||||
|
||||
// Copy message action
|
||||
if (!message.getText().isEmpty()) {
|
||||
final var copyMenuItem = new MenuItem("Copy Text");
|
||||
copyMenuItem.setOnAction(e -> MessageUtil.copyMessageText(message));
|
||||
items.add(copyMenuItem);
|
||||
}
|
||||
|
||||
// Delete message
|
||||
final var deleteMenuItem = new MenuItem("Delete locally");
|
||||
deleteMenuItem.setOnAction(e -> MessageUtil.deleteMessage(message));
|
||||
items.add(deleteMenuItem);
|
||||
|
||||
// As long as these types of messages are not implemented and no caches are
|
||||
// defined for them, we only want them to appear when being online
|
||||
if (client.isOnline()) {
|
||||
|
||||
// Forward menu item
|
||||
final var forwardMenuItem = new MenuItem("Forward");
|
||||
forwardMenuItem.setOnAction(e -> MessageUtil.forwardMessage(message));
|
||||
items.add(forwardMenuItem);
|
||||
|
||||
// Quote menu item
|
||||
final var quoteMenuItem = new MenuItem("Quote");
|
||||
quoteMenuItem.setOnAction(e -> MessageUtil.quoteMessage(message));
|
||||
items.add(quoteMenuItem);
|
||||
}
|
||||
|
||||
// Info actions
|
||||
final var infoMenuItem = new MenuItem("Info");
|
||||
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
|
||||
items.add(infoMenuItem);
|
||||
|
||||
// Handling message attachment display
|
||||
// TODO: Add missing attachment types
|
||||
if (message.hasAttachment()) {
|
||||
switch (message.getAttachment().getType()) {
|
||||
case PICTURE:
|
||||
vbox.getChildren()
|
||||
.add(new ImageView(
|
||||
new Image(new ByteArrayInputStream(message.getAttachment().getData()),
|
||||
256, 256, true, true)));
|
||||
break;
|
||||
case VIDEO:
|
||||
break;
|
||||
case VOICE:
|
||||
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
|
||||
break;
|
||||
case DOCUMENT:
|
||||
break;
|
||||
}
|
||||
final var saveAttachment = new MenuItem("Save attachment");
|
||||
saveAttachment.setOnAction(e -> MessageUtil.saveAttachment(message));
|
||||
items.add(saveAttachment);
|
||||
}
|
||||
// Creating the textLabel
|
||||
final var textLabel = new Label(message.getText());
|
||||
textLabel.setMaxWidth(430);
|
||||
textLabel.setWrapText(true);
|
||||
final var hBoxBottom = new HBox();
|
||||
hBoxBottom.getChildren().add(textLabel);
|
||||
// Setting the message status icon and background color
|
||||
if (message.getSenderID() == localDB.getUser().getID()) {
|
||||
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
|
||||
statusIcon.setPreserveRatio(true);
|
||||
final var space = new Region();
|
||||
HBox.setHgrow(space, Priority.ALWAYS);
|
||||
hBoxBottom.getChildren().add(space);
|
||||
hBoxBottom.getChildren().add(statusIcon);
|
||||
hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
getStyleClass().add("own-message");
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
} else
|
||||
getStyleClass().add("received-message");
|
||||
vbox.getChildren().add(hBoxBottom);
|
||||
// Adjusting height and weight of the cell to the corresponding ListView
|
||||
paddingProperty().setValue(new Insets(5, 20, 5, 20));
|
||||
setContextMenu(contextMenu);
|
||||
setGraphic(vbox);
|
||||
}
|
||||
|
||||
private void loadMessageInfoScene(Message message) {
|
||||
logger.log(Level.FINEST, "message info scene was requested for " + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the message stored by this {@code MessageControl} has been sent by this user
|
||||
* of Envoy
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isOwnMessage() { return ownMessage; }
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
/**
|
||||
* Provides a set of convenience constructors for images that are displayed as profile pictures.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class ProfilePicImageView extends ImageView {
|
||||
|
||||
/**
|
||||
* Creates a new {@code ProfilePicImageView} without a default image.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ProfilePicImageView() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ProfilePicImageView}.
|
||||
*
|
||||
* @param image the image to display
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ProfilePicImageView(Image image) {
|
||||
this(image, 40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ProfilePicImageView}.
|
||||
*
|
||||
* @param image the image to display
|
||||
* @param sizeAndRounding the size and rounding for a circular {@code ProfilePicImageView}
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ProfilePicImageView(Image image, double sizeAndRounding) {
|
||||
this(image, sizeAndRounding, sizeAndRounding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code ProfilePicImageView}.
|
||||
*
|
||||
* @param image the image to display
|
||||
* @param size the size of this {@code ProfilePicImageView}
|
||||
* @param rounding how rounded this {@code ProfilePicImageView} should be
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public ProfilePicImageView(Image image, double size, double rounding) {
|
||||
super(image);
|
||||
final var clip = new Rectangle();
|
||||
clip.setWidth(size);
|
||||
clip.setHeight(size);
|
||||
clip.setArcHeight(rounding);
|
||||
clip.setArcWidth(rounding);
|
||||
setClip(clip);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
import envoy.data.User;
|
||||
|
||||
import envoy.client.util.IconUtil;
|
||||
|
||||
/**
|
||||
* Displays an {@link User} as a quick select control which is used in the quick select list.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class QuickSelectControl extends VBox {
|
||||
|
||||
private final User user;
|
||||
|
||||
/**
|
||||
* Creates an instance of the {@code QuickSelectControl}.
|
||||
*
|
||||
* @param user the contact whose data is used to create this instance.
|
||||
* @param action the action to perform when a contact is removed with this control as a
|
||||
* parameter
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public QuickSelectControl(User user, Consumer<QuickSelectControl> action) {
|
||||
this.user = user;
|
||||
setPadding(new Insets(1, 0, 0, 0));
|
||||
setPrefWidth(37);
|
||||
setMaxWidth(37);
|
||||
setMinWidth(37);
|
||||
var stackPane = new StackPane();
|
||||
stackPane.setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
// Profile picture
|
||||
var picHold = new VBox();
|
||||
picHold.setPadding(new Insets(2, 0, 0, 0));
|
||||
picHold.setPrefHeight(35);
|
||||
picHold.setMaxHeight(35);
|
||||
picHold.setMinHeight(35);
|
||||
var contactProfilePic =
|
||||
new ImageView(IconUtil.loadIconThemeSensitive("user_icon", 32));
|
||||
final var clip = new Rectangle();
|
||||
clip.setWidth(32);
|
||||
clip.setHeight(32);
|
||||
clip.setArcHeight(32);
|
||||
clip.setArcWidth(32);
|
||||
contactProfilePic.setClip(clip);
|
||||
picHold.getChildren().add(contactProfilePic);
|
||||
stackPane.getChildren().add(picHold);
|
||||
|
||||
var hBox = new HBox();
|
||||
hBox.setPrefHeight(12);
|
||||
hBox.setMaxHeight(12);
|
||||
hBox.setMinHeight(12);
|
||||
var region = new Region();
|
||||
hBox.getChildren().add(region);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
|
||||
var removeBtn = new Button();
|
||||
removeBtn.setPrefSize(12, 12);
|
||||
removeBtn.setMaxSize(12, 12);
|
||||
removeBtn.setMinSize(12, 12);
|
||||
removeBtn.setOnMouseClicked(evt -> action.accept(this));
|
||||
removeBtn.setId("remove-button");
|
||||
hBox.getChildren().add(removeBtn);
|
||||
stackPane.getChildren().add(hBox);
|
||||
getChildren().add(stackPane);
|
||||
|
||||
var nameLabel = new Label();
|
||||
nameLabel.setPrefSize(35, 20);
|
||||
nameLabel.setMaxSize(35, 20);
|
||||
nameLabel.setMinSize(35, 20);
|
||||
nameLabel.setText(user.getName());
|
||||
nameLabel.setAlignment(Pos.TOP_CENTER);
|
||||
nameLabel.setPadding(new Insets(0, 5, 0, 0));
|
||||
getChildren().add(nameLabel);
|
||||
|
||||
getStyleClass().add("quick-select");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user whose data is used in this instance
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public User getUser() { return user; }
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package envoy.client.ui.control;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
import envoy.data.User;
|
||||
|
||||
/**
|
||||
* Displays the status of a {@link User}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public final class UserStatusLabel extends Label {
|
||||
|
||||
/**
|
||||
* @param user the user whose status to display
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public UserStatusLabel(User user) {
|
||||
super(user.getStatus().toString());
|
||||
getStyleClass().add(user.getStatus().toString().toLowerCase());
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Defines custom UI controls.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
package envoy.client.ui.control;
|
File diff suppressed because it is too large
Load Diff
@ -1,130 +0,0 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.event.ElementOperation;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.event.contact.ContactSearchRequest;
|
||||
import envoy.event.contact.ContactSearchResult;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
||||
* Created: <strong>07.06.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ContactSearchScene {
|
||||
|
||||
@FXML
|
||||
private ClearableTextField searchBar;
|
||||
|
||||
@FXML
|
||||
private ListView<Chat> chatList;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private LocalDB localDB;
|
||||
|
||||
private static EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @param localDB the local database to which new contacts are added
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
||||
this.sceneContext = sceneContext;
|
||||
this.localDB = localDB;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
chatList.setCellFactory(ContactListCellFactory::new);
|
||||
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
|
||||
eventBus.register(ContactSearchResult.class,
|
||||
response -> Platform.runLater(() -> {
|
||||
chatList.getItems().clear();
|
||||
chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the clear and search button if no text is present in the search bar.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void sendRequest() {
|
||||
final var text = searchBar.getTextField().getText().strip();
|
||||
if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
|
||||
else chatList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the text in the search bar and the items shown in the list.
|
||||
* Additionally disables both clear and search button.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void clear() {
|
||||
searchBar.getTextField().setText(null);
|
||||
chatList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link ContactOperation} for every selected contact to the
|
||||
* server.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
final var chat = chatList.getSelectionModel().getSelectedItem();
|
||||
if (chat != null) {
|
||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setTitle("Add Contact to Contact List");
|
||||
alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
|
||||
// Normally, this would be total BS (we are already on the FX Thread), however
|
||||
// it could be proven that the creation of this dialog wrapped in
|
||||
// Platform.runLater is less error-prone than without it
|
||||
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
|
||||
final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
|
||||
// Sends the event to the server
|
||||
eventBus.dispatch(new SendEvent(event));
|
||||
// Updates the UI
|
||||
eventBus.dispatch(event);
|
||||
logger.log(Level.INFO, "Added contact " + chat.getRecipient());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void newGroupButtonClicked() {
|
||||
sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
|
||||
sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
|
||||
import envoy.data.User;
|
||||
import envoy.event.ElementOperation;
|
||||
import envoy.event.contact.*;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.event.BackEvent;
|
||||
import envoy.client.helper.AlertHelper;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.ui.control.ContactControl;
|
||||
import envoy.client.ui.listcell.ListCellFactory;
|
||||
|
||||
/**
|
||||
* Provides a search bar in which a user name (substring) can be entered. The users with a matching
|
||||
* name are then displayed inside a list view. A {@link UserSearchRequest} is sent on every
|
||||
* keystroke.
|
||||
* <p>
|
||||
* <i>The actual search algorithm is implemented on the server.
|
||||
* <p>
|
||||
* To create a group, a button is available that loads the {@link GroupCreationTab}.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ContactSearchTab implements EventListener {
|
||||
|
||||
@FXML
|
||||
private TextArea searchBar;
|
||||
|
||||
@FXML
|
||||
private ListView<User> userList;
|
||||
|
||||
private User currentlySelectedUser;
|
||||
|
||||
private final Alert alert = new Alert(AlertType.CONFIRMATION);
|
||||
|
||||
private static final Client client = Context.getInstance().getClient();
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
eventBus.registerListener(this);
|
||||
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
|
||||
alert.setTitle("Add User?");
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onUserSearchResult(UserSearchResult result) {
|
||||
Platform.runLater(() -> {
|
||||
userList.getItems().clear();
|
||||
userList.getItems().addAll(result.get());
|
||||
});
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
final var contact = operation.get();
|
||||
if (operation.getOperationType() == ElementOperation.ADD)
|
||||
Platform.runLater(() -> {
|
||||
userList.getItems().remove(contact);
|
||||
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact)
|
||||
&& alert.isShowing())
|
||||
alert.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If text is present, sends a request to the server.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void sendRequest() {
|
||||
final var text = searchBar.getText().strip();
|
||||
if (!text.isBlank())
|
||||
client.send(new UserSearchRequest(text));
|
||||
else
|
||||
userList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the text in the search bar and the items shown in the list. Additionally disables both
|
||||
* clear and search button.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void clear() {
|
||||
searchBar.setText(null);
|
||||
userList.getItems().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link UserOperation} for the selected user to the server.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void userListClicked() {
|
||||
final var user = userList.getSelectionModel().getSelectedItem();
|
||||
if (user != null) {
|
||||
currentlySelectedUser = user;
|
||||
alert.setContentText(
|
||||
"Add user " + currentlySelectedUser.getName() + " to your contacts?");
|
||||
AlertHelper.confirmAction(alert, this::addAsContact);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAsContact() {
|
||||
|
||||
// Sends the event to the server
|
||||
final var event = new UserOperation(currentlySelectedUser, ElementOperation.ADD);
|
||||
client.send(event);
|
||||
|
||||
// Removes the chosen user and updates the UI
|
||||
userList.getItems().remove(currentlySelectedUser);
|
||||
eventBus.dispatch(event);
|
||||
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() {
|
||||
searchBar.setText("");
|
||||
eventBus.dispatch(new BackEvent());
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.client.data.LocalDB;
|
||||
import envoy.client.event.SendEvent;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.listcell.ContactListCellFactory;
|
||||
import envoy.data.Group;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.GroupCreation;
|
||||
import envoy.util.Bounds;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ContactSearchSceneController.java</strong><br>
|
||||
* Created: <strong>07.06.2020</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupCreationScene {
|
||||
|
||||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
@FXML
|
||||
private ClearableTextField groupNameField;
|
||||
|
||||
@FXML
|
||||
private ListView<Chat> chatList;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
chatList.setCellFactory(ContactListCellFactory::new);
|
||||
chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @param localDB the local database from which potential group members can
|
||||
* be selected
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
|
||||
this.sceneContext = sceneContext;
|
||||
Platform.runLater(() -> chatList.getItems()
|
||||
.addAll(localDB.getChats()
|
||||
.stream()
|
||||
.filter(c -> !(c.getRecipient() instanceof Group))
|
||||
.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the {@code createButton} if at least one contact is selected.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, whether the {@code createButton} can be enabled because text is
|
||||
* present in the textfield.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
|
||||
|
||||
/**
|
||||
* Sends a {@link GroupCreation} to the server and closes this scene.
|
||||
* <p>
|
||||
* If the given group name is not valid, an error is displayed instead.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void createButtonClicked() {
|
||||
final var name = groupNameField.getTextField().getText();
|
||||
if (!Bounds.isValidContactName(name)) {
|
||||
new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
||||
groupNameField.getTextField().clear();
|
||||
} else {
|
||||
eventBus.dispatch(new SendEvent(new GroupCreation(name,
|
||||
chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
|
||||
new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
|
||||
sceneContext.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
|
||||
import envoy.data.*;
|
||||
import envoy.event.GroupCreation;
|
||||
import envoy.event.contact.UserOperation;
|
||||
import envoy.util.Bounds;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.event.BackEvent;
|
||||
import envoy.client.ui.control.*;
|
||||
import envoy.client.ui.listcell.ListCellFactory;
|
||||
|
||||
/**
|
||||
* Provides a group creation interface. A group name can be entered in the text field at the top.
|
||||
* Available users (local chat recipients) are displayed inside a list and can be selected (multiple
|
||||
* selection available).
|
||||
* <p>
|
||||
* When the group creation button is pressed, a {@link GroupCreation} is sent to the server. This
|
||||
* controller enforces a valid group name and a non-empty member list (excluding the client user).
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupCreationTab implements EventListener {
|
||||
|
||||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
@FXML
|
||||
private Button cancelButton;
|
||||
|
||||
@FXML
|
||||
private TextArea groupNameField;
|
||||
|
||||
@FXML
|
||||
private ListView<User> userList;
|
||||
|
||||
@FXML
|
||||
private Label errorMessageLabel;
|
||||
|
||||
@FXML
|
||||
private Button proceedDuplicateButton;
|
||||
|
||||
@FXML
|
||||
private Button cancelDuplicateButton;
|
||||
|
||||
@FXML
|
||||
private HBox errorProceedBox;
|
||||
|
||||
@FXML
|
||||
private ListView<QuickSelectControl> quickSelectList;
|
||||
|
||||
private String name;
|
||||
|
||||
private final LocalDB localDB = Context.getInstance().getLocalDB();
|
||||
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
|
||||
createButton.setDisable(true);
|
||||
eventBus.registerListener(this);
|
||||
userList.getItems()
|
||||
.addAll(localDB.getChats()
|
||||
.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(User.class::isInstance)
|
||||
.filter(not(localDB.getUser()::equals))
|
||||
.map(User.class::cast)
|
||||
.collect(Collectors.toList()));
|
||||
resizeQuickSelectSpace(0);
|
||||
quickSelectList.addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the {@code createButton} if at least one contact is selected.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void userListClicked() {
|
||||
if (userList.getSelectionModel().getSelectedItem() != null) {
|
||||
quickSelectList.getItems().add(new QuickSelectControl(
|
||||
userList.getSelectionModel().getSelectedItem(), this::removeFromQuickSelection));
|
||||
createButton.setDisable(
|
||||
quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
|
||||
resizeQuickSelectSpace(60);
|
||||
userList.getItems().remove(userList.getSelectionModel().getSelectedItem());
|
||||
userList.getSelectionModel().clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, whether the {@code createButton} can be enabled because text is present in the text
|
||||
* field.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void textUpdated() {
|
||||
createButton
|
||||
.setDisable(quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a {@link GroupCreation} to the server and closes this scene.
|
||||
* <p>
|
||||
* If the given group name is not valid, an error is displayed instead.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@FXML
|
||||
private void createButtonClicked() {
|
||||
name = groupNameField.getText();
|
||||
if (!Bounds.isValidContactName(name)) {
|
||||
setErrorMessageLabelSize(30);
|
||||
errorMessageLabel.setText("The group name is not valid!");
|
||||
groupNameField.clear();
|
||||
} else if (groupNameAlreadyPresent(name)) {
|
||||
setErrorMessageLabelSize(30);
|
||||
errorMessageLabel.setText("Name does already exist! Proceed anyways?");
|
||||
setProcessPaneSize(30);
|
||||
createButton.setDisable(true);
|
||||
cancelButton.setDisable(true);
|
||||
} else {
|
||||
createGroup(name);
|
||||
eventBus.dispatch(new BackEvent());
|
||||
// Restoring the original design as tabs will always be reused
|
||||
setErrorMessageLabelSize(0);
|
||||
groupNameField.clear();
|
||||
quickSelectList.getItems().forEach(q -> userList.getItems().add(q.getUser()));
|
||||
quickSelectList.getItems().clear();
|
||||
resizeQuickSelectSpace(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group with the given name and all selected members.<br>
|
||||
* Additionally pops the scene automatically.
|
||||
*
|
||||
* @param name the chosen group name
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
private void createGroup(String name) {
|
||||
Context.getInstance()
|
||||
.getClient()
|
||||
.send(new GroupCreation(name, quickSelectList.getItems().stream()
|
||||
.map(q -> q.getUser().getID()).collect(Collectors.toSet())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the proposed group name is already present in the users {@code LocalDB}.
|
||||
*
|
||||
* @param newName the chosen group name
|
||||
* @return true if this name is already present
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean groupNameAlreadyPresent(String newName) {
|
||||
return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance)
|
||||
.map(Contact::getName).anyMatch(newName::equals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an element from the quickSelectList.
|
||||
*
|
||||
* @param element the element to be removed.
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void removeFromQuickSelection(QuickSelectControl element) {
|
||||
quickSelectList.getItems().remove(element);
|
||||
userList.getItems().add(element.getUser());
|
||||
if (quickSelectList.getItems().isEmpty()) {
|
||||
resizeQuickSelectSpace(0);
|
||||
createButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void resizeQuickSelectSpace(int value) {
|
||||
quickSelectList.setPrefHeight(value);
|
||||
quickSelectList.setMaxHeight(value);
|
||||
quickSelectList.setMinHeight(value);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() {
|
||||
eventBus.dispatch(new BackEvent());
|
||||
setErrorMessageLabelSize(0);
|
||||
setProcessPaneSize(0);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void proceedOnNameDuplication() {
|
||||
createButton.setDisable(false);
|
||||
cancelButton.setDisable(false);
|
||||
createGroup(name);
|
||||
eventBus.dispatch(new BackEvent());
|
||||
setErrorMessageLabelSize(0);
|
||||
setProcessPaneSize(0);
|
||||
groupNameField.clear();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void cancelOnNameDuplication() {
|
||||
createButton.setDisable(false);
|
||||
cancelButton.setDisable(false);
|
||||
setErrorMessageLabelSize(0);
|
||||
setProcessPaneSize(0);
|
||||
groupNameField.clear();
|
||||
}
|
||||
|
||||
private void setErrorMessageLabelSize(int value) {
|
||||
errorMessageLabel.setPrefHeight(value);
|
||||
errorMessageLabel.setMinHeight(value);
|
||||
errorMessageLabel.setMaxHeight(value);
|
||||
}
|
||||
|
||||
private void setProcessPaneSize(int value) {
|
||||
proceedDuplicateButton.setPrefHeight(value);
|
||||
proceedDuplicateButton.setMinHeight(value);
|
||||
proceedDuplicateButton.setMaxHeight(value);
|
||||
cancelDuplicateButton.setPrefHeight(value);
|
||||
cancelDuplicateButton.setMinHeight(value);
|
||||
cancelDuplicateButton.setMaxHeight(value);
|
||||
errorProceedBox.setPrefHeight(value);
|
||||
errorProceedBox.setMinHeight(value);
|
||||
errorProceedBox.setMaxHeight(value);
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
Platform.runLater(() -> {
|
||||
switch (operation.getOperationType()) {
|
||||
case ADD:
|
||||
userList.getItems().add(operation.get());
|
||||
break;
|
||||
case REMOVE:
|
||||
userList.getItems().removeIf(operation.get()::equals);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,44 +1,36 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.time.Instant;
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.client.ui.ClearableTextField;
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.data.LoginCredentials;
|
||||
import envoy.data.User;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.event.HandshakeRejection;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.Bounds;
|
||||
import envoy.util.EnvoyLog;
|
||||
import envoy.util.*;
|
||||
|
||||
import envoy.client.data.ClientConfig;
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.client.util.IconUtil;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>LoginDialog.java</strong><br>
|
||||
* Created: <strong>03.04.2020</strong><br>
|
||||
* Controller for the login scene.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class LoginScene {
|
||||
public final class LoginScene implements EventListener {
|
||||
|
||||
@FXML
|
||||
private ClearableTextField userTextField;
|
||||
private TextField userTextField;
|
||||
|
||||
@FXML
|
||||
private PasswordField passwordField;
|
||||
@ -47,81 +39,93 @@ public final class LoginScene {
|
||||
private PasswordField repeatPasswordField;
|
||||
|
||||
@FXML
|
||||
private Label repeatPasswordLabel;
|
||||
|
||||
@FXML
|
||||
private CheckBox registerCheckBox;
|
||||
private Button registerSwitch;
|
||||
|
||||
@FXML
|
||||
private Label connectionLabel;
|
||||
|
||||
private Client client;
|
||||
private LocalDB localDB;
|
||||
private CacheMap cacheMap;
|
||||
private SceneContext sceneContext;
|
||||
@FXML
|
||||
private Button loginButton;
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
||||
private static final EventBus eventBus = EventBus.getInstance();
|
||||
private static final ClientConfig config = ClientConfig.getInstance();
|
||||
@FXML
|
||||
private CheckBox cbStaySignedIn;
|
||||
|
||||
@FXML
|
||||
private Button offlineModeButton;
|
||||
|
||||
@FXML
|
||||
private Label registerTextLabel;
|
||||
|
||||
@FXML
|
||||
private ImageView logo;
|
||||
|
||||
private boolean registration;
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
|
||||
private static final ClientConfig config = ClientConfig.getInstance();
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
|
||||
|
||||
// Show an alert after an unsuccessful handshake
|
||||
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
|
||||
}
|
||||
EventBus.getInstance().registerListener(this);
|
||||
|
||||
/**
|
||||
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
|
||||
*
|
||||
* @param client the client used to perform the handshake
|
||||
* @param localDB the local database used for offline login
|
||||
* @param cacheMap the map of all caches needed
|
||||
* @param sceneContext the scene context used to initialize the chat scene
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
|
||||
this.client = client;
|
||||
this.localDB = localDB;
|
||||
this.cacheMap = cacheMap;
|
||||
this.sceneContext = sceneContext;
|
||||
|
||||
// Prepare handshake
|
||||
localDB.loadIDGenerator();
|
||||
logo.setImage(IconUtil.loadIcon("envoy_logo"));
|
||||
|
||||
// Set initial cursor
|
||||
userTextField.requestFocus();
|
||||
|
||||
// Perform automatic login if configured
|
||||
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void loginButtonPressed() {
|
||||
final String user = userTextField.getText(), pass = passwordField.getText(),
|
||||
repeatPass = repeatPasswordField.getText();
|
||||
final boolean requestToken = cbStaySignedIn.isSelected();
|
||||
|
||||
// Prevent registration with unequal passwords
|
||||
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
|
||||
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
|
||||
if (registration && !pass.equals(repeatPass)) {
|
||||
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one")
|
||||
.showAndWait();
|
||||
repeatPasswordField.clear();
|
||||
} else if (!Bounds.isValidContactName(userTextField.getTextField().getText())) {
|
||||
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
|
||||
userTextField.getTextField().clear();
|
||||
} else performHandshake(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), registerCheckBox.isSelected(),
|
||||
Startup.VERSION));
|
||||
} else if (!Bounds.isValidContactName(user)) {
|
||||
new Alert(AlertType.ERROR,
|
||||
"The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")")
|
||||
.showAndWait();
|
||||
userTextField.clear();
|
||||
} else {
|
||||
Instant lastSync = Startup.loadLastSync(userTextField.getText());
|
||||
Startup.performHandshake(registration
|
||||
? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
|
||||
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void offlineModeButtonPressed() {
|
||||
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
|
||||
Startup.attemptOfflineMode(userTextField.getText());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void registerCheckboxChanged() {
|
||||
private void registerSwitchPressed() {
|
||||
|
||||
// Update button text and register switch
|
||||
if (!registration) {
|
||||
loginButton.setText("Register");
|
||||
loginButton.setPadding(new Insets(2, 116, 2, 116));
|
||||
registerTextLabel.setText("Already an account?");
|
||||
registerSwitch.setText("Login");
|
||||
} else {
|
||||
loginButton.setText("Login");
|
||||
loginButton.setPadding(new Insets(2, 125, 2, 125));
|
||||
registerTextLabel.setText("No account yet?");
|
||||
registerSwitch.setText("Register");
|
||||
}
|
||||
registration = !registration;
|
||||
|
||||
// Make repeat password field and label visible / invisible
|
||||
repeatPasswordField.setVisible(registerCheckBox.isSelected());
|
||||
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
|
||||
repeatPasswordField.setVisible(registration);
|
||||
offlineModeButton.setDisable(registration);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@ -130,70 +134,8 @@ public final class LoginScene {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void performHandshake(LoginCredentials credentials) {
|
||||
try {
|
||||
client.performHandshake(credentials, cacheMap);
|
||||
if (client.isOnline()) {
|
||||
loadChatScene();
|
||||
client.initReceiver(localDB, cacheMap);
|
||||
}
|
||||
} catch (IOException | InterruptedException | TimeoutException e) {
|
||||
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
|
||||
attemptOfflineMode(credentials);
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptOfflineMode(LoginCredentials credentials) {
|
||||
try {
|
||||
// Try entering offline mode
|
||||
localDB.loadUsers();
|
||||
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
|
||||
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||
client.setSender(clientUser);
|
||||
loadChatScene();
|
||||
} catch (final Exception e) {
|
||||
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
|
||||
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadChatScene() {
|
||||
|
||||
// Set client user in local database
|
||||
localDB.setUser(client.getSender());
|
||||
|
||||
// Initialize chats in local database
|
||||
try {
|
||||
localDB.initializeUserStorage();
|
||||
localDB.loadUserData();
|
||||
} catch (final FileNotFoundException e) {
|
||||
// The local database file has not yet been created, probably first login
|
||||
} catch (final Exception e) {
|
||||
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
|
||||
logger.log(Level.WARNING, "Could not load local database: ", e);
|
||||
}
|
||||
|
||||
// Initialize write proxy
|
||||
final var writeProxy = new WriteProxy(client, localDB);
|
||||
|
||||
localDB.synchronize();
|
||||
|
||||
if (client.isOnline()) writeProxy.flushCache();
|
||||
else
|
||||
// Set all contacts to offline mode
|
||||
localDB.getChats()
|
||||
.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(User.class::isInstance)
|
||||
.map(User.class::cast)
|
||||
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
|
||||
|
||||
// Load ChatScene
|
||||
sceneContext.pop();
|
||||
sceneContext.getStage().setMinHeight(400);
|
||||
sceneContext.getStage().setMinWidth(350);
|
||||
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
|
||||
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
|
||||
@Event
|
||||
private void onHandshakeRejection(HandshakeRejection evt) {
|
||||
Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait());
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,23 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.*;
|
||||
|
||||
import envoy.client.ui.SceneContext;
|
||||
import envoy.client.ui.settings.GeneralSettingsPane;
|
||||
import envoy.client.ui.settings.SettingsPane;
|
||||
import envoy.client.data.Context;
|
||||
import envoy.client.data.shortcuts.KeyboardMapping;
|
||||
import envoy.client.ui.listcell.ListCellFactory;
|
||||
import envoy.client.ui.settings.*;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SettingsSceneController.java</strong><br>
|
||||
* Created: <strong>10.04.2020</strong><br>
|
||||
* Controller for the settings scene.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class SettingsScene {
|
||||
public final class SettingsScene implements KeyboardMapping {
|
||||
|
||||
@FXML
|
||||
private ListView<SettingsPane> settingsList;
|
||||
@ -23,26 +25,11 @@ public class SettingsScene {
|
||||
@FXML
|
||||
private TitledPane titledPane;
|
||||
|
||||
private SceneContext sceneContext;
|
||||
|
||||
/**
|
||||
* @param sceneContext enables the user to return to the chat scene
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
settingsList.setCellFactory(listView -> new ListCell<>() {
|
||||
|
||||
@Override
|
||||
protected void updateItem(SettingsPane item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (!empty && item != null) setGraphic(new Label(item.getTitle()));
|
||||
}
|
||||
});
|
||||
|
||||
settingsList.getItems().add(new GeneralSettingsPane());
|
||||
settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
|
||||
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(),
|
||||
new DownloadSettingsPane(), new BugReportPane());
|
||||
}
|
||||
|
||||
@FXML
|
||||
@ -55,5 +42,13 @@ public class SettingsScene {
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { sceneContext.pop(); }
|
||||
private void backButtonClicked() {
|
||||
Context.getInstance().getSceneContext().pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
|
||||
return Map.of(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN),
|
||||
this::backButtonClicked);
|
||||
}
|
||||
}
|
||||
|
25
client/src/main/java/envoy/client/ui/controller/Tabs.java
Normal file
25
client/src/main/java/envoy/client/ui/controller/Tabs.java
Normal file
@ -0,0 +1,25 @@
|
||||
package envoy.client.ui.controller;
|
||||
|
||||
/**
|
||||
* Provides options to select different tabs.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public enum Tabs {
|
||||
|
||||
/**
|
||||
* Selects the {@code contact list} tab.
|
||||
*/
|
||||
CONTACT_LIST,
|
||||
|
||||
/**
|
||||
* Selects the {@code contact search} tab.
|
||||
*/
|
||||
CONTACT_SEARCH,
|
||||
|
||||
/**
|
||||
* Selects the {@code group creation} tab.
|
||||
*/
|
||||
GROUP_CREATION
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
/**
|
||||
* Contains JavaFX scene controllers.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>08.06.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.ui.controller;
|
||||
|
@ -0,0 +1,48 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.scene.*;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
/**
|
||||
* Provides a convenience frame for list cell creation.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @param <T> the type of element displayed by the list cell
|
||||
* @param <U> the type of node as which the list element will be displayed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public abstract class AbstractListCell<T, U extends Node> extends ListCell<T> {
|
||||
|
||||
protected ListView<? extends T> listView;
|
||||
|
||||
/**
|
||||
* @param listView the list view inside of which the cell will be displayed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AbstractListCell(ListView<? extends T> listView) {
|
||||
this.listView = listView;
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
getStyleClass().add("list-element");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (!(empty || item == null)) {
|
||||
setCursor(Cursor.HAND);
|
||||
setGraphic(renderItem(item));
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list item to a node. This can have side effects on the list cell.
|
||||
*
|
||||
* @param item the item to render
|
||||
* @return a node representing the item
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
protected abstract U renderItem(T item);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
import envoy.data.Contact;
|
||||
import envoy.data.Group;
|
||||
import envoy.data.User;
|
||||
|
||||
/**
|
||||
* This class formats a single {@link Contact} into a UI component.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ContactControl.java</strong><br>
|
||||
* Created: <strong>01.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ChatControl extends HBox {
|
||||
|
||||
/**
|
||||
* @param chat the chat to display
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ChatControl(Chat chat) {
|
||||
// Container with contact name
|
||||
final var vBox = new VBox();
|
||||
final var nameLabel = new Label(chat.getRecipient().getName());
|
||||
nameLabel.setWrapText(true);
|
||||
vBox.getChildren().add(nameLabel);
|
||||
if (chat.getRecipient() instanceof User) {
|
||||
// Online status
|
||||
final var user = (User) chat.getRecipient();
|
||||
final var statusLabel = new Label(user.getStatus().toString());
|
||||
statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase());
|
||||
vBox.getChildren().add(statusLabel);
|
||||
} else // Member count
|
||||
vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members"));
|
||||
|
||||
getChildren().add(vBox);
|
||||
if (chat.getUnreadAmount() != 0) {
|
||||
Region spacing = new Region();
|
||||
setHgrow(spacing, Priority.ALWAYS);
|
||||
getChildren().add(spacing);
|
||||
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
|
||||
unreadMessagesLabel.setMinSize(15, 15);
|
||||
var vBox2 = new VBox();
|
||||
vBox2.setAlignment(Pos.CENTER_RIGHT);
|
||||
unreadMessagesLabel.setAlignment(Pos.CENTER);
|
||||
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
|
||||
vBox2.getChildren().add(unreadMessagesLabel);
|
||||
getChildren().add(vBox2);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import envoy.data.User;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.ui.control.ChatControl;
|
||||
import envoy.client.util.UserUtil;
|
||||
|
||||
/**
|
||||
* A list cell containing chats represented as chat controls.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class ChatListCell extends AbstractListCell<Chat, ChatControl> {
|
||||
|
||||
private static final Client client = Context.getInstance().getClient();
|
||||
|
||||
/**
|
||||
* @param listView the list view inside of which the cell will be displayed
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public ChatListCell(ListView<? extends Chat> listView) {
|
||||
super(listView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChatControl renderItem(Chat chat) {
|
||||
if (client.isOnline()) {
|
||||
final var menu = new ContextMenu();
|
||||
final var removeMI = new MenuItem();
|
||||
removeMI.setText(
|
||||
chat.isDisabled() ? "Delete "
|
||||
: chat.getRecipient() instanceof User ? "Block "
|
||||
: "Leave group " + chat.getRecipient().getName());
|
||||
removeMI.setOnAction(
|
||||
chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient())
|
||||
: e -> UserUtil.disableContact(chat.getRecipient()));
|
||||
menu.getItems().add(removeMI);
|
||||
setContextMenu(menu);
|
||||
} else
|
||||
setContextMenu(null);
|
||||
|
||||
// TODO: replace with icon in ChatControl
|
||||
final var chatControl = new ChatControl(chat);
|
||||
if (chat.isDisabled())
|
||||
chatControl.getStyleClass().add("disabled-chat");
|
||||
else
|
||||
chatControl.getStyleClass().remove("disabled-chat");
|
||||
return chatControl;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
import envoy.client.data.Chat;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>UserListCell.java</strong><br>
|
||||
* Created: <strong>28.03.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ContactListCellFactory extends ListCell<Chat> {
|
||||
|
||||
private final ListView<Chat> listView;
|
||||
|
||||
/**
|
||||
* @param listView the list view inside which this cell is contained
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ContactListCellFactory(ListView<Chat> listView) { this.listView = listView; }
|
||||
|
||||
/**
|
||||
* Displays the name of a contact. If the contact is a user, their online status
|
||||
* is displayed as well.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
protected void updateItem(Chat chat, boolean empty) {
|
||||
super.updateItem(chat, empty);
|
||||
if (empty || chat.getRecipient() == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
final var control = new ChatControl(chat);
|
||||
prefWidthProperty().bind(listView.widthProperty().subtract(40));
|
||||
setGraphic(control);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
/**
|
||||
* A generic list cell rendering an item using a provided render function.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @param <T> the type of element displayed by the list cell
|
||||
* @param <U> the type of node as which the list element will be displayed
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class GenericListCell<T, U extends Node> extends AbstractListCell<T, U> {
|
||||
|
||||
private final Function<? super T, U> renderer;
|
||||
|
||||
/**
|
||||
* @param listView the list view inside of which the cell will be displayed
|
||||
* @param renderer a function converting a list item to a node
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public GenericListCell(ListView<? extends T> listView, Function<? super T, U> renderer) {
|
||||
super(listView);
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected U renderItem(T item) {
|
||||
return renderer.apply(item);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* Provides a creation mechanism for generic list cells given a list view and a conversion function.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @param <T> the type of object to display
|
||||
* @param <U> the type of node displayed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class ListCellFactory<T, U extends Node>
|
||||
implements Callback<ListView<T>, ListCell<T>> {
|
||||
|
||||
private final Function<? super T, U> renderer;
|
||||
|
||||
/**
|
||||
* @param renderer a function converting the type to display into a node
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public ListCellFactory(Function<? super T, U> renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListCell<T> call(ListView<T> listView) {
|
||||
return new GenericListCell<>(listView, renderer);
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import envoy.client.ui.AudioControl;
|
||||
import envoy.client.ui.IconUtil;
|
||||
import envoy.data.Message;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.data.User;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* This class formats a single {@link Message} into a UI component.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>MessageControl.java</strong><br>
|
||||
* Created: <strong>01.07.2020</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class MessageControl extends Label {
|
||||
|
||||
private static User client;
|
||||
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param message the message that should be formatted
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public MessageControl(Message message) {
|
||||
// Creating the underlying VBox and the dateLabel
|
||||
final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate())));
|
||||
|
||||
// Creating the actions for the MenuItems
|
||||
final ContextMenu contextMenu = new ContextMenu();
|
||||
final MenuItem copyMenuItem = new MenuItem("Copy");
|
||||
final MenuItem deleteMenuItem = new MenuItem("Delete");
|
||||
final MenuItem forwardMenuItem = new MenuItem("Forward");
|
||||
final MenuItem quoteMenuItem = new MenuItem("Quote");
|
||||
final MenuItem infoMenuItem = new MenuItem("Info");
|
||||
copyMenuItem.setOnAction(e -> copyMessage(message));
|
||||
deleteMenuItem.setOnAction(e -> deleteMessage(message));
|
||||
forwardMenuItem.setOnAction(e -> forwardMessage(message));
|
||||
quoteMenuItem.setOnAction(e -> quoteMessage(message));
|
||||
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
|
||||
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
|
||||
|
||||
// Handling message attachment display
|
||||
if (message.hasAttachment()) {
|
||||
switch (message.getAttachment().getType()) {
|
||||
case PICTURE:
|
||||
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
|
||||
break;
|
||||
case VIDEO:
|
||||
break;
|
||||
case VOICE:
|
||||
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
|
||||
break;
|
||||
case DOCUMENT:
|
||||
break;
|
||||
}
|
||||
final var saveAttachment = new MenuItem("Save attachment");
|
||||
saveAttachment.setOnAction(e -> saveAttachment(message));
|
||||
contextMenu.getItems().add(saveAttachment);
|
||||
}
|
||||
// Creating the textLabel
|
||||
final var textLabel = new Label(message.getText());
|
||||
textLabel.setWrapText(true);
|
||||
vbox.getChildren().add(textLabel);
|
||||
// Setting the message status icon and background color
|
||||
if (message.getSenderID() == client.getID()) {
|
||||
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
|
||||
statusIcon.setPreserveRatio(true);
|
||||
vbox.getChildren().add(statusIcon);
|
||||
getStyleClass().add("own-message");
|
||||
} else getStyleClass().add("received-message");
|
||||
// Adjusting height and weight of the cell to the corresponding ListView
|
||||
paddingProperty().setValue(new Insets(5, 20, 5, 20));
|
||||
setContextMenu(contextMenu);
|
||||
setGraphic(vbox);
|
||||
}
|
||||
|
||||
// Context Menu actions
|
||||
|
||||
private void copyMessage(Message message) {
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
|
||||
}
|
||||
|
||||
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
|
||||
|
||||
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
|
||||
|
||||
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
|
||||
|
||||
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
|
||||
|
||||
private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
|
||||
|
||||
/**
|
||||
* @param client the user who has logged in
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static void setUser(User client) { MessageControl.client = client; }
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.ListView;
|
||||
|
||||
import envoy.data.Message;
|
||||
|
||||
import envoy.client.ui.control.MessageControl;
|
||||
|
||||
/**
|
||||
* A list cell containing messages represented as message controls.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class MessageListCell extends AbstractListCell<Message, MessageControl> {
|
||||
|
||||
/**
|
||||
* @param listView the list view inside of which the cell will be displayed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public MessageListCell(ListView<? extends Message> listView) {
|
||||
super(listView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageControl renderItem(Message message) {
|
||||
final var control = new MessageControl(message);
|
||||
listView.widthProperty().addListener((observable, oldValue,
|
||||
newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
|
||||
adjustPadding((int) listView.getWidth(), control.isOwnMessage());
|
||||
if (control.isOwnMessage())
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
else
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
return control;
|
||||
}
|
||||
|
||||
private void adjustPadding(int listWidth, boolean ownMessage) {
|
||||
int padding = 10 + Math.max((listWidth - 1000) / 2, 0);
|
||||
setPadding(ownMessage ? new Insets(3, padding, 3, 0) : new Insets(3, 0, 3, padding));
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.stage.PopupWindow.AnchorLocation;
|
||||
|
||||
import envoy.data.Message;
|
||||
|
||||
/**
|
||||
* Displays a single message inside the message list.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>MessageListCellFactory.java</strong><br>
|
||||
* Created: <strong>28.03.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class MessageListCellFactory extends ListCell<Message> {
|
||||
|
||||
private final ListView<Message> listView;
|
||||
|
||||
/**
|
||||
* @param listView the list view inside which this cell is contained
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public MessageListCellFactory(ListView<Message> listView) { this.listView = listView; }
|
||||
|
||||
/**
|
||||
* Displays the text, the data of creation and the status of a message.
|
||||
*
|
||||
* @since Envoy v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
protected void updateItem(Message message, boolean empty) {
|
||||
super.updateItem(message, empty);
|
||||
if (empty || message == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
final var control = new MessageControl(message);
|
||||
control.prefWidthProperty().bind(listView.widthProperty().subtract(40));
|
||||
// Creating the Tooltip to deselect a message
|
||||
final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it");
|
||||
tooltip.setWrapText(true);
|
||||
tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
|
||||
setTooltip(tooltip);
|
||||
setGraphic(control);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
/**
|
||||
* This package contains custom list cells that are used to display certain
|
||||
* things.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>30.06.2020</strong><br>
|
||||
* This package contains custom list cells that are used to display certain things.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.ui.listcell;
|
||||
|
@ -0,0 +1,71 @@
|
||||
package envoy.client.ui.settings;
|
||||
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.InputEvent;
|
||||
|
||||
import envoy.event.IssueProposal;
|
||||
|
||||
/**
|
||||
* This class offers the option for users to submit a bug report. Only the title of a bug is needed
|
||||
* to be sent.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class BugReportPane extends OnlineOnlySettingsPane {
|
||||
|
||||
private final Label titleLabel = new Label("Suggest a title for the bug:");
|
||||
private final TextField titleTextField = new TextField();
|
||||
private final Label pleaseExplainLabel =
|
||||
new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
|
||||
private final TextArea errorDetailArea = new TextArea();
|
||||
private final CheckBox showUsernameInBugReport =
|
||||
new CheckBox("Show your username in the bug report?");
|
||||
private final Button submitReportButton = new Button("Submit report");
|
||||
|
||||
private final EventHandler<? super InputEvent> inputEventHandler =
|
||||
e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
|
||||
|
||||
/**
|
||||
* Creates a new {@code BugReportPane}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public BugReportPane() {
|
||||
super("Report a bug");
|
||||
setSpacing(10);
|
||||
setToolTipText("A bug can only be reported while being online");
|
||||
|
||||
// Displaying the label to ask for a title
|
||||
titleLabel.setWrapText(true);
|
||||
getChildren().add(titleLabel);
|
||||
|
||||
// Displaying the TextField where to enter the title of this bug
|
||||
titleTextField.setOnKeyTyped(inputEventHandler);
|
||||
titleTextField.setOnInputMethodTextChanged(inputEventHandler);
|
||||
getChildren().add(titleTextField);
|
||||
|
||||
// Displaying the label to ask for clarification
|
||||
pleaseExplainLabel.setWrapText(true);
|
||||
getChildren().add(pleaseExplainLabel);
|
||||
|
||||
// Displaying the TextArea where to enter the log and/or own description
|
||||
errorDetailArea.setWrapText(true);
|
||||
getChildren().add(errorDetailArea);
|
||||
|
||||
// Displaying the consent button that your user name will be shown
|
||||
showUsernameInBugReport.setSelected(true);
|
||||
getChildren().add(showUsernameInBugReport);
|
||||
|
||||
// Displaying the submitReportButton
|
||||
submitReportButton.setDisable(true);
|
||||
submitReportButton.setOnAction(e -> {
|
||||
String title = titleTextField.getText(), description = errorDetailArea.getText();
|
||||
client.send(
|
||||
showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true)
|
||||
: new IssueProposal(title, description, client.getSender().getName(), true));
|
||||
});
|
||||
getChildren().add(submitReportButton);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package envoy.client.ui.settings;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
|
||||
/**
|
||||
* Displays options for downloading {@link envoy.data.Attachment}s.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public final class DownloadSettingsPane extends SettingsPane {
|
||||
|
||||
/**
|
||||
* Constructs a new {@code DownloadSettingsPane}.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public DownloadSettingsPane() {
|
||||
super("Download");
|
||||
setSpacing(15);
|
||||
setPadding(new Insets(15));
|
||||
|
||||
// Checkbox to disable asking
|
||||
final var checkBox =
|
||||
new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
|
||||
checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
|
||||
checkBox.setTooltip(new Tooltip(
|
||||
"Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
|
||||
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
|
||||
getChildren().add(checkBox);
|
||||
|
||||
// Displaying the default path to save to
|
||||
final var pathLabel =
|
||||
new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
|
||||
pathLabel.setWrapText(true);
|
||||
getChildren().add(pathLabel);
|
||||
final var hbox = new HBox(20);
|
||||
Tooltip.install(hbox,
|
||||
new Tooltip("Determines the location where attachments will be saved to."));
|
||||
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
|
||||
hbox.getChildren().add(currentPath);
|
||||
|
||||
// Setting the default path
|
||||
final var button = new Button("Select");
|
||||
button.setOnAction(e -> {
|
||||
final var directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Select the directory where attachments should be saved to");
|
||||
directoryChooser.setInitialDirectory(settings.getDownloadLocation());
|
||||
final var selectedDirectory =
|
||||
directoryChooser.showDialog(context.getSceneContext().getStage());
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
currentPath.setText(selectedDirectory.getAbsolutePath());
|
||||
settings.setDownloadLocation(selectedDirectory);
|
||||
}
|
||||
});
|
||||
hbox.getChildren().add(button);
|
||||
getChildren().add(hbox);
|
||||
}
|
||||
}
|
@ -1,57 +1,83 @@
|
||||
package envoy.client.ui.settings;
|
||||
|
||||
import java.util.List;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import dev.kske.eventbus.EventBus;
|
||||
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
import envoy.client.data.Settings;
|
||||
import envoy.client.data.SettingsItem;
|
||||
import envoy.client.event.ThemeChangeEvent;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.EventBus;
|
||||
import envoy.client.ui.StatusTrayIcon;
|
||||
import envoy.client.util.UserUtil;
|
||||
|
||||
/**
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>GeneralSettingsPane.java</strong><br>
|
||||
* Created: <strong>18.04.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GeneralSettingsPane extends SettingsPane {
|
||||
|
||||
private static final Settings settings = Settings.getInstance();
|
||||
public final class GeneralSettingsPane extends SettingsPane {
|
||||
|
||||
/**
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public GeneralSettingsPane() {
|
||||
super("General");
|
||||
final var vbox = new VBox();
|
||||
setSpacing(10);
|
||||
|
||||
// TODO: Support other value types
|
||||
List.of("onCloseMode", "enterToSend")
|
||||
.stream()
|
||||
.map(settings.getItems()::get)
|
||||
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
|
||||
.forEach(vbox.getChildren()::add);
|
||||
final var settingsItems = settings.getItems();
|
||||
|
||||
// Add hide on close if supported
|
||||
final var hideOnCloseCheckbox =
|
||||
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
|
||||
final var hideOnCloseTooltip = new Tooltip(StatusTrayIcon.isSupported()
|
||||
? "If selected, Envoy will still be present in the task bar when closed."
|
||||
: "status tray icon is not supported on your system.");
|
||||
hideOnCloseTooltip.setWrapText(true);
|
||||
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
|
||||
hideOnCloseCheckbox.setDisable(!StatusTrayIcon.isSupported());
|
||||
getChildren().add(hideOnCloseCheckbox);
|
||||
|
||||
final var enterToSendCheckbox =
|
||||
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
|
||||
final var enterToSendTooltip = new Tooltip(
|
||||
"When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around.");
|
||||
enterToSendTooltip.setWrapText(true);
|
||||
enterToSendCheckbox.setTooltip(enterToSendTooltip);
|
||||
getChildren().add(enterToSendCheckbox);
|
||||
|
||||
final var askForConfirmationCheckbox =
|
||||
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
|
||||
final var askForConfirmationTooltip =
|
||||
new Tooltip("When selected, nothing will prompt a confirmation dialog");
|
||||
askForConfirmationTooltip.setWrapText(true);
|
||||
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
|
||||
getChildren().add(askForConfirmationCheckbox);
|
||||
|
||||
final var combobox = new ComboBox<String>();
|
||||
combobox.getItems().add("dark");
|
||||
combobox.getItems().add("light");
|
||||
combobox
|
||||
.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
|
||||
combobox.setValue(settings.getCurrentTheme());
|
||||
combobox.setOnAction(
|
||||
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
|
||||
vbox.getChildren().add(combobox);
|
||||
combobox.setOnAction(e -> {
|
||||
settings.setCurrentTheme(combobox.getValue());
|
||||
EventBus.getInstance().dispatch(new ThemeChangeEvent());
|
||||
});
|
||||
getChildren().add(combobox);
|
||||
|
||||
final var statusComboBox = new ComboBox<UserStatus>();
|
||||
statusComboBox.getItems().setAll(UserStatus.values());
|
||||
statusComboBox.setValue(UserStatus.ONLINE);
|
||||
// TODO add action when value is changed
|
||||
statusComboBox.setOnAction(e -> {});
|
||||
vbox.getChildren().add(statusComboBox);
|
||||
statusComboBox.setValue(context.getLocalDB().getUser().getStatus());
|
||||
statusComboBox.setTooltip(new Tooltip("Change your current status"));
|
||||
statusComboBox.setOnAction(e -> UserUtil.changeStatus(statusComboBox.getValue()));
|
||||
getChildren().add(statusComboBox);
|
||||
|
||||
getChildren().add(vbox);
|
||||
final var logoutButton = new Button("Logout");
|
||||
logoutButton.setOnAction(e -> UserUtil.logout());
|
||||
final var logoutTooltip = new Tooltip(
|
||||
"Brings you back to the login screen and removes \"remember me\" status from this account");
|
||||
logoutTooltip.setWrapText(true);
|
||||
logoutButton.setTooltip(logoutTooltip);
|
||||
getChildren().add(logoutButton);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user