diff --git a/.config/detekt/.detekt.yml b/.config/detekt/.detekt.yml index e82d97d21..4a0ea58c2 100644 --- a/.config/detekt/.detekt.yml +++ b/.config/detekt/.detekt.yml @@ -1,65 +1,81 @@ comments: - # License in each file is unnecessary. - AbsentOrWrongFileLicense: - active: false - # Nothing wrong with documenting private methods. - CommentOverPrivateFunction: - active: false - # Nothing wrong with documenting private properties. - CommentOverPrivateProperty: - active: false - # Bugged, see https://github.com/detekt/detekt/issues/4727, fixed in Detekt 1.21.0 - OutdatedDocumentation: - active: false + # [Disagree] License in each file is unnecessary. + AbsentOrWrongFileLicense: + active: false + # [Disagree] Simple private functions should be documented too. + CommentOverPrivateFunction: + active: false + # [Disagree] Simple private properties should be documented too. + CommentOverPrivateProperty: + active: false complexity: - # Static analysis is not good at estimating this. - TooManyFunctions: - thresholdInClasses: 20 - # Acceptable if used sparingly. - LabeledExpression: - active: false + # [Disagree] Acceptable if used sparingly. + LabeledExpression: + active: false + # [False Positive] Solved using resource bundles. All remaining duplicates are resource identifiers. + StringLiteralDuplication: + active: false + # [Exception] Acceptable for helper files. + TooManyFunctions: + excludes: '**/*Helpers.kt' formatting: - # Experimental. Too many false positives. - ArgumentListWrapping: - active: false - # That's part of my code style. - NoConsecutiveBlankLines: - active: false - # Acceptable because of auto-formatting. - MultiLineIfElse: - active: false - -naming: - # Static analysis is not good at estimating this. - FunctionMaxLength: - active: false - # Cannot be suppressed in case of false positives. - MatchingDeclarationName: - active: false + # [Disagree] Acceptable for many short arguments. + ArgumentListWrapping: + active: false + # [Bug] Incorrectly detects violation if semicolon is followed by more than one newline. + EnumWrapping: + active: false + # [Disagree] Multi-line expressions are easier to understand if they start on the next line. + FunctionSignature: + active: false + # [Disagree] Short one-liners are easier to understand. + IfElseWrapping: + active: false + # [Bug] Causes ugly indentation when using named multiline arguments to a function. + MultilineExpressionWrapping: + active: false + # [Disagree] Braces use unnecessary extra space. + MultiLineIfElse: + active: false + # [Disagree] Consecutive blank lines are used consistently to group blocks of code. + NoConsecutiveBlankLines: + active: false + # [Disagree] Required for lists and varargs, but ugly when it is unlikely that the function signature will change. + TrailingCommaOnCallSite: + active: false potential-bugs: - # The alternative is to safe cast and throw an exception, which is equally bad. - UnsafeCast: - active: false + # [Exception] Initialised by scene builder. + LateinitUsage: + ignoreOnClassesPattern: ".*Editor" style: - # Those functions are added as conscious design decisions. - DataClassContainsFunctions: - active: false - # Not if they're settings objects. - DataClassShouldBeImmutable: - active: false - # Not an issue if names are self-explanatory. - DestructuringDeclarationWithTooManyEntries: - active: false - # Such comments are OK. - ForbiddenComment: - active: false - # No braces are easier to read. - MandatoryBracesIfStatements: - active: false - # False positives when there are two newlines after the imports. - SpacingBetweenPackageAndImports: - active: false + # [Disagree] `apply` is confusing to use because of namespace conflicts. + AlsoCouldBeApply: + active: false + # [Disagree] Braces use unnecessary extra space, but consistency helps in legibility. + BracesOnIfStatements: + multiLine: consistent + # [Disagree] Legibility is possible despite consistency because it is wrapped in a multi-line `when` block. + BracesOnWhenStatements: + multiLine: necessary + # [Disagree] Functions on data classes are useful. + DataClassContainsFunctions: + active: false + # [Disagree] Mutable data classes are useful. + DataClassShouldBeImmutable: + active: false + # [Exception] Acceptable in (parameterized) tests. + DestructuringDeclarationWithTooManyEntries: + excludes: '**/test/**' + # [Bug] Fails when used as function expression body. + MultilineRawStringIndentation: + active: false + # [Bug] False positives when there are two newlines after the imports. + SpacingBetweenPackageAndImports: + active: false + # [Bug] Suggests using raw string in constant, but then `trimIndent` is not possible, resulting in weird indenting. + StringShouldBeRawString: + active: false diff --git a/.editorconfig b/.editorconfig index fd1294f9b..c1d462b19 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,6 @@ insert_final_newline = true indent_style = space indent_size = 4 + +[*.yml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index fcaa80568..3af866718 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ -*.dic binary *.sketch binary *.svg binary diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1a3b4e6e5..6d0896530 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,7 +22,7 @@ Pull requests that do not meet the following guidelines are **fine**; these are ### 📚 Documentation Always update related documentation, both code documentation and user instructions such as the [README](../README.md). -Additionally, make sure you update the [change notes](../src/main/resources/META-INF/change-notes.html) if necessary. +Additionally, make sure you update the [change notes](../CHANGELOG.md) if necessary. ### 🧪 Tests If you fixed a bug or added a new feature, make sure you add tests that cover this. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9cccea6cf..f468ab780 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,10 +24,10 @@ assignees: FWDekker **Version information** - - Randomness version [e.g. 2.7.6]: - - IDE version [e.g. IntelliJ Community 2022.3]: - - Operating system [e.g. Windows 11, Ubuntu 22.10, macOS 13.1]: - - Java version [e.g. 11.0.17, 17.0.5]: + - Randomness version [e.g. 2.7.7]: + - IDE version [e.g. IntelliJ Community 2023.1.3]: + - Operating system [e.g. Windows 11, Ubuntu 22.04.2, macOS 13.4.1]: + - Java version [e.g. 17.0.5, 19.0.2]: **Additional context** diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md index ec0c89032..3087a6626 100644 --- a/.github/RELEASE_CHECKLIST.md +++ b/.github/RELEASE_CHECKLIST.md @@ -1,16 +1,32 @@ # Release checklist ## Documentation * Bump the version number according to [Semantic Versioning](https://semver.org/). -* Update `README.md`, `change-notes.html`, and `description.html` if necessary. - * Make sure to preview the change notes in the IDE by loading the plugin. -* Update screenshots in `.github/img/` and on the plugin repository if necessary. - * Distance between bottom of "Refresh" button and top of button bar at bottom is 50 pixels, or the original distance, - whichever is smaller. -* Ensure documentation generates without errors and push documentation to `gh-pages` branch. +* Update [`README.md`](../README.md), [`CHANGELOG.md`](../CHANGELOG.md), and + [`description.html`](../src/main/resources/META-INF/description.html) if necessary. + * Make sure to preview the change notes in the IDE by loading the plugin. + * Make sure the list of acknowledgements is up-to-date. +* Update screenshots and GIFs in `.github/img/` and on the plugin repository if necessary. + * Set the global UI scale to 200% before recording/screenshotting to ensure high-resolution images. + * Use the project in `src/test/resources/screenshots/` to store code snippets in. + Do not store `.idea/`, `.gradle`, and similar build files in this project. + * Hide (inlay) hints and set font size to 20. + * Distance between bottom of "Refresh" button and top of button bar at bottom is 50 pixels, or the original + distance, whichever is smaller. + * On Linux, the screen can be recorded using [peek](https://github.com/phw/peek) or + [SimpleScreenRecorder](https://www.maartenbaert.be/simplescreenrecorder/). + * Reducing GIF size is a difficult process. + The following seems to work fine: + 1. `for f in ./*.webm; do ffmpeg -y -i "$f" -vf "fps=10,scale=768:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "${f%.*}.gif"; done` + 2. Go to [ezgif](https://ezgif.com/optimize) and upload the GIF to reduce in size. + 3. Apply the following optimisations in question; after each result, you can click "Optimize" to apply another filter: + 1. "Color Reduction" to 64 colours + 2. "Optimize Transparency" with 2% fuzz + 3. "Lossy GIF" with compression level 30 +* Ensure documentation generates without errors, and push the documentation to the `gh-pages` branch. ## Verification * Run tests and static analysis one more time. * Try out the plugin yourself and check that old and new features work properly. * Run the plugin verifier. - * Make sure the latest IDE version is in the plugin verifier's configuration. + * Make sure the latest IDE version is in the plugin verifier's configuration. * Ensure settings from the previous version correctly load into the new version. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index d3a7c7b7f..febc65443 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,8 @@ # Security policy ## Supported versions -Only the [latest version](https://github.com/FWDekker/intellij-randomness/releases/latest) is ever supported and supplied with security patches. +Only the [latest version](https://github.com/FWDekker/intellij-randomness/releases/latest) is ever supported and +supplied with security patches. ## Reporting a vulnerability -To report a security vulnerability, send an email to security@fwdekker.com instead of using the issue tracker. +To report a security vulnerability, email `security@fwdekker.com` instead of using the issue tracker. You will be contacted as soon as possible. diff --git a/.github/img/array-insertion-sample.gif b/.github/img/array-insertion-sample.gif new file mode 100644 index 000000000..d717c9b5b Binary files /dev/null and b/.github/img/array-insertion-sample.gif differ diff --git a/.github/img/configuration-sample.gif b/.github/img/configuration-sample.gif new file mode 100644 index 000000000..687e6c71a Binary files /dev/null and b/.github/img/configuration-sample.gif differ diff --git a/.github/img/fast-insertion.png b/.github/img/fast-insertion.png new file mode 100644 index 000000000..c18cc2af3 Binary files /dev/null and b/.github/img/fast-insertion.png differ diff --git a/.github/img/icons.sketch b/.github/img/icons.sketch index d4d6cc278..6cac96c22 100644 Binary files a/.github/img/icons.sketch and b/.github/img/icons.sketch differ diff --git a/.github/img/insertion-sample.gif b/.github/img/insertion-sample.gif new file mode 100644 index 000000000..965c61cc9 Binary files /dev/null and b/.github/img/insertion-sample.gif differ diff --git a/.github/img/live-sample.gif b/.github/img/live-sample.gif deleted file mode 100644 index 1eb848d0f..000000000 Binary files a/.github/img/live-sample.gif and /dev/null differ diff --git a/.github/img/previews.gif b/.github/img/previews.gif new file mode 100644 index 000000000..669dddffc Binary files /dev/null and b/.github/img/previews.gif differ diff --git a/.github/img/shortcuts.png b/.github/img/shortcuts.png new file mode 100644 index 000000000..2010bbcd6 Binary files /dev/null and b/.github/img/shortcuts.png differ diff --git a/.github/img/word-settings.png b/.github/img/word-settings.png deleted file mode 100644 index b877e2f05..000000000 Binary files a/.github/img/word-settings.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..cd344f06a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + paths-ignore: + - 'README.md' + - '.github/**.md' + - '.github/img/**' + pull_request: + paths-ignore: + - 'README.md' + - '.github/**.md' + - '.github/img/**' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + - name: Run checks (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + chmod +x ./gradlew + sudo apt install -y xvfb + xvfb-run --auto-servernum ./gradlew --no-daemon check + ./gradlew --stop + - name: Run checks (Windows) + if: matrix.os == 'windows-latest' + run: | + ./gradlew --no-daemon check + ./gradlew --stop + - uses: codecov/codecov-action@v3 + if: success() && matrix.os == 'ubuntu-latest' + with: + fail_ci_if_error: true + files: build/reports/kover/report.xml diff --git a/.gitignore b/.gitignore index 1c6eefe3e..6f8960ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # Package Files # *.jar *.war +*.nar *.ear *.zip *.tar.gz @@ -21,13 +22,14 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +replay_pid* ## Gradle .gradle -/build/ -/out/ +**/build/ +!src/**/build/ # Ignore Gradle GUI config gradle-app.setting @@ -35,8 +37,14 @@ gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + # Cache of project .gradletasknamecache -# Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -gradle/wrapper/gradle-wrapper.properties +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5d579ecab..000000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: java -os: ["linux"] -dist: trusty -jdk: oraclejdk11 - -# Display Gradle version instead of letting Travis execute './gradlew assemble' by default -install: - - ./gradlew -version - -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - - sleep 3 # give xvfb some time to start - -script: - - ./gradlew check - - ./gradlew jacocoTestReport - -after_success: - - bash <(curl -s https://codecov.io/bash) - -# Caching -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - -notifications: - email: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..2d5f2123a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,452 @@ +# Changelog +## 3.0.0 -- 2023-11-17 +This release brings a major overhaul of how data is generated, allowing you to create your own data types such as IP +addresses or entire JSON objects. +At the same time, it remains just as easy to generate plain numbers. +Check the plugin description for more details and animated usage examples. + +### Added +* Each time you insert an array your demands will be slightly different, so when you insert an array a dialog is shown + in which you can quickly vary the array's settings. +* In addition to a list of standard separators, you can now also choose your own separator for all data types, including + for arrays. +* You can automatically pad (or truncate) integers to a specific length. +* A notification is shown after upgrading to Randomness 3 to inform the user of incompatibilities with settings from + older versions. +* Future backwards compatibility ensures that your settings can always be imported into future versions. + +### Changed +* Randomness now uses templates to generate data. + A template consists of a list of "primitive" data types which are concatenated together. + Data types include the old data types (integer, decimal, string, word, UUID), but also the new date-time and the + template reference. +* Strings no longer consist of customisable symbol sets, but are specified using a regex. +* Words are no longer read from dictionary files, but are stored directly in the settings window. To reuse words in + multiple templates, consider using template references. +* The preview pane now looks more beautiful :-) +* Icons for templates and data types are dynamically generated based on the involved data types. +* Invalid settings are now easier to correct with more specific error messages. +* All strings have been internationalised, to make future translation easier. +* Changelogs are now kept in [keep a changelog](https://keepachangelog.com/en/1.0.0/) style. + +### Deprecated +* Minimum IDE version has been increased to 2022.3. + +### Fixed +* The settings-only popup is now also shown when editing a read-only file. + + +## 2.7.7 -- 2023-06-30 +### Breaking changes +Minimum IDE version has been increased to 2022.2. + +### Fixes +Resolve compatibility issues with upcoming IDE versions. +([#459](https://github.com/FWDekker/intellij-randomness/issues/459)) +([#460](https://github.com/FWDekker/intellij-randomness/issues/460)) + + +## 2.7.6 -- 2022-12-14 +### Breaking changes +Minimum IDE version has been increased to 2022.1. + +### Fixes +Resolve compatibility issues with upcoming IDE versions. + + +## 2.7.5 -- 2022-05-15 +### Breaking changes +Minimum IDE version has been increased to 2021.2. + +### Fixes +* Add prefix and suffix options for strings. +* Ensure consistent capitalisation between previews. + + +## 2.7.4 -- 2021-12-10 +### Fixes +Custom action shortcuts should use current configuration. +([#423](https://github.com/FWDekker/intellij-randomness/issues/423)) + + +## 2.7.3 -- 2021-10-01 +### Breaking changes +Minimum IDE version has been increased to 2020.3. +([#358](https://github.com/FWDekker/intellij-randomness/issues/358)) +([#386](https://github.com/FWDekker/intellij-randomness/issues/386)) + +### Fixes +* Shorter error messages in preview window. +* Resolved critical UI error in upcoming 2021.3 IDEs as result of using incorrect factory. + ([#418](https://github.com/FWDekker/intellij-randomness/issues/418)) + + +## 2.7.2 -- 2021-07-07 +### Fixes +Prevent symbol set settings from being truncated after restarting IDE. +([#382](https://github.com/FWDekker/intellij-randomness/issues/382)) + + +## 2.7.1 -- 2021-07-05 +### Breaking changes +Minimum IDE version has been increased to 2020.2. (#375) + +### New features +* Remove limit on difference between minimum and maximum integer. + ([#367](https://github.com/FWDekker/intellij-randomness/issues/367)) +* Add "byte" integer type to generate integers from -127 to 128. + ([#368](https://github.com/FWDekker/intellij-randomness/issues/368)) +* Input field widths now reflect the expected input sizes. + ([#374](https://github.com/FWDekker/intellij-randomness/issues/374)) +* Significantly improved performance when generating long strings. + ([#373](https://github.com/FWDekker/intellij-randomness/issues/373)) +* Generator timeout prevents IntelliJ from freezing when using excessively complex inputs. + ([#373](https://github.com/FWDekker/intellij-randomness/issues/373)) + +### Fixes +Prevent overflows when using a large range with integers. +([#370](https://github.com/FWDekker/intellij-randomness/issues/370)) + + +## 2.7.0 -- 2020-12-30 +This plugin is also available on the [plugin repository](https://plugins.jetbrains.com/plugin/9836-randomness). + +### Breaking changes +* Minimum IDE version has been increased to 2020.1. + ([#209](https://github.com/FWDekker/intellij-randomness/issues/209), + [#345](https://github.com/FWDekker/intellij-randomness/issues/345), + [#361](https://github.com/FWDekker/intellij-randomness/issues/361)) + +* Extended English dictionary (english_extended.dic) has been removed to improve performance and reduce plugin + size. + ([#352](https://github.com/FWDekker/intellij-randomness/issues/352)) + +### New features +* **🔠 Integer capitalization**.\ + Option to change capitalization of Integers with base greater than 10. + For example, generate 0xFF instead of 0xff. + ([#346](https://github.com/FWDekker/intellij-randomness/issues/346)) +* Short inline explanation of how dictionaries work in the Words settings dialog. + ([#347](https://github.com/FWDekker/intellij-randomness/issues/347)) +* Improved support for drag-and-drop of dictionary files into dictionary table. + ([#350](https://github.com/FWDekker/intellij-randomness/issues/350)) +* Improved error messages for IO failures with dictionaries. + ([#354](https://github.com/FWDekker/intellij-randomness/issues/354)) +* Natural column widths in dictionary table. + ([#354](https://github.com/FWDekker/intellij-randomness/issues/354)) +* Changes to dictionary contents are detected directly while in the Words settings dialog. + ([#354](https://github.com/FWDekker/intellij-randomness/issues/354)) +* Invalid settings are marked as modified even if settings have not been changed. + ([#354](https://github.com/FWDekker/intellij-randomness/issues/354)) + +### Fixes +Listing of data types in Randomness settings dialog has been fixed. +([#341](https://github.com/FWDekker/intellij-randomness/issues/341)) + + +## 2.6.1 -- 2020-06-24 +### New features +* The error report dialogue will now inform you of the privacy policy applicable to the reporting. + +### Fixes +* Excessively long error reports are now partially truncated to prevent HTTP 414 errors on GitHub. +* Reserved URI characters in error reports are now truncated to prevent URI misinterpretations. + + +## 2.6.0 -- 2020-05-14 +### New features +* **💫 Prefixes and suffixes**.\ + Prepend or append strings to your integers and decimals +* Check the list of look-alike symbols by hovering over the option in the string settings. + +### Fixes +* Schemes can now be saved and loaded correctly. +* Deleting a scheme no longer loads that scheme into the default scheme. +* Mnemonics for spinners now actually work. + + +## 2.5.1 -- 2020-02-08 +### New features +* **🚨 Error reporter**.\ + Easily report fatal Randomness errors to GitHub by clicking the report button in IntelliJ's + [event log](https://www.jetbrains.com/help/idea/event-log-tool-window.html). + +### Fixes +* Time-based UUID previews now use fixed seed until preview is refreshed. +* Radio buttons are no longer in a grid, giving the corresponding labels more natural sizes. +* The layout of UUID options has been altered slightly. +* Holding modifier keys in settings-only popup now triggers normal behavior of opening settings window. + + +## 2.5.0 -- 2020-01-24 +**Symbol set settings will be reset when updating to v2.5.0 because of changes in how settings are stored.** +You can safely re-add your custom symbol sets after updating. + +### New features +* **♻️ Scheme switcher**\ + Quickly switch between your schemes by holding Alt + Ctrl while selecting a data type. +* **😀 Emoji**\ + Generate random strings of emoji by adding them to symbol sets. +* **🕵 Look-alike characters**\ + Exclude characters that look like each other (e.g. `1`, `l`, `I`) in generated strings to prevent confusion. +* Change your settings using the Randomness popup even when you don't have an editor opened. +* Slightly friendlier and more accurate error messages for tables. + +### Fixes +* Input fields now correctly resize when shrinking the dialog. +* Symbol set table now makes full use of vertical space. +* Dictionary table no longer exceeds dialog width. +* Adding a new entry to a table using the "Add" button will now allow you to immediately edit the new entry. +* Leading and trailing whitespace characters no longer disappear when expanding a symbol set field. +* Pressing Enter in a table now activates the editor for the currently-selected cell. +* Duplicate symbols in single symbol sets are now ignored. +* Arrow keys now work correctly for all radio buttons. + + +## 2.4.1 -- 2020-01-14 +### Fixes +Resolves an issue causing plugin incompatibility with IDE versions 2018.1 through 2018.3. + + +## 2.4.0 -- 2020-01-07 +**All Randomness settings will be reset when updating to v2.4.0 because of changes in how settings are stored.** +You can safely reconfigure Randomness after updating. + +### New features +* **🗃️ Schemes**\ + Save your Randomness configurations using schemes. Simply create some new schemes to your liking, and you can change + back and forth between your schemes without any typing. +* **🖼️ Icons**\ + In addition to Randomness' new logo, all Randomness actions can now be identified by their unique icons. +* **♻️ Repeat**\ + Hold Alt () while selecting a data type to insert will insert the same value at all + [carets](https://www.jetbrains.com/help/idea/working-with-source-code.html#multiple_cursor). + Like all other Randomness actions, the repeat action can be assigned a shortcut if you want. +* **⌨️ Modifier keys**\ + Hold multiple modifier keys (Alt, Ctrl, Shift) to combine their effects. + For example, hold Ctrl + Shift to change the array settings, or hold Alt + Shift to insert + repeated arrays. +* **📏 Expandable fields**\ + View large symbol sets in a single glance using expandable text fields. + No more horizontal scrolling back and forth. +* Empty tables now contain clickable links for adding new values. +* Array previews now contain multiple random numbers instead of repeating the number 17. +* Groups of settings are separated by horizontal lines instead of being surrounded by borders. + +### Fixes +* The Randomness popup now appears at your caret instead of in the middle of the screen. +* The Randomness popup no longer appears when the editor isn't selected. +* The Randomness popup is now wide enough to display the full header text. +* Invalid number inputs result in preciser error messages. +* Previews that exceed the dialog width now wrap around. +* Adding a new symbol set or dictionary will move focus to the first editable column so you can start typing right away. + + +## 2.3.0 -- 2019-12-09 +### New features +* **Previews**\ + To help you decide what settings to choose, a preview of the data that is generated with your current settings is + shown at the bottom of the settings window. +* **New UUID settings**\ + You can now choose between time-based (version 1) and random (version 4) UUIDs. + Additionally, you can change the capitalisation and remove the dashes if you want. +* **Layout**\ + The improved layout adds vertical space in between groups to make it easier to find settings quickly. +* **Easier range controls**\ + When you set the minimum value to be higher than the maximum value, the maximum value will automatically be increased + as well---and vice versa for changing the maximum value. +* Randomness can now also be found in the + [Generate menu](https://www.jetbrains.com/help/idea/generating-code.html). +* The "space after separator" option in arrays is now disabled if the newline separator is set. +* Random capitalisation mode is now available for words as well. + +### Bug fixes +* The copy button is now disabled when a bundled dictionary is selected in the word settings. +* Invalid controls are now disabled when you open the settings dialog. +* Exceptions caused by corrupted settings are now handled properly. + + +## 2.2.0 -- 2019-08-06 +**Notice**\ +Please note that symbol set settings will be reset when updating to v2.2.0 because of changes in how settings are +stored. +You can re-add your custom symbol sets after updating. + +### New features +* Custom symbol sets for generating strings. +* Inline editing of symbol sets and dictionaries. +* Persistent order of symbol sets and dictionaries. +* Improved error messages. +* More consistent settings layouts. + + +## 2.1.0 -- 2019-07-10 +### New features +* Settings have been moved to the native settings window. +* Retain trailing zeroes in decimals. + +### Bug fixes +* Bundled dictionaries no longer disappear if they are not active when word settings are saved. + + +## 2.0.0 -- 2019-05-16 +**Notice** +Please note that dictionary settings will be reset when updating to v2.0.0 because of changes in how settings are +stored. +You can re-add your custom dictionaries after updating. + +### New features +* Inserting multiple words is now much faster. +* An additional bundled dictionary with simpler English words. +* Some setting defaults have been changed. +* All-around more descriptive error messages. + +### Bug fixes +* Dictionaries are now re-loaded before validating settings. + + +## 1.6.1 -- 2018-09-12 +### New features +* Allow changing quotation marks around generated UUIDs. + + +## 1.6.0 -- 2018-09-11 +### New features +* Generating type 4 UUIDs. +* Generating hexadecimal strings. +* Different capitalisation modes for strings. + +### Bug fixes +* Newline separator for arrays is now an actual newline instead of the text "\n". + + +## 1.5.2 -- 2018-05-27 +### New features +* Dictionary contents are now refreshed when word settings are saved. + + +## 1.5.1 -- 2018-05-15 +### Bug fixes +* Dictionary validity is now also checked when a custom dictionary is added. + + +## 1.5.0 -- 2018-05-13 +### New features +* Add newline character as separator when inserting arrays. +* Add two new capitalistion modes when inserting words: + - `Retain`: Does not change capitalisation w.r.t. dictionary file. + - `First Letter`: Capitalises the first letter of each word. + +### Bug fixes +* Deletion, renaming, and emptiness of custom dictionaries are detected when configuring and inserting random words. + + +## 1.4.0 -- 2018-04-30 +### New features +* Mnemonics for some buttons. +* Integers can be generated in customisable radix. + + +## 1.3.2 -- 2018-01-21 +### New features +* All actions can now be given keybinds separately. +* The Randomness action can now also be found in the Tools menu. + + +## 1.3.1 -- 2018-01-15 +### New features +* Added option to add no brackets around arrays. + +### Bug fixes +* Adjusted default random string length. + + +## 1.3.0 -- 2018-01-09 +### New features +* Added ability to generate arrays/lists/vectors of data. +* Overhauled action popup. Use modifier keys to change behaviour. + + +## 1.2.0 -- 2018-01-05 +### New features +* Added ability to generate random words.\ + Words are selected from dictionary files. + Custom dictionary files can be added by the user. + +### Bug fixes +* Minor typo fixes. + + +## 1.1.0 -- 2017-07-30 +### New features +* Added option to change decimal separator for decimals. +* Added option to change grouping separator for integers and decimals. + +### Bug fixes +* Input is no longer validated while typing. + + +## 1.0.1 -- 2017-07-20 +### New features +* Random integers can now go from `-2^63` to `2^63`. +* More precise input validation messages. + +### Bug fixes +* Excessively large ranges are now rejected. + + +## 1.0.0 -- 2017-07-16 +### New features +* Generated values are now cryptographically safe.\ + (_Note: This is no longer true starting in v1.0.1._) +* Key binding was changed to `Ctrl + R` (or `⌥R`). + +### Bug fixes +* Multiple insertions can now be undone together. +* Miscellaneous performance fixes. + + +## 0.5.0 -- 2017-07-13 +### New features +* All actions have been grouped together under `Insert Random Data`. +* The keyboard shortcut `Ctrl + Alt + R` opens a list of all actions. +* Symbols for string generation can now be changed in the settings. + + +## 0.4.0 -- 2017-07-12 +### New features +* Settings are now saved over multiple IDE sessions. +* Different quotation marks can be chosen for string insertion. +* Settings dialogs are now slightly wider to make input easier to read. + + +## 0.3.0 -- 2017-07-11 +### New features +* Data can be inserted at multiple carets simultaneously. +* Inserted data is highlighted after it is inserted. +* Dialogs now have mnemonics. + +### Bug fixes +* Plugin works again on IntelliJ versions above 2016.1. + + +## 0.2.0 -- 2017-07-10 +**Because of a configuration error, this release only works for IntelliJ 2016.1.** + +### New features +* Random decimals can now be inserted. +* Quotation marks around strings can now be disabled. + +### Bug fixes +* Non-numerical input is no longer accepted where numerical input is expected. +* The minimum value input can no longer exceed the maximum value input. + + +## 0.1.0 -- 2017-07-10 +This first release of the Randomness plugin provides some basic features. + +### Features +* Insert random numbers at caret. +* Insert random strings at caret. +* Change range of generated numbers. +* Change length of generated strings. diff --git a/LICENSE b/LICENSE index 9b0810669..a01edc105 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 F.W. Dekker +Copyright (c) 2017 Florine W. Dekker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5b9e7fc8c..2ab6775d3 100644 --- a/README.md +++ b/README.md @@ -5,68 +5,82 @@

- Release - Documentation + Downloads + Rating + Release
- Travis build status - Line coverage + CI status + Coverage + Documentation

Rather than going to [random.org](https://www.random.org/) or making up your own random data, you can now insert random -numbers, strings, and UUIDs using an IntelliJ action! +numbers, UUIDs, names, IP addresses, and much more using an IntelliJ action! -This plugin is also available on the [plugin repository](https://plugins.jetbrains.com/plugin/9836-randomness). +This plugin is also available on the [plugin repository](https://plugins.jetbrains.com/plugin/9836-randomness)! ## 📖 How to use -Animated sample usage of Randomness. - -To insert random data, press Alt + R (⌥R) and choose the type of data you want to insert. -A different value will be inserted at each caret. +To insert random data, press Alt + R (or ⌥R) to open the list of available templates and choose +one that suits your task. +By default, a different value is inserted at each caret. You can modify this behavior by holding a key while selecting the type of data to insert: -* **Array**: Hold Shift to insert a whole array of values. +* **Array**: Hold Shift to insert a customisable array of values. +* **Repeat**: Hold Alt (or ) to insert the same value at each caret. * **Settings**: Hold Ctrl to open the settings of that data type. -* **Repeat**: Hold Alt () to insert the same value at each caret. -You can also hold multiple modifier keys to combine their effects. +You can hold multiple modifier keys to combine their effects. Randomness can also be found in the main menu under Tools or in Code > Generate. +Animation of how to insert data + ## ✨ Features * 🕸 **Data Types**
- There are five types of data that can be inserted: - 1. **Integers**, such as `7,826,922`, in any base from binary to hexatrigesimal. - 2. **Decimals**, such as `8,816,573.10`, using customisable separators. - 3. **Strings**, such as `"PaQDQqSBEH"`, with custom symbol lists. - 4. **Words**, such as `"Bridge"`, with custom word lists. - 5. **UUIDs**, such as `0caa7b28-fe58-4ba6-a25a-9e5beaaf8f4b`, with or without dashes. - - In addition to these data types, it's also possible to generate entire **arrays** of a data type. + There are six basic data types that can be inserted and customised: + 1. **Integers**, such as `7,826,922`, from a custom range, in any base from binary to hexatrigesimal. + 2. **Decimals**, such as `8,816,573.10`, using customisable separators. + 3. **Strings**, such as `"PaQDQqSBEH"`, with support for reverse regex. + 4. **Words**, such as `"Bridge"`, with predefined or custom word lists. + 5. **UUIDs**, such as `0caa7b28-fe58-4ba6-a25a-9e5beaaf8f4b`, with or without dashes. + 6. **Date-times**, such as `2022-02-03 19:03`, or any other format you want. +* 🧬 **Templates**
+ For complex kinds of data, you can use templates. + A template is a list of data types that should be concatenated to create random data. + Insert **phone numbers**, **email addresses**, **URLs**, **IP addresses**, or any **custom data type** you can think + of. + Of course, Randomness comes bundled with a whole array of predefined templates to help you out. + If needed, you can reuse a template by including it in another template using a **reference**. + + Animation of how to configure templates +* 🗃️ **Arrays**
+ Need a lot of data? + Insert an **entire array** of any template you want. For example, an array of integers might look like `[978, 881, 118, 286, 288]`. -* ⚙ **Settings**
- The way the data is generated can be **adjusted to your demands**. - You can customise the smallest integer to generate, the quotation marks to surround strings with, the number of - elements to put in an array, the decimal separator to use, the capitalisation of strings and UUIDs, and much more. -* 👀 **Previews**
- To **help you decide** what settings to choose, a preview of the data that is generated with your current settings is - shown at the bottom of the settings window. -* 💨 **Shortcuts**
+ You can customise the brackets, delimiter, and number of elements to your liking every time you insert an array, + because no two arrays are the same. + + Animation of how to insert arrays +* ⌨️ **Shortcuts**
Instead of using up all your shortcuts, Randomness only uses the Alt + R (or ⌥R) shortcut by default. - However, you have the option to assign a unique shortcut to each individual data type and each settings window to - **streamline your workflow**. -* 💬 **Symbol sets**
- While Randomness comes with a varied selection of symbols to use for generating strings, it also gives you the option - to **add your own symbols**, with support for Chinese symbols and emoji. -* 📚 **Dictionaries**
- Randomness is bundled with a small English dictionary from which it chooses random words. - However, you may want to add random words from another language or insert random movie quotes. - You can **create your own dictionaries** by creating a text file and putting one option on each line, and saving the - file with the `.dic` extension. - Empty lines and lines that start with a `#` are ignored. + However, to **streamline your workflow**, you can assign shortcuts to any template under your IDE's Keymap + settings. + + Shortcut settings for Randomness +* 💨 **Fast insertion**
+ The insertion popup (shown when you press Alt + R (or ⌥R) by default) is **searchable**: + Just type something in the popup and relevant templates will be filtered out. + Or use the **hotkeys** that are assigned to the first 10 templates in the list: + Press any digit to directly insert the corresponding template. + **Reorder templates** in the settings menu to change which template uses which hotkey. + + Insertion popup +* 👀 **Previews**
+ To **help you decide** what settings to choose, a preview of the template is shown while you're editing. -

Dictionary settings

+ Preview window in Randomness ## 💻 Development @@ -77,29 +91,49 @@ Please also check the [contribution guidelines](.github/CONTRIBUTING.md). ```bash $ gradlew runIde # Open a sandbox IntelliJ instance running the plugin $ gradlew buildPlugin # Build an installable zip of the plugin +$ gradlew signPlugin # Sign built plugin ``` +Signing the plugin requires specific environment variables to be set to refer to appropriate key files. +See [Plugin Signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.html) for more information. ### 🧪 Quality assurance ```bash -$ gradlew test # Run tests -$ gradlew test --tests X # Run tests in class X -$ gradlew check # Run tests and static analysis -$ gradlew jacocoTestReport # Run tests and calculate coverage -$ gradlew runPluginVerifier # Check for compatibility issues +$ gradlew test # Run tests (and collect coverage) +$ gradlew test --tests X # Run tests in class X (package name optional) +$ gradlew test -Pkotest.tags="X" # Run tests matching tag(s) X (also supports not (!), and (&), or (|)) +$ gradlew koverHtmlReport # Create HTML coverage report for previous test run +$ gradlew check # Run tests and static analysis +$ gradlew runPluginVerifier # Check for compatibility issues ``` +#### 🏷️ Tagging and filtering tests +[Kotest tests can be tagged](https://kotest.io/docs/framework/tags.html) to allow selectively running tests. +The tags for Randomness are statically defined in +[`Tags`](https://github.com/FWDekker/intellij-randomness/blob/main/src/test/kotlin/com/fwdekker/randomness/testhelpers/Tags.kt). + +Tag an entire test class by adding `tags(...)` to the class definition, or tag an individual test `context` by +writing `context("foo").config(tags = setOf(...)) {`. +It is not possible to tag an individual `test` due to limitations in Kotest. + +To run only one `context` in some test class `X`, prefix the `context`'s name with `f:` and run with `--tests X`. +The prefix `f:` filters out other `context`s in that test class, and `--tests X` filters out other test classes. +Alternatively, tag the desired `context` with the `Focus` tag and run with `-Pkotest.tags="Focus"` to filter by that +tag. + ### 📚 Documentation ```bash $ gradlew dokkaHtml # Generate documentation ``` ### 🖼 Icons -The icons used by the plugin are found in the [.sketch](.github/img/icons.sketch) file. +The icons used by the plugin are found in [the file `icons.sketch`](.github/img/icons.sketch). You can open this file with [Sketch](https://www.sketch.com/) (macOS), [Lunacy](https://icons8.com/lunacy) (Windows), or [Figma](https://github.com/Figma-Linux/figma-linux) (Linux). ## 🙏 Acknowledgements +I want to thank everyone who contributed something to Randomness, no matter the size of that contribution. + In chronological order of contribution: * Thanks to [Casper Boone](https://github.com/casperboone) for [reporting a bug](https://github.com/FWDekker/intellij-randomness/issues/25) and for @@ -108,7 +142,7 @@ In chronological order of contribution: [suggesting the array data type](https://github.com/FWDekker/intellij-randomness/issues/54)! * Thanks to [Georgios Andreadis](https://github.com/gandreadis) for the [original logo](https://github.com/FWDekker/intellij-randomness/pull/86)! -* Thanks to [Oleksii K.](https://github.com/ok3141) for +* Thanks to [Oleksii](https://github.com/ok3141) for [suggesting the UUID data type](https://github.com/FWDekker/intellij-randomness/issues/88) and for [suggesting the hex symbol set](https://github.com/FWDekker/intellij-randomness/issues/89)! * Thanks to [Meilina Reksoprodjo](https://github.com/meilinar) for help with macOS user testing! @@ -136,9 +170,23 @@ In chronological order of contribution: [reporting that symbol sets didn't get saved](https://github.com/FWDekker/intellij-randomness/issues/382)! * Thanks to [Aleksey Bobyr](https://github.com/Alexsey) for [reporting a critical UI bug in WebStorm EAP](https://github.com/FWDekker/intellij-randomness/issues/418)! -* Thanks to [Xiakitl](https://github.com/Xiakitl) for +* Thanks to [Lukas](https://github.com/LukasAppleFan) for + [helping me find a bug in IntelliJ](https://github.com/FWDekker/intellij-randomness/issues/421)! +* Thanks to [Pascal](https://github.com/theMunichDev) for [reporting a bug with custom shortcuts](https://github.com/FWDekker/intellij-randomness/issues/423)! * Thanks to [Rishi Maharaj](https://github.com/rshmhrj) for - [suggesting to add prefix and postfix options to strings]! + [suggesting to add prefix and postfix options to strings](https://github.com/FWDekker/intellij-randomness/issues/431)! +* Thanks to [Christian Baune](https://github.com/programaths) for + [reporting recurring issues](https://github.com/FWDekker/intellij-randomness/issues/441) + [with load](https://github.com/FWDekker/intellij-randomness/issues/442) + [corrupted settings](https://github.com/FWDekker/intellij-randomness/issues/444)! +* Thanks to [Vladislav Rassokhin](https://github.com/VladRassokhin) for + [reporting an issue with slow actions during indexing](https://github.com/FWDekker/intellij-randomness/issues/445)! +* Thanks to [Luc Everse](https://github.com/cmpsb) for + [suggesting generating non-matching strings](https://github.com/FWDekker/intellij-randomness/issues/447)! +* Thanks to [ForNeVeR](https://github.com/ForNeVeR) for + [reporting a compatibility issue with the IntelliJ EAP](https://github.com/FWDekker/intellij-randomness/issues/459)! +* Thanks to [Vitaly Provodin](https://github.com/vprovodin) for + [also reporting that compatibility issue](https://github.com/FWDekker/intellij-randomness/issues/460)! If I should add, remove, or change anything here, just open an issue or email me! diff --git a/build.gradle.kts b/build.gradle.kts index a24355327..6a8ef223b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,8 @@ +import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.jetbrains.changelog.Changelog +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.net.URL import java.time.Year @@ -7,18 +12,18 @@ fun properties(key: String) = project.findProperty(key).toString() /// Plugins plugins { // Compilation - id("java") - id("org.jetbrains.kotlin.jvm") version "1.8.22" // See also `gradle.properties` - id("org.jetbrains.intellij") version "1.14.2" + id("org.jetbrains.kotlin.jvm") version "1.9.20" // Use latest version, ignoring `gradle.properties` + id("org.jetbrains.intellij") version "1.16.0" // Tests/coverage - id("jacoco") + id("org.jetbrains.kotlinx.kover") version "0.7.4" // Static analysis - id("io.gitlab.arturbosch.detekt") version "1.20.0" // See also `gradle.properties` + id("io.gitlab.arturbosch.detekt") version "1.23.3" // See also `gradle.properties` // Documentation - id("org.jetbrains.dokka") version "1.8.20" + id("org.jetbrains.changelog") version "2.2.0" + id("org.jetbrains.dokka") version "1.9.10" } @@ -29,18 +34,17 @@ repositories { dependencies { implementation("com.fasterxml.uuid:java-uuid-generator:${properties("uuidGeneratorVersion")}") + implementation("com.github.sisyphsu:dateparser:${properties("dateparserVersion")}") + implementation("com.github.curious-odd-man:rgxgen:${properties("rgxgenVersion")}") implementation("com.vdurmont:emoji-java:${properties("emojiVersion")}") api("org.jetbrains.kotlin:kotlin-reflect") - testImplementation("org.assertj:assertj-core:${properties("assertjVersion")}") testImplementation("org.assertj:assertj-swing-junit:${properties("assertjSwingVersion")}") - testImplementation("org.junit.platform:junit-platform-runner:${properties("junitRunnerVersion")}") - testImplementation("org.junit.jupiter:junit-jupiter-api:${properties("junitVersion")}") - testImplementation("org.junit.jupiter:junit-jupiter-engine:${properties("junitVersion")}") + testRuntimeOnly("org.junit.platform:junit-platform-runner:${properties("junitRunnerVersion")}") testImplementation("org.junit.vintage:junit-vintage-engine:${properties("junitVersion")}") - testImplementation("org.mockito.kotlin:mockito-kotlin:${properties("mockitoKotlinVersion")}") - testImplementation("org.spekframework.spek2:spek-dsl-jvm:${properties("spekVersion")}") - testRuntimeOnly("org.spekframework.spek2:spek-runner-junit5:${properties("spekVersion")}") + testImplementation("io.kotest:kotest-assertions-core:${properties("kotestVersion")}") + testImplementation("io.kotest:kotest-framework-datatest:${properties("kotestVersion")}") + testImplementation("io.kotest:kotest-runner-junit5:${properties("kotestVersion")}") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:${properties("detektVersion")}") } @@ -50,72 +54,84 @@ dependencies { tasks { // Compilation withType { - sourceCompatibility = properties("jvmVersion") - targetCompatibility = properties("jvmVersion") + sourceCompatibility = properties("javaVersion") + targetCompatibility = properties("javaVersion") } - withType { + withType { kotlinOptions { - jvmTarget = properties("jvmVersion") + jvmTarget = properties("javaVersion") apiVersion = properties("kotlinApiVersion") languageVersion = properties("kotlinVersion") } } - withType { - jvmTarget = properties("jvmVersion") + withType { + jvmTarget = properties("javaVersion") } intellij { version.set(properties("intellijVersion")) downloadSources.set(true) - updateSinceUntilBuild.set(false) + updateSinceUntilBuild.set(false) // Set in `patchPluginXml` } patchPluginXml { - changeNotes.set(file("src/main/resources/META-INF/change-notes.html").readText()) + changeNotes.set(provider { + changelog.renderItem( + if (changelog.has(properties("version"))) changelog.get(properties("version")) + else changelog.getUnreleased(), + Changelog.OutputType.HTML + ) + }) pluginDescription.set(file("src/main/resources/META-INF/description.html").readText()) sinceBuild.set(properties("pluginSinceBuild")) } + changelog { + repositoryUrl.set("https://github.com/FWDekker/intellij-randomness") + itemPrefix.set("*") + } + + signPlugin { + if (System.getenv("CERTIFICATE_CHAIN") != null) { + certificateChainFile.set(file(System.getenv("CERTIFICATE_CHAIN"))) + privateKeyFile.set(file(System.getenv("PRIVATE_KEY"))) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + } + // Tests/coverage test { - systemProperty("spek2.execution.test.timeout", 0) + systemProperty("java.awt.headless", "false") + if (project.hasProperty("kotest.tags")) systemProperty("kotest.tags", project.findProperty("kotest.tags")!!) useJUnitPlatform { - includeEngines("junit-vintage", "junit-jupiter", "spek2") + if (!project.hasProperty("kotest.tags")) + includeEngines("junit-vintage") + + includeEngines("kotest") } testLogging { - events = setOf(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED) - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + events = setOf(TestLogEvent.FAILED) + exceptionFormat = TestExceptionFormat.FULL } - } - jacoco { - toolVersion = properties("jacocoVersion") + finalizedBy(koverXmlReport) } - jacocoTestReport { - executionData(file("$buildDir/jacoco/test.exec")) - - sourceSets { sourceSets.main } - - reports { - csv.required.set(false) - html.required.set(true) - xml.required.set(true) - xml.outputLocation.set(file("$buildDir/reports/jacoco/report.xml")) + koverReport { + defaults { + html { onCheck = false } + xml { onCheck = false } } - - dependsOn(test) } // Static analysis detekt { - toolVersion = properties("detektVersion") allRules = true - config = files(".config/detekt/.detekt.yml") + config.setFrom(".config/detekt/.detekt.yml") } runPluginVerifier { @@ -125,11 +141,17 @@ tasks { // Documentation dokkaHtml.configure { - pluginsMapConfiguration.set(mapOf( - "org.jetbrains.dokka.base.DokkaBase" to """{ "footerMessage": "© ${Year.now().value} F.W. Dekker" }""" - )) + notCompatibleWithConfigurationCache("cf. https://github.com/Kotlin/dokka/issues/1217") + + pluginsMapConfiguration.set( + mapOf( + "org.jetbrains.dokka.base.DokkaBase" to + """{ "footerMessage": "© ${Year.now().value} Florine W. Dekker" }""" + ) + ) moduleName.set("Randomness v${properties("version")}") offlineMode.set(true) + suppressInheritedMembers.set(true) dokkaSourceSets { named("main") { @@ -137,7 +159,7 @@ tasks { jdkVersion.set(properties("javaVersion").toInt()) - includeNonPublic.set(false) + includeNonPublic.set(true) skipDeprecated.set(false) reportUndocumented.set(true) skipEmptyPackages.set(true) diff --git a/gradle.properties b/gradle.properties index 951324b5f..e4a08a450 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,45 +1,52 @@ -group = com.fwdekker -version = 2.7.7 +group=com.fwdekker +# Version number should also be updated in `com.fwdekker.randomness.PersistentSettings.Companion.CURRENT_VERSION`. +version=3.0.0 # Compatibility -# * If latest is 20xx.y, then support at least [20xx-1].[y+1]. -# e.g., if latest is 2020.3, support at least 2019.4 (aka 2020.1). -# See also https://data.services.jetbrains.com/products?fields=name,releases.version,releases.build&code=IC,CL. -pluginSinceBuild = 222.0 -intellijVersion = 2022.2 -pluginVerifierIdeVersions = IC-2022.2.5, IC-2022.3.3, IC-2023.1.3, CL-2022.2.5, CL-2022.3.3, CL-2023.1.4 +# * `pluginSinceBuild`: +# If latest is 20xx.y, then support at least [20xx-1].[y+1]. For example, if latest is 2020.3, support at least +# 2019.4 (aka 2020.1). +# * `intellijVersion`: +# Use the oldest supported version, because that's what the plugin will be compiled against. +# * `pluginVerifierIdeVersions`: +# For every supported version minor release, include both the IC and the CL release with the highest patch version +# See also https://data.services.jetbrains.com/products?fields=name,releases.version,releases.build&code=IC,CL. +pluginSinceBuild=223.0 +intellijVersion=2022.3 +pluginVerifierIdeVersions=IC-2022.3.3, IC-2023.1.5, IC-2023.2.5, CL-2022.3.3, CL-2023.1.5, CL-2023.2.2 # Targets -# * Java -# * `javaVersion` is the same as `jvmVersion`. -# * Java version should be the one used by the oldest Randomness-supported version of IntelliJ, as listed in -# https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html#intellij-platform-based-products-of-recent-ide-versions +# * Java: +# Java version should be the one used by the oldest Randomness-supported version of IntelliJ. See also +# https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html#intellij-platform-based-products-of-recent-ide-versions. # # * Kotlin # * `kotlinVersion` is the same as `kotlinApiVersion`. -# * Kotlin should also be updated in `plugins` block. -# * Kotlin version should be bundled stdlib version of oldest supported IntelliJ version listed in -# https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library -javaVersion = 17 -jvmVersion = 17 -kotlinVersion = 1.6 -kotlinApiVersion = 1.6 +# * Kotlin version should be bundled stdlib version of oldest supported IntelliJ version. See also +# https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library. +javaVersion=17 +kotlinVersion=1.7 +kotlinApiVersion=1.7 # Dependencies # * Detekt should also be updated in `plugins` block. -# * Check https://github.com/assertj/assertj-swing/releases for valid AssertJ version combinations. -assertjVersion = 3.17.2 -assertjSwingVersion = 3.17.1 -detektVersion = 1.20.0 -emojiVersion = 5.1.1 -jacocoVersion = 0.8.8 -junitVersion = 5.9.1 -junitRunnerVersion = 1.9.1 -mockitoKotlinVersion = 4.1.0 -spekVersion = 2.0.19 -uuidGeneratorVersion = 3.3.0 +# * RgxGen should also be updated in `StringSchemeEditor` link. +assertjSwingVersion=3.17.1 +dateparserVersion=1.0.11 +detektVersion=1.23.3 +emojiVersion=5.1.1 +junitVersion=5.10.1 +junitRunnerVersion=1.10.1 +kotestVersion=5.8.0 +rgxgenVersion=1.4 +uuidGeneratorVersion=4.3.0 + +# Gradle +org.gradle.caching=true +org.gradle.configuration-cache=true # Kotlin -kotlin.code.style = official -kotlin.incremental.useClasspathSnapshot = false -kotlin.stdlib.default.dependency = false +kotlin.code.style=official +kotlin.stdlib.default.dependency=false +# TODO: Workaround for https://jb.gg/intellij-platform-kotlin-oom, will not be necessary as of Kotlin 1.9.0 +kotlin.incremental.useClasspathSnapshot=false diff --git a/gradle/scripts/verifier.gradle b/gradle/scripts/verifier.gradle deleted file mode 100644 index df9b178bc..000000000 --- a/gradle/scripts/verifier.gradle +++ /dev/null @@ -1,4 +0,0 @@ -// WARNING! The plugin verifier script is deprecated. The runPluginVerifier task is built into the -// gradle-intellij-plugin as of version 0.6.0. For more information, see -// https://github.com/JetBrains/gradle-intellij-plugin/issues/385#issuecomment-718796665. -// This file will be removed from the intellij-random repository on 2022-12-20. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dcf0f19c5..5e6b54271 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/packages.md b/packages.md index 0016e6071..ebec40624 100644 --- a/packages.md +++ b/packages.md @@ -1,35 +1,47 @@ # Package com.fwdekker.randomness -Contains classes shared by all sub-packages. +Entry point of the plugin. Contains main classes, shared classes, and helper classes. + +# Package com.fwdekker.randomness.affix + +Decorator for surrounding a value with constant strings such as quotation marks and braces. # Package com.fwdekker.randomness.array -Insertion of random arrays of other types of data. +Decorator for generating multiple values each time. + +# Package com.fwdekker.randomness.datetime + +Scheme for random dates and times. # Package com.fwdekker.randomness.decimal -Insertion of random decimals. +Scheme for random decimal numbers. + +# Package com.fwdekker.randomness.fixedlength + +Decorator for forcing values to a specified length. # Package com.fwdekker.randomness.integer -Insertion of random integers. +Scheme for random non-decimal numbers. # Package com.fwdekker.randomness.string -Insertion of random strings. +Scheme for random and non-random strings. + +# Package com.fwdekker.randomness.template + +Scheme consisting of other schemes. # Package com.fwdekker.randomness.ui -Custom or specialized Swing components. +Custom or specialized UI components. # Package com.fwdekker.randomness.uuid -Insertion of random UUIDs. +Scheme for random UUIDs. # Package com.fwdekker.randomness.word -Insertion of random words. - -# Package icons - -Collection of available icons. +Scheme for random words selected from word lists. diff --git a/settings.gradle b/settings.gradle.kts similarity index 100% rename from settings.gradle rename to settings.gradle.kts diff --git a/src/main/kotlin/com/fwdekker/randomness/Box.kt b/src/main/kotlin/com/fwdekker/randomness/Box.kt new file mode 100644 index 000000000..dabb8e8a5 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/Box.kt @@ -0,0 +1,20 @@ +package com.fwdekker.randomness + + +/** + * A lazily-instantiated reference to an object of type [T]. + * + * @param T the type of the referenced object + * @param generator generates the referenced object when [unaryPlus] is invoked for the first time + * @param value do not assign this field in the constructor; this field is placed in the constructor to ensure Kotlin + * includes it in the automatically-generated [copy] method + */ +data class Box(private val generator: () -> T, private var value: T? = null) { + /** + * If this method is invoked for the first time, [generator] is invoked and the result is returned. In subsequent + * invocations of this method, the previously-generated value is returned each time. + * + * @return the referenced value + */ + operator fun unaryPlus(): T = value ?: generator().also { value = it } +} diff --git a/src/main/kotlin/com/fwdekker/randomness/Bundle.kt b/src/main/kotlin/com/fwdekker/randomness/Bundle.kt new file mode 100644 index 000000000..5fac53f29 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/Bundle.kt @@ -0,0 +1,52 @@ +package com.fwdekker.randomness + +import java.util.MissingResourceException +import java.util.ResourceBundle + + +/** + * Simple accessor for working with internationalized strings. + */ +object Bundle { + /** + * The main bundle for Randomness. + */ + private val RESOURCE_BUNDLE = ResourceBundle.getBundle("randomness") + + + /** + * Returns the string at [key]. + * + * @throws MissingResourceException if no string with [key] can be found + */ + @Throws(MissingResourceException::class) + operator fun invoke(key: String): String = RESOURCE_BUNDLE.getString(key) + + /** + * Returns the string at [key] formatted with [arguments]. + * + * @throws MissingResourceException if no string with [key] can be found + */ + @Throws(MissingResourceException::class) + operator fun invoke(key: String, vararg arguments: Any?): String = this(key).format(*arguments) +} + + +/** + * Returns `true` if [format] is a format string for `this` string, optionally after inserting [args] into [format]. + * + * @throws java.util.MissingFormatArgumentException if [args] has fewer arguments than required for [format] + */ +fun String.matchesFormat(format: String, vararg args: String) = + Regex("%[0-9]+\\\$[Ssd]").findAll(format) + .toList() + .reversed() + .fold(format) { acc, match -> + if (match.value.drop(1).dropLast(2).toInt() > args.size) + acc.replaceRange(match.range, ".*") + else + acc + } + .format(*args) + .let { Regex(it) } + .matches(this) diff --git a/src/main/kotlin/com/fwdekker/randomness/Cache.kt b/src/main/kotlin/com/fwdekker/randomness/Cache.kt deleted file mode 100644 index 1b501e13a..000000000 --- a/src/main/kotlin/com/fwdekker/randomness/Cache.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.fwdekker.randomness - - -/** - * A simple thread-safe cache of objects. - * - * @param K the type of keys - * @param V the type of values - * @property creator a function that maps a key to a value, used to instantiate a value when it is requested and not in - * the cache - */ -class Cache(private val creator: (K) -> V) { - private val values = mutableMapOf() - - - /** - * Returns the value that corresponds to `key`. - * - * If it does not exist, it is instantiated and then returned. If `useCache` is set to false, a new value is always - * instantiated, even if there already is a value for `key`. - * - * @param key the key to look up the value with - * @param useCache whether to return the existing value if it exists. Either way, the result is stored in the cache - * @return the value that corresponds to `key` - */ - @Synchronized - fun get(key: K, useCache: Boolean = true) = - if (useCache) - values.getOrPut(key) { creator(key) } - else - creator(key) - .also { values[key] = it } - - /** - * Removes all keys and values from the cache. - */ - @Synchronized - fun clear() = values.clear() -} diff --git a/src/main/kotlin/com/fwdekker/randomness/CapitalizationMode.kt b/src/main/kotlin/com/fwdekker/randomness/CapitalizationMode.kt index 2771ae710..445e5a106 100644 --- a/src/main/kotlin/com/fwdekker/randomness/CapitalizationMode.kt +++ b/src/main/kotlin/com/fwdekker/randomness/CapitalizationMode.kt @@ -7,80 +7,51 @@ import kotlin.random.Random /** * A mode in which a word should be capitalized. * - * @property descriptor the name of the capitalization mode - * @property transformer the function which capitalizes the given string to the mode's format + * @property transform The function which capitalizes the given string to the mode's format. */ -enum class CapitalizationMode(val descriptor: String, private val transformer: (String, Random) -> String) { +enum class CapitalizationMode(val transform: (String, Random) -> String) { /** * Does not change the string. */ - RETAIN("retain", { string, _ -> string }), + RETAIN({ string, _ -> string }), /** * Makes the first character uppercase and all characters after that lowercase. */ - SENTENCE("sentence", { string, _ -> string.toSentenceCase() }), + SENTENCE({ string, _ -> string.toSentenceCase() }), /** * Makes all characters uppercase. */ - UPPER("upper", { string, _ -> string.uppercase(Locale.getDefault()) }), + UPPER({ string, _ -> string.uppercase(Locale.getDefault()) }), /** * Makes all characters lowercase. */ - LOWER("lower", { string, _ -> string.lowercase(Locale.getDefault()) }), + LOWER({ string, _ -> string.lowercase(Locale.getDefault()) }), /** * Makes the first letter of each word uppercase. */ - FIRST_LETTER("first letter", { string, _ -> string.split(' ').joinToString(" ") { it.toSentenceCase() } }), + FIRST_LETTER({ string, _ -> string.split(' ').joinToString(" ") { it.toSentenceCase() } }), /** * Makes each letter randomly uppercase or lowercase. */ - RANDOM("random", { string, random -> string.toCharArray().map { it.toRandomCase(random) }.joinToString("") }); + RANDOM({ string, random -> string.toCharArray().map { it.toRandomCase(random) }.joinToString("") }), + ; /** - * Invokes [transformer] with [random]. - * - * @param string the string to transform - * @param random the random instance to use for transforming - * @return the returned value of [transformer] + * Returns the localized string name of this mode. */ - fun transform(string: String, random: Random = Random.Default) = transformer(string, random) - - /** - * Returns the descriptor of the capitalization mode. - * - * @return the descriptor of the capitalization mode - */ - override fun toString() = descriptor - - - /** - * Holds static elements. - */ - companion object { - /** - * Returns the capitalization mode with the given name. - * - * @param descriptor the descriptor of the capitalization mode to return - * @return the capitalization mode with the given descriptor - */ - fun getMode(descriptor: String) = - values().firstOrNull { it.descriptor == descriptor } - ?: throw IllegalArgumentException("There does not exist a capitalization mode with name `$descriptor`.") - } + fun toLocalizedString() = + Bundle("shared.capitalization.${toString().replace(' ', '_').lowercase(Locale.getDefault())}") } /** - * Randomly converts this character to uppercase or lowercase. - * - * @param random the source of randomness to use - * @return the uppercase or lowercase version of this character + * Randomly converts this character to uppercase or lowercase using [random] as a source of randomness. */ private fun Char.toRandomCase(random: Random) = if (random.nextBoolean()) this.lowercaseChar() @@ -88,8 +59,6 @@ private fun Char.toRandomCase(random: Random) = /** * Turns the first character uppercase while all other characters become lowercase. - * - * @return the sentence-case form of this string */ private fun String.toSentenceCase() = this.lowercase(Locale.getDefault()).replaceFirstChar { it.uppercaseChar() } diff --git a/src/main/kotlin/com/fwdekker/randomness/DataActions.kt b/src/main/kotlin/com/fwdekker/randomness/DataActions.kt deleted file mode 100644 index 0087b9338..000000000 --- a/src/main/kotlin/com/fwdekker/randomness/DataActions.kt +++ /dev/null @@ -1,440 +0,0 @@ -package com.fwdekker.randomness - -import com.fwdekker.randomness.array.ArrayScheme -import com.fwdekker.randomness.array.ArraySettingsAction -import com.intellij.codeInsight.hint.HintManager -import com.intellij.icons.AllIcons -import com.intellij.ide.actions.QuickSwitchSchemeAction -import com.intellij.openapi.actionSystem.ActionGroup -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import icons.RandomnessIcons -import java.awt.event.ActionEvent -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import javax.swing.Icon -import kotlin.random.Random - - -/** - * Thrown if a random datum could not be generated. - * - * @param message the detail message - * @param cause the cause - */ -class DataGenerationException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) - - -/** - * A group of actions for a particular type of random data that can be generated. - * - * @property icon the icon to display with the action - */ -abstract class DataGroupAction(private val icon: Icon = RandomnessIcons.Data.Base) : ActionGroup() { - /** - * The action used to insert single data. - */ - abstract val insertAction: DataInsertAction - - /** - * The action used to insert arrays of data. - */ - abstract val insertArrayAction: DataInsertArrayAction - - /** - * The action used to insert repeated single data. - */ - abstract val insertRepeatAction: DataInsertRepeatAction - - /** - * The action used to insert repeated arrays of data. - */ - abstract val insertRepeatArrayAction: DataInsertRepeatArrayAction - - /** - * The action used to edit the generator settings for this data type. - */ - abstract val settingsAction: DataSettingsAction - - /** - * The action used to quickly switch between schemes of this data type. - */ - abstract val quickSwitchSchemeAction: DataQuickSwitchSchemeAction<*> - - /** - * The action used to quickly switch between array schemes. - */ - abstract val quickSwitchArraySchemeAction: DataQuickSwitchSchemeAction<*> - - - /** - * Returns the insert action, array insert action, and settings action. - * - * @param event carries information on the invocation place - * @return the insert action, array insert action, and settings action - */ - override fun getChildren(event: AnActionEvent?) = - arrayOf(insertArrayAction, insertRepeatAction, insertRepeatArrayAction, settingsAction, quickSwitchSchemeAction) - - /** - * Chooses one of the three actions to execute based on the key modifiers in [event]. - * - * @param event carries information on the invocation place - */ - override fun actionPerformed(event: AnActionEvent) { - val altPressed = event.modifiers and ActionEvent.ALT_MASK != 0 - val ctrlPressed = event.modifiers and ActionEvent.CTRL_MASK != 0 - val shiftPressed = event.modifiers and ActionEvent.SHIFT_MASK != 0 - - // alt behavior is handled by implementation of `actionPerformed` - when { - altPressed && ctrlPressed && shiftPressed -> quickSwitchArraySchemeAction.actionPerformed(event) - altPressed && ctrlPressed -> quickSwitchSchemeAction.actionPerformed(event) - altPressed && shiftPressed -> insertRepeatArrayAction.actionPerformed(event) - ctrlPressed && shiftPressed -> ArraySettingsAction().actionPerformed(event) - altPressed -> insertRepeatAction.actionPerformed(event) - ctrlPressed -> settingsAction.actionPerformed(event) - shiftPressed -> insertArrayAction.actionPerformed(event) - else -> insertAction.actionPerformed(event) - } - } - - /** - * Sets the title of this action. - * - * @param event carries information on the invocation place - */ - override fun update(event: AnActionEvent) { - super.update(event) - - event.presentation.text = insertAction.name - event.presentation.icon = icon - event.presentation.isPerformGroup = true - event.presentation.isPopupGroup = true - } -} - - -/** - * Inserts randomly generated strings at the event's editor's carets. - * - * @property icon the icon to display with the action - */ -abstract class DataInsertAction(private val icon: Icon) : AnAction() { - /** - * The name of the action to display. - */ - abstract val name: String - - /** - * The random generator used to generate random values. - */ - var random: Random = Random.Default - - - /** - * Sets the title of this action and disables this action if no editor is currently opened. - * - * @param event carries information on the invocation place - */ - override fun update(event: AnActionEvent) { - val presentation = event.presentation - val editor = event.getData(CommonDataKeys.EDITOR) - - presentation.text = name - presentation.icon = icon - presentation.isEnabled = editor != null - } - - /** - * Inserts the data generated by [generateStrings] at the caret(s) in the editor; one datum for each caret. - * - * @param event carries information on the invocation place - */ - @Suppress("ReturnCount") // Result of null checks at start - override fun actionPerformed(event: AnActionEvent) { - val editor = event.getData(CommonDataKeys.EDITOR) - ?: return - val project = event.getData(CommonDataKeys.PROJECT) - ?: return - - val data = - try { - generateStringsTimely(editor.caretModel.caretCount) - } catch (e: DataGenerationException) { - HintManager.getInstance().showErrorHint( - editor, - """ - Randomness was unable to generate random data. - ${if (!e.message.isNullOrBlank()) "The following error was encountered: ${e.message}\n" else ""} - Check your Randomness settings and try again. - """.trimIndent() - ) - return - } - - WriteCommandAction.runWriteCommandAction(project) { - editor.caretModel.allCarets.forEachIndexed { i, caret -> - val start = caret.selectionStart - val end = caret.selectionEnd - val newEnd = start + data[i].length - - editor.document.replaceString(start, end, data[i]) - caret.setSelection(start, newEnd) - } - } - } - - /** - * Generates a random datum. - * - * @return a random datum - * @throws DataGenerationException if data could not be generated - */ - @Throws(DataGenerationException::class) - fun generateString() = generateStrings(1).first() - - /** - * Generates a random datum, or throws an exception if it takes longer than [GENERATOR_TIMEOUT] milliseconds. - * - * @return a random datum - * @throws DataGenerationException if data could not be generated in time - */ - @Throws(DataGenerationException::class) - fun generateStringTimely() = generateTimely { generateString() } - - /** - * Generates random data. - * - * @param count the number of data to generate - * @return random data - * @throws DataGenerationException if data could not be generated - */ - @Throws(DataGenerationException::class) - abstract fun generateStrings(count: Int = 1): List - - /** - * Generates random data, or throws an exception if it takes longer than [GENERATOR_TIMEOUT] milliseconds. - * - * @param count the number of data to generate - * @return random data - * @throws DataGenerationException if data could not be generated in time - */ - @Throws(DataGenerationException::class) - fun generateStringsTimely(count: Int = 1) = generateTimely { generateStrings(count) } - - /** - * Runs the given function and returns its return value, or throws an exception if it takes longer than - * [GENERATOR_TIMEOUT] milliseconds. - * - * @param T the return type of [generator] - * @param generator the function to call - * @return the return value of [generator] - * @throws DataGenerationException if data could not be generated in time - */ - @Throws(DataGenerationException::class) - private fun generateTimely(generator: () -> T): T { - val executor = Executors.newSingleThreadExecutor() - try { - return executor.submit { generator() }.get(GENERATOR_TIMEOUT, TimeUnit.MILLISECONDS) - } catch (e: TimeoutException) { - throw DataGenerationException("Timed out while generating data.", e) - } catch (e: ExecutionException) { - throw DataGenerationException(e.cause?.message ?: e.message, e) - } finally { - executor.shutdown() - } - } - - - /** - * Holds constants. - */ - companion object { - /** - * The timeout in milliseconds before the generator should be interrupted when generating a value. - */ - const val GENERATOR_TIMEOUT = 5000L - } -} - - -/** - * Inserts randomly generated arrays of strings at the event's editor's carets. - * - * @property arrayScheme the scheme to use for generating arrays - * @property dataInsertAction the action to generate data with - * @param icon the icon to display with the action - */ -abstract class DataInsertArrayAction( - private val arrayScheme: () -> ArrayScheme, - private val dataInsertAction: DataInsertAction, - icon: Icon = RandomnessIcons.Data.Array -) : DataInsertAction(icon) { - /** - * Generates array-like strings of random data. - * - * @param count the number of array-like strings to generate - * @return array-like strings of random data - * @throws DataGenerationException if data could not be generated - */ - @Throws(DataGenerationException::class) - override fun generateStrings(count: Int): List { - val arrayScheme = arrayScheme() - if (arrayScheme.count <= 0) - throw DataGenerationException("Array cannot have fewer than 1 element.") - - dataInsertAction.random = random - return dataInsertAction.generateStrings(count * arrayScheme.count) - .chunked(arrayScheme.count) - .map { arrayScheme.arrayify(it) } - } -} - - -/** - * Inserts the same randomly generated string at the event's editor's carets. - * - * @property dataInsertAction the action to generate data with - * @param icon the icon to display with the action - */ -abstract class DataInsertRepeatAction( - private val dataInsertAction: DataInsertAction, - icon: Icon = RandomnessIcons.Data.Array -) : DataInsertAction(icon) { - /** - * Generates a random datum and repeats it [count] times. - * - * @param count the number of times to repeat the data - * @return a random datum, repeated [count] times - * @throws DataGenerationException if data could not be generated - */ - @Throws(DataGenerationException::class) - override fun generateStrings(count: Int): List { - dataInsertAction.random = random - return dataInsertAction.generateString().let { string -> List(count) { string } } - } -} - - -/** - * Inserts the same randomly generated array of strings at the event's editor's carets. - * - * @property dataInsertArrayAction the action to generate data with - * @param icon the icon to display with the action - */ -abstract class DataInsertRepeatArrayAction( - private val dataInsertArrayAction: DataInsertArrayAction, - icon: Icon = RandomnessIcons.Data.Array -) : DataInsertAction(icon) { - /** - * Generates a random array-like string of random data and repeats it [count] times. - * - * @param count the number of times to repeat the data - * @return a random array-like string of random data and repeats it [count] times - * @throws DataGenerationException if data could not be generated - */ - @Throws(DataGenerationException::class) - override fun generateStrings(count: Int) = - dataInsertArrayAction.generateString().let { string -> List(count) { string } } -} - - -/** - * Opens the settings window for changing settings. - * - * @param icon the icon to display with the action - */ -abstract class DataSettingsAction(private val icon: Icon = RandomnessIcons.Data.Settings) : AnAction() { - /** - * The name of the action. - */ - abstract val name: String - - /** - * The class of the configurable maintaining the settings. - */ - protected abstract val configurableClass: Class> - - - /** - * Sets the title of this action. - * - * @param event carries information on the invocation place - */ - override fun update(event: AnActionEvent) { - super.update(event) - - event.presentation.text = name - event.presentation.icon = icon - } - - /** - * Opens the IntelliJ settings menu at the right location to adjust the `Configurable` of type [configurableClass]. - * - * @param event carries information on the invocation place - */ - override fun actionPerformed(event: AnActionEvent) = - ShowSettingsUtil.getInstance().showSettingsDialog(event.project, configurableClass) -} - - -/** - * Opens a popup to allow the user to quickly switch to the selected scheme. - * - * @param T the type of scheme that can be switched between - * @property settings the settings containing the schemes that can be switched between - * @property icon the icon to present with this action - */ -abstract class DataQuickSwitchSchemeAction>( - private val settings: Settings<*, T>, - private val icon: Icon = RandomnessIcons.Data.Settings -) : QuickSwitchSchemeAction(true) { - /** - * The name of the action. - */ - abstract val name: String - - - /** - * Sets the title and icon of this action. - * - * @param event carries information on the invocation place - */ - override fun update(event: AnActionEvent) { - super.update(event) - - event.presentation.text = name - event.presentation.icon = icon - } - - /** - * Adds actions for all schemes in `settings` to the given group. - * - * @param project ignored - * @param group the group to add actions to - * @param dataContext ignored - */ - override fun fillActions(project: Project?, group: DefaultActionGroup, dataContext: DataContext) { - val current = settings.currentScheme - - settings.schemes.forEach { scheme -> - val icon = if (scheme === current) AllIcons.Actions.Forward else ourNotCurrentAction - - group.add(object : DumbAwareAction(scheme.myName, "", icon) { - override fun actionPerformed(event: AnActionEvent) { - settings.currentSchemeName = scheme.name - } - }) - } - } -} diff --git a/src/main/kotlin/com/fwdekker/randomness/DummyActions.kt b/src/main/kotlin/com/fwdekker/randomness/DummyActions.kt deleted file mode 100644 index 1fc5e8450..000000000 --- a/src/main/kotlin/com/fwdekker/randomness/DummyActions.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.fwdekker.randomness - -import com.fwdekker.randomness.array.ArrayScheme -import com.fwdekker.randomness.array.ArraySettings -import icons.RandomnessIcons -import kotlin.random.Random - - -/** - * Inserts a dummy value. - * - * Mostly for testing and demonstration purposes. - * - * @property dummySupplier generates dummy values to insert - */ -class DummyInsertAction(private val dummySupplier: (Random) -> String) : DataInsertAction(RandomnessIcons.Data.Base) { - override val name = "Random Dummy" - - override fun generateStrings(count: Int) = List(count) { dummySupplier(random) } -} - -/** - * Inserts an array of dummy values. - * - * Mostly for testing and demonstration purposes. - * - * @param arrayScheme the scheme to use for generating arrays - * @param dummySupplier generates dummy values to insert - */ -class DummyInsertArrayAction( - arrayScheme: () -> ArrayScheme = { ArraySettings.default.currentScheme }, - dummySupplier: (Random) -> String -) : DataInsertArrayAction(arrayScheme, DummyInsertAction(dummySupplier), RandomnessIcons.Data.Array) { - override val name = "Random Dummy Array" -} - -/** - * Inserts a repeated array of dummy values. - * - * Mostly for testing and demonstration purposes. - * - * @param dummySupplier generates dummy values to insert - */ -class DummyInsertRepeatAction(dummySupplier: (Random) -> String) : - DataInsertRepeatAction(DummyInsertAction(dummySupplier), RandomnessIcons.Data.Repeat) { - override val name = "Random Repeat Dummy" -} - -/** - * Inserts a repeated array of dummy values. - * - * Mostly for testing and demonstration purposes. - * - * @param arrayScheme the scheme to use for generating arrays - * @param dummySupplier generates dummy values to insert - */ -class DummyInsertRepeatArrayAction( - arrayScheme: () -> ArrayScheme = { ArraySettings.default.currentScheme }, - dummySupplier: (Random) -> String -) : DataInsertRepeatArrayAction(DummyInsertArrayAction(arrayScheme, dummySupplier), RandomnessIcons.Data.Repeat) { - override val name = "Random Repeat Dummy" -} diff --git a/src/main/kotlin/com/fwdekker/randomness/ErrorReporter.kt b/src/main/kotlin/com/fwdekker/randomness/ErrorReporter.kt index 0201c66ae..21a19fdb1 100644 --- a/src/main/kotlin/com/fwdekker/randomness/ErrorReporter.kt +++ b/src/main/kotlin/com/fwdekker/randomness/ErrorReporter.kt @@ -2,10 +2,8 @@ package com.fwdekker.randomness import com.intellij.ide.BrowserUtil import com.intellij.ide.DataManager -import com.intellij.ide.plugins.IdeaPluginDescriptor import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.ErrorReportSubmitter import com.intellij.openapi.diagnostic.IdeaLoggingEvent import com.intellij.openapi.diagnostic.SubmittedReportInfo @@ -13,29 +11,26 @@ import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.util.Consumer import java.awt.Component -import java.net.IDN -import java.net.URI -import java.net.URL +import java.net.URLEncoder +import java.nio.charset.StandardCharsets /** * A report submitter that opens a pre-filled issue creation form on Randomness' GitHub repository. * * This class pertains to reports of exceptions that are not caught by the plugin and end up being shown to the user - * as a notification by IntelliJ. + * as a notification by the IDE. */ class ErrorReporter : ErrorReportSubmitter() { /** * Returns the text that is displayed in the button to report the error. - * - * @return the text that is displayed in the button to report the error */ - override fun getReportActionText() = "Report on GitHub" + override fun getReportActionText() = Bundle("reporter.report") /** - * Submits the exception as desired by the user. + * Submits the exception by opening the browser to create an issue on GitHub. * - * @param events the events that caused the exception + * @param events ignored * @param additionalInfo additional information provided by the user * @param parentComponent ignored * @param consumer ignored @@ -45,21 +40,19 @@ class ErrorReporter : ErrorReportSubmitter() { events: Array, additionalInfo: String?, parentComponent: Component, - consumer: Consumer + consumer: Consumer, ): Boolean { val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent)) - object : Backgroundable(project, "Opening GitHub in browser") { + object : Backgroundable(project, Bundle("reporter.opening")) { override fun run(indicator: ProgressIndicator) { - BrowserUtil.open(getIssueUrl(events, additionalInfo)) - ApplicationManager.getApplication().invokeLater { - consumer.consume( - SubmittedReportInfo( - "https://github.com/FWDekker/intellij-randomness/issues", - "Issue on GitHub", - SubmittedReportInfo.SubmissionStatus.NEW_ISSUE - ) + BrowserUtil.open(getIssueUrl(additionalInfo)) + consumer.consume( + SubmittedReportInfo( + "https://github.com/FWDekker/intellij-randomness/issues", + Bundle("reporter.issue"), + SubmittedReportInfo.SubmissionStatus.NEW_ISSUE ) - } + ) } }.queue() return true @@ -67,122 +60,48 @@ class ErrorReporter : ErrorReportSubmitter() { /** * Returns the privacy notice text. - * - * @return the privacy notice text */ - override fun getPrivacyNoticeText() = - """ - Pressing the Report button will open a form on a web page with the details of this error filled in. - Submitting the form requires a GitHub account and is subject to - GitHub's privacy policy. - """.trimIndent() + override fun getPrivacyNoticeText() = Bundle("reporter.privacy_notice") /** - * Constructs a URL to create an issue with the given information that is below the maximum URL limit. - * - * @param events the events that caused the exception - * @param additionalInfo additional information provided by the user - * @return a URL to create an issue with the given information that is below the maximum URL limit + * Constructs a URL to create an issue that provides [additionalInfo] and is below the maximum URL limit. */ - // Public for testability - fun getIssueUrl(events: Array, additionalInfo: String?): String { + fun getIssueUrl(additionalInfo: String?): String { val baseUrl = "https://github.com/FWDekker/intellij-randomness/issues/new?body=" + val additionalInfoSection = createMarkdownSection( "Additional info", - if (additionalInfo.isNullOrBlank()) "_No additional information provided._" - else additionalInfo + if (additionalInfo.isNullOrBlank()) MORE_DETAIL_MESSAGE else additionalInfo ) - val stackTracesSection = createMarkdownSection("Stacktraces", formatEvents(events)) + val stacktraceSection = createMarkdownSection("Stacktraces", STACKTRACE_MESSAGE) val versionSection = createMarkdownSection("Version information", getFormattedVersionInformation()) - val candidates = listOf( - additionalInfoSection + stackTracesSection + versionSection, - additionalInfoSection + versionSection, - stackTracesSection + versionSection, - versionSection, - "" - ) - return baseUrl + candidates.first { encodeUrl(baseUrl + it).length <= MAX_URL_LENGTH } - .replace(' ', '+') - .filterNot { it in listOf('#', '&', ';') } + return URLEncoder.encode(additionalInfoSection + stacktraceSection + versionSection, StandardCharsets.UTF_8) + .replace("%2B", "+") + .let { baseUrl + it } } /** - * Creates a Markdown "section" containing the title in bold followed by the contents on the next line, finalized by - * two newlines. - * - * @param title the title of the section - * @param contents the contents of the section - * @return a Markdown "section" with the given title and contents - */ - private fun createMarkdownSection(title: String, contents: String) = "**${title.trim()}**\n${contents.trim()}\n\n" - - /** - * Formats IDEA events as Markdown-style code blocks inside spoilers. - * - * @param events the events to format - */ - private fun formatEvents(events: Array) = - events.mapIndexed { i, event -> - wrapInMarkdownSpoiler( - title = "Stacktrace ${i + 1}/${events.size}", - contents = wrapInJavaCodeBlock(event.throwableText.trim()) - ) - }.joinToString("\n\n") - - /** - * Creates a Markdown-style Java code block. - * - * @param contents the contents of the code block - * @return a Markdown-style Java code block - */ - private fun wrapInJavaCodeBlock(contents: String) = "```java\n$contents\n```" - - /** - * Creates a Markdown-style spoiler with the given title and contents. - * - * @param title the title, which is the only thing that is displayed when the contents are hidden - * @param contents the contents which are initially hidden - * @return a Markdown-style spoiler with the given title - */ - private fun wrapInMarkdownSpoiler(title: String, contents: String) = - "
\n$title\n

\n\n$contents\n\n

\n
" - - /** - * Returns the version number of Randomness, or `null` if it could not be determined. - * - * @return the version number of Randomness, or `null` if it could not be determined + * Creates a Markdown "section" containing the [title] in bold followed by the [contents] on the next line, + * finalized by two newlines. */ - private fun getPluginVersion() = - if (pluginDescriptor is IdeaPluginDescriptor) (pluginDescriptor as IdeaPluginDescriptor).version - else null + private fun createMarkdownSection(title: String, contents: String) = + """ + **${title.trim()}** + ${contents.trim()} + """.trimIndent() /** * Returns version information on the user's environment as a Markdown-style list. - * - * @return version information on the user's environment as a Markdown-style list */ private fun getFormattedVersionInformation() = - listOf( - Pair("Randomness version", getPluginVersion() ?: "_Unknown_"), - Pair("IDE version", ApplicationInfo.getInstance().apiVersion), - Pair("Operating system", System.getProperty("os.name")), - Pair("Java version", System.getProperty("java.version")) - ).joinToString("\n") { "- ${it.first}: ${it.second}" } - - /** - * Correctly encodes a string describing a URL. - * - * Taken from https://stackoverflow.com/a/25735202. - * - * @param urlString the string to encode - * @return an encoded URL - */ - private fun encodeUrl(urlString: String) = - URL(urlString.replace(' ', '+')) - .let { URI(it.protocol, it.userInfo, IDN.toASCII(it.host), it.port, it.path, it.query, it.ref) } - .toASCIIString() + """ + - Randomness version: ${pluginDescriptor?.version ?: "_Unknown_"} + - IDE version: ${ApplicationInfo.getInstance().apiVersion} + - Operating system: ${System.getProperty("os.name")} + - Java version: ${System.getProperty("java.version")} + """.trimIndent() /** @@ -190,8 +109,15 @@ class ErrorReporter : ErrorReportSubmitter() { */ companion object { /** - * Maximum URL length supported by GitHub, experimentally verified. + * Message asking the user to provide more information about the exception. + */ + const val MORE_DETAIL_MESSAGE = + "Please describe your issue in more detail here. What were you doing when the exception occurred?" + + /** + * Message asking the user to provide stacktrace information. */ - const val MAX_URL_LENGTH = 8000 + const val STACKTRACE_MESSAGE = + "Please paste the full stacktrace from the IDE's error popup below.\n```java\n\n```" } } diff --git a/src/main/kotlin/com/fwdekker/randomness/Icons.kt b/src/main/kotlin/com/fwdekker/randomness/Icons.kt new file mode 100644 index 000000000..6f2a0912b --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/Icons.kt @@ -0,0 +1,345 @@ +package com.fwdekker.randomness + +import com.intellij.openapi.util.IconLoader +import com.intellij.ui.ColorUtil +import com.intellij.util.IconUtil +import java.awt.Color +import java.awt.Component +import java.awt.Graphics +import java.awt.image.RGBImageFilter +import javax.swing.Icon +import kotlin.math.atan2 + + +/** + * Basic Randomness icons. + */ +object Icons { + /** + * The main icon of Randomness. + */ + val RANDOMNESS = IconLoader.findIcon("/icons/randomness.svg", this.javaClass.classLoader)!! + + /** + * The template icon for template icons. + */ + val TEMPLATE = IconLoader.findIcon("/icons/template.svg", this.javaClass.classLoader)!! + + /** + * The template icon for scheme icons. + */ + val SCHEME = IconLoader.findIcon("/icons/scheme.svg", this.javaClass.classLoader)!! + + /** + * An icon for settings. + */ + val SETTINGS = IconLoader.findIcon("/icons/settings.svg", this.javaClass.classLoader)!! + + /** + * A filled-in version of [SETTINGS]. + */ + val SETTINGS_FILLED = IconLoader.findIcon("/icons/settings-filled.svg", this.javaClass.classLoader)!! + + /** + * An icon for arrays. + */ + val ARRAY = IconLoader.findIcon("/icons/array.svg", this.javaClass.classLoader)!! + + /** + * A filled-in version of [ARRAY]. + */ + val ARRAY_FILLED = IconLoader.findIcon("/icons/array-filled.svg", this.javaClass.classLoader)!! + + /** + * An icon for references. + */ + val REFERENCE = IconLoader.findIcon("/icons/reference.svg", this.javaClass.classLoader)!! + + /** + * A filled-in version of [REFERENCE]. + */ + val REFERENCE_FILLED = IconLoader.findIcon("/icons/reference-filled.svg", this.javaClass.classLoader)!! + + /** + * An icon for repeated insertions. + */ + val REPEAT = IconLoader.findIcon("/icons/repeat.svg", this.javaClass.classLoader)!! + + /** + * A filled-in version of [REPEAT]. + */ + val REPEAT_FILLED = IconLoader.findIcon("/icons/repeat-filled.svg", this.javaClass.classLoader)!! +} + + +/** + * A colored icon with some text in it. + * + * @property base The underlying icon which should be given color; must be square. + * @property text The text to display inside the [base]. + * @property colors The colors to give to the [base]. + */ +data class TypeIcon(val base: Icon, val text: String, val colors: List) : Icon { + init { + require(colors.isNotEmpty()) { "At least one color must be defined." } + } + + + /** + * Paints the colored text icon. + * + * @param c a [Component] to get properties useful for painting + * @param g the graphics context + * @param x the X coordinate of the icon's top-left corner + * @param y the Y coordinate of the icon's top-left corner + */ + override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) { + if (c == null || g == null) return + + val filter = RadialColorReplacementFilter(colors, Pair(iconWidth / 2, iconHeight / 2)) + IconUtil.filterIcon(base, { filter }, c).paintIcon(c, g, x, y) + + val textIcon = IconUtil.textToIcon(text, c, FONT_SIZE * iconWidth) + textIcon.paintIcon(c, g, x + (iconWidth - textIcon.iconWidth) / 2, y + (iconHeight - textIcon.iconHeight) / 2) + } + + /** + * The width of the base icon. + */ + override fun getIconWidth() = base.iconWidth + + /** + * The height of the base icon. + */ + override fun getIconHeight() = base.iconHeight + + + /** + * Holds constants. + */ + companion object { + /** + * The scale of the text inside the icon relative to the icon's size. + */ + const val FONT_SIZE = 12f / 32f + + + /** + * Returns a single icon that describes all [icons], or `null` if [icons] is empty. + */ + fun combine(icons: Collection): TypeIcon? = + if (icons.isEmpty()) null + else TypeIcon( + Icons.TEMPLATE, + if (icons.map { it.text }.toSet().size == 1) icons.first().text else "", + icons.flatMap { it.colors } + ) + } +} + +/** + * An overlay icon, which can be displayed on top of other icons. + * + * This icon is drawn as the [base] surrounded by a small margin of background color, which creates visual distance + * between the overlay and the rest of the icon this overlay is shown in top of. The background color is determined when + * the icon is drawn. + * + * @property base The base of the icon; must be square. + * @property background The background shape to ensure that the small margin of background color is also applied inside + * the [base], or `null` if [base] is already a solid shape. + */ +data class OverlayIcon(val base: Icon, val background: Icon? = null) : Icon { + /** + * Paints the overlay icon. + * + * @param c a [Component] to get properties useful for painting + * @param g the graphics context + * @param x the X coordinate of the icon's top-left corner + * @param y the Y coordinate of the icon's top-left corner + */ + override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) { + if (c == null || g == null) return + + IconUtil.filterIcon(background ?: base, { RadialColorReplacementFilter(listOf(c.background)) }, c) + .paintIcon(c, g, x, y) + IconUtil.scale(base, c, 1 - 2 * MARGIN) + .paintIcon(c, g, x + (MARGIN * iconWidth).toInt(), y + (MARGIN * iconHeight).toInt()) + } + + /** + * The width of the base icon. + */ + override fun getIconWidth() = base.iconWidth + + /** + * The height of the base icon. + */ + override fun getIconHeight() = base.iconHeight + + + /** + * Holds constants. + */ + companion object { + /** + * The margin around the base image that is filled with background color. + * + * This number is a fraction relative to the base image's size. + */ + const val MARGIN = 4f / 32 + + + /** + * Overlay icon for arrays. + */ + val ARRAY by lazy { OverlayIcon(Icons.ARRAY, Icons.ARRAY_FILLED) } + + /** + * Overlay icon for template references. + */ + val REFERENCE by lazy { OverlayIcon(Icons.REFERENCE, Icons.REFERENCE_FILLED) } + + /** + * Overlay icon for repeated insertion. + */ + val REPEAT by lazy { OverlayIcon(Icons.REPEAT, Icons.REPEAT_FILLED) } + + /** + * Overlay icon for settings. + */ + val SETTINGS by lazy { OverlayIcon(Icons.SETTINGS, Icons.SETTINGS_FILLED) } + } +} + +/** + * An icon with various icons displayed on top of it as overlays. + * + * @property base The underlying base icon. + * @property overlays The various icons that are overlayed on top of [base]. + */ +data class OverlayedIcon(val base: Icon, val overlays: List = emptyList()) : Icon { + init { + require(base.iconWidth == base.iconHeight) { "Base must be square." } + require(overlays.all { it.iconWidth == it.iconHeight }) { "Overlays must be square." } + require(overlays.map { it.iconWidth }.toSet().size <= 1) { "All overlays must have same size." } + } + + + /** + * Returns a copy of this icon that has [icon] as an additional overlay icon. + */ + fun plusOverlay(icon: Icon) = copy(overlays = overlays + icon) + + + /** + * Paints the scheme icon. + * + * @param c a [Component] to get properties useful for painting + * @param g the graphics context + * @param x the X coordinate of the icon's top-left corner + * @param y the Y coordinate of the icon's top-left corner + */ + override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) { + if (c == null || g == null) return + + base.paintIcon(c, g, x, y) + overlays.forEachIndexed { i, overlay -> + val overlaySize = iconWidth.toFloat() / OVERLAYS_PER_ROW + val overlayX = (i % OVERLAYS_PER_ROW * overlaySize).toInt() + val overlayY = (i / OVERLAYS_PER_ROW * overlaySize).toInt() + + IconUtil.scale(overlay, null, overlaySize / overlay.iconWidth).paintIcon(c, g, overlayX, overlayY) + } + } + + /** + * The width of the base icon. + */ + override fun getIconWidth() = base.iconWidth + + /** + * The height of the base icon. + */ + override fun getIconHeight() = base.iconHeight + + + /** + * Holds constants. + */ + companion object { + /** + * Number of overlays displayed per row. + */ + const val OVERLAYS_PER_ROW = 2 + } +} + + +/** + * Replaces all colors with one of [colors] depending on the angle relative to [center]. + * + * @param colors the colors that should be used, in clockwise order starting north-west + * @param center the center relative to which colors should be calculated; not required if only one color is given + */ +class RadialColorReplacementFilter( + private val colors: List, + private val center: Pair? = null, +) : RGBImageFilter() { + init { + require(colors.isNotEmpty()) { "At least one color must be defined." } + require(colors.size == 1 || center != null) { "Center must be defined if more than one color is given." } + } + + + /** + * Returns the color to be displayed at ([x], [y]), considering the coordinates relative to the [center] and the + * relative alpha of the encountered color. + * + * @param x the X coordinate of the pixel + * @param y the Y coordinate of the pixel + * @param rgb `0` if and only if the pixel's color should be replaced + * @return `0` if [rgb] is `0`, or one of [colors] with its alpha shifted by [rgb]'s alpha otherwise + */ + @Suppress("UseJBColor") // Filtering works the same in both themes + override fun filterRGB(x: Int, y: Int, rgb: Int) = + if (rgb == 0) 0 + else if (center == null || colors.size == 1) shiftAlpha(colors[0], Color(rgb, true)).rgb + else shiftAlpha(positionToColor(Pair(x - center.first, y - center.second)), Color(rgb, true)).rgb + + + /** + * Returns [toShift] which has its alpha multiplied by that of [shiftBy]. + */ + private fun shiftAlpha(toShift: Color, shiftBy: Color) = + ColorUtil.withAlpha(toShift, asFraction(toShift.alpha) * asFraction(shiftBy.alpha)) + + /** + * Represents a [number] in the range `[0, 256)` as a fraction of that range. + */ + private fun asFraction(number: Int) = number / COMPONENT_MAX.toDouble() + + /** + * Returns the appropriate color from [colors] for an [offset] relative to the [center]. + */ + private fun positionToColor(offset: Pair): Color { + val angle = 2 * Math.PI - (atan2(offset.second.toDouble(), offset.first.toDouble()) + STARTING_ANGLE) + val index = angle / (2 * Math.PI / colors.size) + return colors[Math.floorMod(index.toInt(), colors.size)] + } + + + /** + * Holds constants. + */ + companion object { + /** + * Maximum value for an RGB component. + */ + const val COMPONENT_MAX = 255 + + /** + * The angle in radians at which the first color should start being displayed. + */ + const val STARTING_ANGLE = -(3 * Math.PI / 4) + } +} diff --git a/src/main/kotlin/com/fwdekker/randomness/InsertAction.kt b/src/main/kotlin/com/fwdekker/randomness/InsertAction.kt new file mode 100644 index 000000000..70e6b6aa5 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/InsertAction.kt @@ -0,0 +1,109 @@ +package com.fwdekker.randomness + +import com.fwdekker.randomness.Timely.generateTimely +import com.intellij.codeInsight.hint.HintManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.options.newEditor.SettingsDialogFactory +import javax.swing.Icon + + +/** + * Inserts strings in the editor. + * + * If [configurable] is not `null` when [actionPerformed] is invoked, a modal editor is shown to edit that + * [configurable] right before inserting the strings to allow for last-minute adjustments to how the strings are + * generated. + * + * @property repeat `true` if and only if the same value should be inserted at each caret. + * @property text The text that identifies the action to the user. + * @param description the optional description of the action + * @param icon the icon that represents the action + */ +abstract class InsertAction( + val repeat: Boolean = false, + val text: String, + description: String? = null, + icon: Icon? = null, +) : AnAction(text, description, icon) { + /** + * The configurable to open as soon as the action is performed but before the strings are inserted. + * + * Use this to make modifications to settings right before inserting strings. + */ + protected open val configurable: Configurable? = null + + + /** + * Specifies the thread in which [update] is invoked. + */ + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + /** + * Sets the title of this action and disables this action if no editor is currently opened. + * + * @param event carries contextual information + */ + override fun update(event: AnActionEvent) { + val presentation = event.presentation + val editor = event.getData(CommonDataKeys.EDITOR) + + presentation.isEnabled = editor?.document?.isWritable == true + } + + /** + * Inserts the data generated by the scheme at the caret(s) in the editor; one datum for each caret. + * + * @param event carries contextual information + */ + @Suppress("detekt:ReturnCount") // Result of null checks at start + override fun actionPerformed(event: AnActionEvent) { + val editor = event.getData(CommonDataKeys.EDITOR) ?: return + val project = event.getData(CommonDataKeys.PROJECT) ?: return + + configurable?.also { + if (!SettingsDialogFactory.getInstance().create(project, text, it, false, false).showAndGet()) + return + } + + val data = + try { + generateTimely { + if (repeat) + generateStrings(1).single().let { string -> List(editor.caretModel.caretCount) { string } } + else + generateStrings(editor.caretModel.caretCount) + } + } catch (exception: DataGenerationException) { + HintManager.getInstance().showErrorHint( + editor, + if (exception.message.isNullOrBlank()) + Bundle("shared.error.could_not_generate") + else + Bundle("shared.error.could_not_generate.no_message", exception.message) + ) + return + } + + WriteCommandAction.runWriteCommandAction(project) { + editor.caretModel.allCarets.forEachIndexed { i, caret -> + val start = caret.selectionStart + val end = caret.selectionEnd + val newEnd = start + data[i].length + + editor.document.replaceString(start, end, data[i]) + caret.setSelection(start, newEnd) + } + } + } + + + /** + * Generates [count] strings. + */ + abstract fun generateStrings(count: Int): List +} diff --git a/src/main/kotlin/com/fwdekker/randomness/ListHelpers.kt b/src/main/kotlin/com/fwdekker/randomness/ListHelpers.kt new file mode 100644 index 000000000..dfbad7c3d --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/ListHelpers.kt @@ -0,0 +1,10 @@ +package com.fwdekker.randomness + + +/** + * Removes all elements from this collection and adds all elements from [collection]. + */ +fun MutableCollection.setAll(collection: Collection) { + clear() + addAll(collection) +} diff --git a/src/main/kotlin/com/fwdekker/randomness/Notifier.kt b/src/main/kotlin/com/fwdekker/randomness/Notifier.kt new file mode 100644 index 000000000..977215931 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/Notifier.kt @@ -0,0 +1,64 @@ +package com.fwdekker.randomness + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.StartupActivity +import com.intellij.openapi.ui.MessageDialogBuilder + + +/** + * Displays notifications when a project is opened. + */ +class Notifier : StartupActivity { + /** + * Shows startup notifications. + */ + override fun runActivity(project: Project) = showWelcomeToV3(project) + + /** + * Shows a notification introducing the user to version 3 of Randomness. + */ + private fun showWelcomeToV3(project: Project) { + val key = "notifications.welcome_to_v3" + val oldConfig = PathManager.getOptionsFile("randomness") + + val propComp = PropertiesComponent.getInstance() + val propKey = "com.fwdekker.randomness.$key" + if (propComp.isTrueValue(propKey) || !oldConfig.exists()) + return + + NotificationGroupManager.getInstance() + .getNotificationGroup("com.fwdekker.randomness.updates") + .createNotification(Bundle("$key.title"), Bundle("$key.content"), NotificationType.INFORMATION) + .setSubtitle(Bundle("$key.subtitle")) + .setIcon(Icons.RANDOMNESS) + .setSuggestionType(true) + .addAction(object : NotificationAction(Bundle("$key.delete_old_config")) { + override fun actionPerformed(event: AnActionEvent, notification: Notification) { + MessageDialogBuilder + .yesNo(Bundle("$key.delete_old_config_confirm"), "", Icons.RANDOMNESS) + .ask(project) + .also { + if (it) { + oldConfig.delete() + propComp.setValue(propKey, true) + notification.hideBalloon() + } + } + } + }) + .addAction(object : NotificationAction(Bundle("$key.do_not_ask_again")) { + override fun actionPerformed(event: AnActionEvent, notification: Notification) { + propComp.setValue(propKey, true) + notification.hideBalloon() + } + }) + .notify(project) + } +} diff --git a/src/main/kotlin/com/fwdekker/randomness/PopupAction.kt b/src/main/kotlin/com/fwdekker/randomness/PopupAction.kt index 812e85163..9a28f312b 100644 --- a/src/main/kotlin/com/fwdekker/randomness/PopupAction.kt +++ b/src/main/kotlin/com/fwdekker/randomness/PopupAction.kt @@ -1,72 +1,72 @@ package com.fwdekker.randomness -import com.fwdekker.randomness.array.ArraySettingsAction -import com.fwdekker.randomness.decimal.DecimalGroupAction -import com.fwdekker.randomness.decimal.DecimalSettingsAction -import com.fwdekker.randomness.integer.IntegerGroupAction -import com.fwdekker.randomness.integer.IntegerSettingsAction -import com.fwdekker.randomness.string.StringGroupAction -import com.fwdekker.randomness.string.StringSettingsAction -import com.fwdekker.randomness.ui.registerModifierActions -import com.fwdekker.randomness.uuid.UuidGroupAction -import com.fwdekker.randomness.uuid.UuidSettingsAction -import com.fwdekker.randomness.word.WordGroupAction -import com.fwdekker.randomness.word.WordSettingsAction +import com.fwdekker.randomness.template.TemplateGroupAction +import com.fwdekker.randomness.template.TemplateSettingsAction import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.Separator import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.popup.list.ListPopupImpl -import icons.RandomnessIcons import java.awt.event.ActionEvent +import java.awt.event.KeyEvent +import java.util.Locale +import javax.swing.AbstractAction +import javax.swing.KeyStroke /** * Shows a popup for all available Randomness actions. */ -class PopupAction : AnAction() { +class PopupAction : AnAction(Icons.RANDOMNESS) { /** - * Whether the user focused the editor when opening this popup. + * `true` if and only if the user focused a non-viewer editor when opening this popup. */ - private var hasEditor: Boolean = true + private var isEditable: Boolean = true + /** + * Specifies the thread in which [update] is invoked. + */ + override fun getActionUpdateThread() = ActionUpdateThread.EDT + /** * Sets the icon of this action. * - * @param event carries information on the invocation place + * @param event carries contextual information */ override fun update(event: AnActionEvent) { - event.presentation.icon = RandomnessIcons.Data.Base - hasEditor = event.getData(CommonDataKeys.EDITOR) != null + event.presentation.icon = Icons.RANDOMNESS + + // Running this in [actionPerformed] always sets it to `true` + isEditable = event.getData(CommonDataKeys.EDITOR)?.document?.isWritable == true } /** * Displays a popup with all actions provided by Randomness. * - * @param event carries information on the invocation place + * @param event carries contextual information */ override fun actionPerformed(event: AnActionEvent) { - val popupGroup = if (hasEditor) PopupGroup() else SettingsOnlyPopupGroup() + val popupGroup = if (isEditable) PopupGroup() else SettingsOnlyPopupGroup() val popup = JBPopupFactory.getInstance() .createActionGroupPopup( - TITLE, popupGroup, event.dataContext, - JBPopupFactory.ActionSelectionAid.NUMBERING, + Bundle("popup.title"), popupGroup, event.dataContext, + JBPopupFactory.ActionSelectionAid.ALPHA_NUMBERING, true ) as ListPopupImpl - popup.speedSearch.setEnabled(false) - if (hasEditor) { - popup.setCaption(TITLE) - popup.setAdText(AD_TEXT) + if (isEditable) { + popup.setCaption(Bundle("popup.title")) + popup.setAdText(Bundle("popup.ad")) popup.registerModifierActions { this.captionModifier(it) } } else { - popup.setCaption(CTRL_TITLE) - popup.setAdText("Editor is not selected. Displaying settings only.") - popup.registerModifierActions { CTRL_TITLE } + popup.setCaption(Bundle("popup.title.ctrl")) + popup.setAdText(Bundle("popup.ad.settings_only")) + popup.registerModifierActions { Bundle("popup.title.ctrl") } } popup.showInBestPositionFor(event.dataContext) @@ -74,12 +74,9 @@ class PopupAction : AnAction() { /** - * Returns the desired title for the popup given an event. - * - * @param event the event on which the title should be based - * @return the desired title for the popup given an event + * Returns the desired title for the popup given [event]. */ - @Suppress("ComplexMethod") // Cannot be simplified + @Suppress("detekt:ComplexMethod") // Cannot be simplified private fun captionModifier(event: ActionEvent?): String { val modifiers = event?.modifiers ?: 0 val altPressed = modifiers and ActionEvent.ALT_MASK != 0 @@ -87,58 +84,46 @@ class PopupAction : AnAction() { val shiftPressed = modifiers and ActionEvent.SHIFT_MASK != 0 return when { - altPressed && ctrlPressed && shiftPressed -> ALT_CTRL_SHIFT_TITLE - altPressed && ctrlPressed -> ALT_CTRL_TITLE - altPressed && shiftPressed -> ALT_SHIFT_TITLE - ctrlPressed && shiftPressed -> CTRL_SHIFT_TITLE - altPressed -> ALT_TITLE - ctrlPressed -> CTRL_TITLE - shiftPressed -> SHIFT_TITLE - else -> TITLE + altPressed && ctrlPressed && shiftPressed -> Bundle("popup.title.alt_ctrl_shift") + altPressed && ctrlPressed -> Bundle("popup.title.alt_ctrl") + altPressed && shiftPressed -> Bundle("popup.title.alt_shift") + ctrlPressed && shiftPressed -> Bundle("popup.title.ctrl_shift") + altPressed -> Bundle("popup.title.alt") + ctrlPressed -> Bundle("popup.title.ctrl") + shiftPressed -> Bundle("popup.title.shift") + else -> Bundle("popup.title") } } /** - * The `ActionGroup` containing all Randomness actions. + * The [ActionGroup] containing all Randomness actions. */ private class PopupGroup : ActionGroup() { /** * Returns all group actions. * - * @param event carries information on the invocation place + * @param event carries contextual information */ override fun getChildren(event: AnActionEvent?) = - arrayOf( - IntegerGroupAction(), - DecimalGroupAction(), - StringGroupAction(), - WordGroupAction(), - UuidGroupAction(), - Separator(), - ArraySettingsAction() - ) + Settings.DEFAULT.templates.map { TemplateGroupAction(it) }.toTypedArray() + + Separator() + + TemplateSettingsAction() } /** - * The `ActionGroup` containing only settings-related actions. + * The [ActionGroup] containing only settings-related actions. */ private class SettingsOnlyPopupGroup : ActionGroup() { /** * Returns all settings actions. * - * @param event carries information on the invocation place + * @param event carries contextual information */ override fun getChildren(event: AnActionEvent?) = - arrayOf( - IntegerSettingsAction(), - DecimalSettingsAction(), - StringSettingsAction(), - WordSettingsAction(), - UuidSettingsAction(), - Separator(), - ArraySettingsAction() - ) + Settings.DEFAULT.templates.map { TemplateSettingsAction(it) }.toTypedArray() + + Separator() + + TemplateSettingsAction() } @@ -147,48 +132,111 @@ class PopupAction : AnAction() { */ companion object { /** - * The default popup title. - */ - const val TITLE = "Insert Data" - - /** - * The popup title while the alt key is held down. - */ - const val ALT_TITLE = "Insert Repeated Data" - - /** - * The popup title when the alt and control keys are held down. - */ - const val ALT_CTRL_TITLE = "Quick Switch Scheme" - - /** - * The popup title when the alt, control, and shift keys are held down. - */ - const val ALT_CTRL_SHIFT_TITLE = "Quick Switch Array Scheme" - - /** - * The popup title when the alt and shift keys are held down. + * Returns the mnemonic for the entry at the [index]th row. + * + * @see JBPopupFactory.ActionSelectionAid.ALPHA_NUMBERING */ - const val ALT_SHIFT_TITLE = "Insert Repeated Array" + fun indexToMnemonic(index: Int) = + (('1'..'9') + '0' + ('A'..'Z')).getOrNull(index) + } +} - /** - * The popup title when the control key is held down. - */ - const val CTRL_TITLE = "Change Settings" - /** - * The popup title when the control and shift keys are held down. - */ - const val CTRL_SHIFT_TITLE = "Change Array Settings" +/** + * An [AbstractAction] that uses [myActionPerformed] as the implementation of its [actionPerformed] method. + * + * @param myActionPerformed the code to execute in [actionPerformed] + */ +private class SimpleAbstractAction(private val myActionPerformed: (ActionEvent?) -> Unit) : AbstractAction() { + /** + * @see myActionPerformed + */ + override fun actionPerformed(event: ActionEvent?) = myActionPerformed(event) +} - /** - * The popup title when the shift key is held down. - */ - const val SHIFT_TITLE = "Insert Array" +/** + * Returns the cartesian product of [this] and [other]. + * + * By requiring both lists to actually be lists of lists, this method can be chained. + * + * Consider the following examples, using a simplified notation for lists for readability: + * ``` + * $ [[1, 2]] * [[3, 4]] + * [[1, 3], [1, 4], [2, 3], [2, 4]] + * + * $ [[1, 2]] * [[3, 4]] * [[5, 6]] + * [[1, 3, 5], [1, 3, 6], [1, 4, 5], [1, 4, 6], [2, 3, 5], [2, 3, 6], [2, 4, 5], [2, 4, 6]] + * ``` + */ +private operator fun List>.times(other: List>) = + this.flatMap { t1 -> other.map { t2 -> t1 + t2 } } - /** - * The text shown at the bottom of the popup. - */ - const val AD_TEXT = "Shift = Array. Ctrl = Settings. Alt = Repeat." +/** + * Registers actions such that actions can be selected while holding (combinations of) modifier keys. + * + * All combinations of modifier keys are registered for events. Additionally, the [captionModifier] function is invoked + * every time the user presses or releases any modifier key, even while holding other modifier keys. + * + * Events are also registered for pressing the Enter key (with or without modifier keys) to invoke the action that is + * currently highlighted, and for pressing one of the numbers 1-9 (with or without modifier keys) to invoke the action + * at that index in the popup. + * + * @param captionModifier returns the caption to set based on the event + */ +fun ListPopupImpl.registerModifierActions(captionModifier: (ActionEvent?) -> String) { + val modifiers = listOf(listOf("alt"), listOf("control"), listOf("shift")) + val optionalModifiers = listOf(listOf("")) + modifiers + + (modifiers * optionalModifiers * optionalModifiers).forEach { (a, b, c) -> + registerAction( + "${a}${b}${c}Released", + KeyStroke.getKeyStroke("$b $c released ${a.uppercase(Locale.getDefault())}"), + SimpleAbstractAction { setCaption(captionModifier(it)) } + ) + registerAction( + "${a}${b}${c}Pressed", + KeyStroke.getKeyStroke("$a $b $c pressed ${a.uppercase(Locale.getDefault())}"), + SimpleAbstractAction { setCaption(captionModifier(it)) } + ) + registerAction( + "${a}${b}${c}invokeAction", + KeyStroke.getKeyStroke("$a $b $c pressed ENTER"), + SimpleAbstractAction { event -> + event ?: return@SimpleAbstractAction + + handleSelect( + true, + KeyEvent( + component, + event.id, event.getWhen(), event.modifiers, + KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED, KeyEvent.KEY_LOCATION_UNKNOWN + ) + ) + } + ) + + @Suppress("detekt:ForEachOnRange") // Not relevant for such small numbers + (0 until list.model.size) + .associateWith { PopupAction.indexToMnemonic(it) } + .filterValues { it != null } + .forEach { (index, mnemonic) -> + registerAction( + "${a}${b}${c}invokeAction$mnemonic", + KeyStroke.getKeyStroke("$a $b $c pressed $mnemonic"), + SimpleAbstractAction { event -> + event ?: return@SimpleAbstractAction + + list.addSelectionInterval(index, index) + handleSelect( + true, + KeyEvent( + component, + event.id, event.getWhen(), event.modifiers, + KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED, KeyEvent.KEY_LOCATION_UNKNOWN + ) + ) + } + ) + } } } diff --git a/src/main/kotlin/com/fwdekker/randomness/Scheme.kt b/src/main/kotlin/com/fwdekker/randomness/Scheme.kt new file mode 100644 index 000000000..c0ccef3b0 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/Scheme.kt @@ -0,0 +1,132 @@ +package com.fwdekker.randomness + +import com.fwdekker.randomness.affix.AffixDecorator +import com.fwdekker.randomness.array.ArrayDecorator +import com.fwdekker.randomness.fixedlength.FixedLengthDecorator +import com.intellij.util.xmlb.annotations.Transient +import com.intellij.util.xmlb.annotations.XCollection +import kotlin.random.Random + + +/** + * A scheme is a [State] that is also a configurable random number generator. + * + * Schemes may use [DecoratorScheme]s to extend their functionality. + */ +abstract class Scheme : State() { + /** + * The name of the scheme as shown to the user. + */ + abstract val name: String + + /** + * The icon signifying the type of data represented by this scheme, ignoring its [decorators], or `null` if this + * scheme does not represent any kind of data, as is the case for [DecoratorScheme]s. + */ + @get:Transient + open val typeIcon: TypeIcon? get() = null + + /** + * The icon signifying this scheme in its entirety, or `null` if it does not have an icon. + */ + @get:Transient + open val icon: OverlayedIcon? get() = typeIcon?.let { OverlayedIcon(it, decorators.mapNotNull(Scheme::icon)) } + + /** + * Additional logic that determines how strings are generated. + * + * Decorators are automatically applied when [generateStrings] is invoked. To generate strings without using + * decorators, use [generateUndecoratedStrings]. Decorators are applied in ascending order. That is, the output of + * the scheme is fed into the decorator at index `0`, and that output is fed into the decorator at index `1`, and so + * on. + */ + @get:Transient + @get:XCollection(elementTypes = [AffixDecorator::class, ArrayDecorator::class, FixedLengthDecorator::class]) + abstract val decorators: List + + /** + * The random number generator used to generate random values. + */ + @get:Transient + var random: Random = Random.Default + + + override fun applyContext(context: Box) { + super.applyContext(context) + decorators.forEach { it.applyContext(context) } + } + + + /** + * Generates [count] random decorated data according to the settings in this scheme and its decorators. + * + * By default, this method applies the decorators on the output of [generateUndecoratedStrings]. Override this + * method if the scheme should interact with its decorators in a different way. + * + * @throws DataGenerationException if data could not be generated + */ + @Throws(DataGenerationException::class) + open fun generateStrings(count: Int = 1): List { + doValidate()?.also { throw DataGenerationException(it) } + + return decorators + .fold(this::generateUndecoratedStrings) { previousGenerator, currentScheme -> + currentScheme.random = random + currentScheme.generator = previousGenerator + currentScheme::generateStrings + } + .invoke(count) + } + + /** + * Generates [count] random data according to the settings in this scheme, ignoring settings from decorators. + * + * @throws DataGenerationException if data could not be generated + * @see generateStrings + */ + @Throws(DataGenerationException::class) + protected abstract fun generateUndecoratedStrings(count: Int = 1): List + + + abstract override fun deepCopy(retainUuid: Boolean): Scheme +} + +/** + * Transparently extends or alters the functionality of a [Scheme] with a decorating function. + * + * Requires that [generator] is set before invoking [generateStrings]. + */ +@Suppress("detekt:LateinitUsage") // Alternatives not feasible +abstract class DecoratorScheme : Scheme() { + /** + * Whether this decorator is enabled, or whether any invocation of [generateStrings] should be passed directly to + * the [generator]. + */ + protected open val isEnabled: Boolean = true + + /** + * The generating function whose output should be decorated. + */ + @get:Transient + lateinit var generator: (Int) -> List + + + override fun generateStrings(count: Int): List { + doValidate()?.also { throw DataGenerationException(it) } + + return if (isEnabled) super.generateStrings(count) + else generator(count) + } + + + abstract override fun deepCopy(retainUuid: Boolean): DecoratorScheme +} + + +/** + * Thrown if a random datum could not be generated. + * + * @param message the detail message + * @param cause the cause + */ +class DataGenerationException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) diff --git a/src/main/kotlin/com/fwdekker/randomness/SchemeEditor.kt b/src/main/kotlin/com/fwdekker/randomness/SchemeEditor.kt new file mode 100644 index 000000000..7f0f421e1 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/SchemeEditor.kt @@ -0,0 +1,80 @@ +package com.fwdekker.randomness + +import com.fwdekker.randomness.ui.addChangeListenerTo +import com.intellij.openapi.Disposable +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.util.Disposer +import javax.swing.JComponent + + +/** + * An editor for a [Scheme]. + * + * @param S the type of scheme edited in this editor + * @property scheme The scheme edited in this editor. + */ +abstract class SchemeEditor(val scheme: S) : Disposable { + /** + * The root component of the editor. + */ + abstract val rootComponent: DialogPanel + + /** + * The components contained within this editor that determine the editor's current state. + */ + val components: Collection + get() = rootComponent.components.filterNot { it.name == null } + extraComponents + + /** + * The additional [components] that determine the editor's current state but do not have a name. + * + * Do not register [SchemeEditor]s here; use the [decoratorEditors] field for that. + */ + protected val extraComponents = mutableListOf() + + /** + * The [SchemeEditor]s of [scheme]'s [DecoratorScheme]s. + * + * The editors registered in this list are automatically reset and applied in [reset] and [apply], respectively. + */ + protected val decoratorEditors = mutableListOf>() + + /** + * The component that this editor prefers to be focused when the editor is focused. + */ + open val preferredFocusedComponent: JComponent? + get() = components.filterIsInstance().firstOrNull { it.isVisible } + + + /** + * Resets the editor's state to that of [scheme]. + * + * If [apply] has been called, then [reset] resets to the state at the last invocation of [apply]. + */ + fun reset() { + rootComponent.reset() + decoratorEditors.forEach { it.reset() } + } + + /** + * Saves the editor's state into [scheme]. + */ + fun apply() { + rootComponent.apply() + decoratorEditors.forEach { it.apply() } + } + + + /** + * Ensures [listener] is invoked on every change in this editor. + */ + @Suppress("detekt:SpreadOperator") // Acceptable because this method is called rarely + fun addChangeListener(listener: () -> Unit) = + addChangeListenerTo(*(components + decoratorEditors).toTypedArray(), listener = listener) + + + /** + * Disposes this editor's resources. + */ + override fun dispose() = decoratorEditors.forEach { Disposer.dispose(it) } +} diff --git a/src/main/kotlin/com/fwdekker/randomness/Settings.kt b/src/main/kotlin/com/fwdekker/randomness/Settings.kt index 7d905c675..90c244b0d 100644 --- a/src/main/kotlin/com/fwdekker/randomness/Settings.kt +++ b/src/main/kotlin/com/fwdekker/randomness/Settings.kt @@ -1,102 +1,122 @@ package com.fwdekker.randomness +import com.fwdekker.randomness.PersistentSettings.Companion.CURRENT_VERSION +import com.fwdekker.randomness.template.Template +import com.fwdekker.randomness.template.TemplateList import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.SettingsCategory +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.util.xmlb.XmlSerializer +import com.intellij.util.xmlb.annotations.OptionTag import com.intellij.util.xmlb.annotations.Transient +import org.jdom.Element +import java.lang.module.ModuleDescriptor.Version +import com.intellij.openapi.components.State as JBState /** - * Settings are composed of [Scheme]s and persist these over IDE restarts. + * Contains references to various [State] objects. * - * @param SELF the type of settings that should be persisted; should be a self reference - * @param SCHEME the type of scheme that the settings consist of + * @property version The version of Randomness with which these settings were created. + * @property templateList The template list. */ -interface Settings> : PersistentStateComponent { +data class Settings( + var version: String = CURRENT_VERSION, + @OptionTag + val templateList: TemplateList = TemplateList(), +) : State() { /** - * The various schemes that are contained within the settings. + * @see TemplateList.templates */ - var schemes: MutableList + @get:Transient + val templates: MutableList