Compare commits

..

No commits in common. "develop" and "v0.2-beta" have entirely different histories.

170 changed files with 3382 additions and 4491 deletions

37
Jenkinsfile vendored
View File

@ -1,37 +0,0 @@
pipeline {
agent any
options {
ansiColor('xterm')
}
stages {
stage('Build') {
steps {
sh 'mvn -DskipTests clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '*/target/surefire-reports/*.xml'
}
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('KSKE SonarQube') {
sh 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar'
}
}
}
}
post {
success {
archiveArtifacts artifacts: 'client/target/envoy-client-*-shaded.jar, server/target/envoy-server-jar-with-dependencies.jar'
}
}
}

View File

@ -17,30 +17,11 @@ 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. On the settings page some convenience features can be configured, as well as the color theme.
Additional info on how to use Envoy can be found [here](https://git.kske.dev/zdm/envoy/wiki) in the client section.
### 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 ## Server Administrator
To set up an Envoy server, download the package from the release page. To set up an Envoy server, download the package from the release page.
To configure the behavior of Envoy Server, please have a look at the [documentation](https://git.kske.dev/zdm/envoy/wiki), specifically the server part. 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 ## Programmer
@ -67,5 +48,5 @@ Envoy is organized as a Maven project that is split into three modules.
* Non-blocking connectivity infrastructure based on `java.nio` * Non-blocking connectivity infrastructure based on `java.nio`
* Processors to handle incoming events * Processors to handle incoming events
* Database connectivity * Database connectivity
* Database entities * Databse entities
* Utility classes to check client version compatability and password validity * Utility classes to check client version compatability and password validity

View File

@ -129,4 +129,364 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=11 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 org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter

File diff suppressed because one or more lines are too long

View File

@ -21,12 +21,12 @@
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>15</version> <version>11.0.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>15</version> <version>11.0.2</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -7,8 +7,8 @@ import envoy.client.ui.Startup;
/** /**
* Triggers application startup. * Triggers application startup.
* <p> * <p>
* To allow Maven shading, the main method has to be separated from the {@link Startup} class which * To allow Maven shading, the main method has to be separated from the
* extends {@link Application}. * {@link Startup} class which extends {@link Application}.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -25,7 +25,8 @@ public final class Main {
/** /**
* Starts the application. * 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 * @since Envoy Client v0.1-beta
*/ */
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -35,9 +35,7 @@ public final class Cache<T> implements Consumer<T>, Serializable {
} }
@Override @Override
public String toString() { public String toString() { return String.format("Cache[elements=" + elements + "]"); }
return String.format("Cache[elements=" + elements + "]");
}
/** /**
* Sets the processor to which cached elements are relayed. * Sets the processor to which cached elements are relayed.
@ -54,8 +52,7 @@ public final class Cache<T> implements Consumer<T>, Serializable {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void relay() { public void relay() {
if (processor == null) if (processor == null) throw new IllegalStateException("Processor is not defined");
throw new IllegalStateException("Processor is not defined");
elements.forEach(processor::accept); elements.forEach(processor::accept);
elements.clear(); elements.clear();
} }
@ -65,7 +62,5 @@ public final class Cache<T> implements Consumer<T>, Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void clear() { public void clear() { elements.clear(); }
elements.clear();
}
} }

View File

@ -4,7 +4,8 @@ import java.io.Serializable;
import java.util.*; import java.util.*;
/** /**
* Stores a heterogeneous map of {@link Cache} objects with different type parameters. * Stores a heterogeneous map of {@link Cache} objects with different type
* parameters.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -23,9 +24,7 @@ public final class CacheMap implements Serializable {
* @param cache the cache to store * @param cache the cache to store
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> void put(Class<T> key, Cache<T> cache) { public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
map.put(key, cache);
}
/** /**
* Returns a cache mapped by a class. * Returns a cache mapped by a class.
@ -35,9 +34,7 @@ public final class CacheMap implements Serializable {
* @return the cache * @return the cache
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> Cache<T> get(Class<T> key) { public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); }
return (Cache<T>) map.get(key);
}
/** /**
* Returns a cache mapped by a class or any of its subclasses. * Returns a cache mapped by a class or any of its subclasses.
@ -67,7 +64,5 @@ public final class CacheMap implements Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void clear() { public void clear() { map.values().forEach(Cache::clear); }
map.values().forEach(Cache::clear);
}
} }

View File

@ -3,17 +3,16 @@ package envoy.client.data;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import javafx.beans.property.*;
import javafx.collections.*; import javafx.collections.*;
import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.client.net.WriteProxy;
/** /**
* Represents a chat between two {@link User}s as a list of {@link Message} objects. * Represents a chat between two {@link User}s
* as a list of {@link Message} objects.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Leon Hofmeister * @author Leon Hofmeister
@ -22,21 +21,17 @@ import envoy.client.net.WriteProxy;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected final Contact recipient; protected final Contact recipient;
protected boolean disabled; protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected boolean underlyingContactDeleted;
protected int unreadAmount;
/** /**
* Stores the last time an {@link envoy.event.IsTyping} event has been sent. * Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/ */
protected transient long lastWritingEvent; protected transient long lastWritingEvent;
protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**
@ -47,14 +42,11 @@ public class Chat implements Serializable {
* @param recipient the user who receives the messages * @param recipient the user who receives the messages
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public Chat(Contact recipient) { public Chat(Contact recipient) { this.recipient = recipient; }
this.recipient = recipient;
}
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
stream.defaultReadObject(); stream.defaultReadObject();
messages = FXCollections.observableList((List<Message>) stream.readObject()); messages = FXCollections.observableList((List<Message>) stream.readObject());
totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount);
} }
private void writeObject(ObjectOutputStream stream) throws IOException { private void writeObject(ObjectOutputStream stream) throws IOException {
@ -63,14 +55,7 @@ public class Chat implements Serializable {
} }
@Override @Override
public String toString() { public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); }
return String.format(
"%s[recipient=%s,messages=%d,disabled=%b]",
getClass().getSimpleName(),
recipient,
messages.size(),
disabled);
}
/** /**
* Generates a hash code based on the recipient. * Generates a hash code based on the recipient.
@ -78,9 +63,7 @@ public class Chat implements Serializable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
public int hashCode() { public int hashCode() { return Objects.hash(recipient); }
return Objects.hash(recipient);
}
/** /**
* Tests equality to another object based on the recipient. * Tests equality to another object based on the recipient.
@ -89,46 +72,39 @@ public class Chat implements Serializable {
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) if (this == obj) return true;
return true; if (!(obj instanceof Chat)) return false;
if (!(obj instanceof Chat)) final Chat other = (Chat) obj;
return false;
final var other = (Chat) obj;
return Objects.equals(recipient, other.recipient); return Objects.equals(recipient, other.recipient);
} }
/** /**
* Sets the status of all chat messages received from the recipient to {@code READ} starting * Sets the status of all chat messages received from the recipient to
* from the bottom and stopping once a read message is found. * {@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 * @param writeProxy the write proxy instance used to notify the server about
* changes * the message status changes
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void read(WriteProxy writeProxy) { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final var m = messages.get(i); final Message m = messages.get(i);
if (m.getSenderID() == recipient.getID()) if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
if (m.getStatus() == MessageStatus.READ) else {
break; m.setStatus(MessageStatus.READ);
else { writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
m.setStatus(MessageStatus.READ); }
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
}
} }
totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount);
unreadAmount = 0; unreadAmount = 0;
} }
/** /**
* @return {@code true} if the newest message received in the chat doesn't have the status * @return {@code true} if the newest message received in the chat doesn't have
* {@code READ} * the status {@code READ}
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public boolean isUnread() { public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
return !messages.isEmpty()
&& messages.get(messages.size() - 1).getStatus() != MessageStatus.READ;
}
/** /**
* Inserts a message at the correct place according to its creation date. * Inserts a message at the correct place according to its creation date.
@ -145,32 +121,12 @@ public class Chat implements Serializable {
messages.add(0, message); messages.add(0, message);
} }
/**
* 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 boolean remove(long messageID) {
return messages.removeIf(m -> m.getID() == messageID);
}
/**
* @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. * Increments the amount of unread messages.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void incrementUnreadAmount() { public void incrementUnreadAmount() { unreadAmount++; }
++unreadAmount;
totalUnreadAmount.set(totalUnreadAmount.get() + 1);
}
/** /**
* @return the amount of unread messages in this chat * @return the amount of unread messages in this chat
@ -191,7 +147,8 @@ public class Chat implements Serializable {
public Contact getRecipient() { return recipient; } public Contact getRecipient() { return recipient; }
/** /**
* @return the last known time a {@link envoy.event.IsTyping} event has been sent * @return the last known time a {@link envoy.event.IsTyping} event has been
* sent
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public long getLastWritingEvent() { return lastWritingEvent; } public long getLastWritingEvent() { return lastWritingEvent; }
@ -201,25 +158,5 @@ public class Chat implements Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void lastWritingEventWasNow() { public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
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; }
} }

View File

@ -5,8 +5,8 @@ import static java.util.function.Function.identity;
import envoy.data.Config; import envoy.data.Config;
/** /**
* Implements a configuration specific to the Envoy Client with default values and convenience * Implements a configuration specific to the Envoy Client with default values
* methods. * and convenience methods.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -20,8 +20,7 @@ public final class ClientConfig extends Config {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static ClientConfig getInstance() { public static ClientConfig getInstance() {
if (config == null) if (config == null) config = new ClientConfig();
config = new ClientConfig();
return config; return config;
} }
@ -48,7 +47,5 @@ public final class ClientConfig extends Config {
* @return the amount of minutes after which the local database should be saved * @return the amount of minutes after which the local database should be saved
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Integer getLocalDBSaveInterval() { public Integer getLocalDBSaveInterval() { return (Integer) items.get("localDBSaveInterval").get(); }
return (Integer) items.get("localDBSaveInterval").get();
}
} }

View File

@ -36,8 +36,7 @@ public class Context {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void initWriteProxy() { public void initWriteProxy() {
if (localDB == null) if (localDB == null) throw new IllegalStateException("The LocalDB has to be initialized!");
throw new IllegalStateException("The LocalDB has to be initialized!");
writeProxy = new WriteProxy(client, localDB); writeProxy = new WriteProxy(client, localDB);
} }

View File

@ -2,14 +2,14 @@ package envoy.client.data;
import java.time.Instant; import java.time.Instant;
import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.GroupMessageStatusChange; import envoy.event.GroupMessageStatusChange;
import envoy.client.net.WriteProxy;
/** /**
* Represents a chat between a user and a group as a list of messages. * Represents a chat between a user and a group
* as a list of messages.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -25,7 +25,7 @@ public final class GroupChat extends Chat {
* @param recipient the group whose members receive the messages * @param recipient the group whose members receive the messages
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public GroupChat(User sender, Group recipient) { public GroupChat(User sender, Contact recipient) {
super(recipient); super(recipient);
this.sender = sender; this.sender = sender;
} }
@ -34,14 +34,11 @@ public final class GroupChat extends Chat {
public void read(WriteProxy writeProxy) { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i); final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) else {
break; gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
else { writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, Instant.now(), sender.getID()));
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ); }
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(),
MessageStatus.READ, Instant.now(), sender.getID()));
}
} }
unreadAmount = 0; unreadAmount = 0;
} }

View File

@ -1,40 +1,35 @@
package envoy.client.data; package envoy.client.data;
import static java.util.function.Predicate.not;
import java.io.*; import java.io.*;
import java.nio.channels.*; import java.nio.channels.*;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.logging.*; import java.util.logging.*;
import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.collections.*; import javafx.collections.*;
import dev.kske.eventbus.core.*; import envoy.client.event.*;
import dev.kske.eventbus.core.Event;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.*;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.*; import envoy.util.*;
import envoy.client.event.*; import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
/** /**
* Stores information about the current {@link User} and their {@link Chat}s. For message ID * Stores information about the current {@link User} and their {@link Chat}s.
* generation a {@link IDGenerator} is stored as well. * For message ID generation a {@link IDGenerator} is stored as well.
* <p> * <p>
* The managed objects are stored inside a folder in the local file system. * The managed objects are stored inside a folder in the local file system.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public final class LocalDB { public final class LocalDB implements EventListener {
// Data // Data
private User user; private User user;
@ -43,7 +38,6 @@ public final class LocalDB {
private IDGenerator idGenerator; private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap(); private CacheMap cacheMap = new CacheMap();
private String authToken; private String authToken;
private boolean contactsChanged;
// Auto save timer // Auto save timer
private Timer autoSaver; private Timer autoSaver;
@ -73,11 +67,8 @@ public final class LocalDB {
EventBus.getInstance().registerListener(this); EventBus.getInstance().registerListener(this);
// Ensure that the database directory exists // Ensure that the database directory exists
if (!dbDir.exists()) if (!dbDir.exists()) dbDir.mkdirs();
dbDir.mkdirs(); else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
else if (!dbDir.isDirectory())
throw new IOException(
String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
// Lock the directory // Lock the directory
lock(); lock();
@ -96,7 +87,8 @@ public final class LocalDB {
} }
/** /**
* Ensured that only one Envoy instance is using this local database by creating a lock file. * Ensured that only one Envoy instance is using this local database by creating
* a lock file.
* The lock file is deleted on application exit. * The lock file is deleted on application exit.
* *
* @throws EnvoyException if the lock cannot by acquired * @throws EnvoyException if the lock cannot by acquired
@ -105,19 +97,17 @@ public final class LocalDB {
private synchronized void lock() throws EnvoyException { private synchronized void lock() throws EnvoyException {
final var file = new File(dbDir, "instance.lock"); final var file = new File(dbDir, "instance.lock");
try { try {
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
StandardOpenOption.WRITE);
instanceLock = fc.tryLock(); instanceLock = fc.tryLock();
if (instanceLock == null) if (instanceLock == null) throw new EnvoyException("Another Envoy instance is using this local database!");
throw new EnvoyException("Another Envoy instance is using this local database!");
} catch (final IOException e) { } catch (final IOException e) {
throw new EnvoyException("Could not create lock file!", e); throw new EnvoyException("Could not create lock file!", e);
} }
} }
/** /**
* Loads the local user registry {@code users.db}, the id generator {@code id_gen.db} and last * Loads the local user registry {@code users.db}, the id generator
* login file {@code last_login.db}. * {@code id_gen.db} and last login file {@code last_login.db}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@ -142,46 +132,10 @@ public final class LocalDB {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public synchronized void loadUserData() throws ClassNotFoundException, IOException { public synchronized void loadUserData() throws ClassNotFoundException, IOException {
if (user == null) if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
throw new IllegalStateException("Client user is null, cannot initialize user storage");
userFile = new File(dbDir, user.getID() + ".db"); userFile = new File(dbDir, user.getID() + ".db");
try (var in = new ObjectInputStream(new FileInputStream(userFile))) { try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
Chat.getTotalUnreadAmount().set(0); chats = FXCollections.observableList((List<Chat>) in.readObject());
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(); cacheMap = (CacheMap) in.readObject();
lastSync = (Instant) in.readObject(); lastSync = (Instant) in.readObject();
} finally { } finally {
@ -190,34 +144,30 @@ public final class LocalDB {
} }
/** /**
* Synchronizes the contact list of the client user with the chat and user storage. * Synchronizes the contact list of the client user with the chat and user
* storage.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void synchronize() { private void synchronize() {
user.getContacts().stream() user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
.filter(u -> u instanceof User && !users.containsKey(u.getName()))
.forEach(u -> users.put(u.getName(), (User) u));
users.put(user.getName(), user); users.put(user.getName(), user);
// Synchronize user status data // Synchronize user status data
for (final var contact : user.getContacts()) for (final var contact : users.values())
if (contact instanceof User) if (contact instanceof User) getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(contact.getStatus()); });
getChat(contact.getID()).ifPresent(chat -> {
((User) chat.getRecipient()).setStatus(((User) contact).getStatus());
});
// Create missing chats // Create missing chats
user.getContacts() user.getContacts()
.stream() .stream()
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty()) .filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, (Group) c)) .map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
.forEach(chats::add); .forEach(chats::add);
} }
/** /**
* Initializes a timer that automatically saves this local database after a period of time * Initializes a timer that automatically saves this local database after a
* specified in the settings. * period of time specified in the settings.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@ -232,148 +182,68 @@ public final class LocalDB {
autoSaver.schedule(new TimerTask() { autoSaver.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() { save(); }
save();
}
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000); }, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
} }
/** /**
* Stores all users. If the client user is specified, their chats will be stored as well. The * Stores all users. If the client user is specified, their chats will be stored
* message id generator will also be saved if present. * as well. The message id generator will also be saved if present.
* *
* @throws IOException if the saving process failed * @throws IOException if the saving process failed
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
@Event(EnvoyCloseEvent.class) @Event(eventType = EnvoyCloseEvent.class, priority = 1000)
@Priority(500)
private synchronized void save() { private synchronized void save() {
EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database...");
// Stop saving if this account has been deleted
if (userFile == null)
return;
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
// Save users // Save users
try { try {
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save user data and last sync time stamp // Save user data and last sync time stamp
if (user != null) if (user != null) SerializationUtils
SerializationUtils .write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
.write(userFile, new ArrayList<>(chats), cacheMap,
Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
// Save last login information // Save last login information
if (authToken != null) if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
SerializationUtils.write(lastLoginFile, user, authToken);
// Save ID generator // Save ID generator
if (hasIDGenerator()) if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
SerializationUtils.write(idGeneratorFile, idGenerator);
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", e);
e);
} }
} }
/** @Event(priority = 150)
* Deletes any local remnant of this user. private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); }
*
* @since Envoy Client v0.3-beta
*/
public void delete() {
try {
// Save ID generator - can be used for other users in that db @Event(priority = 150)
if (hasIDGenerator())
SerializationUtils.write(idGeneratorFile, idGenerator);
} catch (final IOException e) {
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ",
e);
}
if (lastLoginFile != null)
lastLoginFile.delete();
userFile.delete();
users.remove(user.getName());
userFile = null;
onLogout();
}
@Event
@Priority(500)
private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT)
msg.nextStatus();
}
@Event
@Priority(500)
private void onGroupMessage(GroupMessage msg) { private void onGroupMessage(GroupMessage msg) {
// TODO: Cancel event once EventBus is updated // TODO: Cancel event once EventBus is updated
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ) if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
logger.warning("The groupMessage has the unexpected status " + msg.getStatus()); logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
} }
@Event @Event(priority = 150)
@Priority(500) private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); }
private void onMessageStatusChange(MessageStatusChange evt) {
getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get()));
}
@Event @Event(priority = 150)
@Priority(500)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID()) this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
.ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
} }
@Event @Event(priority = 150)
@Priority(500)
private void onUserStatusChange(UserStatusChange evt) { private void onUserStatusChange(UserStatusChange evt) {
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast) this.getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get()));
.ifPresent(u -> u.setStatus(evt.get()));
} }
@Event @Event(priority = 150)
@Priority(500) private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); }
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 @Event(priority = 150)
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) { private void onNameChange(NameChange evt) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny() chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get()));
.ifPresent(c -> c.setName(evt.get()));
} }
/** /**
@ -383,17 +253,14 @@ public final class LocalDB {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@Event @Event
private void onNewAuthToken(NewAuthToken evt) { private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
authToken = evt.get();
}
/** /**
* Deletes all associations to the current user. * Deletes all associations to the current user.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@Event(Logout.class) @Event(eventType = Logout.class, priority = 100)
@Priority(50)
private void onLogout() { private void onLogout() {
autoSaver.cancel(); autoSaver.cancel();
autoSaveRestart = true; autoSaveRestart = true;
@ -407,53 +274,8 @@ public final class LocalDB {
} }
/** /**
* Deletes the message with the given ID, if present. * @return a {@code Map<String, User>} of all users stored locally with their
* * user names as keys
* @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(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));
}
@Event
@Priority(500)
private void onAccountDeletion(AccountDeletion deletion) {
if (user.getID() == deletion.get())
logger.log(Level.WARNING,
"I have been informed by the server that I have been deleted without even knowing it...");
getChat(deletion.get()).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 * @since Envoy Client v0.2-alpha
*/ */
public Map<String, User> getUsers() { return users; } public Map<String, User> getUsers() { return users; }
@ -466,8 +288,7 @@ public final class LocalDB {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T extends Message> Optional<T> getMessage(long id) { public <T extends Message> Optional<T> getMessage(long id) {
return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream) return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
.filter(m -> m.getID() == id).findAny();
} }
/** /**
@ -477,12 +298,11 @@ public final class LocalDB {
* @return an optional containing the chat * @return an optional containing the chat
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Optional<Chat> getChat(long recipientID) { public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny();
}
/** /**
* @return all saved {@link Chat} objects that list the client user as the sender * @return all saved {@link Chat} objects that list the client user as the
* sender
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
**/ **/
public ObservableList<Chat> getChats() { return chats; } public ObservableList<Chat> getChats() { return chats; }
@ -509,17 +329,14 @@ public final class LocalDB {
* @param idGenerator the message ID generator to set * @param idGenerator the message ID generator to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
@Event @Event(priority = 150)
@Priority(150)
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; } public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
/** /**
* @return {@code true} if an {@link IDGenerator} is present * @return {@code true} if an {@link IDGenerator} is present
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public boolean hasIDGenerator() { public boolean hasIDGenerator() { return idGenerator != null; }
return idGenerator != null;
}
/** /**
* @return the cache map for messages and message status changes * @return the cache map for messages and message status changes

View File

@ -5,22 +5,23 @@ import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import dev.kske.eventbus.core.*; import envoy.client.event.EnvoyCloseEvent;
import envoy.util.*; import envoy.util.*;
import envoy.client.event.EnvoyCloseEvent; import dev.kske.eventbus.*;
import dev.kske.eventbus.EventListener;
/** /**
* Manages all application settings, which are different objects that can be changed during runtime * Manages all application settings, which are different objects that can be
* and serialized them by using either the file system or the {@link Preferences} API. * changed during runtime and serialized them by using either the file system or
* the {@link Preferences} API.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public final class Settings { public final class Settings implements EventListener {
// Actual settings accessible by the rest of the application // Actual settings accessible by the rest of the application
private Map<String, SettingsItem<?>> items; private Map<String, SettingsItem<?>> items;
@ -28,8 +29,7 @@ public final class Settings {
/** /**
* Settings are stored in this file. * Settings are stored in this file.
*/ */
private static final File settingsFile = private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
/** /**
* Singleton instance of this class. * Singleton instance of this class.
@ -37,8 +37,8 @@ public final class Settings {
private static Settings settings = new Settings(); private static Settings settings = new Settings();
/** /**
* The way to instantiate the settings. Is set to private to deny other instances of that * The way to instantiate the settings. Is set to private to deny other
* object. * instances of that object.
* *
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
@ -68,7 +68,7 @@ public final class Settings {
* @throws IOException if an error occurs while saving the themes * @throws IOException if an error occurs while saving the themes
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
@Event(EnvoyCloseEvent.class) @Event(eventType = EnvoyCloseEvent.class, priority = 900)
private void save() { private void save() {
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings..."); EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
@ -76,27 +76,20 @@ public final class Settings {
try { try {
SerializationUtils.write(settingsFile, items); SerializationUtils.write(settingsFile, items);
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ", EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ", e);
e);
} }
} }
private void supplementDefaults() { private void supplementDefaults() {
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
"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("hideOnClose", items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
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", items.putIfAbsent("downloadLocation",
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
"Download location", "The location where files will be saved to"));
"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("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?",
"Should downloads be saved without asking?"));
items.putIfAbsent("askForConfirmation", items.putIfAbsent("askForConfirmation",
new SettingsItem<>(true, "Ask for confirmation", new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things"));
"Will ask for confirmation before doing certain things"));
} }
/** /**
@ -111,9 +104,7 @@ public final class Settings {
* @param themeName the name to set * @param themeName the name to set
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setCurrentTheme(String themeName) { public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
((SettingsItem<String>) items.get("currentTheme")).set(themeName);
}
/** /**
* @return true if the currently used theme is one of the default themes * @return true if the currently used theme is one of the default themes
@ -125,8 +116,9 @@ public final class Settings {
} }
/** /**
* @return {@code true}, if pressing the {@code Enter} key suffices to send a message. Otherwise * @return {@code true}, if pressing the {@code Enter} key suffices to send a
* it has to be pressed in conjunction with the {@code Control} key. * message. Otherwise it has to be pressed in conjunction with the
* {@code Control} key.
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); } public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
@ -134,27 +126,26 @@ public final class Settings {
/** /**
* Changes the keystrokes performed by the user to send a message. * 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} * @param enterToSend If set to {@code true} a message can be sent by pressing
* key. Otherwise it has to be pressed in conjunction with the * the {@code Enter} key. Otherwise it has to be pressed in
* {@code Control} key. * conjunction with the {@code Control} key.
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setEnterToSend(boolean enterToSend) { public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend);
}
/** /**
* @return whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment} * @return whether Envoy will prompt a dialogue before saving an
* {@link envoy.data.Attachment}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Boolean isDownloadSavedWithoutAsking() { public Boolean isDownloadSavedWithoutAsking() { return (Boolean) items.get("autoSaveDownloads").get(); }
return (Boolean) items.get("autoSaveDownloads").get();
}
/** /**
* Sets whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment}. * 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 * @param autosaveDownload whether a download should be saved without asking
* before
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) { public void setDownloadSavedWithoutAsking(boolean autosaveDownload) {
@ -173,9 +164,7 @@ public final class Settings {
* @param downloadLocation the path to set * @param downloadLocation the path to set
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void setDownloadLocation(File downloadLocation) { public void setDownloadLocation(File downloadLocation) { ((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation); }
((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation);
}
/** /**
* @return the current on close mode. * @return the current on close mode.
@ -189,24 +178,21 @@ public final class Settings {
* @param hideOnClose whether the application should be minimized on close * @param hideOnClose whether the application should be minimized on close
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setHideOnClose(boolean hideOnClose) { public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); }
((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose);
}
/** /**
* @return whether a confirmation dialog should be displayed before certain actions * @return whether a confirmation dialog should be displayed before certain
* actions
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Boolean isAskForConfirmation() { public Boolean isAskForConfirmation() { return (Boolean) items.get("askForConfirmation").get(); }
return (Boolean) items.get("askForConfirmation").get();
}
/** /**
* Changes the behavior of calling certain functionality by displaying a confirmation dialog * Changes the behavior of calling certain functionality by displaying a
* before executing it. * confirmation dialog before executing it.
* *
* @param askForConfirmation whether confirmation dialogs should be displayed before certain * @param askForConfirmation whether confirmation dialogs should be displayed
* actions * before certain actions
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setAskForConfirmation(boolean askForConfirmation) { public void setAskForConfirmation(boolean askForConfirmation) {

View File

@ -1,11 +1,13 @@
package envoy.client.data; package envoy.client.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.function.Consumer;
import javax.swing.JComponent; import javax.swing.JComponent;
/** /**
* Encapsulates a persistent value that is directly or indirectly mutable by the user. * Encapsulates a persistent value that is directly or indirectly mutable by the
* user.
* *
* @param <T> the type of this {@link SettingsItem}'s value * @param <T> the type of this {@link SettingsItem}'s value
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -16,11 +18,14 @@ public final class SettingsItem<T> implements Serializable {
private T value; private T value;
private String userFriendlyName, description; private String userFriendlyName, description;
private transient Consumer<T> changeHandler;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Initializes a {@link SettingsItem}. The default value's class will be mapped to a * Initializes a {@link SettingsItem}. The default value's class will be mapped
* {@link JComponent} that can be used to display this {@link SettingsItem} to the user. * to a {@link JComponent} that can be used to display this {@link SettingsItem}
* to the user.
* *
* @param value the default value * @param value the default value
* @param userFriendlyName the user friendly name (short) * @param userFriendlyName the user friendly name (short)
@ -37,18 +42,17 @@ public final class SettingsItem<T> implements Serializable {
* @return the value * @return the value
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public T get() { public T get() { return value; }
return value;
}
/** /**
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if defined, it * Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
* will be invoked with this value. * defined, it will be invoked with this value.
* *
* @param value the value to set * @param value the value to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void set(T value) { public void set(T value) {
if (changeHandler != null && value != this.value) changeHandler.accept(value);
this.value = value; this.value = value;
} }
@ -62,9 +66,7 @@ public final class SettingsItem<T> implements Serializable {
* @param userFriendlyName the userFriendlyName to set * @param userFriendlyName the userFriendlyName to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setUserFriendlyName(String userFriendlyName) { public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
this.userFriendlyName = userFriendlyName;
}
/** /**
* @return the description * @return the description
@ -77,4 +79,17 @@ public final class SettingsItem<T> implements Serializable {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setDescription(String description) { this.description = description; } 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);
}
} }

View File

@ -22,9 +22,7 @@ public final class AudioPlayer {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public AudioPlayer() { public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
this(AudioRecorder.DEFAULT_AUDIO_FORMAT);
}
/** /**
* Initializes the player with a given audio format. * Initializes the player with a given audio format.

View File

@ -20,8 +20,7 @@ public final class AudioRecorder {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static final AudioFormat DEFAULT_AUDIO_FORMAT = public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
new AudioFormat(16000, 16, 1, true, false);
/** /**
* The format in which audio files will be saved. * The format in which audio files will be saved.
@ -39,9 +38,7 @@ public final class AudioRecorder {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public AudioRecorder() { public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
this(DEFAULT_AUDIO_FORMAT);
}
/** /**
* Initializes the recorder with a given audio format. * Initializes the recorder with a given audio format.

View File

@ -1,20 +0,0 @@
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);
}

View File

@ -0,0 +1,30 @@
package envoy.client.data.commands;
import java.util.function.Supplier;
/**
* 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 OnCall {
/**
* Performs class specific actions when a {@link SystemCommand} has been called.
*
* @since Envoy Client v0.2-beta
*/
void onCall();
/**
* Performs actions that can only be performed by classes that are not
* {@link SystemCommand}s when a SystemCommand has been called.
*
* @param consumer the action to perform when this {@link SystemCommand} has
* been called
* @since Envoy Client v0.2-beta
*/
void onCall(Supplier<Void> consumer);
}

View File

@ -1,20 +1,23 @@
package envoy.client.data.commands; package envoy.client.data.commands;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.*;
/** /**
* This class is the base class of all {@code SystemCommands} and contains an action and a number of * This class is the base class of all {@code SystemCommands} and contains an
* arguments that should be used as input for this function. No {@code SystemCommand} can return * action and a number of arguments that should be used as input for this
* anything. Every {@code SystemCommand} must have as argument type {@code List<String>} so that the * function.
* words following the indicator String can be used as input of the function. This approach has one * No {@code SystemCommand} can return anything.
* limitation:<br> * Every {@code SystemCommand} must have as argument type {@code List<String>} so
* <b>Order matters!</b> Changing the order of arguments will likely result in unexpected behavior. * 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 * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public final class SystemCommand implements Callable { public final class SystemCommand implements OnCall {
protected int relevance; protected int relevance;
@ -25,8 +28,8 @@ public final class SystemCommand implements Callable {
/** /**
* This function takes a {@code List<String>} as argument because automatically * This function takes a {@code List<String>} as argument because automatically
* {@code SystemCommand#numberOfArguments} words following the necessary command will be put * {@code SystemCommand#numberOfArguments} words following the necessary command
* into this list. * will be put into this list.
* *
* @see String#split(String) * @see String#split(String)
*/ */
@ -45,14 +48,19 @@ public final class SystemCommand implements Callable {
* @param description the description of this {@code SystemCommand} * @param description the description of this {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand(Consumer<List<String>> action, int numberOfArguments, public SystemCommand(Consumer<List<String>> action, int numberOfArguments, List<String> defaults, String description) {
List<String> defaults, String description) {
this.numberOfArguments = numberOfArguments; this.numberOfArguments = numberOfArguments;
this.action = action; this.action = action;
this.defaults = defaults == null ? new ArrayList<>() : defaults; this.defaults = defaults == null ? new ArrayList<>() : defaults;
this.description = description; this.description = description;
} }
/**
* @return the action that should be performed
* @since Envoy Client v0.2-beta
*/
public Consumer<List<String>> getAction() { return action; }
/** /**
* @return the argument count of the command * @return the argument count of the command
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -77,10 +85,20 @@ public final class SystemCommand implements Callable {
*/ */
public void setRelevance(int relevance) { this.relevance = relevance; } public void setRelevance(int relevance) { this.relevance = relevance; }
/**
* Increments the relevance of this {@code SystemCommand}.
*/
@Override @Override
public void call(List<String> arguments) { public void onCall() { relevance++; }
action.accept(arguments);
++relevance; /**
* Increments the relevance of this {@code SystemCommand} and executes the
* supplier.
*/
@Override
public void onCall(Supplier<Void> consumer) {
onCall();
consumer.get();
} }
/** /**
@ -90,27 +108,21 @@ public final class SystemCommand implements Callable {
public List<String> getDefaults() { return defaults; } public List<String> getDefaults() { return defaults; }
@Override @Override
public int hashCode() { public int hashCode() { return Objects.hash(action); }
return Objects.hash(action);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) if (this == obj) return true;
return true; if (obj == null) return false;
if (obj == null) if (getClass() != obj.getClass()) return false;
return false; final SystemCommand other = (SystemCommand) obj;
if (getClass() != obj.getClass())
return false;
final var other = (SystemCommand) obj;
return Objects.equals(action, other.action); return Objects.equals(action, other.action);
} }
@Override @Override
public String toString() { public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
+ ", " + (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "")
+ (description != null ? "description=" + description + ", " : "") + (defaults != null ? "defaults=" + defaults : "") + "]";
+ (defaults != null ? "defaults=" + defaults : "") + "]";
} }
} }

View File

@ -20,21 +20,18 @@ public final class SystemCommandBuilder {
private final SystemCommandMap commandsMap; private final SystemCommandMap commandsMap;
/** /**
* Creates a new {@code SystemCommandsBuilder} without underlying {@link SystemCommandMap}. * Creates a new {@code SystemCommandsBuilder} without underlying
* {@link SystemCommandMap}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommandBuilder() { public SystemCommandBuilder() { this(null); }
this(null);
}
/** /**
* @param commandsMap the map to use when calling build (optional) * @param commandsMap the map to use when calling build (optional)
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommandBuilder(SystemCommandMap commandsMap) { public SystemCommandBuilder(SystemCommandMap commandsMap) { this.commandsMap = commandsMap; }
this.commandsMap = commandsMap;
}
/** /**
* @param numberOfArguments the numberOfArguments to set * @param numberOfArguments the numberOfArguments to set
@ -107,14 +104,12 @@ public final class SystemCommandBuilder {
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build() { public SystemCommand build() { return build(true); }
return build(true);
}
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the previous * {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
* value.<br> * previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
@ -127,8 +122,8 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * 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 * {@code SystemCommand#numberOfArguments} will be set to use the rest of the
* argument, regardless of the previous value.<br> * string as argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
@ -141,25 +136,27 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. At the end, this * Automatically adds the built object to the given map.
* {@code SystemCommandBuilder} <b>can</b> be reset but must not be. * 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> * @param reset whether this {@code SystemCommandBuilder} should be reset
* This can be useful if another command wants to execute something similar * afterwards.<br>
* This can be useful if another command wants to execute something
* similar
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build(boolean reset) { public SystemCommand build(boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description); final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance); sc.setRelevance(relevance);
if (reset) if (reset) reset();
reset();
return sc; return sc;
} }
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data. Automatically adds the * Builds a {@code SystemCommand} based upon the previously entered data.
* built object to the given map. * Automatically adds the built object to the given map.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap} * {@link SystemCommandMap}
@ -167,14 +164,13 @@ public final class SystemCommandBuilder {
* @throws NullPointerException if no map has been assigned to this builder * @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build(String command) { public SystemCommand build(String command) { return build(command, true); }
return build(command, true);
}
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments} * Automatically adds the built object to the given map.
* will be set to 0, regardless of the previous value.<br> * {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
* previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
@ -190,8 +186,9 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments} * Automatically adds the built object to the given map.
* will be set to use the rest of the string as argument, regardless of the previous value.<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. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
@ -207,13 +204,17 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. At the end, this * Automatically adds the built object to the given map.
* {@code SystemCommandBuilder} <b>can</b> be reset but must not be. * 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 * @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap} * {@link SystemCommandMap}
* @param reset whether this {@code SystemCommandBuilder} should be reset afterwards.<br> * @param reset whether this {@code SystemCommandBuilder} should be reset
* This can be useful if another command wants to execute something similar * afterwards.<br>
* This can be useful if another command wants to execute
* something
* similar
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder * @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -221,12 +222,9 @@ public final class SystemCommandBuilder {
public SystemCommand build(String command, boolean reset) { public SystemCommand build(String command, boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description); final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance); sc.setRelevance(relevance);
if (commandsMap != null) if (commandsMap != null) commandsMap.add(command, sc);
commandsMap.add(command, sc); else throw new NullPointerException("No map in SystemCommandsBuilder present");
else if (reset) reset();
throw new NullPointerException("No map in SystemCommandsBuilder present");
if (reset)
reset();
return sc; return sc;
} }
} }

View File

@ -1,243 +1,162 @@
package envoy.client.data.commands; package envoy.client.data.commands;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.*; import java.util.logging.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Stores all {@link SystemCommand}s used. SystemCommands can be called using an activator char and * This class stores all {@link SystemCommand}s used.
* 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 * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public final class SystemCommandMap { public final class SystemCommandMap {
private final Character activator; private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Pattern commandPattern = private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$");
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class); 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. * Adds a new command to the map if the command name is valid.
* *
* @param command the input string to execute the given action * @param command the input string to execute the
* @param systemCommand the command to add - can be built using {@link SystemCommandBuilder} * given action
* @param systemCommand the command to add - can be built using
* {@link SystemCommandBuilder}
* @see SystemCommandMap#isValidKey(String) * @see SystemCommandMap#isValidKey(String)
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void add(String command, SystemCommand systemCommand) { public void add(String command, SystemCommand systemCommand) {
if (isValidKey(command)) if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand);
systemCommands.put(command.toLowerCase(), systemCommand);
} }
/** /**
* This method checks if the input String is a key in the map and returns the wrapped System * This method checks if the input String is a key in the map and returns the
* command if present. * wrapped System command if present.
* It will return an empty optional if the value after the slash is not a key in
* the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* map).
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, ""));}<br> * {@code Button button = new Button();}
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "*example xyz ..."}<br> * user input: {@code "/example xyz ..."}<br>
* {@code systemCommands.get("example xyz ...")} or * {@code systemCommands.get("example xyz ...")} or
* {@code systemCommands.get("*example xyz ...")} result: * {@code systemCommands.get("/example xyz ...")}
* {@code Optional<SystemCommand>.get() != null} * result: {@code Optional<SystemCommand>}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return the wrapped system command, if present * @return the wrapped system command, if present
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Optional<SystemCommand> get(String input) { public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); }
return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase())));
}
/** /**
* This method ensures that the activator of a {@link SystemCommand} is stripped.<br> * This method ensures that the "/" of a {@link SystemCommand} is stripped.<br>
* It only checks the word beginning from the first non-blank position in the input. It returns * It returns the command as (most likely) entered as key in the map for the
* the command as (most likely) entered as key in the map for the first word of the text.<br> * first word of the text.<br>
* Activators in the middle of the word will be disregarded. * It should only be called on strings that contain a "/" at position 0/-1.
* *
* @param raw the input * @param raw the input
* @return the command as entered in the map * @return the command as entered in the map
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote this method will (most likely) not return anything useful if whatever is entered * @apiNote this method will (most likely) not return anything useful if
* after the activator is not a system command. Only exception: for recommendation * whatever is entered after the slash is not a system command. Only
* purposes. * exception: for recommendation purposes.
*/ */
public String getCommand(String raw) { public String getCommand(String raw) {
final var trimmed = raw.stripLeading(); final var trimmed = raw.stripLeading();
final var index = trimmed.indexOf(' ');
// Entering only the activator should not throw an error return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
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 * Examines whether a key can be put in the map and logs it with
* key violates API constrictions.<br> * {@code Level.WARNING} if that key violates API constrictions.<br>
* (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>) * (allowed chars are <b>a-zA-Z0-9_:!()?.,;-</b>)
* <p> * <p>
* The approach to not throw an exception was taken so that an ugly try-catch block for every * The approach to not throw an exception was taken so that an ugly try-catch
* addition to the system commands map could be avoided, an error that should only occur during * block for every addition to the system commands map could be avoided, an
* implementation and not in production. * error that should only occur during implementation and not in production.
* *
* @param command the key to examine * @param command the key to examine
* @return whether this key can be used in the map * @return whether this key can be used in the map
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean isValidKey(String command) { public boolean isValidKey(String command) {
final var valid = commandPattern.matcher(command).matches(); final boolean valid = commandPattern.matcher(command).matches();
if (!valid) if (!valid) logger.log(Level.WARNING,
logger.log(Level.WARNING,
"The command \"" + command "The command \"" + command
+ "\" is not valid. As it might cause problems when executed, it will not be entered into the map. Only the characters " + "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
+ commandPattern + "are allowed"); + commandPattern + "are allowed");
return valid; return valid;
} }
/** /**
* Takes a 'raw' string (the whole input) and checks if the activator is the first visible * Takes a 'raw' string (the whole input) and checks if "/" is the first visible
* character and then checks if a command is present after that activator. If that is the case, * character and then checks if a command is present after that "/". If that is
* it will be executed. * the case, it will be executed.
* <p>
* *
* @param raw the raw input string * @param raw the raw input string
* @return whether a command could be found and successfully executed * @return whether a command could be found
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean executeIfPresent(String raw) { public boolean executeIfAnyPresent(String raw) {
// possibly a command was detected and could be executed // possibly a command was detected and could be executed
final var raw2 = raw.stripLeading(); final var raw2 = raw.stripLeading();
final var commandFound = activator == null || raw2.startsWith(activator.toString()) final var commandFound = raw2.startsWith("/") ? executeIfPresent(raw2) : false;
? executeAvailableCommand(raw2)
: false;
// the command was executed successfully - no further checking needed // the command was executed successfully - no further checking needed
if (commandFound) if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2));
logger.log(Level.FINE, "executed system command " + getCommand(raw2));
return commandFound; return commandFound;
} }
/** /**
* Retrieves the recommendations based on the current input entered.<br> * This method checks if the input String is a key in the map and executes the
* The first word is used for the recommendations and it does not matter if the activator is at * wrapped System command if present.
* its beginning or not.<br> * Its intended usage is after a "/" has been detected in the input String.
* If recommendations are present, the given function will be executed on the * It will do nothing if the value after the slash is not a key in
* recommendations.<br> * the map, which is a valid case (i.e. input="3/4" and "4" is not a key in the
* Otherwise nothing will be done.<br> * map).
*
* @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> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}<br> * {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> {button.setText(text.get(0))}, * {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
* 1, null, ""));}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "*example xyz ..."}<br> * user input: {@code "/example xyz ..."}<br>
* {@code systemCommands.executeIfPresent("example xyz ...")} or * {@code systemCommands.executeIfPresent("example xyz ...")}
* {@code systemCommands.executeIfPresent("*example xyz ...")} result: * result: {@code button.getText()=="xyz"}
* {@code button.getText()=="xyz"}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return whether a command could be found and successfully executed * @return whether a command could be found
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private boolean executeAvailableCommand(String input) { public boolean executeIfPresent(String input) {
final var command = getCommand(input); final var command = getCommand(input);
final var value = get(command); final var value = get(command);
final var commandExecuted = new AtomicBoolean(value.isPresent());
value.ifPresent(systemCommand -> { value.ifPresent(systemCommand -> {
// Splitting the String so that the leading command including the first " " is // 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 // removed and only as many following words as allowed by the system command
// persist // persist
final var arguments = extractArguments(input, systemCommand); final var arguments = extractArguments(input, systemCommand);
// Executing the function // Executing the function
try { try {
systemCommand.call(arguments); systemCommand.getAction().accept(arguments);
} catch (final NumberFormatException e) { systemCommand.onCall();
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) { } catch (final Exception e) {
logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e); logger.log(Level.WARNING, "The 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(); return value.isPresent();
} }
/** /**
@ -249,76 +168,87 @@ public final class SystemCommandMap {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private List<String> extractArguments(String input, SystemCommand systemCommand) { private List<String> extractArguments(String input, SystemCommand systemCommand) {
// no more arguments follow after the command (e.g. text = "/DABR") // no more arguments follow after the command (e.g. text = "/DABR")
final var indexOfSpace = input.indexOf(" "); final var indexOfSpace = input.indexOf(" ");
if (indexOfSpace < 0) if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand);
return supplementDefaults(new String[] {}, systemCommand);
// the arguments behind a system command // the arguments behind a system command
final var remainingString = input.substring(indexOfSpace + 1); final var remainingString = input.substring(indexOfSpace + 1);
final var numberOfArguments = systemCommand.getNumberOfArguments(); final var numberOfArguments = systemCommand.getNumberOfArguments();
// splitting those arguments and supplying default values // splitting those arguments and supplying default values
final var textArguments = remainingString.split(" ", -1); final var textArguments = remainingString.split(" ", -1);
final var originalArguments = final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments;
numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments)
: textArguments;
final var arguments = supplementDefaults(originalArguments, systemCommand); final var arguments = supplementDefaults(originalArguments, systemCommand);
return arguments; return arguments;
} }
/**
* 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 "/" is at its beginning or not.<br>
* If none are present, nothing will be done.<br>
* Otherwise the given function will be executed on the recommendations.<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);
}
/** /**
* Recommends commands based upon the currently entered input.<br> * Recommends commands based upon the currently entered input.<br>
* In the current implementation, all that gets checked is whether a key contains this input. * In the current implementation, all we check is whether a key contains this
* This might be updated later on. * input. This might be updated later on.
* *
* @param partialCommand the partially entered command * @param partialCommand the partially entered command
* @return a set of all commands that match this input * @return a set of all commands that match this input
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private Set<String> recommendCommands(String partialCommand) { private Set<String> recommendCommands(String partialCommand) {
// current implementation only looks if input is contained within a command, // current implementation only looks if input is contained within a command,
// might be updated // might be updated
return systemCommands.keySet() return systemCommands.keySet()
.stream() .stream()
.filter(command -> command.contains(partialCommand)) .filter(command -> command.contains(partialCommand))
.sorted( .sorted((command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance()))
(command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(),
systemCommands.get(command2).getRelevance()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
/** /**
* Supplies the default values for arguments if none are present in the text for any argument. *
* <br> * Supplies the default values for arguments if none are present in the text for
* any argument. <br>
* Will only work for {@code SystemCommand}s whose argument counter is bigger
* than 1.
* *
* @param textArguments the arguments that were parsed from the text * @param textArguments the arguments that were parsed from the text
* @param toEvaluate the system command whose default values should be used * @param toEvaluate the system command whose default values should be used
* @return the final argument list * @return the final argument list
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote this method will insert an empty String if the size of the list given to the * @apiNote this method will insert an empty String if the size of the list
* {@code SystemCommand} is smaller than its argument counter and no more text * given to the {@code SystemCommand} is smaller than its argument
* arguments could be found. * counter and no more text arguments could be found.
*/ */
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) { private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
final var defaults = toEvaluate.getDefaults(); final var defaults = toEvaluate.getDefaults();
final var numberOfArguments = toEvaluate.getNumberOfArguments(); final var numberOfArguments = toEvaluate.getNumberOfArguments();
final List<String> result = new ArrayList<>(); final List<String> result = new ArrayList<>();
if (toEvaluate.getNumberOfArguments() > 0) if (toEvaluate.getNumberOfArguments() > 0) for (int index = 0; index < numberOfArguments; index++) {
for (var index = 0; index < numberOfArguments; index++) { String textArg = null;
String textArg = null; if (index < textArguments.length) textArg = textArguments[index];
if (index < textArguments.length) // Set the argument at position index to the current argument of the text, if it
textArg = textArguments[index]; // is present. Otherwise the default for that argument will be taken if present.
// In the worst case, an empty String will be used.
// Set the argument at position index to the current argument of the text, if it result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : "");
// 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 result;
} }
@ -327,10 +257,4 @@ public final class SystemCommandMap {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Map<String, SystemCommand> getSystemCommands() { return systemCommands; } 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; }
} }

View File

@ -1,77 +0,0 @@
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.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);
}
}

View File

@ -1,79 +0,0 @@
package envoy.client.data.shortcuts;
import java.util.*;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.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);
}
}

View File

@ -1,24 +0,0 @@
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();
}

View File

@ -1,7 +0,0 @@
/**
* 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;

View File

@ -1,22 +0,0 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Signifies the deletion of an account.
*
* @author Leon Hofmeister
* @since Envoy Common v0.3-beta
*/
public class AccountDeletion extends Event<Long> {
private static final long serialVersionUID = 1L;
/**
* @param value the ID of the contact that was deleted
* @since Envoy Common v0.3-beta
*/
public AccountDeletion(Long value) {
super(value);
}
}

View File

@ -1,23 +0,0 @@
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);
}
}

View File

@ -3,8 +3,9 @@ package envoy.client.event;
import envoy.event.Event.Valueless; import envoy.event.Event.Valueless;
/** /**
* This event notifies various Envoy components of the application being about to shut down. This * This event notifies various Envoy components of the application being about
* allows the graceful closing of connections, persisting local data etc. * to shut down. This allows the graceful closing of connections, persisting
* local data etc.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta

View File

@ -1,22 +0,0 @@
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);
}
}

View File

@ -1,23 +0,0 @@
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);
}
}

View File

@ -15,19 +15,22 @@ public final class AlertHelper {
private AlertHelper() {} private AlertHelper() {}
/** /**
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()} returns * Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()}
* {@code true}. Immediately executes the action if no dialog was requested or the dialog was * returns {@code true}.
* exited with a confirmation. Does nothing if the dialog was closed without clicking on OK. * 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 alert the (customized) alert to show. <strong>Should not be shown
* already</strong>
* @param action the action to perform in case of success * @param action the action to perform in case of success
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void confirmAction(Alert alert, Runnable action) { public static void confirmAction(Alert alert, Runnable action) {
alert.setHeight(225);
alert.setWidth(400);
alert.setHeaderText(""); alert.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation()) if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run()); else action.run();
else
action.run();
} }
} }

View File

@ -1,10 +1,16 @@
package envoy.client.helper; package envoy.client.helper;
import dev.kske.eventbus.core.EventBus; import java.util.logging.Level;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent; import envoy.client.event.*;
import envoy.client.ui.StatusTrayIcon; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/** /**
* Simplifies shutdown actions. * Simplifies shutdown actions.
@ -18,28 +24,34 @@ public final class ShutdownHelper {
/** /**
* Exits Envoy or minimizes it, depending on the current state of * Exits Envoy or minimizes it, depending on the current state of
* {@link Settings#isHideOnClose()} and {@link StatusTrayIcon#isSupported()}. * {@link Settings#isHideOnClose()}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void exit() { public static void exit() {
exit(false); if (Settings.getInstance().isHideOnClose()) Context.getInstance().getStage().setIconified(true);
}
/**
* 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 { else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent()); EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0); System.exit(0);
} }
} }
/**
* Logs the current user out and reopens
* {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
});
}
} }

View File

@ -5,26 +5,25 @@ import java.net.Socket;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.logging.*; import java.util.logging.*;
import dev.kske.eventbus.core.*; import envoy.client.data.*;
import dev.kske.eventbus.core.Event; import envoy.client.event.EnvoyCloseEvent;
import envoy.data.*; import envoy.data.*;
import envoy.event.*; import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import envoy.client.data.ClientConfig; import dev.kske.eventbus.*;
import envoy.client.event.EnvoyCloseEvent; import dev.kske.eventbus.Event;
/** /**
* Establishes a connection to the server, performs a handshake and delivers certain objects to the * Establishes a connection to the server, performs a handshake and delivers
* server. * certain objects to the server.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public final class Client implements Closeable { public final class Client implements EventListener, Closeable {
// Connection handling // Connection handling
private Socket socket; private Socket socket;
@ -45,30 +44,26 @@ public final class Client implements Closeable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Client() { public Client() { eventBus.registerListener(this); }
eventBus.registerListener(this);
}
/** /**
* Enters the online mode by acquiring a user ID from the server. As a connection has to be * Enters the online mode by acquiring a user ID from the server. As a
* established and a handshake has to be made, this method will block for up to 5 seconds. If * connection has to be established and a handshake has to be made, this method
* the handshake does exceed this time limit, an exception is thrown. * 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 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 TimeoutException if the server could not be reached
* @throws IOException if the login credentials could not be written * @throws IOException if the login credentials could not be written
* @throws InterruptedException if the current thread is interrupted while waiting for the * @throws InterruptedException if the current thread is interrupted while
* handshake response * waiting for the handshake response
*/ */
public void performHandshake(LoginCredentials credentials) public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully");
if (online)
throw new IllegalStateException("Handshake has already been performed successfully");
rejected = false;
// Establish TCP connection // Establish TCP connection
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort()); socket = new Socket(config.getServer(), config.getPort());
logger.log(Level.FINE, "Successfully established TCP connection to server"); logger.log(Level.FINE, "Successfully established TCP connection to server");
@ -78,6 +73,9 @@ public final class Client implements Closeable {
// Register user creation processor, contact list processor, message cache and // Register user creation processor, contact list processor, message cache and
// authentication token // authentication token
receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessor(User.class, sender -> this.sender = sender);
receiver.registerProcessors(cacheMap.getMap());
rejected = false;
// Start receiver // Start receiver
receiver.start(); receiver.start();
@ -97,22 +95,44 @@ public final class Client implements Closeable {
return; return;
} }
if (System.currentTimeMillis() - start > 5000) { if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
rejected = true;
socket.close();
receiver.removeAllProcessors();
throw new TimeoutException("Did not log in after 5 seconds");
}
Thread.sleep(500); Thread.sleep(500);
} }
// Remove handshake specific processors
receiver.removeAllProcessors();
online = true; online = true;
logger.log(Level.INFO, "Handshake completed."); logger.log(Level.INFO, "Handshake completed.");
} }
/**
* 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 cacheMap the map of all caches needed
* @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 {
checkOnline();
// Remove all processors as they are only used during the handshake
receiver.removeAllProcessors();
// Relay cached messages and message status changes
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();
// Relay caches
cacheMap.getMap().values().forEach(Cache::relay);
}
/** /**
* Sends an object to the server. * Sends an object to the server.
* *
@ -126,14 +146,14 @@ public final class Client implements Closeable {
logger.log(Level.FINE, "Sending " + obj); logger.log(Level.FINE, "Sending " + obj);
try { try {
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
} catch (final IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
/** /**
* Sends a message to the server. The message's status will be incremented once it was delivered * Sends a message to the server. The message's status will be incremented once
* successfully. * it was delivered successfully.
* *
* @param message the message to send * @param message the message to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -153,15 +173,11 @@ public final class Client implements Closeable {
send(new IDGeneratorRequest()); send(new IDGeneratorRequest());
} }
@Event(HandshakeRejection.class) @Event(eventType = HandshakeRejection.class, priority = 1000)
@Priority(1000) private void onHandshakeRejection() { rejected = true; }
private void onHandshakeRejection() {
rejected = true;
}
@Override @Override
@Event(EnvoyCloseEvent.class) @Event(eventType = EnvoyCloseEvent.class, priority = 800)
@Priority(50)
public void close() { public void close() {
if (online) { if (online) {
logger.log(Level.INFO, "Closing connection..."); logger.log(Level.INFO, "Closing connection...");
@ -183,10 +199,7 @@ public final class Client implements Closeable {
* @throws IllegalStateException if the client is not online * @throws IllegalStateException if the client is not online
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
private void checkOnline() throws IllegalStateException { private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); }
if (!online)
throw new IllegalStateException("Client is not online");
}
/** /**
* @return the {@link User} as which this client is logged in * @return the {@link User} as which this client is logged in

View File

@ -6,12 +6,13 @@ import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.*; import java.util.logging.*;
import dev.kske.eventbus.core.EventBus;
import envoy.util.*; import envoy.util.*;
import dev.kske.eventbus.*;
/** /**
* Receives objects from the server and passes them to processor objects based on their class. * Receives objects from the server and passes them to processor objects based
* on their class.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -39,7 +40,8 @@ public final class Receiver extends Thread {
} }
/** /**
* 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 * @since Envoy Client v0.3-alpha
*/ */
@ -64,19 +66,15 @@ public final class Receiver extends Thread {
// Server has stopped sending, i.e. because he went offline // Server has stopped sending, i.e. because he went offline
if (bytesRead == -1) { if (bytesRead == -1) {
isAlive = false; isAlive = false;
logger.log(Level.INFO, logger.log(Level.INFO, "Lost connection to the server. Exiting receiver...");
"Lost connection to the server. Exiting receiver...");
continue; continue;
} }
logger.log(Level.WARNING, logger.log(Level.WARNING,
String.format( String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
"LV encoding violated: expected %d bytes, received %d bytes. Discarding object...",
len, bytesRead));
continue; continue;
} }
try (ObjectInputStream oin = try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
final Object obj = oin.readObject(); final Object obj = oin.readObject();
logger.log(Level.FINE, "Received " + obj); logger.log(Level.FINE, "Received " + obj);
@ -85,19 +83,12 @@ public final class Receiver extends Thread {
final Consumer processor = processors.get(obj.getClass()); final Consumer processor = processors.get(obj.getClass());
// Dispatch to the processor if present // Dispatch to the processor if present
if (processor != null) if (processor != null) processor.accept(obj);
processor.accept(obj); // Dispatch to the event bus if the object is an event without a processor
// Dispatch to the event bus if the object has no processor else if (obj instanceof IEvent) eventBus.dispatch((IEvent) obj);
else
eventBus.dispatch(obj);
// TODO: Log DeadEvent from Event Bus 1.1.0
// Notify if no processor could be located // Notify if no processor could be located
// else else logger.log(Level.WARNING,
// logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
// String.format(
// "The received object has the %s for which no processor is defined.",
// obj.getClass()));
} }
} catch (final SocketException | EOFException e) { } catch (final SocketException | EOFException e) {
// Connection probably closed by client. // Connection probably closed by client.
@ -109,16 +100,14 @@ public final class Receiver extends Thread {
} }
/** /**
* Adds an object processor to this {@link Receiver}. It will be called once an object of the * Adds an object processor to this {@link Receiver}. It will be called once an
* accepted class has been received. * object of the accepted class has been received.
* *
* @param processorClass the object class accepted by the processor * @param processorClass the object class accepted by the processor
* @param processor the object processor * @param processor the object processor
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); }
processors.put(processorClass, processor);
}
/** /**
* Adds a map of object processors to this {@link Receiver}. * Adds a map of object processors to this {@link Receiver}.
@ -126,16 +115,12 @@ public final class Receiver extends Thread {
* @param processors the processors to add the processors to add * @param processors the processors to add the processors to add
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); }
this.processors.putAll(processors);
}
/** /**
* Removes all object processors registered at this {@link Receiver}. * Removes all object processors registered at this {@link Receiver}.
* *
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void removeAllProcessors() { public void removeAllProcessors() { processors.clear(); }
processors.clear();
}
} }

View File

@ -2,15 +2,15 @@ package envoy.client.net;
import java.util.logging.*; import java.util.logging.*;
import envoy.client.data.*;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.*;
/** /**
* Implements methods to send {@link Message}s and {@link MessageStatusChange}s to the server or * Implements methods to send {@link Message}s and
* cache them inside a {@link LocalDB} depending on the online status. * {@link MessageStatusChange}s to the server or cache them inside a
* {@link LocalDB} depending on the online status.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -23,11 +23,12 @@ public final class WriteProxy {
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class); private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
/** /**
* Initializes a write proxy using a client and a local database. The corresponding cache * Initializes a write proxy using a client and a local database. The
* processors are injected into the caches. * corresponding cache processors are injected into the caches.
* *
* @param client the client instance used to send messages and events if online * @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 * @param localDB the local database used to cache messages and events if
* offline
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public WriteProxy(Client client, LocalDB localDB) { public WriteProxy(Client client, LocalDB localDB) {
@ -46,39 +47,34 @@ public final class WriteProxy {
} }
/** /**
* 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 * @since Envoy Client v0.3-alpha
*/ */
public void flushCache() { public void flushCache() { localDB.getCacheMap().getMap().values().forEach(Cache::relay); }
localDB.getCacheMap().getMap().values().forEach(Cache::relay);
}
/** /**
* Delivers a message to the server if online. Otherwise the message is cached inside the local * Delivers a message to the server if online. Otherwise the message is cached
* database. * inside the local database.
* *
* @param message the message to send * @param message the message to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessage(Message message) { public void writeMessage(Message message) {
if (client.isOnline()) if (client.isOnline()) client.sendMessage(message);
client.sendMessage(message); else localDB.getCacheMap().getApplicable(Message.class).accept(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 * Delivers a message status change event to the server if online. Otherwise the
* inside the local database. * event is cached inside the local database.
* *
* @param evt the event to send * @param evt the event to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessageStatusChange(MessageStatusChange evt) { public void writeMessageStatusChange(MessageStatusChange evt) {
if (client.isOnline()) if (client.isOnline()) client.send(evt);
client.send(evt); else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
else
localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
} }
} }

View File

@ -1,8 +1,8 @@
package envoy.client.ui; package envoy.client.ui;
/** /**
* This interface defines an action that should be performed when a scene gets restored from the * This interface defines an action that should be performed when a scene gets
* scene stack in {@link SceneContext}. * restored from the scene stack in {@link SceneContext}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -12,7 +12,8 @@ public interface Restorable {
/** /**
* This method is getting called when a scene gets restored.<br> * 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 * @since Envoy Client v0.1-beta
*/ */

View File

@ -4,34 +4,75 @@ import java.io.IOException;
import java.util.Stack; import java.util.Stack;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.*; import javafx.scene.*;
import javafx.scene.input.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import dev.kske.eventbus.core.*; import envoy.client.data.Settings;
import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.Settings; import dev.kske.eventbus.*;
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 * Manages a stack of scenes. The most recently added scene is displayed inside
* scene is removed from the stack, its predecessor is displayed. * a stage. When a scene is removed from the stack, its predecessor is
* displayed.
* <p> * <p>
* When a scene is loaded, the style sheet for the current theme is applied to it. * When a scene is loaded, the style sheet for the current theme is applied to
* it.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @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.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public enum SceneInfo {
/**
* The main scene in which the chat screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which the settings screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) { this.path = path; }
}
private final Stage stage; private final Stage stage;
private final Stack<Parent> roots = new Stack<>(); private final FXMLLoader loader = new FXMLLoader();
private final Stack<Object> controllers = new Stack<>(); private final Stack<Scene> sceneStack = new Stack<>();
private final Stack<Object> controllerStack = new Stack<>();
private Scene scene; private static final Settings settings = Settings.getInstance();
/** /**
* Initializes the scene context. * Initializes the scene context.
@ -47,44 +88,44 @@ public final class SceneContext {
/** /**
* Loads a new scene specified by a scene info. * Loads a new scene specified by a scene info.
* *
* @param info specifies the scene to load * @param sceneInfo specifies the scene to load
* @throws RuntimeException if the loading process fails * @throws RuntimeException if the loading process fails
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void load(SceneInfo info) { public void load(SceneInfo sceneInfo) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + info); EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
loader.setRoot(null);
loader.setController(null);
try { try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
controllerStack.push(loader.getController());
// Load root node and controller sceneStack.push(scene);
var loader = new FXMLLoader(); stage.setScene(scene);
Parent root = loader.load(getClass().getResourceAsStream(info.path));
Object controller = loader.getController();
roots.push(root);
controllers.push(controller);
if (scene == null) { // Add the option to exit Linux-like with "Control" + "Q"
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
// One-time scene initialization // Add the option to logout using "Control"+"Shift"+"L" if not in login scene
scene = new Scene(root, stage.getWidth(), stage.getHeight()); if (sceneInfo != SceneInfo.LOGIN_SCENE) scene.getAccelerators()
applyCSS(); .put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
stage.setScene(scene);
} else {
scene.setRoot(root);
}
// Remove previous keyboard shortcuts // Add the option to open the settings scene with "Control"+"S", if being in
scene.getAccelerators().clear(); // chat scene
if (sceneInfo.equals(SceneInfo.CHAT_SCENE))
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), () -> load(SceneInfo.SETTINGS_SCENE));
// Supply the global custom keyboard shortcuts for that scene // The LoginScene is the only scene not intended to be resized
scene.getAccelerators() // As strange as it seems, this is needed as otherwise the LoginScene won't be
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(info)); // displayed on some OS (...Debian...)
stage.sizeToScene();
// Supply the scene specific keyboard shortcuts Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
if (controller instanceof KeyboardMapping) applyCSS();
scene.getAccelerators() stage.show();
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts()); } catch (final IOException e) {
} catch (IOException e) { EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@ -96,55 +137,48 @@ public final class SceneContext {
*/ */
public void pop() { public void pop() {
// Pop current root node and controller // Pop scene and controller
roots.pop(); sceneStack.pop();
controllers.pop(); controllerStack.pop();
// Apply new scene if present // Apply new scene if present
if (!roots.isEmpty()) { if (!sceneStack.isEmpty()) {
scene.setRoot(roots.peek()); final var newScene = sceneStack.peek();
stage.setScene(newScene);
// Invoke restore if controller is restorable applyCSS();
var controller = controllers.peek(); stage.sizeToScene();
if (controller instanceof Restorable) // If the controller implements the Restorable interface,
((Restorable) controller).onRestore(); // the actions to perform on restoration will be executed here
} else { final var controller = controllerStack.peek();
if (controller instanceof Restorable) ((Restorable) controller).onRestore();
// Remove the current scene entirely
scene = null;
stage.setScene(null);
} }
stage.show();
} }
private void applyCSS() { private void applyCSS() {
if (scene != null) { if (!sceneStack.isEmpty()) {
var styleSheets = scene.getStylesheets(); final var styleSheets = stage.getScene().getStylesheets();
var themeCSS = "/css/" + Settings.getInstance().getCurrentTheme() + ".css"; final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
styleSheets.clear(); styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm());
getClass().getResource(themeCSS).toExternalForm());
} }
} }
@Event(Logout.class) @Event(eventType = Logout.class, priority = 150)
@Priority(150)
private void onLogout() { private void onLogout() {
roots.clear(); sceneStack.clear();
controllers.clear(); controllerStack.clear();
} }
@Event(ThemeChangeEvent.class) @Event(priority = 150, eventType = ThemeChangeEvent.class)
@Priority(150) private void onThemeChange() { applyCSS(); }
private void onThemeChange() {
applyCSS();
}
/** /**
* @param <T> the type of the controller * @param <T> the type of the controller
* @return the controller used by the current scene * @return the controller used by the current scene
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> T getController() { return (T) controllers.peek(); } public <T> T getController() { return (T) controllerStack.peek(); }
/** /**
* @return the stage in which the scenes are displayed * @return the stage in which the scenes are displayed
@ -156,5 +190,5 @@ public final class SceneContext {
* @return whether the scene stack is empty * @return whether the scene stack is empty
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean isEmpty() { return roots.isEmpty(); } public boolean isEmpty() { return sceneStack.isEmpty(); }
} }

View File

@ -1,40 +0,0 @@
package envoy.client.ui;
/**
* Contains information about different scenes and their FXML resource files.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public enum SceneInfo {
/**
* The main scene in which the chat screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which the settings screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) {
this.path = path;
}
}

View File

@ -10,18 +10,17 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.UserStatusChange;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene; import envoy.client.ui.controller.LoginScene;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/** /**
* Handles application startup. * Handles application startup.
@ -37,7 +36,7 @@ public final class Startup extends Application {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static final String VERSION = "0.2-beta"; public static final String VERSION = "0.1-beta";
private static LocalDB localDB; private static LocalDB localDB;
@ -47,8 +46,8 @@ public final class Startup extends Application {
private static final Logger logger = EnvoyLog.getLogger(Startup.class); private static final Logger logger = EnvoyLog.getLogger(Startup.class);
/** /**
* Loads the configuration, initializes the client and the local database and delegates the rest * Loads the configuration, initializes the client and the local database and
* of the startup process to {@link LoginScene}. * delegates the rest of the startup process to {@link LoginScene}.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -57,8 +56,7 @@ public final class Startup extends Application {
// Initialize config and logger // Initialize config and logger
try { try {
config.loadAll(Startup.class, "client.properties", config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0]));
getParameters().getRaw().toArray(new String[0]));
EnvoyLog.initialize(config); EnvoyLog.initialize(config);
} catch (final IllegalStateException e) { } catch (final IllegalStateException e) {
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e); new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
@ -86,25 +84,20 @@ public final class Startup extends Application {
stage.setTitle("Envoy"); stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
// Configure global shortcuts
EnvoyShortcutConfig.initializeEnvoyShortcuts();
// Create scene context // Create scene context
final var sceneContext = new SceneContext(stage); final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext); context.setSceneContext(sceneContext);
// Authenticate with token if present or load login scene // Authenticate with token if present
if (localDB.getAuthToken() != null) { if (localDB.getAuthToken() != null) {
logger.info("Attempting authentication with token..."); logger.info("Attempting authentication with token...");
localDB.loadUserData(); localDB.loadUserData();
if (!performHandshake( if (!performHandshake(
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
} else } else
// Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
stage.show();
} }
/** /**
@ -115,23 +108,18 @@ public final class Startup extends Application {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static boolean performHandshake(LoginCredentials credentials) { public static boolean performHandshake(LoginCredentials credentials) {
final var originalStatus = final var cacheMap = new CacheMap();
localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus(); 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>());
try { try {
client.performHandshake(credentials); client.performHandshake(credentials, cacheMap);
if (client.isOnline()) { if (client.isOnline()) {
// Restore the original status as the server automatically returns status ONLINE
client.getSender().setStatus(originalStatus);
loadChatScene(); loadChatScene();
client.initReceiver(localDB, cacheMap);
// Request an ID generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext())
client.requestIDGenerator();
return true; return true;
} else } else return false;
return false;
} catch (IOException | InterruptedException | TimeoutException e) { } catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
return attemptOfflineMode(credentials.getIdentifier()); return attemptOfflineMode(credentials.getIdentifier());
@ -139,8 +127,8 @@ public final class Startup extends Application {
} }
/** /**
* Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode for a given * Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode
* user. * for a given user.
* *
* @param identifier the identifier of the user - currently his username * @param identifier the identifier of the user - currently his username
* @return whether the offline mode could be entered * @return whether the offline mode could be entered
@ -150,8 +138,7 @@ public final class Startup extends Application {
try { try {
// Try entering offline mode // Try entering offline mode
final User clientUser = localDB.getUsers().get(identifier); final User clientUser = localDB.getUsers().get(identifier);
if (clientUser == null) if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser); client.setSender(clientUser);
loadChatScene(); loadChatScene();
return true; return true;
@ -183,8 +170,7 @@ public final class Startup extends Application {
private static void loadChatScene() { private static void loadChatScene() {
// Set client user in local database // Set client user in local database
final var user = client.getSender(); localDB.setUser(client.getSender());
localDB.setUser(user);
// Initialize chats in local database // Initialize chats in local database
try { try {
@ -192,22 +178,14 @@ public final class Startup extends Application {
} catch (final FileNotFoundException e) { } catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login // The local database file has not yet been created, probably first login
} catch (final Exception e) { } catch (final Exception e) {
new Alert(AlertType.ERROR, new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
"Error while loading local database: " + e + "\nChats will not be stored locally.")
.showAndWait();
logger.log(Level.WARNING, "Could not load local database: ", e); logger.log(Level.WARNING, "Could not load local database: ", e);
} }
context.initWriteProxy(); context.initWriteProxy();
if (client.isOnline()) { if (client.isOnline()) context.getWriteProxy().flushCache();
context.getWriteProxy().flushCache(); else
// 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 // Set all contacts to offline mode
localDB.getChats() localDB.getChats()
.stream() .stream()
@ -219,26 +197,26 @@ public final class Startup extends Application {
final var stage = context.getStage(); final var stage = context.getStage();
// Pop LoginScene if present // Pop LoginScene if present
if (!context.getSceneContext().isEmpty()) if (!context.getSceneContext().isEmpty()) context.getSceneContext().pop();
context.getSceneContext().pop();
// Load ChatScene // Load ChatScene
stage.setMinHeight(400); stage.setMinHeight(400);
stage.setMinWidth(843); stage.setMinWidth(843);
context.getSceneContext().load(SceneInfo.CHAT_SCENE); context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE);
stage.centerOnScreen(); stage.centerOnScreen();
// Exit or minimize the stage when a close request occurs if (StatusTrayIcon.isSupported()) {
stage.setOnCloseRequest(
e -> {
ShutdownHelper.exit();
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
e.consume();
});
// Initialize status tray icon // Exit or minimize the stage when a close request occurs
if (StatusTrayIcon.isSupported()) stage.setOnCloseRequest(e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose()) e.consume(); });
new StatusTrayIcon(stage).show();
// Initialize status tray icon
final var trayIcon = new StatusTrayIcon(stage);
Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> {
if ((Boolean) c) trayIcon.show();
else trayIcon.hide();
});
}
// Start auto save thread // Start auto save thread
localDB.initAutoSave(); localDB.initAutoSave();

View File

@ -1,62 +1,36 @@
package envoy.client.ui; package envoy.client.ui;
import static java.awt.Image.SCALE_SMOOTH;
import java.awt.*; import java.awt.*;
import java.awt.TrayIcon.MessageType; import java.awt.TrayIcon.MessageType;
import java.awt.image.BufferedImage;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.stage.Stage; import javafx.stage.Stage;
import dev.kske.eventbus.core.Event;
import dev.kske.eventbus.core.EventBus;
import envoy.data.Message;
import envoy.data.User.UserStatus;
import envoy.client.data.*;
import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.util.*; import envoy.client.util.IconUtil;
import envoy.data.Message;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
/** /**
* 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 * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public final class StatusTrayIcon { public final class StatusTrayIcon implements EventListener {
/** /**
* The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This * The {@link TrayIcon} provided by the System Tray API for controlling the
* includes displaying the icon, but also creating notifications when new messages are received. * system tray. This includes displaying the icon, but also creating
* notifications when new messages are received.
*/ */
private final TrayIcon trayIcon; private final TrayIcon trayIcon;
/** /**
* A received {@link Message} is only displayed as a system tray notification if this variable * A received {@link Message} is only displayed as a system tray notification if
* is set to {@code true}. * this variable is set to {@code true}.
*/ */
private boolean displayMessageNotification; private boolean displayMessages;
/**
* The size of the tray icon's image.
*/
private final Dimension size;
/**
* 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;
private static final Font unreadMessageFont = new Font("sans-serif", Font.PLAIN, 8);
/** /**
* @return {@code true} if the status tray icon is supported on this platform * @return {@code true} if the status tray icon is supported on this platform
@ -65,56 +39,31 @@ public final class StatusTrayIcon {
public static boolean isSupported() { return SystemTray.isSupported(); } public static boolean isSupported() { return SystemTray.isSupported(); }
/** /**
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up menu. * 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 * @param stage the stage whose focus determines if message
* notifications are displayed
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public StatusTrayIcon(Stage stage) { public StatusTrayIcon(Stage stage) {
size = SystemTray.getSystemTray().getTrayIconSize(); trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy");
logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width, trayIcon.setImageAutoSize(true);
size.height, SCALE_SMOOTH); trayIcon.setToolTip("You are notified if you have unread messages.");
final var popup = new PopupMenu(); final PopupMenu popup = new PopupMenu();
// Adding the exit menu item final MenuItem exitMenuItem = new MenuItem("Exit");
final var exitMenuItem = new MenuItem("Exit"); exitMenuItem.addActionListener(evt -> ShutdownHelper.exit());
exitMenuItem.addActionListener(evt -> ShutdownHelper.exit(true));
popup.add(exitMenuItem); popup.add(exitMenuItem);
// Adding the logout menu item trayIcon.setPopupMenu(popup);
final var logoutMenuItem = new MenuItem("Logout");
logoutMenuItem.addActionListener(evt -> Platform.runLater(UserUtil::logout));
popup.add(logoutMenuItem);
// Adding the status change items // Only display messages if the stage is not focused
final var statusSubMenu = new Menu("Change status"); stage.focusedProperty().addListener((ov, onHidden, onShown) -> displayMessages = !ov.getValue());
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);
// Initialize the icon
trayIcon = new TrayIcon(createImage(), "Envoy", popup);
// 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 // Show the window if the user clicks on the icon
trayIcon.addActionListener(evt -> Platform.runLater(() -> { trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); }));
stage.setIconified(false);
stage.toFront();
stage.requestFocus();
}));
// Start processing message events // Start processing message events
EventBus.getInstance().registerListener(this); EventBus.getInstance().registerListener(this);
@ -128,7 +77,7 @@ public final class StatusTrayIcon {
public void show() { public void show() {
try { try {
SystemTray.getSystemTray().add(trayIcon); SystemTray.getSystemTray().add(trayIcon);
} catch (AWTException e) {} } catch (final AWTException e) {}
} }
/** /**
@ -136,89 +85,13 @@ public final class StatusTrayIcon {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@Event(Logout.class) public void hide() { SystemTray.getSystemTray().remove(trayIcon); }
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 @Event
private void onMessage(Message message) { private void onMessage(Message message) {
if (displayMessageNotification) if (displayMessages) trayIcon.displayMessage(
trayIcon message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received",
.displayMessage(message.hasAttachment() message.getText(),
? "New " + message.getAttachment().getType().toString().toLowerCase() MessageType.INFO);
+ " 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;
} }
} }

View File

@ -1,194 +0,0 @@
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.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 messageDependentCommandDescription =
" 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(messageDependentCommandDescription)).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; }
}

View File

@ -1,7 +0,0 @@
/**
* Contains classes that influence the appearance and behavior of ChatScene.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
package envoy.client.ui.chatscene;

View File

@ -6,11 +6,10 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import envoy.client.data.audio.AudioPlayer;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.audio.AudioPlayer;
/** /**
* Enables the play back of audio clips through a button. * Enables the play back of audio clips through a button.
* *
@ -19,7 +18,7 @@ import envoy.client.data.audio.AudioPlayer;
*/ */
public final class AudioControl extends HBox { public final class AudioControl extends HBox {
private final AudioPlayer player = new AudioPlayer(); private AudioPlayer player = new AudioPlayer();
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class); private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);

View File

@ -2,15 +2,16 @@ package envoy.client.ui.control;
import javafx.geometry.*; import javafx.geometry.*;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
/** /**
* Displays a chat using a contact control for the recipient and a label for the unread message * Displays a chat using a contact control for the recipient and a label for the
* count. * unread message count.
* *
* @see ContactControl * @see ContactControl
* @author Leon Hofmeister * @author Leon Hofmeister
@ -18,12 +19,10 @@ import envoy.client.util.IconUtil;
*/ */
public final class ChatControl extends HBox { public final class ChatControl extends HBox {
private static Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32), private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32); groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
/** /**
* Creates a new {@code ChatControl}.
*
* @param chat the chat to display * @param chat the chat to display
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -32,12 +31,17 @@ public final class ChatControl extends HBox {
setPadding(new Insets(0, 0, 3, 0)); setPadding(new Insets(0, 0, 3, 0));
// Profile picture // Profile picture
var contactProfilePic = ImageView contactProfilePic = new ImageView(chat instanceof GroupChat ? groupIcon : userIcon);
new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32); final var clip = new Rectangle();
clip.setWidth(32);
clip.setHeight(32);
clip.setArcHeight(32);
clip.setArcWidth(32);
contactProfilePic.setClip(clip);
getChildren().add(contactProfilePic); getChildren().add(contactProfilePic);
// Spacing // Spacing
var leftSpacing = new Region(); final var leftSpacing = new Region();
leftSpacing.setPrefSize(8, 0); leftSpacing.setPrefSize(8, 0);
leftSpacing.setMinSize(8, 0); leftSpacing.setMinSize(8, 0);
leftSpacing.setMaxSize(8, 0); leftSpacing.setMaxSize(8, 0);
@ -48,26 +52,18 @@ public final class ChatControl extends HBox {
// Unread messages // Unread messages
if (chat.getUnreadAmount() != 0) { if (chat.getUnreadAmount() != 0) {
var spacing = new Region(); final var spacing = new Region();
setHgrow(spacing, Priority.ALWAYS); setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing); getChildren().add(spacing);
var unreadMessagesLabel = new Label( final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
chat.getUnreadAmount() > 99 ? "99+" : String.valueOf(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15); unreadMessagesLabel.setMinSize(15, 15);
unreadMessagesLabel.setAlignment(Pos.CENTER_RIGHT); final var vbox = new VBox();
vbox.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.setAlignment(Pos.CENTER);
unreadMessagesLabel.getStyleClass().add("unread-messages-amount"); unreadMessagesLabel.getStyleClass().add("unread-messages-amount");
getChildren().add(unreadMessagesLabel); vbox.getChildren().add(unreadMessagesLabel);
getChildren().add(vbox);
} }
getStyleClass().add("list-element"); getStyleClass().add("list-element");
} }
/**
* Reloads the default icons.
*
* @since Envoy Client v0.3-beta
*/
public static void reloadDefaultChatIcons() {
userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32);
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
}
} }

View File

@ -6,46 +6,34 @@ import javafx.scene.layout.VBox;
import envoy.data.*; import envoy.data.*;
/** /**
* Displays information about a contact in two rows. The first row contains the name. The second row * Displays information about a contact in two rows. The first row contains the
* contains the online status (user) or the member count (group). * name. The second row contains the online status (user) or the member count
* (group).
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public final class ContactControl extends VBox { public final class ContactControl extends VBox {
private final Contact contact;
/** /**
* @param contact the contact to display * @param contact the contact to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ContactControl(Contact contact) { public ContactControl(Contact contact) {
this.contact = contact;
// Name label // Name label
final var nameLabel = new Label(contact.getName()); final var nameLabel = new Label(contact.getName());
getChildren().add(nameLabel); getChildren().add(nameLabel);
// Online status (user) or member count (group) // Online status (user) or member count (group)
getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact) if (contact instanceof User) {
: new GroupSizeLabel((Group) contact)); final var status = ((User) contact).getStatus().toString();
final var statusLabel = new Label(status);
statusLabel.getStyleClass().add(status.toLowerCase());
getChildren().add(statusLabel);
} else {
getChildren().add(new Label(contact.getContacts().size() + " members"));
}
getStyleClass().add("list-element"); 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));
}
} }

View File

@ -1,23 +0,0 @@
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" : ""));
}
}

View File

@ -1,6 +1,8 @@
package envoy.client.ui.control; package envoy.client.ui.control;
import java.io.ByteArrayInputStream; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.*;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map; import java.util.Map;
@ -10,15 +12,15 @@ import javafx.geometry.*;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.*; import javafx.scene.image.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import envoy.client.data.*;
import envoy.client.ui.*;
import envoy.client.util.IconUtil;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.util.EnvoyLog; 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. * This class transforms a single {@link Message} into a UI component.
* *
@ -30,25 +32,21 @@ public final class MessageControl extends Label {
private final boolean ownMessage; private final boolean ownMessage;
private final LocalDB localDB = context.getLocalDB(); private final LocalDB localDB = Context.getInstance().getLocalDB();
private final Client client = context.getClient(); private final SceneContext sceneContext = Context.getInstance().getSceneContext();
private static final Context context = Context.getInstance(); private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
private static final DateTimeFormatter dateFormat = .withZone(ZoneId.systemDefault());
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
.withZone(ZoneId.systemDefault()); private static final Settings settings = Settings.getInstance();
private static final Map<MessageStatus, Image> statusImages = private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Logger logger =
EnvoyLog.getLogger(MessageControl.class);
/** /**
*
* @param message the message that should be formatted * @param message the message that should be formatted
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public MessageControl(Message message) { public MessageControl(Message message) {
ownMessage = message.getSenderID() == localDB.getUser().getID();
// Creating the underlying VBox and the dateLabel // Creating the underlying VBox and the dateLabel
final var hbox = new HBox(); final var hbox = new HBox();
if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) { if (message.getSenderID() != localDB.getUser().getID() && message instanceof GroupMessage) {
@ -69,40 +67,18 @@ public final class MessageControl extends Label {
final var vbox = new VBox(hbox); final var vbox = new VBox(hbox);
// Creating the actions for the MenuItems // Creating the actions for the MenuItems
final var contextMenu = new ContextMenu(); final var contextMenu = new ContextMenu();
final var items = contextMenu.getItems(); final var copyMenuItem = new MenuItem("Copy");
final var deleteMenuItem = new MenuItem("Delete");
// Copy message action final var forwardMenuItem = new MenuItem("Forward");
if (!message.getText().isEmpty()) { final var quoteMenuItem = new MenuItem("Quote");
final var copyMenuItem = new MenuItem("Copy Text"); final var infoMenuItem = new MenuItem("Info");
copyMenuItem.setOnAction(e -> MessageUtil.copyMessageText(message)); copyMenuItem.setOnAction(e -> copyMessage(message));
items.add(copyMenuItem); deleteMenuItem.setOnAction(e -> deleteMessage(message));
} forwardMenuItem.setOnAction(e -> forwardMessage(message));
quoteMenuItem.setOnAction(e -> quoteMessage(message));
// 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)); infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
items.add(infoMenuItem); contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
// Handling message attachment display // Handling message attachment display
// TODO: Add missing attachment types // TODO: Add missing attachment types
@ -110,9 +86,7 @@ public final class MessageControl extends Label {
switch (message.getAttachment().getType()) { switch (message.getAttachment().getType()) {
case PICTURE: case PICTURE:
vbox.getChildren() vbox.getChildren()
.add(new ImageView( .add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
new Image(new ByteArrayInputStream(message.getAttachment().getData()),
256, 256, true, true)));
break; break;
case VIDEO: case VIDEO:
break; break;
@ -123,8 +97,8 @@ public final class MessageControl extends Label {
break; break;
} }
final var saveAttachment = new MenuItem("Save attachment"); final var saveAttachment = new MenuItem("Save attachment");
saveAttachment.setOnAction(e -> MessageUtil.saveAttachment(message)); saveAttachment.setOnAction(e -> saveAttachment(message));
items.add(saveAttachment); contextMenu.getItems().add(saveAttachment);
} }
// Creating the textLabel // Creating the textLabel
final var textLabel = new Label(message.getText()); final var textLabel = new Label(message.getText());
@ -142,9 +116,12 @@ public final class MessageControl extends Label {
hBoxBottom.getChildren().add(statusIcon); hBoxBottom.getChildren().add(statusIcon);
hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT); hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT);
getStyleClass().add("own-message"); getStyleClass().add("own-message");
ownMessage = true;
hbox.setAlignment(Pos.CENTER_RIGHT); hbox.setAlignment(Pos.CENTER_RIGHT);
} else } else {
getStyleClass().add("received-message"); getStyleClass().add("received-message");
ownMessage = false;
}
vbox.getChildren().add(hBoxBottom); vbox.getChildren().add(hBoxBottom);
// Adjusting height and weight of the cell to the corresponding ListView // Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20)); paddingProperty().setValue(new Insets(5, 20, 5, 20));
@ -152,13 +129,44 @@ public final class MessageControl extends Label {
setGraphic(vbox); setGraphic(vbox);
} }
private void loadMessageInfoScene(Message message) { // Context Menu actions
logger.log(Level.FINEST, "message info scene was requested for " + message);
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) {
File file;
final var fileName = message.getAttachment().getName();
final var downloadLocation = settings.getDownloadLocation();
// Show save file dialog, if the user did not opt-out
if (!settings.isDownloadSavedWithoutAsking()) {
final var fileChooser = new FileChooser();
fileChooser.setInitialFileName(fileName);
fileChooser.setInitialDirectory(downloadLocation);
file = fileChooser.showSaveDialog(sceneContext.getStage());
} else file = new File(downloadLocation, fileName);
// A file was selected
if (file != null) try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(message.getAttachment().getData());
logger.log(Level.FINE, "Attachment of message was saved at " + file.getAbsolutePath());
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
}
} }
/** /**
* @return whether the message stored by this {@code MessageControl} has been sent by this user * @return whether the message stored by this {@code MessageControl} has been
* of Envoy * sent by this user of Envoy
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public boolean isOwnMessage() { return ownMessage; } public boolean isOwnMessage() { return ownMessage; }

View File

@ -16,9 +16,7 @@ public final class ProfilePicImageView extends ImageView {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView() { public ProfilePicImageView() { this(null); }
this(null);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.
@ -26,20 +24,17 @@ public final class ProfilePicImageView extends ImageView {
* @param image the image to display * @param image the image to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView(Image image) { public ProfilePicImageView(Image image) { this(image, 40); }
this(image, 40);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.
* *
* @param image the image to display * @param image the image to display
* @param sizeAndRounding the size and rounding for a circular {@code ProfilePicImageView} * @param sizeAndRounding the size and rounding for a circular
* {@code ProfilePicImageView}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView(Image image, double sizeAndRounding) { public ProfilePicImageView(Image image, double sizeAndRounding) { this(image, sizeAndRounding, sizeAndRounding); }
this(image, sizeAndRounding, sizeAndRounding);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.

View File

@ -1,94 +0,0 @@
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&auml;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; }
}

View File

@ -1,4 +1,4 @@
package envoy.client.ui.chatscene; package envoy.client.ui.control;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -7,8 +7,8 @@ import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
/** /**
* Displays a context menu that offers an additional option when one of its menu items has been * Displays a context menu that offers an additional option when one of
* clicked. * its menu items has been clicked.
* <p> * <p>
* Current options are: * Current options are:
* <ul> * <ul>
@ -24,8 +24,9 @@ import javafx.scene.input.Clipboard;
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is * @apiNote please refrain from using
* already used by this component * {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/ */
public class TextInputContextMenu extends ContextMenu { public class TextInputContextMenu extends ContextMenu {
@ -37,10 +38,11 @@ public class TextInputContextMenu extends ContextMenu {
private final MenuItem deleteMI = new MenuItem("Delete selection"); private final MenuItem deleteMI = new MenuItem("Delete selection");
private final MenuItem clearMI = new MenuItem("Clear"); private final MenuItem clearMI = new MenuItem("Clear");
private final MenuItem selectAllMI = new MenuItem("Select all"); private final MenuItem selectAllMI = new MenuItem("Select all");
private final MenuItem separatorMI = new SeparatorMenuItem();
/** /**
* Creates a new {@code TextInputContextMenu} with an optional action when this menu was * Creates a new {@code TextInputContextMenu} with an optional action when
* clicked. Currently shows: * this menu was clicked. Currently shows:
* <ul> * <ul>
* <li>undo</li> * <li>undo</li>
* <li>redo</li> * <li>redo</li>
@ -52,12 +54,14 @@ public class TextInputContextMenu extends ContextMenu {
* <li>Select all</li> * <li>Select all</li>
* </ul> * </ul>
* *
* @param control the text input component to display this {@code ContextMenu} * @param control the text input component to display this
* @param menuItemClicked the second action to perform when a menu item of this context menu has * {@code ContextMenu}
* been clicked * @param menuItemClicked the second action to perform when a menu item of this
* context menu has been clicked
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is * @apiNote please refrain from using
* already used by this component * {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/ */
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) { public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
@ -78,7 +82,6 @@ public class TextInputContextMenu extends ContextMenu {
copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty()); copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty()); deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
clearMI.disableProperty().bind(control.textProperty().isEmpty()); clearMI.disableProperty().bind(control.textProperty().isEmpty());
selectAllMI.disableProperty().bind(control.textProperty().isEmpty());
setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString())); setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()));
selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE); selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
@ -86,22 +89,17 @@ public class TextInputContextMenu extends ContextMenu {
// Add all items to the ContextMenu // Add all items to the ContextMenu
getItems().add(undoMI); getItems().add(undoMI);
getItems().add(redoMI); getItems().add(redoMI);
getItems().add(new SeparatorMenuItem());
getItems().add(cutMI); getItems().add(cutMI);
getItems().add(copyMI); getItems().add(copyMI);
getItems().add(pasteMI); getItems().add(pasteMI);
getItems().add(new SeparatorMenuItem()); getItems().add(separatorMI);
getItems().add(deleteMI); getItems().add(deleteMI);
getItems().add(clearMI); getItems().add(clearMI);
getItems().add(new SeparatorMenuItem()); getItems().add(separatorMI);
getItems().add(selectAllMI); getItems().add(selectAllMI);
} }
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, Consumer<ActionEvent> additionalAction) {
Consumer<ActionEvent> additionalAction) { return e -> { originalAction.accept(e); additionalAction.accept(e); };
return e -> {
originalAction.accept(e);
additionalAction.accept(e);
};
} }
} }

View File

@ -1,23 +0,0 @@
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());
}
}

View File

@ -1,14 +1,12 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import static envoy.client.ui.SceneInfo.SETTINGS_SCENE;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map; import java.util.Random;
import java.util.logging.*; import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
@ -16,38 +14,37 @@ import javafx.application.Platform;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.FilteredList;
import javafx.fxml.*; import javafx.fxml.*;
import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.*; import javafx.scene.image.*;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration; import javafx.util.Duration;
import dev.kske.eventbus.core.*; import envoy.client.data.*;
import dev.kske.eventbus.core.Event; import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.commands.*;
import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.*;
import envoy.client.ui.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*;
import envoy.client.util.*;
import envoy.data.*; import envoy.data.*;
import envoy.data.Attachment.AttachmentType; import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.UserOperation; import envoy.event.contact.ContactOperation;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.*; import dev.kske.eventbus.*;
import envoy.client.data.audio.AudioRecorder; import dev.kske.eventbus.Event;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.event.*;
import envoy.client.net.*;
import envoy.client.ui.*;
import envoy.client.ui.chatscene.*;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*;
import envoy.client.util.*;
/** /**
* Controller for the chat scene. * Controller for the chat scene.
@ -55,7 +52,13 @@ import envoy.client.util.*;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ChatScene implements Restorable, KeyboardMapping { public final class ChatScene implements EventListener, Restorable {
@FXML
private GridPane scene;
@FXML
private Label contactLabel;
@FXML @FXML
private ListView<Message> messageList; private ListView<Message> messageList;
@ -84,21 +87,27 @@ public final class ChatScene implements Restorable, KeyboardMapping {
@FXML @FXML
private Button newContactButton; private Button newContactButton;
@FXML
private TextArea messageTextArea;
@FXML @FXML
private Label remainingChars; private Label remainingChars;
@FXML @FXML
private Label infoLabel; private Label infoLabel;
@FXML
private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
@FXML @FXML
private Label topBarContactLabel; private Label topBarContactLabel;
@FXML @FXML
private Label topBarStatusLabel; private Label topBarStatusLabel;
@FXML
private ImageView attachmentView;
@FXML @FXML
private ImageView clientProfilePic; private ImageView clientProfilePic;
@ -106,10 +115,10 @@ public final class ChatScene implements Restorable, KeyboardMapping {
private ImageView recipientProfilePic; private ImageView recipientProfilePic;
@FXML @FXML
private TextArea messageTextArea; private TextArea contactSearch;
@FXML @FXML
private TextArea contactSearch; private VBox contactOperations;
@FXML @FXML
private TabPane tabPane; private TabPane tabPane;
@ -123,26 +132,22 @@ public final class ChatScene implements Restorable, KeyboardMapping {
@FXML @FXML
private HBox contactSpecificOnlineOperations; private HBox contactSpecificOnlineOperations;
@FXML
private HBox ownContactControl;
private Chat currentChat; private Chat currentChat;
private FilteredList<Chat> chats; private FilteredList<Chat> chats;
private boolean recording;
private Attachment pendingAttachment; private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled; private boolean postingPermanentlyDisabled;
private boolean isCustomAttachmentImage; private boolean isCustomAttachmentImage;
private ChatSceneCommands commands;
private final LocalDB localDB = context.getLocalDB(); private final LocalDB localDB = context.getLocalDB();
private final Client client = context.getClient(); private final Client client = context.getClient();
private final WriteProxy writeProxy = context.getWriteProxy(); private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext(); private final SceneContext sceneContext = context.getSceneContext();
private final AudioRecorder recorder = new AudioRecorder(); private final AudioRecorder recorder = new AudioRecorder();
private final Tooltip onlyIfOnlineTooltip = private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
new Tooltip("You need to be online to do this"); private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this");
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
IconUtil.loadIconThemeSensitive("attachment_present", 20);
private static final Settings settings = Settings.getInstance(); private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
@ -159,28 +164,22 @@ public final class ChatScene implements Restorable, KeyboardMapping {
@FXML @FXML
private void initialize() { private void initialize() {
eventBus.registerListener(this); eventBus.registerListener(this);
commands = new ChatSceneCommands(messageList, this);
// Initialize message and user rendering // Initialize message and user rendering
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
chatList.setCellFactory(ChatListCell::new); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
// JavaFX provides an internal way of populating the context menu of a text // JavaFX provides an internal way of populating the context menu of a text
// area. // area.
// We, however, need additional functionality. // We, however, need additional functionality.
messageTextArea.setContextMenu( messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
// Set the icons of buttons and image views // Set the icons of buttons and image views
settingsButton.setGraphic( settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
new ImageView(IconUtil.loadIconThemeSensitive("settings", 22))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic( attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic( messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
onlyIfOnlineTooltip.setShowDelay(Duration.millis(250)); onlyIfOnlineTooltip.setShowDelay(Duration.millis(250));
final var clip = new Rectangle(); final var clip = new Rectangle();
@ -191,28 +190,23 @@ public final class ChatScene implements Restorable, KeyboardMapping {
clientProfilePic.setClip(clip); clientProfilePic.setClip(clip);
chatList.setItems(chats = new FilteredList<>(localDB.getChats())); chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName());
// Set the design of the box in the upper-left corner initializeSystemCommandsMap();
generateOwnStatusControl();
Platform.runLater(() -> { Platform.runLater(() -> {
final var online = client.isOnline(); final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative // no check will be performed in case it has already been disabled - a negative
// GroupCreationResult might have been returned // GroupCreationResult might have been returned
if (!newGroupButton.isDisabled()) if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online);
newGroupButton.setDisable(!online);
newContactButton.setDisable(!online); newContactButton.setDisable(!online);
if (online) if (online) try {
try { Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip); contactSearchTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml")));
contactSearchTab.setContent(new FXMLLoader() groupCreationTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml")));
.load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml"))); } catch (final IOException e) {
groupCreationTab.setContent(new FXMLLoader() logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
.load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml"))); }
} catch (final IOException e) {
logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
}
else { else {
Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip); Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
updateInfoLabel("You are offline", "info-label-warning"); updateInfoLabel("You are offline", "info-label-warning");
@ -220,40 +214,33 @@ public final class ChatScene implements Restorable, KeyboardMapping {
}); });
} }
@Event(BackEvent.class) @Event(eventType = BackEvent.class)
private void onBackEvent() { private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); }
tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal());
}
@Event @Event(includeSubtypes = true)
@Polymorphic
private void onMessage(Message message) { private void onMessage(Message message) {
// The sender of the message is the recipient of the chat // The sender of the message is the recipient of the chat
// Exceptions: this user is the sender (sync) or group message (group is // Exceptions: this user is the sender (sync) or group message (group is
// recipient) // recipient)
final var ownMessage = message.getSenderID() == localDB.getUser().getID(); final boolean ownMessage = message.getSenderID() == localDB.getUser().getID();
final var recipientID = final var recipientID = message instanceof GroupMessage || ownMessage ? message.getRecipientID() : message.getSenderID();
message instanceof GroupMessage || ownMessage ? message.getRecipientID()
: message.getSenderID();
localDB.getChat(recipientID).ifPresent(chat -> { localDB.getChat(recipientID).ifPresent(chat -> {
chat.insert(message);
// Read current chat or increment unread amount
if (chat.equals(currentChat)) {
currentChat.read(writeProxy);
Platform.runLater(this::scrollToMessageListEnd);
} else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top
Platform.runLater(() -> { Platform.runLater(() -> {
chat.insert(message);
// Read current chat or increment unread amount
if (chat.equals(currentChat)) {
currentChat.read(writeProxy);
scrollToMessageListEnd();
} else if (!ownMessage && message.getStatus() != MessageStatus.READ)
chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top
chats.getSource().remove(chat); chats.getSource().remove(chat);
((ObservableList<Chat>) chats.getSource()).add(0, chat); ((ObservableList<Chat>) chats.getSource()).add(0, chat);
if (chat.equals(currentChat)) if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
chatList.getSelectionModel().select(0);
}); });
}); });
} }
@ -263,49 +250,30 @@ public final class ChatScene implements Restorable, KeyboardMapping {
// Update UI if in current chat and the current user was the sender of the // Update UI if in current chat and the current user was the sender of the
// message // message
if (currentChat != null) if (currentChat != null) localDB.getMessage(evt.getID())
localDB.getMessage(evt.getID()) .filter(msg -> msg.getSenderID() == client.getSender().getID())
.filter(msg -> msg.getSenderID() == client.getSender().getID()) .ifPresent(msg -> Platform.runLater(messageList::refresh));
.ifPresent(msg -> Platform.runLater(messageList::refresh));
} }
@Event(eventType = UserStatusChange.class)
private void onUserStatusChange() { Platform.runLater(chatList::refresh); }
@Event @Event
private void onUserStatusChange(UserStatusChange statusChange) { private void onContactOperation(ContactOperation operation) {
Platform.runLater(() -> { final var contact = operation.get();
chatList.refresh(); switch (operation.getOperationType()) {
case ADD:
// Replacing the display in the top bar if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
if (currentChat != null && currentChat.getRecipient().getID() == statusChange.getID()) { final var chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
topBarStatusLabel.getStyleClass().clear(); Platform.runLater(() -> ((ObservableList<Chat>) chats.getSource()).add(0, chat));
topBarStatusLabel.setText(statusChange.get().toString()); break;
topBarStatusLabel.getStyleClass().add(statusChange.get().toString().toLowerCase()); case REMOVE:
} Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact)));
}); break;
}
} }
@Event @Event(eventType = NoAttachments.class)
private void onUserOperation(UserOperation operation) {
// All ADD dependent logic resides in LocalDB
if (operation.getOperationType().equals(ElementOperation.REMOVE))
Platform.runLater(() -> disableChat(new ContactDisabled(operation.get())));
}
@Event
private void onGroupResize(GroupResize resize) {
final var chatFound = localDB.getChat(resize.getGroupID());
chatFound.ifPresent(chat -> Platform.runLater(() -> {
chatList.refresh();
// Update the top-bar status label if all conditions apply
if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient()))
topBarStatusLabel
.setText(chat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
}));
}
@Event(NoAttachments.class)
private void onNoAttachments() { private void onNoAttachments() {
Platform.runLater(() -> { Platform.runLater(() -> {
attachmentButton.setDisable(true); attachmentButton.setDisable(true);
@ -319,50 +287,59 @@ public final class ChatScene implements Restorable, KeyboardMapping {
} }
@Event @Event
@Priority(150) private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(!result.get())); }
private void onGroupCreationResult(GroupCreationResult result) {
Platform.runLater(() -> newGroupButton.setDisable(result.get() == null));
}
@Event(ThemeChangeEvent.class) @Event(eventType = ThemeChangeEvent.class)
private void onThemeChange() { private void onThemeChange() {
ChatControl.reloadDefaultChatIcons(); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
settingsButton.setGraphic( voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20); DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
attachmentView.setImage( attachmentView.setImage(isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE);
isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE); messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
messageSearchButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
if (currentChat != null) // TODO: cache image
if (currentChat.getRecipient() instanceof User) if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
else
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
@Event(Logout.class) @Event(eventType = Logout.class, priority = 200)
@Priority(200) private void onLogout() { eventBus.removeListener(this); }
private void onLogout() {
eventBus.removeListener(this);
}
@Event(AccountDeletion.class) /**
private void onAccountDeletion() { * Initializes all {@code SystemCommands} used in {@code ChatScene}.
Platform.runLater(chatList::refresh); *
* @since Envoy Client v0.2-beta
*/
private void initializeSystemCommandsMap() {
final var builder = new SystemCommandBuilder(messageTextAreaCommands);
// Do A Barrel roll initialization
final var random = new Random();
builder.setAction(text -> 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 -> ShutdownHelper.logout()).setNumberOfArguments(0).setDescription("Logs you out.").build("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 -> sceneContext.load(SceneInfo.SETTINGS_SCENE))
.setNumberOfArguments(0)
.setDescription("Opens the settings screen")
.build("settings");
} }
@Override @Override
public void onRestore() { public void onRestore() { updateRemainingCharsLabel(); }
updateRemainingCharsLabel();
}
/** /**
* Actions to perform when the list of contacts has been clicked. * Actions to perform when the list of contacts has been clicked.
@ -371,13 +348,9 @@ public final class ChatScene implements Restorable, KeyboardMapping {
*/ */
@FXML @FXML
private void chatListClicked() { private void chatListClicked() {
if (chatList.getSelectionModel().isEmpty()) if (chatList.getSelectionModel().isEmpty()) return;
return;
final var chat = chatList.getSelectionModel().getSelectedItem();
if (chat == null)
return;
final var user = chat.getRecipient(); final var user = chatList.getSelectionModel().getSelectedItem().getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes // LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
@ -389,6 +362,7 @@ public final class ChatScene implements Restorable, KeyboardMapping {
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount(); final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount();
messageList.scrollTo(scrollIndex); messageList.scrollTo(scrollIndex);
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex); logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
deleteContactMenuItem.setText("Delete " + user.getName());
// Read the current chat // Read the current chat
currentChat.read(writeProxy); currentChat.read(writeProxy);
@ -396,50 +370,38 @@ public final class ChatScene implements Restorable, KeyboardMapping {
// Discard the pending attachment // Discard the pending attachment
if (recorder.isRecording()) { if (recorder.isRecording()) {
recorder.cancel(); recorder.cancel();
voiceButton.setGraphic(new ImageView( recording = false;
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null);
} }
pendingAttachment = null; pendingAttachment = null;
updateAttachmentView(false); updateAttachmentView(false);
remainingChars.setVisible(true); remainingChars.setVisible(true);
remainingChars remainingChars
.setText(String.format("remaining chars: %d/%d", .setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
} }
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
// Enable or disable the necessary UI controls voiceButton.setDisable(!recorder.isSupported());
final var chatEditable = currentChat == null || currentChat.isDisabled(); attachmentButton.setDisable(false);
messageTextArea.setDisable(chatEditable || postingPermanentlyDisabled);
voiceButton.setDisable(!recorder.isSupported() || chatEditable);
attachmentButton.setDisable(chatEditable);
chatList.refresh(); chatList.refresh();
// Design the top bar
if (currentChat != null) { if (currentChat != null) {
topBarContactLabel.setText(currentChat.getRecipient().getName()); topBarContactLabel.setText(currentChat.getRecipient().getName());
topBarContactLabel.setVisible(true);
topBarStatusLabel.setVisible(true);
if (currentChat.getRecipient() instanceof User) { if (currentChat.getRecipient() instanceof User) {
final var status = ((User) currentChat.getRecipient()).getStatus().toString(); final String status = ((User) currentChat.getRecipient()).getStatus().toString();
topBarStatusLabel.setText(status); topBarStatusLabel.setText(status);
topBarStatusLabel.getStyleClass().clear();
topBarStatusLabel.getStyleClass().add(status.toLowerCase()); topBarStatusLabel.getStyleClass().add(status.toLowerCase());
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
} else { } else {
topBarStatusLabel topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
.setText(currentChat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
topBarStatusLabel.getStyleClass().clear();
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
final var clip = new Rectangle(); final Rectangle clip = new Rectangle();
clip.setWidth(43); clip.setWidth(43);
clip.setHeight(43); clip.setHeight(43);
clip.setArcHeight(43); clip.setArcHeight(43);
clip.setArcWidth(43); clip.setArcWidth(43);
recipientProfilePic.setClip(clip); recipientProfilePic.setClip(clip);
messageSearchButton.setVisible(true); messageSearchButton.setVisible(true);
} }
} }
@ -450,9 +412,7 @@ public final class ChatScene implements Restorable, KeyboardMapping {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void settingsButtonClicked() { private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); }
sceneContext.load(SETTINGS_SCENE);
}
/** /**
* Actions to perform when the "Add Contact" - Button has been clicked. * Actions to perform when the "Add Contact" - Button has been clicked.
@ -460,35 +420,29 @@ public final class ChatScene implements Restorable, KeyboardMapping {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void addContactButtonClicked() { private void addContactButtonClicked() { tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal()); }
tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal());
}
@FXML @FXML
private void groupCreationButtonClicked() { private void groupCreationButtonClicked() { tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal()); }
tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal());
}
@FXML @FXML
private void voiceButtonClicked() { private void voiceButtonClicked() {
new Thread(() -> { new Thread(() -> {
try { try {
if (!recorder.isRecording()) { if (!recording) {
recording = true;
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setText("Recording"); voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView( voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
}); });
recorder.start(); recorder.start();
} else { } else {
pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_" pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss") + DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()) + "." + AudioRecorder.FILE_FORMAT,
.format(LocalDateTime.now()) AttachmentType.VOICE);
+ "." + AudioRecorder.FILE_FORMAT, recording = false;
AttachmentType.VOICE);
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView( voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null); voiceButton.setText(null);
checkPostConditions(false); checkPostConditions(false);
updateAttachmentView(true); updateAttachmentView(true);
@ -496,8 +450,7 @@ public final class ChatScene implements Restorable, KeyboardMapping {
} }
} catch (final EnvoyException e) { } catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e); logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
} }
}).start(); }).start();
} }
@ -511,16 +464,15 @@ public final class ChatScene implements Restorable, KeyboardMapping {
fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters() fileChooser.getExtensionFilters()
.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"), .addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
new FileChooser.ExtensionFilter("Videos", "*.mp4"), new FileChooser.ExtensionFilter("Videos", "*.mp4"),
new FileChooser.ExtensionFilter("All Files", "*.*")); new FileChooser.ExtensionFilter("All Files", "*.*"));
final var file = fileChooser.showOpenDialog(sceneContext.getStage()); final var file = fileChooser.showOpenDialog(sceneContext.getStage());
if (file != null) { if (file != null) {
// Check max file size // Check max file size
if (file.length() > 16E6) { if (file.length() > 16E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!") new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!").showAndWait();
.showAndWait();
return; return;
} }
@ -542,8 +494,7 @@ public final class ChatScene implements Restorable, KeyboardMapping {
checkPostConditions(false); checkPostConditions(false);
// Setting the preview image as image of the attachmentView // Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE) { if (type == AttachmentType.PICTURE) {
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
isCustomAttachmentImage = true; isCustomAttachmentImage = true;
} }
attachmentView.setVisible(true); attachmentView.setVisible(true);
@ -554,13 +505,14 @@ public final class ChatScene implements Restorable, KeyboardMapping {
} }
/** /**
* Rotates every element in our application by {@code rotations}*360° in {@code an}. * Rotates every element in our application by {@code rotations}*360° in
* {@code an}.
* *
* @param rotations the amount of times the scene is rotated by 360° * @param rotations the amount of times the scene is rotated by 360°
* @param animationTime the time in seconds that this animation lasts * @param animationTime the time in seconds that this animation lasts
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void doABarrelRoll(int rotations, double animationTime) { private void doABarrelRoll(int rotations, double animationTime) {
// Limiting the rotations and duration // Limiting the rotations and duration
rotations = Math.min(rotations, 100000); rotations = Math.min(rotations, 100000);
rotations = Math.max(rotations, 1); rotations = Math.max(rotations, 1);
@ -571,8 +523,7 @@ public final class ChatScene implements Restorable, KeyboardMapping {
final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this); final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this);
for (final var node : rotatableNodes) { for (final var node : rotatableNodes) {
// Sets the animation duration to {animationTime} // Sets the animation duration to {animationTime}
final var rotateTransition = final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node);
new RotateTransition(Duration.seconds(animationTime), node);
// rotates every element {rotations} times // rotates every element {rotations} times
rotateTransition.setByAngle(rotations * 360); rotateTransition.setByAngle(rotations * 360);
rotateTransition.play(); rotateTransition.play();
@ -583,8 +534,9 @@ public final class ChatScene implements Restorable, KeyboardMapping {
} }
/** /**
* Checks the text length of the {@code messageTextArea}, adjusts the {@code remainingChars} * Checks the text length of the {@code messageTextArea}, adjusts the
* label and checks whether to send the message automatically. * {@code remainingChars} label and checks whether to send the message
* automatically.
* *
* @param e the key event that will be analyzed for a post request * @param e the key event that will be analyzed for a post request
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -597,20 +549,29 @@ public final class ChatScene implements Restorable, KeyboardMapping {
// Sending an IsTyping event if none has been sent for // Sending an IsTyping event if none has been sent for
// IsTyping#millisecondsActive // IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent() if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
+ IsTyping.millisecondsActive <= System.currentTimeMillis()) { client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID()));
client.send(new IsTyping(currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow(); currentChat.lastWritingEventWasNow();
} }
// KeyPressed will be called before the char has been added to the text, hence // KeyPressed will be called before the char has been added to the text, hence
// this is needed for the first char // this is needed for the first char
if (messageTextArea.getText().length() == 1 && e != null) if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e);
checkPostConditions(e);
// This is needed for the messageTA context menu // This is needed for the messageTA context menu
else if (e == null) else if (e == null) checkPostConditions(false);
checkPostConditions(false); }
/**
* Returns the id that should be used to send things to the server: the id of
* 'our' {@link User} if the recipient of that object is another User, else the
* id of the {@link Group} 'our' user is sending to.
*
* @return an id that can be sent to the server
* @since Envoy Client v0.2-beta
*/
private long getChatID() {
return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID();
} }
/** /**
@ -620,25 +581,21 @@ public final class ChatScene implements Restorable, KeyboardMapping {
@FXML @FXML
private void checkPostConditions(KeyEvent e) { private void checkPostConditions(KeyEvent e) {
final var enterPressed = e.getCode() == KeyCode.ENTER; final var enterPressed = e.getCode() == KeyCode.ENTER;
final var messagePosted = final var messagePosted = enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown() : false;
enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown()
: false;
if (messagePosted) { if (messagePosted) {
// Removing an inserted line break if added by pressing enter // Removing an inserted line break if added by pressing enter
final var text = messageTextArea.getText(); final var text = messageTextArea.getText();
final var textPosition = messageTextArea.getCaretPosition() - 1; final var textPosition = messageTextArea.getCaretPosition() - 1;
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n') if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
messageTextArea messageTextArea.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
} }
// if control is pressed, the enter press is originally invalidated. Here it'll // if control is pressed, the enter press is originally invalidated. Here it'll
// be inserted again // be inserted again
else if (enterPressed && e.isControlDown()) { else if (enterPressed && e.isControlDown()) {
var caretPosition = messageTextArea.getCaretPosition(); var caretPosition = messageTextArea.getCaretPosition();
messageTextArea.setText(new StringBuilder(messageTextArea.getText()) messageTextArea.setText(new StringBuilder(messageTextArea.getText()).insert(caretPosition, '\n').toString());
.insert(caretPosition, '\n').toString());
messageTextArea.positionCaret(++caretPosition); messageTextArea.positionCaret(++caretPosition);
} }
checkPostConditions(messagePosted); checkPostConditions(messagePosted);
@ -646,10 +603,8 @@ public final class ChatScene implements Restorable, KeyboardMapping {
private void checkPostConditions(boolean postMessage) { private void checkPostConditions(boolean postMessage) {
if (!postingPermanentlyDisabled) { if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && postMessage) if (!postButton.isDisabled() && postMessage) postMessage();
postMessage(); postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null);
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null
|| currentChat == null);
} else { } else {
final var noMoreMessaging = "Go online to send messages"; final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging)) if (!infoLabel.getText().equals(noMoreMessaging))
@ -683,14 +638,13 @@ public final class ChatScene implements Restorable, KeyboardMapping {
private void updateRemainingCharsLabel() { private void updateRemainingCharsLabel() {
final var currentLength = messageTextArea.getText().length(); final var currentLength = messageTextArea.getText().length();
final var remainingLength = MAX_MESSAGE_LENGTH - currentLength; final var remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1)); remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
} }
/** /**
* Sends a new {@link Message} or {@link GroupMessage} to the server based on the text entered * Sends a new {@link Message} or {@link GroupMessage} to the server based on
* in the {@code messageTextArea} and the given attachment. * the text entered in the {@code messageTextArea} and the given attachment.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -705,11 +659,10 @@ public final class ChatScene implements Restorable, KeyboardMapping {
return; return;
} }
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
if (!commands.getChatSceneCommands().executeIfPresent(text)) { if (!messageTextAreaCommands.executeIfAnyPresent(text)) {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
currentChat.getRecipient().getID(), localDB.getIDGenerator()) .setText(text);
.setText(text);
// Setting an attachment, if present // Setting an attachment, if present
if (pendingAttachment != null) { if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment); builder.setAttachment(pendingAttachment);
@ -717,9 +670,8 @@ public final class ChatScene implements Restorable, KeyboardMapping {
updateAttachmentView(false); updateAttachmentView(false);
} }
// Building the final message // Building the final message
final var message = currentChat.getRecipient() instanceof Group final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
? builder.buildGroupMessage((Group) currentChat.getRecipient()) : builder.build();
: builder.build();
// Send message // Send message
writeProxy.writeMessage(message); writeProxy.writeMessage(message);
@ -730,15 +682,14 @@ public final class ChatScene implements Restorable, KeyboardMapping {
Platform.runLater(() -> { Platform.runLater(() -> {
chats.getSource().remove(currentChat); chats.getSource().remove(currentChat);
((ObservableList<Chat>) chats.getSource()).add(0, currentChat); ((ObservableList<Chat>) chats.getSource()).add(0, currentChat);
chatList.getSelectionModel().select(0);
localDB.getChats().remove(currentChat); localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat); localDB.getChats().add(0, currentChat);
chatList.getSelectionModel().select(0);
}); });
scrollToMessageListEnd(); scrollToMessageListEnd();
// Request a new ID generator if all IDs were used // Request a new ID generator if all IDs were used
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIDGenerator();
client.requestIDGenerator();
} }
// Clear text field and disable post button // Clear text field and disable post button
@ -753,16 +704,14 @@ public final class ChatScene implements Restorable, KeyboardMapping {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void scrollToMessageListEnd() { private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); }
messageList.scrollTo(messageList.getItems().size() - 1);
}
/** /**
* Updates the {@code infoLabel}. * Updates the {@code infoLabel}.
* *
* @param text the text to use * @param text the text to use
* @param infoLabelID the id the the {@code infoLabel} should have so that it can be styled * @param infoLabelID the id the the {@code infoLabel} should have so that it
* accordingly in CSS * can be styled accordingly in CSS
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void updateInfoLabel(String text, String infoLabelID) { private void updateInfoLabel(String text, String infoLabelID) {
@ -773,91 +722,26 @@ public final class ChatScene implements Restorable, KeyboardMapping {
/** /**
* Updates the {@code attachmentView} in terms of visibility.<br> * Updates the {@code attachmentView} in terms of visibility.<br>
* Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image * Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE}
* is currently present. * if another image is currently present.
* *
* @param visible whether the {@code attachmentView} should be displayed * @param visible whether the {@code attachmentView} should be displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void updateAttachmentView(boolean visible) { private void updateAttachmentView(boolean visible) {
if (!(attachmentView.getImage() == null if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|| attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)))
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
attachmentView.setVisible(visible); attachmentView.setVisible(visible);
} }
@Event(OwnStatusChange.class) // Context menu actions
@Priority(50)
private void generateOwnStatusControl() {
// Update the own user status if present @FXML
if (ownContactControl.getChildren().get(1) instanceof ContactControl) private void deleteContact() { try {} catch (final NullPointerException e) {} }
((ContactControl) ownContactControl.getChildren().get(1)).replaceInfoLabel();
else {
// Else prepend it to the HBox children
final var ownUserControl = new ContactControl(localDB.getUser());
ownUserControl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(ownUserControl, javafx.scene.layout.Priority.NEVER);
ownContactControl.getChildren().add(1, ownUserControl);
}
}
/**
* Redesigns the UI when the {@link Chat} of the given contact has been marked as disabled.
*
* @param event the contact whose chat got disabled
* @since Envoy Client v0.3-beta
*/
@Event
public void disableChat(ContactDisabled event) {
chatList.refresh();
final var recipient = event.get();
// Decrement member count for groups
if (recipient instanceof Group)
topBarStatusLabel.setText(recipient.getContacts().size() + " member"
+ (recipient.getContacts().size() != 1 ? "s" : ""));
if (currentChat != null && currentChat.getRecipient().equals(recipient)) {
messageTextArea.setDisable(true);
voiceButton.setDisable(true);
attachmentButton.setDisable(true);
pendingAttachment = null;
messageList.getStyleClass().clear();
messageList.getStyleClass().add("disabled-chat");
}
}
/**
* Resets every component back to its inital state before a chat was selected.
*
* @since Envoy Client v0.3-beta
*/
public void resetState() {
currentChat = null;
chatList.getSelectionModel().clearSelection();
messageList.getItems().clear();
messageTextArea.setDisable(true);
attachmentView.setImage(null);
topBarContactLabel.setVisible(false);
topBarStatusLabel.setVisible(false);
messageSearchButton.setVisible(false);
messageTextArea.clear();
messageTextArea.setDisable(true);
attachmentButton.setDisable(true);
voiceButton.setDisable(true);
remainingChars.setVisible(false);
pendingAttachment = null;
recipientProfilePic.setImage(null);
if (recorder.isRecording())
recorder.cancel();
}
@FXML @FXML
private void copyAndPostMessage() { private void copyAndPostMessage() {
final var messageText = messageTextArea.getText(); final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard() Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
.setContents(new StringSelection(messageText), null);
final var image = attachmentView.getImage(); final var image = attachmentView.getImage();
final var messageAttachment = pendingAttachment; final var messageAttachment = pendingAttachment;
postMessage(); postMessage();
@ -865,45 +749,13 @@ public final class ChatScene implements Restorable, KeyboardMapping {
updateRemainingCharsLabel(); updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank()); postButton.setDisable(messageText.isBlank());
attachmentView.setImage(image); attachmentView.setImage(image);
if (attachmentView.getImage() != null) if (attachmentView.getImage() != null) attachmentView.setVisible(true);
attachmentView.setVisible(true);
pendingAttachment = messageAttachment; pendingAttachment = messageAttachment;
} }
/**
* Clears the current message selection.
*
* @since Envoy Client v0.3-beta
*/
public void clearMessageSelection() {
messageList.getSelectionModel().clearSelection();
}
@FXML @FXML
private void searchContacts() { private void searchContacts() {
chats.setPredicate(contactSearch.getText().isBlank() ? c -> true chats.setPredicate(contactSearch.getText().isBlank() ? c -> true
: c -> c.getRecipient().getName().toLowerCase() : c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase()));
.contains(contactSearch.getText().toLowerCase()));
}
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.<KeyCombination, Runnable>of(
// Delete text before the caret with "Control" + U
new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(messageTextArea.getCaretPosition()));
checkPostConditions(false);
// Delete text after the caret with "Control" + K
}, new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(0, messageTextArea.getCaretPosition()));
checkPostConditions(false);
messageTextArea.positionCaret(messageTextArea.getText().length());
});
} }
} }

View File

@ -7,34 +7,34 @@ import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.core.*;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.event.BackEvent; import envoy.client.event.BackEvent;
import envoy.client.helper.AlertHelper; import envoy.client.helper.AlertHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.control.ContactControl; import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
/** /**
* Provides a search bar in which a user name (substring) can be entered. The users with a matching * Provides a search bar in which a user name (substring) can be entered. The
* name are then displayed inside a list view. A {@link UserSearchRequest} is sent on every * users with a matching name are then displayed inside a list view. A
* keystroke. * {@link UserSearchRequest} is sent on every keystroke.
* <p> * <p>
* <i>The actual search algorithm is implemented on the server. * <i>The actual search algorithm is implemented on the server.
* <p> * <p>
* To create a group, a button is available that loads the {@link GroupCreationTab}. * To create a group, a button is available that loads the
* {@link GroupCreationTab}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class ContactSearchTab { public class ContactSearchTab implements EventListener {
@FXML @FXML
private TextArea searchBar; private TextArea searchBar;
@ -59,22 +59,16 @@ public class ContactSearchTab {
@Event @Event
private void onUserSearchResult(UserSearchResult result) { private void onUserSearchResult(UserSearchResult result) {
Platform.runLater(() -> { Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(result.get()); });
userList.getItems().clear();
userList.getItems().addAll(result.get());
});
} }
@Event @Event
private void onUserOperation(UserOperation operation) { private void onContactOperation(ContactOperation operation) {
final var contact = operation.get(); final var contact = operation.get();
if (operation.getOperationType() == ElementOperation.ADD) if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> {
Platform.runLater(() -> { userList.getItems().remove(contact);
userList.getItems().remove(contact); if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close();
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) });
&& alert.isShowing())
alert.close();
});
} }
/** /**
@ -85,15 +79,13 @@ public class ContactSearchTab {
@FXML @FXML
private void sendRequest() { private void sendRequest() {
final var text = searchBar.getText().strip(); final var text = searchBar.getText().strip();
if (!text.isBlank()) if (!text.isBlank()) client.send(new UserSearchRequest(text));
client.send(new UserSearchRequest(text)); else userList.getItems().clear();
else
userList.getItems().clear();
} }
/** /**
* Clears the text in the search bar and the items shown in the list. Additionally disables both * Clears the text in the search bar and the items shown in the list.
* clear and search button. * Additionally disables both clear and search button.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -104,7 +96,8 @@ public class ContactSearchTab {
} }
/** /**
* Sends an {@link UserOperation} for the selected user to the server. * Sends an {@link ContactOperation} for the selected user to the
* server.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -113,8 +106,7 @@ public class ContactSearchTab {
final var user = userList.getSelectionModel().getSelectedItem(); final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) { if (user != null) {
currentlySelectedUser = user; currentlySelectedUser = user;
alert.setContentText( alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?");
"Add user " + currentlySelectedUser.getName() + " to your contacts?");
AlertHelper.confirmAction(alert, this::addAsContact); AlertHelper.confirmAction(alert, this::addAsContact);
} }
} }
@ -122,7 +114,7 @@ public class ContactSearchTab {
private void addAsContact() { private void addAsContact() {
// Sends the event to the server // Sends the event to the server
final var event = new UserOperation(currentlySelectedUser, ElementOperation.ADD); final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
client.send(event); client.send(event);
// Removes the chosen user and updates the UI // Removes the chosen user and updates the UI
@ -132,8 +124,5 @@ public class ContactSearchTab {
} }
@FXML @FXML
private void backButtonClicked() { private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
searchBar.setText("");
eventBus.dispatch(new BackEvent());
}
} }

View File

@ -7,33 +7,32 @@ import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import dev.kske.eventbus.core.*; import envoy.client.data.*;
import envoy.client.event.BackEvent;
import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupCreation; import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation; import envoy.event.contact.ContactOperation;
import envoy.util.Bounds; import envoy.util.Bounds;
import envoy.client.data.*; import dev.kske.eventbus.*;
import envoy.client.event.*;
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. * Provides a group creation interface. A group name can be entered in the text
* Available users (local chat recipients) are displayed inside a list and can be selected (multiple * field at the top. Available users (local chat recipients) are displayed
* selection available). * inside a list and can be selected (multiple selection available).
* <p> * <p>
* When the group creation button is pressed, a {@link GroupCreation} is sent to the server. This * When the group creation button is pressed, a {@link GroupCreation} is sent to
* controller enforces a valid group name and a non-empty member list (excluding the client user). * the server. This controller enforces a valid group name and a non-empty
* member list (excluding the client user).
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public class GroupCreationTab { public class GroupCreationTab implements EventListener {
@FXML @FXML
private Button createButton; private Button createButton;
@ -59,9 +58,6 @@ public class GroupCreationTab {
@FXML @FXML
private HBox errorProceedBox; private HBox errorProceedBox;
@FXML
private ListView<QuickSelectControl> quickSelectList;
private String name; private String name;
private final LocalDB localDB = Context.getInstance().getLocalDB(); private final LocalDB localDB = Context.getInstance().getLocalDB();
@ -71,6 +67,7 @@ public class GroupCreationTab {
@FXML @FXML
private void initialize() { private void initialize() {
userList.setCellFactory(new ListCellFactory<>(ContactControl::new)); userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
userList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
createButton.setDisable(true); createButton.setDisable(true);
eventBus.registerListener(this); eventBus.registerListener(this);
userList.getItems() userList.getItems()
@ -81,8 +78,6 @@ public class GroupCreationTab {
.filter(not(localDB.getUser()::equals)) .filter(not(localDB.getUser()::equals))
.map(User.class::cast) .map(User.class::cast)
.collect(Collectors.toList())); .collect(Collectors.toList()));
resizeQuickSelectSpace(0);
quickSelectList.addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);
} }
/** /**
@ -91,29 +86,16 @@ public class GroupCreationTab {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void userListClicked() { private void userListClicked() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
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 * Checks, whether the {@code createButton} can be enabled because text is
* field. * present in the text field.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void textUpdated() { private void textUpdated() { createButton.setDisable(userList.getSelectionModel().isEmpty() || groupNameField.getText().isBlank()); }
createButton
.setDisable(quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
}
/** /**
* Sends a {@link GroupCreation} to the server and closes this scene. * Sends a {@link GroupCreation} to the server and closes this scene.
@ -141,9 +123,6 @@ public class GroupCreationTab {
// Restoring the original design as tabs will always be reused // Restoring the original design as tabs will always be reused
setErrorMessageLabelSize(0); setErrorMessageLabelSize(0);
groupNameField.clear(); groupNameField.clear();
quickSelectList.getItems().forEach(q -> userList.getItems().add(q.getUser()));
quickSelectList.getItems().clear();
resizeQuickSelectSpace(0);
} }
} }
@ -157,41 +136,19 @@ public class GroupCreationTab {
private void createGroup(String name) { private void createGroup(String name) {
Context.getInstance() Context.getInstance()
.getClient() .getClient()
.send(new GroupCreation(name, quickSelectList.getItems().stream() .send(new GroupCreation(name, userList.getSelectionModel().getSelectedItems().stream().map(User::getID).collect(Collectors.toSet())));
.map(q -> q.getUser().getID()).collect(Collectors.toSet())));
} }
/** /**
* Returns true if the proposed group name is already present in the users {@code LocalDB}. * Returns true if the proposed group name is already present in the users
* {@code LocalDB}.
* *
* @param newName the chosen group name * @param newName the chosen group name
* @return true if this name is already present * @return true if this name is already present
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public boolean groupNameAlreadyPresent(String newName) { public boolean groupNameAlreadyPresent(String newName) {
return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance) return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance).map(Contact::getName).anyMatch(newName::equals);
.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 @FXML
@ -240,11 +197,11 @@ public class GroupCreationTab {
} }
@Event @Event
private void onUserOperation(UserOperation operation) { private void onContactOperation(ContactOperation operation) {
Platform.runLater(() -> { if (operation.get() instanceof User) Platform.runLater(() -> {
switch (operation.getOperationType()) { switch (operation.getOperationType()) {
case ADD: case ADD:
userList.getItems().add(operation.get()); userList.getItems().add((User) operation.get());
break; break;
case REMOVE: case REMOVE:
userList.getItems().removeIf(operation.get()::equals); userList.getItems().removeIf(operation.get()::equals);
@ -252,10 +209,4 @@ public class GroupCreationTab {
} }
}); });
} }
@Event
private void onAccountDeletion(AccountDeletion deletion) {
final var deletedID = deletion.get();
Platform.runLater(() -> userList.getItems().removeIf(user -> (user.getID() == deletedID)));
}
} }

View File

@ -10,15 +10,14 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import dev.kske.eventbus.core.*; import envoy.client.data.ClientConfig;
import envoy.client.ui.*;
import envoy.client.util.IconUtil;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.event.HandshakeRejection; import envoy.event.HandshakeRejection;
import envoy.util.*; import envoy.util.*;
import envoy.client.data.ClientConfig; import dev.kske.eventbus.*;
import envoy.client.ui.Startup;
import envoy.client.util.IconUtil;
/** /**
* Controller for the login scene. * Controller for the login scene.
@ -27,7 +26,7 @@ import envoy.client.util.IconUtil;
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class LoginScene { public final class LoginScene implements EventListener {
@FXML @FXML
private TextField userTextField; private TextField userTextField;
@ -79,32 +78,25 @@ public final class LoginScene {
@FXML @FXML
private void loginButtonPressed() { private void loginButtonPressed() {
final String user = userTextField.getText(), pass = passwordField.getText(), final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText();
repeatPass = repeatPasswordField.getText();
final boolean requestToken = cbStaySignedIn.isSelected(); final boolean requestToken = cbStaySignedIn.isSelected();
// Prevent registration with unequal passwords // Prevent registration with unequal passwords
if (registration && !pass.equals(repeatPass)) { if (registration && !pass.equals(repeatPass)) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one") new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
.showAndWait();
repeatPasswordField.clear(); repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(user)) { } else if (!Bounds.isValidContactName(user)) {
new Alert(AlertType.ERROR, new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
"The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")")
.showAndWait();
userTextField.clear(); userTextField.clear();
} else { } else {
Instant lastSync = Startup.loadLastSync(userTextField.getText()); Instant lastSync = Startup.loadLastSync(userTextField.getText());
Startup.performHandshake(registration Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync) : LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
} }
} }
@FXML @FXML
private void offlineModeButtonPressed() { private void offlineModeButtonPressed() { Startup.attemptOfflineMode(userTextField.getText()); }
Startup.attemptOfflineMode(userTextField.getText());
}
@FXML @FXML
private void registerSwitchPressed() { private void registerSwitchPressed() {
@ -135,7 +127,5 @@ public final class LoginScene {
} }
@Event @Event
private void onHandshakeRejection(HandshakeRejection evt) { private void onHandshakeRejection(HandshakeRejection evt) { Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait()); }
Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait());
}
} }

View File

@ -1,13 +1,9 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import java.util.Map;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.*;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
import envoy.client.ui.settings.*; import envoy.client.ui.settings.*;
@ -17,7 +13,7 @@ import envoy.client.ui.settings.*;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class SettingsScene implements KeyboardMapping { public final class SettingsScene {
@FXML @FXML
private ListView<SettingsPane> settingsList; private ListView<SettingsPane> settingsList;
@ -28,8 +24,7 @@ public final class SettingsScene implements KeyboardMapping {
@FXML @FXML
private void initialize() { private void initialize() {
settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle()))); settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(), settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(), new DownloadSettingsPane(), new BugReportPane());
new DownloadSettingsPane(), new BugReportPane());
} }
@FXML @FXML
@ -42,13 +37,5 @@ public final class SettingsScene implements KeyboardMapping {
} }
@FXML @FXML
private void backButtonClicked() { private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); }
Context.getInstance().getSceneContext().pop();
}
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.of(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN),
this::backButtonClicked);
}
} }

View File

@ -33,7 +33,6 @@ public abstract class AbstractListCell<T, U extends Node> extends ListCell<T> {
setGraphic(renderItem(item)); setGraphic(renderItem(item));
} else { } else {
setGraphic(null); setGraphic(null);
setCursor(Cursor.DEFAULT);
} }
} }

View File

@ -1,55 +0,0 @@
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;
}
}

View File

@ -28,7 +28,5 @@ public final class GenericListCell<T, U extends Node> extends AbstractListCell<T
} }
@Override @Override
protected U renderItem(T item) { protected U renderItem(T item) { return renderer.apply(item); }
return renderer.apply(item);
}
} }

View File

@ -7,15 +7,15 @@ import javafx.scene.control.*;
import javafx.util.Callback; import javafx.util.Callback;
/** /**
* Provides a creation mechanism for generic list cells given a list view and a conversion function. * Provides a creation mechanism for generic list cells given a list view and a
* conversion function.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> the type of object to display * @param <T> the type of object to display
* @param <U> the type of node displayed * @param <U> the type of node displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ListCellFactory<T, U extends Node> public final class ListCellFactory<T, U extends Node> implements Callback<ListView<T>, ListCell<T>> {
implements Callback<ListView<T>, ListCell<T>> {
private final Function<? super T, U> renderer; private final Function<? super T, U> renderer;
@ -23,12 +23,8 @@ public final class ListCellFactory<T, U extends Node>
* @param renderer a function converting the type to display into a node * @param renderer a function converting the type to display into a node
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public ListCellFactory(Function<? super T, U> renderer) { public ListCellFactory(Function<? super T, U> renderer) { this.renderer = renderer; }
this.renderer = renderer;
}
@Override @Override
public ListCell<T> call(ListView<T> listView) { public ListCell<T> call(ListView<T> listView) { return new GenericListCell<>(listView, renderer); }
return new GenericListCell<>(listView, renderer);
}
} }

View File

@ -3,9 +3,8 @@ package envoy.client.ui.listcell;
import javafx.geometry.*; import javafx.geometry.*;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import envoy.data.Message;
import envoy.client.ui.control.MessageControl; import envoy.client.ui.control.MessageControl;
import envoy.data.Message;
/** /**
* A list cell containing messages represented as message controls. * A list cell containing messages represented as message controls.
@ -19,25 +18,20 @@ public final class MessageListCell extends AbstractListCell<Message, MessageCont
* @param listView the list view inside of which the cell will be displayed * @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public MessageListCell(ListView<? extends Message> listView) { public MessageListCell(ListView<? extends Message> listView) { super(listView); }
super(listView);
}
@Override @Override
protected MessageControl renderItem(Message message) { protected MessageControl renderItem(Message message) {
final var control = new MessageControl(message); final var control = new MessageControl(message);
listView.widthProperty().addListener((observable, oldValue, listView.widthProperty().addListener((observable, oldValue, newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
adjustPadding((int) listView.getWidth(), control.isOwnMessage()); adjustPadding((int) listView.getWidth(), control.isOwnMessage());
if (control.isOwnMessage()) if (control.isOwnMessage()) setAlignment(Pos.CENTER_RIGHT);
setAlignment(Pos.CENTER_RIGHT); else setAlignment(Pos.CENTER_LEFT);
else
setAlignment(Pos.CENTER_LEFT);
return control; return control;
} }
private void adjustPadding(int listWidth, boolean ownMessage) { private void adjustPadding(int listWidth, boolean ownMessage) {
int padding = 10 + Math.max((listWidth - 1000) / 2, 0); int padding = 10 + Math.max((listWidth - 1000) / 2, 0);
setPadding(ownMessage ? new Insets(3, padding, 3, 0) : new Insets(3, 0, 3, padding)); setPadding(ownMessage ? new Insets(0, padding, 6, 0) : new Insets(0, 0, 6, padding));
} }
} }

View File

@ -1,5 +1,6 @@
/** /**
* This package contains custom list cells that are used to display certain things. * This package contains custom list cells that are used to display certain
* things.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -7,8 +7,8 @@ import javafx.scene.input.InputEvent;
import envoy.event.IssueProposal; import envoy.event.IssueProposal;
/** /**
* This class offers the option for users to submit a bug report. Only the title of a bug is needed * This class offers the option for users to submit a bug report. Only the title
* to be sent. * of a bug is needed to be sent.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -17,15 +17,12 @@ public final class BugReportPane extends OnlineOnlySettingsPane {
private final Label titleLabel = new Label("Suggest a title for the bug:"); private final Label titleLabel = new Label("Suggest a title for the bug:");
private final TextField titleTextField = new TextField(); private final TextField titleTextField = new TextField();
private final Label pleaseExplainLabel = private final Label pleaseExplainLabel = new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
private final TextArea errorDetailArea = new TextArea(); private final TextArea errorDetailArea = new TextArea();
private final CheckBox showUsernameInBugReport = private final CheckBox showUsernameInBugReport = new CheckBox("Show your username in the bug report?");
new CheckBox("Show your username in the bug report?");
private final Button submitReportButton = new Button("Submit report"); private final Button submitReportButton = new Button("Submit report");
private final EventHandler<? super InputEvent> inputEventHandler = private final EventHandler<? super InputEvent> inputEventHandler = e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
/** /**
* Creates a new {@code BugReportPane}. * Creates a new {@code BugReportPane}.
@ -62,9 +59,7 @@ public final class BugReportPane extends OnlineOnlySettingsPane {
submitReportButton.setDisable(true); submitReportButton.setDisable(true);
submitReportButton.setOnAction(e -> { submitReportButton.setOnAction(e -> {
String title = titleTextField.getText(), description = errorDetailArea.getText(); String title = titleTextField.getText(), description = errorDetailArea.getText();
client.send( client.send(showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true) : new IssueProposal(title, description, client.getSender().getName(), true));
showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true)
: new IssueProposal(title, description, client.getSender().getName(), true));
}); });
getChildren().add(submitReportButton); getChildren().add(submitReportButton);
} }

View File

@ -5,6 +5,8 @@ import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import envoy.client.data.Context;
/** /**
* Displays options for downloading {@link envoy.data.Attachment}s. * Displays options for downloading {@link envoy.data.Attachment}s.
* *
@ -24,22 +26,18 @@ public final class DownloadSettingsPane extends SettingsPane {
setPadding(new Insets(15)); setPadding(new Insets(15));
// Checkbox to disable asking // Checkbox to disable asking
final var checkBox = final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
checkBox.setSelected(settings.isDownloadSavedWithoutAsking()); checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
checkBox.setTooltip(new Tooltip( checkBox.setTooltip(new Tooltip("Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
"Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected())); checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
getChildren().add(checkBox); getChildren().add(checkBox);
// Displaying the default path to save to // Displaying the default path to save to
final var pathLabel = final var pathLabel = new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
pathLabel.setWrapText(true); pathLabel.setWrapText(true);
getChildren().add(pathLabel); getChildren().add(pathLabel);
final var hbox = new HBox(20); final var hbox = new HBox(20);
Tooltip.install(hbox, Tooltip.install(hbox, new Tooltip("Determines the location where attachments will be saved to."));
new Tooltip("Determines the location where attachments will be saved to."));
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath()); final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
hbox.getChildren().add(currentPath); hbox.getChildren().add(currentPath);
@ -49,8 +47,7 @@ public final class DownloadSettingsPane extends SettingsPane {
final var directoryChooser = new DirectoryChooser(); final var directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select the directory where attachments should be saved to"); directoryChooser.setTitle("Select the directory where attachments should be saved to");
directoryChooser.setInitialDirectory(settings.getDownloadLocation()); directoryChooser.setInitialDirectory(settings.getDownloadLocation());
final var selectedDirectory = final var selectedDirectory = directoryChooser.showDialog(Context.getInstance().getSceneContext().getStage());
directoryChooser.showDialog(context.getSceneContext().getStage());
if (selectedDirectory != null) { if (selectedDirectory != null) {
currentPath.setText(selectedDirectory.getAbsolutePath()); currentPath.setText(selectedDirectory.getAbsolutePath());

View File

@ -2,14 +2,12 @@ package envoy.client.ui.settings;
import javafx.scene.control.*; import javafx.scene.control.*;
import dev.kske.eventbus.core.EventBus;
import envoy.data.User.UserStatus;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.ui.StatusTrayIcon; import envoy.client.helper.ShutdownHelper;
import envoy.client.util.UserUtil; import envoy.data.User.UserStatus;
import dev.kske.eventbus.EventBus;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -24,31 +22,23 @@ public final class GeneralSettingsPane extends SettingsPane {
super("General"); super("General");
setSpacing(10); setSpacing(10);
final var settingsItems = settings.getItems(); // TODO: Support other value types
final var settingsItems = settings.getItems();
// Add hide on close if supported final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
final var hideOnCloseCheckbox = final var hideOnCloseTooltip = new Tooltip("If selected, Envoy will still be present in the task bar when closed.");
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); hideOnCloseTooltip.setWrapText(true);
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip); hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
hideOnCloseCheckbox.setDisable(!StatusTrayIcon.isSupported());
getChildren().add(hideOnCloseCheckbox); getChildren().add(hideOnCloseCheckbox);
final var enterToSendCheckbox = final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
final var enterToSendTooltip = new Tooltip( 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."); "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); enterToSendTooltip.setWrapText(true);
enterToSendCheckbox.setTooltip(enterToSendTooltip); enterToSendCheckbox.setTooltip(enterToSendTooltip);
getChildren().add(enterToSendCheckbox); getChildren().add(enterToSendCheckbox);
final var askForConfirmationCheckbox = final var askForConfirmationCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation")); final var askForConfirmationTooltip = new Tooltip("When selected, nothing will prompt a confirmation dialog");
final var askForConfirmationTooltip =
new Tooltip("When selected, nothing will prompt a confirmation dialog");
askForConfirmationTooltip.setWrapText(true); askForConfirmationTooltip.setWrapText(true);
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip); askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
getChildren().add(askForConfirmationCheckbox); getChildren().add(askForConfirmationCheckbox);
@ -56,26 +46,22 @@ public final class GeneralSettingsPane extends SettingsPane {
final var combobox = new ComboBox<String>(); final var combobox = new ComboBox<String>();
combobox.getItems().add("dark"); combobox.getItems().add("dark");
combobox.getItems().add("light"); combobox.getItems().add("light");
combobox combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme()); combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction(e -> { combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
settings.setCurrentTheme(combobox.getValue());
EventBus.getInstance().dispatch(new ThemeChangeEvent());
});
getChildren().add(combobox); getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>(); final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values()); statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(context.getLocalDB().getUser().getStatus()); statusComboBox.setValue(UserStatus.ONLINE);
statusComboBox.setTooltip(new Tooltip("Change your current status")); statusComboBox.setTooltip(new Tooltip("Change your current status"));
statusComboBox.setOnAction(e -> UserUtil.changeStatus(statusComboBox.getValue())); // TODO add action when value is changed
statusComboBox.setOnAction(e -> {});
getChildren().add(statusComboBox); getChildren().add(statusComboBox);
final var logoutButton = new Button("Logout"); final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> UserUtil.logout()); logoutButton.setOnAction(e -> ShutdownHelper.logout());
final var logoutTooltip = new Tooltip( final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account");
"Brings you back to the login screen and removes \"remember me\" status from this account");
logoutTooltip.setWrapText(true); logoutTooltip.setWrapText(true);
logoutButton.setTooltip(logoutTooltip); logoutButton.setTooltip(logoutTooltip);
getChildren().add(logoutButton); getChildren().add(logoutButton);

View File

@ -5,13 +5,14 @@ import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import envoy.client.data.Context;
import envoy.client.net.Client; import envoy.client.net.Client;
/** /**
* Inheriting from this class signifies that options should only be available if the * Inheriting from this class signifies that options should only be available if
* {@link envoy.data.User} is currently online. If the user is currently offline, all * the {@link envoy.data.User} is currently online. If the user is currently
* {@link javafx.scene.Node} variables will be disabled and a {@link Tooltip} will be displayed for * offline, all {@link javafx.scene.Node} variables will be disabled and a
* the whole node. * {@link Tooltip} will be displayed for the whole node.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -19,10 +20,9 @@ import envoy.client.net.Client;
*/ */
public abstract class OnlineOnlySettingsPane extends SettingsPane { public abstract class OnlineOnlySettingsPane extends SettingsPane {
protected final Client client = context.getClient(); protected final Client client = Context.getInstance().getClient();
private final Tooltip beOnlineReminder = private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account.");
new Tooltip("You need to be online to modify your account.");
/** /**
* @param title the title of this pane * @param title the title of this pane
@ -34,17 +34,14 @@ public abstract class OnlineOnlySettingsPane extends SettingsPane {
setDisable(!client.isOnline()); setDisable(!client.isOnline());
if (!client.isOnline()) { if (!client.isOnline()) {
final var infoLabel = final var infoLabel = new Label("You shall not pass!\n(... Unless you would happen to be online)");
new Label("You shall not pass!\n(... Unless you would happen to be online)");
infoLabel.setId("info-label-warning"); infoLabel.setId("info-label-warning");
infoLabel.setWrapText(true); infoLabel.setWrapText(true);
getChildren().add(infoLabel); getChildren().add(infoLabel);
setBackground(new Background( setBackground(new Background(new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY)));
new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY)));
Tooltip.install(this, beOnlineReminder); Tooltip.install(this, beOnlineReminder);
} else } else Tooltip.uninstall(this, beOnlineReminder);
Tooltip.uninstall(this, beOnlineReminder);
} }
/** /**
@ -53,7 +50,5 @@ public abstract class OnlineOnlySettingsPane extends SettingsPane {
* @param text the text to display * @param text the text to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
protected void setToolTipText(String text) { protected void setToolTipText(String text) { beOnlineReminder.setText(text); }
beOnlineReminder.setText(text);
}
} }

View File

@ -2,7 +2,7 @@ package envoy.client.ui.settings;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import envoy.client.data.*; import envoy.client.data.Settings;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -12,12 +12,9 @@ public abstract class SettingsPane extends VBox {
protected String title; protected String title;
protected static final Settings settings = Settings.getInstance(); protected static final Settings settings = Settings.getInstance();
protected static final Context context = Context.getInstance();
protected SettingsPane(String title) { protected SettingsPane(String title) { this.title = title; }
this.title = title;
}
/** /**
* @return the title of this settings pane * @return the title of this settings pane

View File

@ -13,15 +13,14 @@ import javafx.scene.image.*;
import javafx.scene.input.InputEvent; import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.core.EventBus;
import envoy.client.data.Context;
import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil;
import envoy.event.*; import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import envoy.client.ui.control.ProfilePicImageView; import dev.kske.eventbus.EventBus;
import envoy.client.util.*;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -39,7 +38,6 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
private final PasswordField newPasswordField = new PasswordField(); private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField(); private final PasswordField repeatNewPasswordField = new PasswordField();
private final Button saveButton = new Button("Save"); private final Button saveButton = new Button("Save");
private final Button deleteAccountButton = new Button("Delete Account (Locally)");
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
@ -61,24 +59,21 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
profilePic.setFitWidth(60); profilePic.setFitWidth(60);
profilePic.setFitHeight(60); profilePic.setFitHeight(60);
profilePic.setOnMouseClicked(e -> { profilePic.setOnMouseClicked(e -> {
if (!client.isOnline()) if (!client.isOnline()) return;
return;
final var pictureChooser = new FileChooser(); final var pictureChooser = new FileChooser();
pictureChooser.setTitle("Select a new profile pic"); pictureChooser.setTitle("Select a new profile pic");
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home"))); pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
pictureChooser.getExtensionFilters().add( pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
final var file = pictureChooser.showOpenDialog(context.getSceneContext().getStage()); final var file = pictureChooser.showOpenDialog(Context.getInstance().getSceneContext().getStage());
if (file != null) { if (file != null) {
// Check max file size // Check max file size
// TODO: Move to config // TODO: Move to config
if (file.length() > 5E6) { if (file.length() > 5E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!") new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!").showAndWait();
.showAndWait();
return; return;
} }
@ -98,8 +93,8 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
newUsername = username; newUsername = username;
usernameTextField.setText(username); usernameTextField.setText(username);
final EventHandler<? super InputEvent> textChanged = e -> { final EventHandler<? super InputEvent> textChanged = e -> {
newUsername = usernameTextField.getText(); newUsername = usernameTextField.getText();
usernameChanged = newUsername != username; usernameChanged = newUsername != username;
}; };
usernameTextField.setOnInputMethodTextChanged(textChanged); usernameTextField.setOnInputMethodTextChanged(textChanged);
usernameTextField.setOnKeyTyped(textChanged); usernameTextField.setOnKeyTyped(textChanged);
@ -108,25 +103,15 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// "Displaying" the password change mechanism // "Displaying" the password change mechanism
final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() }; final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() };
final Label[] passwordLabels = final Label[] passwordLabels = { new Label("Enter current password:"), new Label("Enter new password:"),
{ new Label("Enter current password:"), new Label("Enter new password:"),
new Label("Repeat new password:") }; new Label("Repeat new password:") };
final PasswordField[] passwordFields = final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField };
{ currentPasswordField, newPasswordField, repeatNewPasswordField }; final EventHandler<? super InputEvent> passwordEntered = e -> {
final EventHandler<? super InputEvent> passwordEntered = newPassword = newPasswordField.getText();
e -> { validPassword = newPassword.equals(repeatNewPasswordField.getText())
newPassword = && !newPasswordField.getText().isBlank();
newPasswordField };
.getText();
validPassword =
newPassword.equals(
repeatNewPasswordField
.getText())
&& !newPasswordField
.getText()
.isBlank();
};
newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered);
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered); repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
@ -141,22 +126,9 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
} }
// Displaying the save button // Displaying the save button
saveButton saveButton.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText()));
.setOnAction(e -> save(currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT); saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton); getChildren().add(saveButton);
// Displaying the delete account button
deleteAccountButton.setAlignment(Pos.BASELINE_CENTER);
deleteAccountButton.setOnAction(e -> UserUtil.deleteAccount());
deleteAccountButton.setText("Delete Account (locally)");
deleteAccountButton.setPrefHeight(25);
deleteAccountButton.getStyleClass().clear();
deleteAccountButton.getStyleClass().add("danger-button");
final var tooltip = new Tooltip("Remote deletion is currently unsupported.");
tooltip.setShowDelay(Duration.millis(100));
deleteAccountButton.setTooltip(tooltip);
getChildren().add(deleteAccountButton);
} }
/** /**
@ -165,11 +137,11 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
* @param username the new username * @param username the new username
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private void save(String oldPassword) { private void save(long userID, String oldPassword) {
// The profile pic was changed // The profile pic was changed
if (profilePicChanged) { if (profilePicChanged) {
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes); final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID);
eventBus.dispatch(profilePicChangeEvent); eventBus.dispatch(profilePicChangeEvent);
client.send(profilePicChangeEvent); client.send(profilePicChangeEvent);
logger.log(Level.INFO, "The user just changed his profile pic."); logger.log(Level.INFO, "The user just changed his profile pic.");
@ -178,16 +150,14 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// The username was changed // The username was changed
final var validContactName = Bounds.isValidContactName(newUsername); final var validContactName = Bounds.isValidContactName(newUsername);
if (usernameChanged && validContactName) { if (usernameChanged && validContactName) {
final var nameChangeEvent = new NameChange(client.getSender().getID(), newUsername); final var nameChangeEvent = new NameChange(userID, newUsername);
eventBus.dispatch(nameChangeEvent); eventBus.dispatch(nameChangeEvent);
client.send(nameChangeEvent); client.send(nameChangeEvent);
logger.log(Level.INFO, "The user just changed his name to " + newUsername + "."); logger.log(Level.INFO, "The user just changed his name to " + newUsername + ".");
} else if (!validContactName) { } else if (!validContactName) {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid username"); alert.setTitle("Invalid username");
alert.setContentText( alert.setContentText("The entered username does not conform with the naming limitations: " + Bounds.CONTACT_NAME_PATTERN);
"The entered username does not conform with the naming limitations: "
+ Bounds.CONTACT_NAME_PATTERN);
alert.showAndWait(); alert.showAndWait();
logger.log(Level.INFO, "An invalid username was requested."); logger.log(Level.INFO, "An invalid username was requested.");
return; return;
@ -195,7 +165,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// The password was changed // The password was changed
if (validPassword) { if (validPassword) {
client.send(new PasswordChangeRequest(newPassword, oldPassword)); client.send(new PasswordChangeRequest(newPassword, oldPassword, userID));
logger.log(Level.INFO, "The user just tried to change his password!"); logger.log(Level.INFO, "The user just tried to change his password!");
} else if (!(validPassword || newPassword.isBlank())) { } else if (!(validPassword || newPassword.isBlank())) {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);

View File

@ -1,5 +1,6 @@
/** /**
* This package contains classes used for representing the settings visually. * This package contains classes used for representing the settings
* visually.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -9,22 +9,18 @@ import javax.imageio.ImageIO;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import envoy.client.data.Settings;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.Settings;
/** /**
* Provides static utility methods for loading icons from the resource folder. * Provides static utility methods for loading icons from the resource
* folder.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class IconUtil { public final class IconUtil {
private static final HashMap<String, Image> cache = new HashMap<>();
private static final HashMap<String, Image> scaledCache = new HashMap<>();
private static final HashMap<String, BufferedImage> awtCache = new HashMap<>();
private IconUtil() {} private IconUtil() {}
/** /**
@ -34,10 +30,7 @@ public final class IconUtil {
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path) { public static Image load(String path) { return new Image(IconUtil.class.getResource(path).toExternalForm()); }
return cache.computeIfAbsent(path,
p -> new Image(IconUtil.class.getResource(p).toExternalForm()));
}
/** /**
* Loads an image from the resource folder and scales it to the given size. * Loads an image from the resource folder and scales it to the given size.
@ -48,14 +41,12 @@ public final class IconUtil {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path, int size) { public static Image load(String path, int size) {
return scaledCache.computeIfAbsent(path + size, return new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
p -> new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true,
true));
} }
/** /**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the resource folder. * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* <p> * resource folder.<br>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
* @param name the image name without the .png suffix * @param name the image name without the .png suffix
@ -64,13 +55,11 @@ public final class IconUtil {
* @apiNote let's load a sample image {@code /icons/abc.png}.<br> * @apiNote let's load a sample image {@code /icons/abc.png}.<br>
* To do that, we only have to call {@code IconUtil.loadIcon("abc")} * To do that, we only have to call {@code IconUtil.loadIcon("abc")}
*/ */
public static Image loadIcon(String name) { public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
return load("/icons/" + name + ".png");
}
/** /**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the resource folder and * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* scales it to the given size.<br> * resource folder and scales it to the given size.<br>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
* @param name the image name without the .png suffix * @param name the image name without the .png suffix
@ -78,19 +67,20 @@ public final class IconUtil {
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br> * @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
* To do that, we only have to call {@code IconUtil.loadIcon("abc", 16)} * To do that, we only have to call
* {@code IconUtil.loadIcon("abc", 16)}
*/ */
public static Image loadIcon(String name, int size) { public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
return load("/icons/" + name + ".png", size);
}
/** /**
* Loads a {@code .png} image whose design depends on the currently active theme from the * Loads a {@code .png} image whose design depends on the currently active theme
* sub-folder {@code /icons/dark/} or {@code /icons/light/} of the resource folder. * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* resource folder.
* <p> * <p>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
* @param name the image name without the "black" or "white" suffix and without the .png suffix * @param name the image name without the "black" or "white" suffix and without
* the .png suffix
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
@ -98,14 +88,12 @@ public final class IconUtil {
* To do that theme sensitive, we only have to call * To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc")} * {@code IconUtil.loadIconThemeSensitive("abc")}
*/ */
public static Image loadIconThemeSensitive(String name) { public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
return loadIcon(themeSpecificSubFolder() + name);
}
/** /**
* Loads a {@code .png} image whose design depends on the currently active theme from the * Loads a {@code .png} image whose design depends on the currently active theme
* sub-folder {@code /icons/dark/} or {@code /icons/light/} of the resource folder and scales it * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* to the given size. * resource folder and scales it to the given size.
* <p> * <p>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
@ -118,19 +106,20 @@ public final class IconUtil {
* To do that theme sensitive, we only have to call * To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc", 16)} * {@code IconUtil.loadIconThemeSensitive("abc", 16)}
*/ */
public static Image loadIconThemeSensitive(String name, int size) { public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
return loadIcon(themeSpecificSubFolder() + name, size);
}
/** /**
* Loads images specified by an enum. The images have to be named like the lowercase enum *
* constants with {@code .png} extension and be located inside a folder with the lowercase name * Loads images specified by an enum. The images have to be named like the
* of the enum, which must be contained inside the {@code /icons/} folder. * lowercase enum constants with {@code .png} extension and be located inside a
* folder with the lowercase name of the enum, which must be contained inside
* the {@code /icons/} folder.
* *
* @param <T> the enum that specifies the images to load * @param <T> the enum that specifies the images to load
* @param enumClass the class of the enum * @param enumClass the class of the enum
* @param size the size to scale the images to * @param size the size to scale the images to
* @return a map containing the loaded images with the corresponding enum constants as keys * @return a map containing the loaded images with the corresponding enum
* constants as keys
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) { public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) {
@ -149,29 +138,25 @@ public final class IconUtil {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static BufferedImage loadAWTCompatible(String path) { public static BufferedImage loadAWTCompatible(String path) {
return awtCache.computeIfAbsent(path, p -> { BufferedImage image = null;
try { try {
return ImageIO.read(IconUtil.class.getResource(path)); image = ImageIO.read(IconUtil.class.getResource(path));
} catch (IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
String.format("Could not load image at path %s: ", path), e); }
return null; return image;
}
});
} }
/** /**
* This method should be called if the display of an image depends upon the currently active * This method should be called if the display of an image depends upon the
* theme.<br> * currently active theme.<br>
* In case of a default theme, the string returned will be ({@code dark/} or {@code light/}), * In case of a default theme, the string returned will be
* otherwise it will be empty. * ({@code dark/} or {@code light/}), otherwise it will be empty.
* *
* @return the theme specific folder * @return the theme specific folder
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private static String themeSpecificSubFolder() { private static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
? Settings.getInstance().getCurrentTheme() + "/"
: "";
} }
} }

View File

@ -1,116 +0,0 @@
package envoy.client.util;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.*;
import java.util.logging.*;
import javafx.stage.FileChooser;
import dev.kske.eventbus.core.EventBus;
import envoy.data.Message;
import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.event.MessageDeletion;
import envoy.client.ui.controller.ChatScene;
/**
* Contains methods that are commonly used for {@link Message}s.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public class MessageUtil {
private MessageUtil() {}
private static Logger logger = EnvoyLog.getLogger(MessageUtil.class);
/**
* Copies the text of the given message to the System Clipboard.
*
* @param message the message whose text to copy
* @since Envoy Client v0.3-beta
*/
public static void copyMessageText(Message message) {
logger.log(Level.FINEST,
"A copy of message text \"" + message.getText() + "\" was requested");
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(message.getText()), null);
}
/**
* Deletes the given message.
*
* @param message the message to delete
* @since Envoy Client v0.3-beta
*/
public static void deleteMessage(Message message) {
final var messageDeletionEvent = new MessageDeletion(message.getID());
final var controller =
Context.getInstance().getSceneContext().getController();
if (controller instanceof ChatScene)
((ChatScene) controller).clearMessageSelection();
// Removing the message locally
EventBus.getInstance().dispatch(messageDeletionEvent);
logger.log(Level.FINEST, "message deletion was requested for " + message);
}
/**
* Forwards the given message. Currently not implemented.
*
* @param message the message to forward
* @since Envoy Client v0.3-beta
*/
public static void forwardMessage(Message message) {
logger.log(Level.FINEST, "Message forwarding was requested for " + message);
}
/**
* Quotes the given message. Currently not implemented.
*
* @param message the message to quote
* @since Envoy Client v0.3-beta
*/
public static void quoteMessage(Message message) {
logger.log(Level.FINEST, "Message quotation was requested for " + message);
}
/**
* Saves the attachment of a message, if present.
*
* @param message the message whose attachment to save
* @throws IllegalStateException if no attachment is present in the message
* @since Envoy Client v0.3-beta
*/
public static void saveAttachment(Message message) {
if (!message.hasAttachment())
throw new IllegalArgumentException("Cannot save a non-existing attachment");
File file;
final var fileName = message.getAttachment().getName();
final var downloadLocation = Settings.getInstance().getDownloadLocation();
// Show save file dialog, if the user did not opt-out
if (!Settings.getInstance().isDownloadSavedWithoutAsking()) {
final var fileChooser = new FileChooser();
fileChooser.setInitialFileName(fileName);
fileChooser.setInitialDirectory(downloadLocation);
file = fileChooser.showSaveDialog(Context.getInstance().getSceneContext().getStage());
} else
file = new File(downloadLocation, fileName);
// A file was selected
if (file != null)
try (var fos = new FileOutputStream(file)) {
fos.write(message.getAttachment().getData());
logger.log(Level.FINE,
"Attachment of message was saved at " + file.getAbsolutePath());
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
}
}
}

View File

@ -14,43 +14,47 @@ public final class ReflectionUtil {
private ReflectionUtil() {} private ReflectionUtil() {}
/** /**
* Gets all declared variable values of the given instance that have the specified class. * Gets all declared variable values of the given instance that have the
* specified class.
* <p> * <p>
* (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a GUI class). * (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a
* GUI class).
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the object * @param <T> the type of the object
* @param <R> the type to return * @param <R> the type to return
* @param instance the instance of a given class whose values are to be evaluated * @param instance the instance of a given class whose values are to be
* evaluated
* @param typeToReturn the type of variable to return * @param typeToReturn the type of variable to return
* @return all variables in the given instance that have the requested type * @return all variables in the given instance that have the requested type
* @throws RuntimeException if an exception occurs * @throws RuntimeException if an exception occurs
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance, public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance, Class<R> typeToReturn) {
Class<R> typeToReturn) { return Arrays.stream(instance.getClass().getDeclaredFields()).filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> {
return Arrays.stream(instance.getClass().getDeclaredFields()) try {
.filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> { field.setAccessible(true);
try { return typeToReturn.cast(field.get(instance));
field.setAccessible(true); } catch (IllegalArgumentException | IllegalAccessException e) {
return typeToReturn.cast(field.get(instance)); throw new RuntimeException(e);
} catch (IllegalArgumentException | IllegalAccessException e) { }
throw new RuntimeException(e); });
}
});
} }
/** /**
* Gets all declared variables of the given instance that are children of {@code Node}. * Gets all declared variables of the given instance that are children of
* {@code Node}.
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the instance * @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be evaluated * @param instance the instance of a given class whose values are to be
* @return all variables of the given object that have the requested type as {@code Stream} * evaluated
* @return all variables of the given object that have the requested type as
* {@code Stream}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) { public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) {
@ -58,13 +62,15 @@ public final class ReflectionUtil {
} }
/** /**
* Gets all declared variables of the given instance that are children of {@code Node}<br> * Gets all declared variables of the given instance that are children of
* {@code Node}<br>
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the instance * @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be evaluated * @param instance the instance of a given class whose values are to be
* evaluated
* @return all variables of the given object that have the requested type * @return all variables of the given object that have the requested type
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */

View File

@ -1,155 +0,0 @@
package envoy.client.util;
import java.util.logging.*;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.core.EventBus;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.event.contact.UserOperation;
import envoy.util.EnvoyLog;
import envoy.client.data.Context;
import envoy.client.event.*;
import envoy.client.helper.*;
import envoy.client.ui.SceneInfo;
import envoy.client.ui.controller.ChatScene;
/**
* Contains methods that change something about the currently logged in user.
*
* @author Leon Hofmeister
* @since Envoy Client v0.3-beta
*/
public final class UserUtil {
private static final Context context = Context.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserUtil.class);
private UserUtil() {}
/**
* Logs the current user out and reopens {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
context.getSceneContext().load(SceneInfo.LOGIN_SCENE);
logger.log(Level.INFO, "A logout occurred.");
});
}
/**
* Notifies the application that the status of the currently logged in user has changed.
*
* @param newStatus the new status
* @since Envoy Client v0.3-beta
*/
public static void changeStatus(UserStatus newStatus) {
// Sending the already active status is a valid action
if (newStatus.equals(context.getLocalDB().getUser().getStatus()))
return;
else {
EventBus.getInstance().dispatch(new OwnStatusChange(newStatus));
if (context.getClient().isOnline())
context.getClient()
.send(new UserStatusChange(context.getLocalDB().getUser().getID(), newStatus));
logger.log(Level.INFO, "A manual status change occurred.");
}
}
/**
* Removes the given contact.
*
* @param block the contact that should be removed
* @since Envoy Client v0.3-beta
*/
public static void disableContact(Contact block) {
if (!context.getClient().isOnline() || block == null)
return;
else {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setContentText("Are you sure you want to "
+ (block instanceof User ? "block " : "leave group ") + block.getName() + "?");
AlertHelper.confirmAction(alert, () -> {
final var isUser = block instanceof User;
context.getClient()
.send(isUser ? new UserOperation((User) block, ElementOperation.REMOVE)
: new GroupResize(context.getLocalDB().getUser(), (Group) block,
ElementOperation.REMOVE));
if (!isUser)
block.getContacts().remove(context.getLocalDB().getUser());
EventBus.getInstance().dispatch(new ContactDisabled(block));
logger.log(Level.INFO, isUser ? "A user was blocked." : "The user left a group.");
});
}
}
/**
* Deletes the given contact with all his messages entirely.
*
* @param delete the contact to delete
* @since Envoy Client v0.3-beta
*/
public static void deleteContact(Contact delete) {
if (delete == null)
return;
else {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setContentText("Are you sure you want to delete " + delete.getName()
+ " entirely? All messages with this contact will be deleted. This action cannot be undone.");
AlertHelper.confirmAction(alert, () -> {
context.getLocalDB().getUsers().remove(delete.getName());
context.getLocalDB().getChats()
.removeIf(chat -> chat.getRecipient().equals(delete));
if (context.getSceneContext().getController() instanceof ChatScene)
((ChatScene) context.getSceneContext().getController()).resetState();
logger.log(Level.INFO, "A contact with all his messages was deleted.");
});
}
}
/**
* Deletes anything pointing to this user, independent of client or server. Will do nothing if
* the client is currently offline.
*
* @since Envoy Client v0.3-beta
*/
public static void deleteAccount() {
// Show the first wall of defense, if not disabled by the user
final var outerAlert = new Alert(AlertType.CONFIRMATION);
outerAlert.setContentText(
"Are you sure you want to delete your account entirely? This action can seriously not be undone.");
outerAlert.setTitle("Delete Account?");
AlertHelper.confirmAction(outerAlert, () -> {
// Show the final wall of defense in every case
final var lastAlert = new Alert(AlertType.WARNING,
"Do you REALLY want to delete your account? Last Warning. Proceed?",
ButtonType.CANCEL, ButtonType.OK);
lastAlert.setTitle("Delete Account?");
lastAlert.showAndWait().filter(ButtonType.OK::equals).ifPresent(b2 -> {
// Delete the account
// TODO: Notify server of account deletion
context.getLocalDB().delete();
logger.log(Level.INFO, "The user just deleted his account. Goodbye.");
ShutdownHelper.exit(true);
});
});
}
}

View File

@ -1,5 +1,6 @@
/** /**
* This module contains all classes defining the client application of the Envoy project. * This module contains all classes defining the client application of the Envoy
* project.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister
@ -16,13 +17,11 @@ module envoy.client {
requires javafx.fxml; requires javafx.fxml;
requires javafx.base; requires javafx.base;
requires javafx.graphics; requires javafx.graphics;
requires dev.kske.eventbus.core;
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus.core; opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus.core; opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
opens envoy.client.ui.chatscene to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus.core;
opens envoy.client.ui.control to javafx.graphics, javafx.fxml; opens envoy.client.ui.control to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util; opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus.core; opens envoy.client.net to dev.kske.eventbus;
opens envoy.client.data to dev.kske.eventbus.core; opens envoy.client.data to dev.kske.eventbus;
} }

View File

@ -1,5 +1,6 @@
fileLevelBarrier=OFF
consoleLevelBarrier=FINER
server=localhost server=localhost
port=8080 port=8080
localDB=localDB
localDBSaveInterval=2 localDBSaveInterval=2
consoleLevelBarrier=FINER
fileLevelBarrier=OFF

View File

@ -4,10 +4,8 @@
.context-menu, .context-menu > * { .context-menu, .context-menu > * {
-fx-background-radius: 15.0px; -fx-background-radius: 15.0px;
} /*TODO: solution below does not work */
-fx-background-color: transparent;
.list-cell:selected, .menu-item:hover, .combo-box-popup .list-view .list-cell:selected {
-fx-background-color: #454c4f;
} }
#text-enter-container, #contact-search-enter-container { #text-enter-container, #contact-search-enter-container {
@ -41,7 +39,7 @@
-fx-scale-y: 1.05; -fx-scale-y: 1.05;
} }
.label, .quick-select { .label {
-fx-background-color: transparent; -fx-background-color: transparent;
} }
@ -70,17 +68,6 @@
-fx-text-fill: gray; -fx-text-fill: gray;
} }
.danger-button {
-fx-background-color: red;
-fx-text-fill: white;
-fx-background-radius: 0.2em;
}
.danger-button:hover {
-fx-scale-x: 1.05;
-fx-scale-y: 1.05;
}
.received-message { .received-message {
-fx-alignment: center-left; -fx-alignment: center-left;
-fx-background-radius: 1.3em; -fx-background-radius: 1.3em;
@ -151,24 +138,7 @@
.tab-pane { .tab-pane {
-fx-tab-max-height: 0.0 ; -fx-tab-max-height: 0.0 ;
} }
.tab-pane .tab-header-area { .tab-pane .tab-header-area {
visibility: hidden ; visibility: hidden ;
-fx-padding: -20.0 0.0 0.0 0.0; -fx-padding: -20.0 0.0 0.0 0.0;
} }
.disabled-chat {
-fx-background-color: #0000FF;
}
#quick-select-list .scroll-bar:horizontal{
-fx-pref-height: 0.0;
-fx-max-height: 0.0;
-fx-min-height: 0.0;
}
#quick-select-list .scroll-bar:vertical{
-fx-pref-width: 0.0;
-fx-max-width: 0.0;
-fx-min-width: 0.0;
}

View File

@ -18,10 +18,14 @@
-fx-background-color: lightgray; -fx-background-color: lightgray;
} }
#message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item, .combo-box-popup .list-view .list-cell, #quick-select-list { #message-list, .text-field, .password-field, .tooltip, .pane, .pane .content, .vbox, .titled-pane > .title, .titled-pane > *.content, .context-menu, .menu-item {
-fx-background-color: #222222; -fx-background-color: #222222;
} }
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
-fx-background-color: #690099;
}
.received-message { .received-message {
-fx-background-color: gray; -fx-background-color: gray;
} }
@ -69,7 +73,7 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow, .list-cell { .scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow {
-fx-background-color: transparent; -fx-background-color: transparent;
} }
@ -83,8 +87,3 @@
-fx-text-fill: white; -fx-text-fill: white;
-fx-background-color: transparent; -fx-background-color: transparent;
} }
#remove-button {
-fx-background-color: red;
-fx-background-radius: 1em;
}

View File

@ -18,6 +18,10 @@
-fx-background-color: #E3E3E3; -fx-background-color: #E3E3E3;
} }
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
-fx-background-color: #805959;
}
.received-message { .received-message {
-fx-background-color: lightgray; -fx-background-color: lightgray;
} }
@ -30,10 +34,6 @@
-fx-background-color: black; -fx-background-color: black;
} }
.tooltip {
-fx-text-fill: black;
}
#login-input-field { #login-input-field {
-fx-border-color: black; -fx-border-color: black;
} }

View File

@ -20,18 +20,13 @@
<?import javafx.scene.layout.RowConstraints?> <?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<?import javafx.stage.Screen?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" <GridPane fx:id="scene" maxHeight="-Infinity"
minHeight="400.0" minWidth="500.0" maxWidth="-Infinity" minHeight="400.0" minWidth="500.0"
prefWidth="${screen.visualBounds.width}" prefHeight="1152.0" prefWidth="2042.0"
prefHeight="${screen.visualBounds.height}"
xmlns="http://javafx.com/javafx/11.0.1" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.ChatScene"> fx:controller="envoy.client.ui.controller.ChatScene">
<fx:define>
<Screen fx:factory="getPrimary" fx:id="screen" />
</fx:define>
<columnConstraints> <columnConstraints>
<ColumnConstraints hgrow="NEVER" <ColumnConstraints hgrow="NEVER"
maxWidth="327.99997965494794" minWidth="-Infinity" prefWidth="317.0" /> maxWidth="327.99997965494794" minWidth="-Infinity" prefWidth="317.0" />
@ -57,7 +52,8 @@
<content> <content>
<AnchorPane minHeight="0.0" minWidth="0.0"> <AnchorPane minHeight="0.0" minWidth="0.0">
<children> <children>
<VBox prefHeight="3000.0" prefWidth="316.0"> <VBox fx:id="contactOperations" prefHeight="3000.0"
prefWidth="316.0">
<children> <children>
<VBox id="search-panel" maxHeight="-Infinity" <VBox id="search-panel" maxHeight="-Infinity"
minHeight="-Infinity" prefHeight="80.0" prefWidth="316.0"> minHeight="-Infinity" prefHeight="80.0" prefWidth="316.0">
@ -125,6 +121,15 @@
<ListView id="chat-list" fx:id="chatList" <ListView id="chat-list" fx:id="chatList"
focusTraversable="false" onMouseClicked="#chatListClicked" focusTraversable="false" onMouseClicked="#chatListClicked"
prefWidth="316.0" VBox.vgrow="ALWAYS"> prefWidth="316.0" VBox.vgrow="ALWAYS">
<contextMenu>
<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
<items>
<MenuItem fx:id="deleteContactMenuItem"
mnemonicParsing="false" onAction="#deleteContact"
text="Delete" />
</items>
</ContextMenu>
</contextMenu>
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="2.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
</padding> </padding>
@ -145,27 +150,50 @@
<Insets right="1.0" /> <Insets right="1.0" />
</GridPane.margin> </GridPane.margin>
</TabPane> </TabPane>
<HBox id="top-bar" alignment="CENTER_LEFT" prefHeight="100.0" <HBox id="top-bar" alignment="CENTER_LEFT" prefHeight="100.0">
fx:id="ownContactControl">
<children> <children>
<ImageView id="profile-pic" fx:id="clientProfilePic" <ImageView id="profile-pic" fx:id="clientProfilePic"
fitHeight="43.0" fitWidth="43.0" pickOnBounds="true" fitHeight="43.0" fitWidth="43.0" pickOnBounds="true"
preserveRatio="true"> preserveRatio="true">
<HBox.margin> <HBox.margin>
<Insets left="15.0" top="5.0" right="10.0" /> <Insets left="15.0" top="5.0" />
</HBox.margin> </HBox.margin>
</ImageView> </ImageView>
<Region id="transparent-background" HBox.hgrow="ALWAYS" /> <Label id="transparent-background" fx:id="contactLabel"
<Button fx:id="settingsButton" mnemonicParsing="false" prefHeight="27.0" prefWidth="134.0">
onAction="#settingsButtonClicked" prefHeight="30.0"
prefWidth="30.0" alignment="CENTER_RIGHT">
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
<font>
<Font size="18.0" />
</font>
<HBox.margin> <HBox.margin>
<Insets bottom="35.0" left="5.0" top="35.0" right="10.0" /> <Insets left="10.0" top="5.0" />
</HBox.margin> </HBox.margin>
</Button> </Label>
<Region id="transparent-background" prefHeight="77.0"
prefWidth="115.0" />
<VBox id="transparent-background" alignment="CENTER_RIGHT"
prefHeight="200.0" prefWidth="100.0" spacing="5.0">
<children>
<Button fx:id="settingsButton" mnemonicParsing="true"
onAction="#settingsButtonClicked" prefHeight="30.0"
prefWidth="30.0" text="">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<VBox.margin>
<Insets />
</VBox.margin>
</Button>
</children>
<HBox.margin>
<Insets right="10.0" />
</HBox.margin>
<opaqueInsets>
<Insets />
</opaqueInsets>
</VBox>
</children> </children>
<GridPane.margin> <GridPane.margin>
<Insets bottom="1.0" right="1.0" /> <Insets bottom="1.0" right="1.0" />

View File

@ -64,7 +64,6 @@
<Insets bottom="5.0" top="5" /> <Insets bottom="5.0" top="5" />
</VBox.margin> </VBox.margin>
</Label> </Label>
<ListView fx:id="quickSelectList" id="quick-select-list" orientation="HORIZONTAL" prefHeight="60.0" />
<ListView id="chat-list" fx:id="userList" focusTraversable="false" onMouseClicked="#userListClicked" prefWidth="316.0" VBox.vgrow="ALWAYS"> <ListView id="chat-list" fx:id="userList" focusTraversable="false" onMouseClicked="#userListClicked" prefWidth="316.0" VBox.vgrow="ALWAYS">
<contextMenu> <contextMenu>
<ContextMenu anchorLocation="CONTENT_TOP_LEFT" /> <ContextMenu anchorLocation="CONTENT_TOP_LEFT" />

View File

@ -7,16 +7,11 @@
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox alignment="TOP_RIGHT" maxHeight="-Infinity" minHeight="400.0" <VBox alignment="TOP_RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="envoy.client.ui.controller.SettingsScene">
minWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.SettingsScene">
<children> <children>
<HBox prefHeight="389.0" prefWidth="600.0"> <HBox prefHeight="389.0" prefWidth="600.0">
<children> <children>
<ListView id="message-list" fx:id="settingsList" <ListView id="message-list" fx:id="settingsList" onMouseClicked="#settingsListClicked" prefHeight="200.0" prefWidth="200.0">
onMouseClicked="#settingsListClicked" prefHeight="200.0"
prefWidth="200.0">
<opaqueInsets> <opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets> </opaqueInsets>
@ -27,8 +22,7 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</ListView> </ListView>
<TitledPane fx:id="titledPane" collapsible="false" <TitledPane fx:id="titledPane" collapsible="false" prefHeight="400.0" prefWidth="400.0">
prefHeight="400.0" prefWidth="400.0">
<HBox.margin> <HBox.margin>
<Insets bottom="10.0" left="5.0" right="10.0" top="10.0" /> <Insets bottom="10.0" left="5.0" right="10.0" top="10.0" />
</HBox.margin> </HBox.margin>
@ -38,8 +32,7 @@
</TitledPane> </TitledPane>
</children> </children>
</HBox> </HBox>
<Button defaultButton="true" mnemonicParsing="true" <Button defaultButton="true" mnemonicParsing="true" onMouseClicked="#backButtonClicked" text="_Back">
onMouseClicked="#backButtonClicked" text="_Back">
<opaqueInsets> <opaqueInsets>
<Insets /> <Insets />
</opaqueInsets> </opaqueInsets>

View File

@ -128,4 +128,364 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=11 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 org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter

File diff suppressed because one or more lines are too long

View File

@ -12,11 +12,18 @@
<version>0.2-beta</version> <version>0.2-beta</version>
</parent> </parent>
<repositories>
<repository>
<id>kske-repo</id>
<url>https://kske.dev/maven-repo</url>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>dev.kske</groupId> <groupId>dev.kske</groupId>
<artifactId>event-bus-core</artifactId> <artifactId>event-bus</artifactId>
<version>1.0.0</version> <version>0.0.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View File

@ -1,11 +1,10 @@
package envoy.data; package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
/** /**
* This interface should be used for any type supposed to be a {@link Message} attachment (i.e. * This interface should be used for any type supposed to be a {@link Message}
* images or sound). * attachment (i.e. images or sound).
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -64,9 +63,9 @@ public final class Attachment implements Serializable {
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Attachment(byte[] data, String name, AttachmentType type) { public Attachment(byte[] data, String name, AttachmentType type) {
this.data = Objects.requireNonNull(data); this.data = data;
this.name = Objects.requireNonNull(name); this.name = name;
this.type = Objects.requireNonNull(type); this.type = type;
} }
/** /**

View File

@ -9,13 +9,15 @@ import java.util.stream.Collectors;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Manages all application settings that are set during application startup by either loading them * Manages all application settings that are set during application startup by
* from the {@link Properties} file (default values) {@code client.properties} or parsing them from * either loading them from the {@link Properties} file (default values)
* the command line arguments of the application. * {@code client.properties} or parsing them from the command line arguments of
* the application.
* <p> * <p>
* All items inside the {@code Config} are supposed to either be supplied over default value or over * All items inside the {@code Config} are supposed to either be supplied over
* command line argument. Developers that fail to provide default values will be greeted with an * default value or over command line argument. Developers that fail to provide
* error message the next time they try to start Envoy... * default values will be greeted with an error message the next time they try
* to start Envoy...
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -42,14 +44,15 @@ public class Config {
*/ */
private void load(Properties properties) { private void load(Properties properties) {
items.entrySet().stream().filter(e -> properties.containsKey(e.getKey())) items.entrySet().stream().filter(e -> properties.containsKey(e.getKey()))
.forEach(e -> e.getValue().parse(properties.getProperty(e.getKey()))); .forEach(e -> e.getValue().parse(properties.getProperty(e.getKey())));
} }
/** /**
* Parses config items from an array of command line arguments. * Parses config items from an array of command line arguments.
* *
* @param args the command line arguments to parse * @param args the command line arguments to parse
* @throws IllegalStateException if a malformed command line argument has been supplied * @throws IllegalStateException if a malformed command line argument has been
* supplied
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
private void load(String[] args) { private void load(String[] args) {
@ -58,7 +61,7 @@ public class Config {
if (args[i].startsWith("--")) { if (args[i].startsWith("--")) {
if (args[i].length() == 2) if (args[i].length() == 2)
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
final String commandLong = args[i].substring(2); final String commandLong = args[i].substring(2);
if (item.getCommandLong().equals(commandLong)) { if (item.getCommandLong().equals(commandLong)) {
item.parse(args[++i]); item.parse(args[++i]);
@ -67,7 +70,7 @@ public class Config {
} else if (args[i].startsWith("-")) { } else if (args[i].startsWith("-")) {
if (args[i].length() == 1) if (args[i].length() == 1)
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
final String commandShort = args[i].substring(1); final String commandShort = args[i].substring(1);
if (item.getCommandShort().equals(commandShort)) { if (item.getCommandShort().equals(commandShort)) {
item.parse(args[++i]); item.parse(args[++i]);
@ -75,36 +78,35 @@ public class Config {
} }
} else } else
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
} }
/** /**
* Supplies default values from the given .properties file and parses the configuration from an * Supplies default values from the given .properties file and parses the
* array of command line arguments. * configuration from an array of command line arguments.
* *
* @param declaringClass the class calling this method * @param declaringClass the class calling this method
* @param propertiesFilePath the path to where the .properties file can be found - will be only * @param propertiesFilePath the path to where the .properties file can be found
* the file name if it is located directly inside the * - will be only the file name if it is located
* {@code src/main/resources} folder * directly inside the {@code src/main/resources}
* folder
* @param args the command line arguments to parse * @param args the command line arguments to parse
* @throws IllegalStateException if this method is getting called again or if a malformed * @throws IllegalStateException if this method is getting called again or if a
* command line argument has been supplied * malformed command line argument has been
* supplied
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void loadAll(Class<?> declaringClass, String propertiesFilePath, String[] args) { public void loadAll(Class<?> declaringClass, String propertiesFilePath, String[] args) {
if (modificationDisabled) if (modificationDisabled)
throw new IllegalStateException( throw new IllegalStateException("Cannot change config after isInitialized has been called");
"Cannot change config after isInitialized has been called");
// Load the defaults from the given .properties file first // Load the defaults from the given .properties file first
final var properties = new Properties(); final var properties = new Properties();
try { try {
properties properties.load(declaringClass.getClassLoader().getResourceAsStream(propertiesFilePath));
.load(declaringClass.getClassLoader().getResourceAsStream(propertiesFilePath));
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(Config.class).log(Level.SEVERE, EnvoyLog.getLogger(Config.class).log(Level.SEVERE, "An error occurred when reading in the configuration: ",
"An error occurred when reading in the configuration: ", e);
e);
} }
load(properties); load(properties);
@ -120,13 +122,13 @@ public class Config {
} }
/** /**
* @throws IllegalStateException if a {@link ConfigItem} has not been initialized * @throws IllegalStateException if a {@link ConfigItem} has not been
* initialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
private void isInitialized() { private void isInitialized() {
String uninitialized = items.values().stream().filter(c -> c.get() == null) String uninitialized = items.values().stream().filter(c -> c.get() == null).map(ConfigItem::getCommandLong).collect(Collectors.joining(", "));
.map(ConfigItem::getCommandLong).collect(Collectors.joining(", ")); if(!uninitialized.isEmpty())
if (!uninitialized.isEmpty())
throw new IllegalStateException("Config items uninitialized: " + uninitialized); throw new IllegalStateException("Config items uninitialized: " + uninitialized);
} }
@ -146,11 +148,11 @@ public class Config {
* @param <T> the type of the {@link ConfigItem} * @param <T> the type of the {@link ConfigItem}
* @param commandName the key for this config item as well as its long name * @param commandName the key for this config item as well as its long name
* @param commandShort the abbreviation of this config item * @param commandShort the abbreviation of this config item
* @param parseFunction the {@code Function<String, T>} that parses the value from a string * @param parseFunction the {@code Function<String, T>} that parses the value
* from a string
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
protected <T> void put(String commandName, String commandShort, protected <T> void put(String commandName, String commandShort, Function<String, T> parseFunction) {
Function<String, T> parseFunction) {
items.put(commandName, new ConfigItem<>(commandName, commandShort, parseFunction)); items.put(commandName, new ConfigItem<>(commandName, commandShort, parseFunction));
} }
@ -158,13 +160,17 @@ public class Config {
* @return the directory in which all local files are saves * @return the directory in which all local files are saves
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); } public File getHomeDirectory() {
return (File) items.get("homeDirectory").get();
}
/** /**
* @return the minimal {@link Level} to log inside the log file * @return the minimal {@link Level} to log inside the log file
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); } public Level getFileLevelBarrier() {
return (Level) items.get("fileLevelBarrier").get();
}
/** /**
* @return the minimal {@link Level} to log inside the console * @return the minimal {@link Level} to log inside the console

View File

@ -3,8 +3,8 @@ package envoy.data;
import java.util.function.Function; import java.util.function.Function;
/** /**
* Contains a single {@link Config} value as well as the corresponding command line arguments and * Contains a single {@link Config} value as well as the corresponding command
* its default value. * line arguments and its default value.
* <p> * <p>
* All {@code ConfigItem}s are automatically mandatory. * All {@code ConfigItem}s are automatically mandatory.
* *
@ -24,7 +24,8 @@ public final class ConfigItem<T> {
* *
* @param commandLong the long command line argument to set this value * @param commandLong the long command line argument to set this value
* @param commandShort the short command line argument to set this value * @param commandShort the short command line argument to set this value
* @param parseFunction the {@code Function<String, T>} that parses the value from a string * @param parseFunction the {@code Function<String, T>} that parses the value
* from a string
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction) { public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction) {
@ -39,18 +40,18 @@ public final class ConfigItem<T> {
* @param input the string to parse from * @param input the string to parse from
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void parse(String input) { public void parse(String input) { value = parseFunction.apply(input); }
value = parseFunction.apply(input);
}
/** /**
* @return The long command line argument to set the value of this {@link ConfigItem} * @return The long command line argument to set the value of this
* {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public String getCommandLong() { return commandLong; } public String getCommandLong() { return commandLong; }
/** /**
* @return The short command line argument to set the value of this {@link ConfigItem} * @return The short command line argument to set the value of this
* {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public String getCommandShort() { return commandShort; } public String getCommandShort() { return commandShort; }
@ -59,9 +60,7 @@ public final class ConfigItem<T> {
* @return the value of this {@link ConfigItem} * @return the value of this {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public T get() { public T get() { return value; }
return value;
}
/** /**
* @param value the value to set * @param value the value to set

View File

@ -29,8 +29,8 @@ public abstract class Contact implements Serializable {
*/ */
public Contact(long id, String name, Set<? extends Contact> contacts) { public Contact(long id, String name, Set<? extends Contact> contacts) {
this.id = id; this.id = id;
this.name = Objects.requireNonNull(name); this.name = name;
this.contacts = contacts == null ? new HashSet<>() : contacts; this.contacts = contacts;
} }
/** /**
@ -57,23 +57,19 @@ public abstract class Contact implements Serializable {
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
@Override @Override
public final int hashCode() { public final int hashCode() { return Objects.hash(id); }
return Objects.hash(id);
}
/** /**
* Tests equality to another object. If that object is a contact as well, equality is determined * Tests equality to another object. If that object is a contact as well,
* by the ID. * equality is determined by the ID.
* *
* @param obj the object to test for equality to this contact * @param obj the object to test for equality to this contact
* @return {code true} if both objects are contacts and have identical IDs * @return {code true} if both objects are contacts and have identical IDs
*/ */
@Override @Override
public final boolean equals(Object obj) { public final boolean equals(Object obj) {
if (this == obj) if (this == obj) return true;
return true; if (!(obj instanceof Contact)) return false;
if (!(obj instanceof Contact))
return false;
return id == ((Contact) obj).id; return id == ((Contact) obj).id;
} }

View File

@ -18,9 +18,7 @@ public final class Group extends Contact {
* @param name the name of this group * @param name the name of this group
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Group(long id, String name) { public Group(long id, String name) { this(id, name, new HashSet<User>()); }
this(id, name, new HashSet<User>());
}
/** /**
* Creates an instance of a {@link Group}. * Creates an instance of a {@link Group}.
@ -30,14 +28,10 @@ public final class Group extends Contact {
* @param members all members that should be preinitialized * @param members all members that should be preinitialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Group(long id, String name, Set<User> members) { public Group(long id, String name, Set<User> members) { super(id, name, members); }
super(id, name, members);
}
@Override @Override
public String toString() { public String toString() { return String.format("Group[id=%d,name=%s,%d member(s)]", id, name, contacts.size()); }
return String.format("Group[id=%d,name=%s,%d member(s)]", id, name, contacts.size());
}
private void readObject(ObjectInputStream inputStream) throws Exception { private void readObject(ObjectInputStream inputStream) throws Exception {
inputStream.defaultReadObject(); inputStream.defaultReadObject();

View File

@ -14,9 +14,11 @@ public final class GroupMessage extends Message {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Initializes a {@link GroupMessage} with values for all of its properties. The use of this * Initializes a {@link GroupMessage} with values for all of its properties. The
* constructor is only intended for the {@link MessageBuilder} class, as this class provides * use
* {@code null} checks and default values for all properties. * of this constructor is only intended for the {@link MessageBuilder} class, as
* this class provides {@code null} checks and default values for all
* properties.
* *
* @param id unique ID * @param id unique ID
* @param senderID the ID of the user who sends the message * @param senderID the ID of the user who sends the message
@ -26,20 +28,17 @@ public final class GroupMessage extends Message {
* @param readDate the read date of the message * @param readDate the read date of the message
* @param text the text content of the message * @param text the text content of the message
* @param attachment the attachment of the message, if present * @param attachment the attachment of the message, if present
* @param status the current {@link Message.MessageStatus} of the message * @param status the current {@link Message.MessageStatus} of the
* message
* @param forwarded whether this message was forwarded * @param forwarded whether this message was forwarded
* @param memberStatuses a map of all members and their status according to this * @param memberStatuses a map of all members and their status according to this
* {@link GroupMessage} * {@link GroupMessage}
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
GroupMessage(long id, long senderID, long groupID, Instant creationDate, Instant receivedDate, GroupMessage(long id, long senderID, long groupID, Instant creationDate, Instant receivedDate, Instant readDate, String text,
Instant readDate, String text, Attachment attachment, MessageStatus status, boolean forwarded, Map<Long, MessageStatus> memberStatuses) {
Attachment attachment, MessageStatus status, boolean forwarded, super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status, forwarded);
Map<Long, MessageStatus> memberStatuses) { this.memberStatuses = memberStatuses;
super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status,
forwarded);
this.memberStatuses =
memberStatuses == null ? new HashMap<>() : memberStatuses;
} }
/** /**

View File

@ -2,13 +2,15 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import dev.kske.eventbus.IEvent;
/** /**
* Generates increasing IDs between two numbers. * Generates increasing IDs between two numbers.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public final class IDGenerator implements Serializable { public final class IDGenerator implements IEvent, Serializable {
private final long end; private final long end;
private long current; private long current;
@ -28,25 +30,20 @@ public final class IDGenerator implements Serializable {
} }
@Override @Override
public String toString() { public String toString() { return String.format("IDGenerator[current=%d,end=%d]", current, end); }
return String.format("IDGenerator[current=%d,end=%d]", current, end);
}
/** /**
* @return {@code true} if there are unused IDs remaining * @return {@code true} if there are unused IDs remaining
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public boolean hasNext() { public boolean hasNext() { return current < end; }
return current < end;
}
/** /**
* @return the next ID * @return the next ID
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public long next() { public long next() {
if (!hasNext()) if (!hasNext()) throw new IllegalStateException("All IDs have been used");
throw new IllegalStateException("All IDs have been used");
return current++; return current++;
} }
} }

View File

@ -2,12 +2,13 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
/** /**
* Contains a {@link User}'s login / registration information as well as the client version. * Contains a {@link User}'s login / registration information as well as the
* client version.
* <p> * <p>
* If the authentication is performed with a token, the token is stored instead of the password. * If the authentication is performed with a token, the token is stored instead
* of the password.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
@ -20,15 +21,15 @@ public final class LoginCredentials implements Serializable {
private static final long serialVersionUID = 4; private static final long serialVersionUID = 4;
private LoginCredentials(String identifier, String password, boolean registration, private LoginCredentials(String identifier, String password, boolean registration, boolean token, boolean requestToken, String clientVersion,
boolean token, boolean requestToken, String clientVersion, Instant lastSync) { Instant lastSync) {
this.identifier = Objects.requireNonNull(identifier); this.identifier = identifier;
this.password = Objects.requireNonNull(password); this.password = password;
this.registration = registration; this.registration = registration;
this.token = token; this.token = token;
this.requestToken = requestToken; this.requestToken = requestToken;
this.clientVersion = Objects.requireNonNull(clientVersion); this.clientVersion = clientVersion;
this.lastSync = lastSync == null ? Instant.EPOCH : lastSync; this.lastSync = lastSync;
} }
/** /**
@ -42,10 +43,8 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials login(String identifier, String password, boolean requestToken, public static LoginCredentials login(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) {
String clientVersion, Instant lastSync) { return new LoginCredentials(identifier, password, false, false, requestToken, clientVersion, lastSync);
return new LoginCredentials(identifier, password, false, false, requestToken, clientVersion,
lastSync);
} }
/** /**
@ -58,8 +57,7 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials loginWithToken(String identifier, String token, public static LoginCredentials loginWithToken(String identifier, String token, String clientVersion, Instant lastSync) {
String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, token, false, true, false, clientVersion, lastSync); return new LoginCredentials(identifier, token, false, true, false, clientVersion, lastSync);
} }
@ -74,23 +72,19 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials registration(String identifier, String password, public static LoginCredentials registration(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) {
boolean requestToken, return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion, lastSync);
String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion,
lastSync);
} }
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format("LoginCredentials[identifier=%s,registration=%b,token=%b,requestToken=%b,clientVersion=%s,lastSync=%s]",
"LoginCredentials[identifier=%s,registration=%b,token=%b,requestToken=%b,clientVersion=%s,lastSync=%s]", identifier,
identifier, registration,
registration, token,
token, requestToken,
requestToken, clientVersion,
clientVersion, lastSync);
lastSync);
} }
/** /**
@ -106,27 +100,24 @@ public final class LoginCredentials implements Serializable {
public String getPassword() { return password; } public String getPassword() { return password; }
/** /**
* @return {@code true} if these credentials are used for user registration instead of user * @return {@code true} if these credentials are used for user registration
* login * instead of user login
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public boolean isRegistration() { return registration; } public boolean isRegistration() { return registration; }
/** /**
* @return {@code true} if these credentials use an authentication token instead of a password * @return {@code true} if these credentials use an authentication token instead
* of a password
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public boolean usesToken() { public boolean usesToken() { return token; }
return token;
}
/** /**
* @return {@code true} if the server should generate a new authentication token * @return {@code true} if the server should generate a new authentication token
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public boolean requestToken() { public boolean requestToken() { return requestToken; }
return requestToken;
}
/** /**
* @return the version of the client sending these credentials * @return the version of the client sending these credentials

View File

@ -2,17 +2,19 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import dev.kske.eventbus.IEvent;
/** /**
* Represents a unique message with a unique, numeric ID. Further metadata includes the sender and * Represents a unique message with a unique, numeric ID. Further metadata
* recipient {@link User}s, as well as the creation date and the current {@link MessageStatus}.<br> * includes the sender and recipient {@link User}s, as well as the creation
* date and the current {@link MessageStatus}.<br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public class Message implements Serializable { public class Message implements Serializable, IEvent {
/** /**
* This enumeration defines all possible statuses a {link Message} can have. * This enumeration defines all possible statuses a {link Message} can have.
@ -54,9 +56,10 @@ public class Message implements Serializable {
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**
* Initializes a {@link Message} with values for all of its properties. The use of this * Initializes a {@link Message} with values for all of its properties. The use
* constructor is only intended for the {@link MessageBuilder} class, as this class provides * of this constructor is only intended for the {@link MessageBuilder} class, as
* {@code null} checks and default values for all properties. * this class provides {@code null} checks and default values for all
* properties.
* *
* @param id unique ID * @param id unique ID
* @param senderID the ID of the user who sends the message * @param senderID the ID of the user who sends the message
@ -70,18 +73,17 @@ public class Message implements Serializable {
* @param forwarded whether this message was forwarded * @param forwarded whether this message was forwarded
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
Message(long id, long senderID, long recipientID, Instant creationDate, Instant receivedDate, Message(long id, long senderID, long recipientID, Instant creationDate, Instant receivedDate, Instant readDate, String text,
Instant readDate, String text, Attachment attachment, MessageStatus status, boolean forwarded) {
Attachment attachment, MessageStatus status, boolean forwarded) {
this.id = id; this.id = id;
this.senderID = senderID; this.senderID = senderID;
this.recipientID = recipientID; this.recipientID = recipientID;
this.creationDate = creationDate; this.creationDate = creationDate;
this.receivedDate = receivedDate; this.receivedDate = receivedDate;
this.readDate = readDate; this.readDate = readDate;
this.text = text == null ? "" : text; this.text = text;
this.attachment = attachment; this.attachment = attachment;
this.status = Objects.requireNonNull(status); this.status = status;
this.forwarded = forwarded; this.forwarded = forwarded;
} }
@ -99,23 +101,21 @@ public class Message implements Serializable {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public void nextStatus() { public void nextStatus() {
if (status == MessageStatus.READ) if (status == MessageStatus.READ) throw new IllegalStateException("Message status READ is already reached");
throw new IllegalStateException("Message status READ is already reached");
status = MessageStatus.values()[status.ordinal() + 1]; status = MessageStatus.values()[status.ordinal() + 1];
} }
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format("Message[id=%d,sender=%s,recipient=%s,date=%s,status=%s,text=%s,forwarded=%b,hasAttachment=%b]",
"Message[id=%d,sender=%s,recipient=%s,date=%s,status=%s,text=%s,forwarded=%b,hasAttachment=%b]", id,
id, senderID,
senderID, recipientID,
recipientID, creationDate,
creationDate, status,
status, text,
text, forwarded,
forwarded, attachment != null);
attachment != null);
} }
/** /**
@ -149,7 +149,8 @@ public class Message implements Serializable {
public Instant getReceivedDate() { return receivedDate; } public Instant getReceivedDate() { return receivedDate; }
/** /**
* @param receivedDate the date at which the message has been received by the sender * @param receivedDate the date at which the message has been received by the
* sender
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public void setReceivedDate(Instant receivedDate) { this.receivedDate = receivedDate; } public void setReceivedDate(Instant receivedDate) { this.receivedDate = receivedDate; }
@ -182,9 +183,7 @@ public class Message implements Serializable {
* @return {@code true} if an attachment is present * @return {@code true} if an attachment is present
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public boolean hasAttachment() { public boolean hasAttachment() { return attachment != null; }
return attachment != null;
}
/** /**
* @return the current status of this message * @return the current status of this message
@ -197,8 +196,7 @@ public class Message implements Serializable {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public void setStatus(MessageStatus status) { public void setStatus(MessageStatus status) {
if (status.ordinal() < this.status.ordinal()) if (status.ordinal() < this.status.ordinal()) throw new IllegalStateException("This message is moving backwards in time");
throw new IllegalStateException("This message is moving backwards in time");
this.status = status; this.status = status;
} }

View File

@ -25,21 +25,20 @@ public final class MessageBuilder {
private boolean forwarded; private boolean forwarded;
/** /**
* Creates an instance of {@link MessageBuilder} with all mandatory values without defaults for * Creates an instance of {@link MessageBuilder} with all mandatory values
* the {@link Message} class. * without defaults for the {@link Message} class.
* *
* @param senderID the ID of the user who sends the {@link Message} * @param senderID the ID of the user who sends the {@link Message}
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
* @param idGenerator the ID generator used to generate a unique {@link Message} id * @param idGenerator the ID generator used to generate a unique {@link Message}
* id
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public MessageBuilder(long senderID, long recipientID, IDGenerator idGenerator) { public MessageBuilder(long senderID, long recipientID, IDGenerator idGenerator) { this(senderID, recipientID, idGenerator.next()); }
this(senderID, recipientID, idGenerator.next());
}
/** /**
* Creates an instance of {@link MessageBuilder} with all mandatory values without defaults for * Creates an instance of {@link MessageBuilder} with all mandatory values
* the {@link Message} class. * without defaults for the {@link Message} class.
* *
* @param senderID the ID of the user who sends the {@link Message} * @param senderID the ID of the user who sends the {@link Message}
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
@ -53,12 +52,14 @@ public final class MessageBuilder {
} }
/** /**
* This constructor transforms a given {@link Message} into a new message for a new receiver. * This constructor transforms a given {@link Message} into a new message for a
* new receiver.
* This makes it especially useful in the case of forwarding messages. * This makes it especially useful in the case of forwarding messages.
* *
* @param msg the message to copy * @param msg the message to copy
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
* @param iDGenerator the ID generator used to generate a unique {@link Message} id * @param iDGenerator the ID generator used to generate a unique {@link Message}
* id
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
public MessageBuilder(Message msg, long recipientID, IDGenerator iDGenerator) { public MessageBuilder(Message msg, long recipientID, IDGenerator iDGenerator) {
@ -71,69 +72,79 @@ public final class MessageBuilder {
} }
/** /**
* Creates an instance of {@link Message} with the previously supplied values. If a mandatory * Creates an instance of {@link Message} with the previously supplied values.
* value is not set, a default value will be used instead:<br> * If a mandatory value is not set, a default value will be used instead:<br>
* <br> * <br>
* {@code date} {@code Instant.now()} and {@code null} for {@code receivedDate} and * {@code date}
* {@code readDate} <br> * {@code Instant.now()} and {@code null} for {@code receivedDate} and
* {@code text} {@code ""} <br> * {@code readDate}
* {@code status} {@code MessageStatus.WAITING} * <br>
* {@code text}
* {@code ""}
* <br>
* {@code status}
* {@code MessageStatus.WAITING}
* *
* @return a new instance of {@link Message} * @return a new instance of {@link Message}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public Message build() { public Message build() {
supplyDefaults(); supplyDefaults();
return new Message(id, senderID, recipientID, creationDate, receivedDate, readDate, text, return new Message(id, senderID, recipientID, creationDate, receivedDate, readDate, text, attachment, status, forwarded);
attachment, status, forwarded);
} }
/** /**
* Creates an instance of {@link GroupMessage} with the previously supplied values. <br> * Creates an instance of {@link GroupMessage} with the previously supplied
* values. <br>
* <b> Sets all member statuses to {@link MessageStatus#WAITING}.</b><br> * <b> Sets all member statuses to {@link MessageStatus#WAITING}.</b><br>
* If a mandatory value is not set, a default value will be used instead:<br> * If a mandatory value is not set, a default value will be used
* instead:<br>
* <br>
* {@code time stamp}
* {@code Instant.now()}
* <br>
* {@code text}
* {@code ""}
* <br> * <br>
* {@code time stamp} {@code Instant.now()} <br>
* {@code text} {@code ""} <br>
* *
* @param group the {@link Group} that is used to fill the map of member statuses * @param group the {@link Group} that is used to fill the map of member
* statuses
* @return a new instance of {@link GroupMessage} * @return a new instance of {@link GroupMessage}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public GroupMessage buildGroupMessage(Group group) { public GroupMessage buildGroupMessage(Group group) {
final var memberStatuses = new HashMap<Long, Message.MessageStatus>(); final var memberStatuses = new HashMap<Long, Message.MessageStatus>();
group.getContacts() group.getContacts().forEach(user -> memberStatuses.put(user.getID(), MessageStatus.WAITING));
.forEach(user -> memberStatuses.put(user.getID(), MessageStatus.WAITING));
return buildGroupMessage(group, memberStatuses); return buildGroupMessage(group, memberStatuses);
} }
/** /**
* Creates an instance of {@link GroupMessage} with the previously supplied values. If a * Creates an instance of {@link GroupMessage} with the previously supplied
* mandatory value is not set, a default value will be used instead:<br> * values. If a mandatory value is not set, a default value will be used
* instead:<br>
* <br> * <br>
* {@code time stamp} {@code Instant.now()} <br> * {@code time stamp}
* {@code text} {@code ""} * {@code Instant.now()}
* <br>
* {@code text}
* {@code ""}
* *
* @param group the {@link Group} that is used to fill the map of member statuses * @param group the {@link Group} that is used to fill the map of
* member statuses
* @param memberStatuses the map of all current statuses * @param memberStatuses the map of all current statuses
* @return a new instance of {@link GroupMessage} * @return a new instance of {@link GroupMessage}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public GroupMessage buildGroupMessage(Group group, Map<Long, MessageStatus> memberStatuses) { public GroupMessage buildGroupMessage(Group group, Map<Long, MessageStatus> memberStatuses) {
if (group == null || memberStatuses == null) if (group == null || memberStatuses == null) throw new NullPointerException();
throw new NullPointerException();
supplyDefaults(); supplyDefaults();
return new GroupMessage(id, senderID, recipientID, creationDate, receivedDate, readDate, return new GroupMessage(id, senderID, recipientID, creationDate, receivedDate, readDate, text, attachment, status, forwarded, memberStatuses);
text, attachment, status, forwarded, memberStatuses);
} }
private void supplyDefaults() { private void supplyDefaults() {
if (creationDate == null) if (creationDate == null) creationDate = Instant.now();
creationDate = Instant.now(); if (text == null) text = "";
if (text == null) if (status == null) status = MessageStatus.WAITING;
text = "";
if (status == null)
status = MessageStatus.WAITING;
} }
/** /**
@ -177,7 +188,8 @@ public final class MessageBuilder {
} }
/** /**
* @param attachment the {@link Attachment} of the {@link Message} to create * @param attachment the {@link Attachment} of the {@link Message} to
* create
* @return this {@link MessageBuilder} * @return this {@link MessageBuilder}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */

View File

@ -4,7 +4,8 @@ import java.io.*;
import java.util.*; import java.util.*;
/** /**
* Represents a unique user with a unique, numeric ID, a name and a current {@link UserStatus}.<br> * Represents a unique user with a unique, numeric ID, a name and a current
* {@link UserStatus}.<br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
@ -33,7 +34,8 @@ public final class User extends Contact {
ONLINE, ONLINE,
/** /**
* select this, if a user is online but unavailable at the moment (sudden interruption) * select this, if a user is online but unavailable at the moment (sudden
* interruption)
*/ */
AWAY, AWAY,
@ -50,7 +52,8 @@ public final class User extends Contact {
/** /**
* Initializes a {@link User}. <br> * Initializes a {@link User}. <br>
* The {@link UserStatus} is set to {@link UserStatus#ONLINE}. No contacts are initialized. * The {@link UserStatus} is set to {@link UserStatus#ONLINE}.
* No contacts are initialized.
* *
* @param id unique ID * @param id unique ID
* @param name user name * @param name user name
@ -86,13 +89,12 @@ public final class User extends Contact {
*/ */
public User(long id, String name, UserStatus status, Set<Contact> contacts) { public User(long id, String name, UserStatus status, Set<Contact> contacts) {
super(id, name, contacts); super(id, name, contacts);
this.status = Objects.requireNonNull(status); this.status = status;
} }
@Override @Override
public String toString() { public String toString() {
return String.format("User[id=%d,name=%s,status=%s", id, name, status) return String.format("User[id=%d,name=%s,status=%s", id, name, status) + (contacts.isEmpty() ? "]" : "," + contacts.size() + " contact(s)]");
+ (contacts.isEmpty() ? "]" : "," + contacts.size() + " contact(s)]");
} }
/** /**
@ -117,18 +119,15 @@ public final class User extends Contact {
private void writeObject(ObjectOutputStream outputStream) throws Exception { private void writeObject(ObjectOutputStream outputStream) throws Exception {
outputStream.defaultWriteObject(); outputStream.defaultWriteObject();
if (serializeContacts) { if (serializeContacts) {
getContacts().stream().filter(User.class::isInstance).map(User.class::cast) getContacts().stream().filter(User.class::isInstance).map(User.class::cast).forEach(user -> user.serializeContacts = false);
.forEach(user -> user.serializeContacts = false);
outputStream.writeObject(getContacts()); outputStream.writeObject(getContacts());
} else } else outputStream.writeObject(new HashSet<>());
outputStream.writeObject(new HashSet<>());
} }
/** /**
* @param serializeContacts whether the contacts of this {@link User} should be serialized * @param serializeContacts whether the contacts of this {@link User} should be
* serialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void serializeContacts(boolean serializeContacts) { public void serializeContacts(boolean serializeContacts) { this.serializeContacts = serializeContacts; }
this.serializeContacts = serializeContacts;
}
} }

View File

@ -1,5 +1,6 @@
/** /**
* This package contains all data objects that are used both by Envoy Client and by Envoy Server. * This package contains all data objects that are used both by Envoy Client and
* by Envoy Server.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer

View File

@ -3,7 +3,8 @@ package envoy.event;
/** /**
* This enum declares all modification possibilities for a given container. * This enum declares all modification possibilities for a given container.
* <p> * <p>
* These can be: {@link ElementOperation#ADD} or {@link ElementOperation#REMOVE}. * These can be: {@link ElementOperation#ADD} or
* {@link ElementOperation#REMOVE}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -11,12 +12,14 @@ package envoy.event;
public enum ElementOperation { public enum ElementOperation {
/** /**
* Select this element, if the given element should be added to the given container. * Select this element, if the given element should be added to the given
* container.
*/ */
ADD, ADD,
/** /**
* Select this element, if the given element should be removed from the given container. * Select this element, if the given element should be removed from the given
* container.
*/ */
REMOVE REMOVE
} }

View File

@ -1,46 +1,33 @@
package envoy.event; package envoy.event;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
import dev.kske.eventbus.IEvent;
/** /**
* This class serves as a convenience base class for all events. It provides a generic value. For * This class serves as a convenience base class for all events. It implements
* events without a value there also is {@link envoy.event.Event.Valueless}. * the {@link IEvent} interface and provides a generic value. For events without
* a value there also is {@link envoy.event.Event.Valueless}.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> the type of the Event * @param <T> the type of the Event
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public abstract class Event<T> implements Serializable { public abstract class Event<T> implements IEvent, Serializable {
protected final T value; protected final T value;
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Event(T value) { protected Event(T value) { this.value = value; }
this(value, false);
}
/**
* This constructor is reserved for {@link Valueless} events. No other event should contain null
* values. Only use if really necessary. Using this constructor with {@code true} implies that
* the user has to manually check if the value of the event is null.
*/
protected Event(T value, boolean canBeNull) {
this.value = canBeNull ? value : Objects.requireNonNull(value);
}
/** /**
* @return the data associated with this event * @return the data associated with this event
*/ */
public T get() { public T get() { return value; }
return value;
}
@Override @Override
public String toString() { public String toString() { return String.format("%s[value=%s]", this.getClass().getSimpleName(), value); }
return String.format("%s[value=%s]", this.getClass().getSimpleName(), value);
}
/** /**
* Serves as a super class for events that do not carry a value. * Serves as a super class for events that do not carry a value.
@ -52,13 +39,9 @@ public abstract class Event<T> implements Serializable {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Valueless() { protected Valueless() { super(null); }
super(null, true);
}
@Override @Override
public String toString() { public String toString() { return this.getClass().getSimpleName(); }
return this.getClass().getSimpleName();
}
} }
} }

View File

@ -17,19 +17,20 @@ public final class GroupCreation extends Event<String> {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/** /**
* @param name the name of this group at creation time * @param value the name of this group at creation time
* @param initialMemberIDs the IDs of all {@link User}s that should be group members from the * @param initialMemberIDs the IDs of all {@link User}s that should be group
* beginning on (excluding the creator of this group) * members from the beginning on (excluding the creator
* of this group)
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public GroupCreation(String name, Set<Long> initialMemberIDs) { public GroupCreation(String value, Set<Long> initialMemberIDs) {
super(name); super(value);
this.initialMemberIDs = initialMemberIDs != null ? initialMemberIDs : new HashSet<>(); this.initialMemberIDs = initialMemberIDs != null ? initialMemberIDs : new HashSet<>();
} }
/** /**
* @return the IDs of all {@link User}s that are members from the beginning (excluding the * @return the IDs of all {@link User}s that are members from the beginning
* creator of this group) * (excluding the creator of this group)
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Set<Long> getInitialMemberIDs() { return initialMemberIDs; } public Set<Long> getInitialMemberIDs() { return initialMemberIDs; }

Some files were not shown because too many files have changed in this diff Show More