{"id":18024065,"url":"https://github.com/thomasnield/dirtyfx","last_synced_at":"2026-03-03T20:32:49.846Z","repository":{"id":144612204,"uuid":"136370289","full_name":"thomasnield/DirtyFX","owner":"thomasnield","description":"Dirty state-tracking properties and collections for JavaFX","archived":false,"fork":false,"pushed_at":"2020-03-19T12:53:22.000Z","size":596,"stargazers_count":34,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-08T14:38:33.668Z","etag":null,"topics":["java","javafx","kotlin","tornadofx"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thomasnield.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-06-06T18:29:45.000Z","updated_at":"2025-01-03T14:41:50.000Z","dependencies_parsed_at":null,"dependency_job_id":"271da855-d38b-43bb-bdc6-5be9b0881404","html_url":"https://github.com/thomasnield/DirtyFX","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/thomasnield/DirtyFX","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasnield%2FDirtyFX","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasnield%2FDirtyFX/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasnield%2FDirtyFX/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasnield%2FDirtyFX/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomasnield","download_url":"https://codeload.github.com/thomasnield/DirtyFX/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasnield%2FDirtyFX/sbom","scorecard":{"id":882508,"data":{"date":"2025-08-11","repo":{"name":"github.com/thomasnield/DirtyFX","commit":"f496026aae09a529354aebe1f41bd43e088c0eb1"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.9,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":9,"reason":"binaries present in source code","details":["Warn: binary detected: gradle/wrapper/gradle-wrapper.jar:1"],"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-24T08:45:05.107Z","repository_id":144612204,"created_at":"2025-08-24T08:45:05.107Z","updated_at":"2025-08-24T08:45:05.107Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30058292,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T18:21:05.932Z","status":"ssl_error","status_checked_at":"2026-03-03T18:20:59.341Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","javafx","kotlin","tornadofx"],"created_at":"2024-10-30T07:11:49.167Z","updated_at":"2026-03-03T20:32:49.817Z","avatar_url":"https://github.com/thomasnield.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![](logo.jpg)\n\n\n### Dirty state-tracking properties and collections for JavaFX\n\nI built this library out of tremendous need to keep track of dirty states in various JavaFX Properties and Collections, as well as rebaseline and reset against the dirty values. \n\nYou may likely use this library for CRUD operations, while allowing users the ability to review dirty edits before commiting or reverting them. For instance, you can bind the dirty states to red text formatting [like in this demo application](https://github.com/thomasnield/rxkotlinfx-tornadofx-demo). \n\n![](dirty_demo.gif)\n\nHere are the current types:\n\n```\nDirtyBooleanProperty\nDirtyIntegerProperty\nDirtyLongProperty\nDirtyObjectProperty\nDirtyStringProperty\nDirtyObservableList\nDirtyObservableMap\nDirtyObservableSet\nDirtyListProperty\nDirtySetProperty\nDirtyMapProperty\nDirtyReadOnlyWrapper\nCompositeDirtyProperty\n```\n\nEach of these dirty-tracking components have the same behaviors as their vanilla JavaFX counterparts, but have three additional methods/properties:\n\n|Property/Method|Description|\n|----|----|\n|isDirtyProperty()|Read-only ObservableValue indicating whether this item is dirty|\n|isDirty|Delegates to isDirtyProperty()|\n|reset()|Sets the value back to the baseline value, removing dirty state|\n|rebaseline()|Sets the baseline value to the current value, resetting dirty state|\n\nNote this was built in Kotlin, but works with both Java and Kotlin.\n\n\n### Java Usage\n\n```java\n// Initialize with \"Hello\" being the baseline\nDirtyObjectProperty\u003cString\u003e myProperty = new DirtyObjectProperty\u003c\u003e(\"Hello\");\n\n// Setting existing value should not change\nmyProperty.setValue(\"Hello\");\nAssert.assertFalse(myProperty.isDirty());\n\n// Changing the value will result in a dirty state\nmyProperty.setValue(\"World\");\nAssert.assertTrue(myProperty.isDirty());\n\n// Rebaselining will set the current value to be the new baseline\nmyProperty.rebaseline();\nAssert.assertFalse(myProperty.isDirty());\n\n// Reset will restore the baseline as the current value, and no longer be dirty\nmyProperty.setValue(\"Greetings\");\nmyProperty.reset();\nAssert.assertEquals(\"World\", myProperty.getValue());\nAssert.assertFalse(myProperty.isDirty());\n```\n\n### CompositeDirtyProperty\n\nThe `CompositeDirtyProperty` is a powerful utility that can manage dirty properties in sweeping fashion. All the `DirtyXXX` types in this library implement `DirtyProperty`, and the `CompositeDirtyProperty` accepts any number of `DirtyProperty` items and aggregates their state.\n\nIt can also `rebaseline()` or `reset()` all the items it is tracking.\n\n```java\nCompositeDirtyProperty composite = new CompositeDirtyProperty();\n\nDirtyObjectProperty\u003cInteger\u003e property1 = new DirtyObjectProperty\u003c\u003e(3);\nDirtyObjectProperty\u003cInteger\u003e property2 = new DirtyObjectProperty\u003c\u003e(2);\nDirtyObservableList\u003cString\u003e list1 = new DirtyObservableList\u003c\u003e(Arrays.asList(\"Alpha\",\"Beta\",\"Gamma\"));\nDirtyObservableList\u003cString\u003e list2 = new DirtyObservableList\u003c\u003e(Arrays.asList(\"Zeta\",\"Theta\",\"Eta\"));\n\ncomposite.addAll(property1,property2,list1,list2);\nAssert.assertFalse(composite.isDirty());\n\nproperty1.setValue(3);\nAssert.assertFalse(composite.isDirty());\n\nproperty1.setValue(4);\nAssert.assertTrue(composite.isDirty());\n\ncomposite.reset();\nAssert.assertEquals(3, (int) property1.getValue());\nAssert.assertFalse(composite.isDirty());\n\nlist1.add(\"Delta\");\nAssert.assertTrue(composite.isDirty());\n\ncomposite.rebaseline();\nAssert.assertFalse(composite.isDirty());\nAssert.assertArrayEquals(list1.toArray(), new String[] {\"Alpha\", \"Beta\", \"Gamma\", \"Delta\"});\n```\n\n\n### Example - CRUD Interface\n\nHere is a simple CRUD-like user interface built with Kotlin and [TornadoFX](https://github.com/edvin/tornadofx). \n\n![](dirty_demo.gif)\n\n```kotlin \nimport javafx.application.Application\nimport javafx.beans.property.SimpleObjectProperty\nimport javafx.collections.FXCollections\nimport javafx.geometry.Orientation\nimport javafx.scene.paint.Color\nimport org.nield.dirtyfx.beans.DirtyObjectProperty\nimport org.nield.dirtyfx.beans.DirtyStringProperty\nimport org.nield.dirtyfx.extensions.addTo\nimport org.nield.dirtyfx.tracking.CompositeDirtyProperty\nimport tornadofx.*\nimport java.time.LocalDate\n\n\nfun main(args: Array\u003cString\u003e) {\n    Application.launch(MyApp::class.java, *args)\n}\n\nclass MyApp: App(MyView::class)\n\nclass MyView: View() {\n\n    val customers = FXCollections.observableArrayList(\n            Person(\"Samantha\",\"Stuart\",LocalDate.of(1981,12,4)),\n            Person(\"Tom\",\"Marks\",LocalDate.of(2001,1,23)),\n            Person(\"Stuart\",\"Gills\",LocalDate.of(1989,5,23)),\n            Person(\"Nicole\",\"Williams\",LocalDate.of(1998,8,11))\n    )\n\n    val selectedCustomer = SimpleObjectProperty\u003cPerson\u003e()\n\n    override val root = borderpane {\n\n        left = toolbar {\n            orientation = Orientation.VERTICAL\n\n            button(\"RESET\") {\n                setOnAction {\n                    selectedCustomer.get()?.reset()\n                }\n            }\n\n            button(\"SAVE\") {\n                setOnAction {\n                    selectedCustomer.get()?.save()\n                }\n            }\n        }\n\n        center = tableview(customers) {\n\n            selectedCustomer.bind(selectionModel.selectedItemProperty())\n\n            column(\"FIRST NAME\", Person::firstNameProperty) {\n                makeEditable()\n                cellDecorator {\n                    rowItem.firstNameProperty.isDirtyProperty().addListener { o, oldValue, newValue -\u003e\n                        textFill = if (newValue) Color.RED else Color.BLACK\n                    }\n                }\n            }\n\n            column(\"LAST NAME\", Person::lastNameProperty) {\n                makeEditable()\n                cellDecorator {\n                    rowItem.lastNameProperty.isDirtyProperty().addListener { o, oldValue, newValue -\u003e\n                        textFill = if (newValue) Color.RED else Color.BLACK\n                    }\n                }\n            }\n\n            column(\"BIRTHDAY\", Person::birthdayProperty) {\n                makeEditable()\n                cellDecorator {\n                    rowItem.birthdayProperty.isDirtyProperty().addListener { o, oldValue, newValue -\u003e\n                        textFill = if (newValue) Color.RED else Color.BLACK\n                    }\n                }\n            }\n\n            column(\"IS DIRTY\", Person::isDirtyProperty)\n\n            isEditable = true\n        }\n    }\n}\n\nclass Person(firstName: String, lastName: String, birthday: LocalDate) {\n\n    val isDirtyProperty = CompositeDirtyProperty()\n    val isDirty by isDirtyProperty\n\n    val firstNameProperty = DirtyStringProperty(firstName).addTo(isDirtyProperty)\n    var firstName by firstNameProperty\n\n    val lastNameProperty = DirtyStringProperty(lastName).addTo(isDirtyProperty)\n    var lastName by lastNameProperty\n\n    val birthdayProperty = DirtyObjectProperty(birthday).addTo(isDirtyProperty)\n    var birthday by birthdayProperty\n\n\n    fun reset() = isDirtyProperty.reset()\n    fun save() = isDirtyProperty.rebaseline()\n}\n```\n\n### Dependencies\n\n**Maven**\n\n```xml \n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.nield\u003c/groupId\u003e\n    \u003cartifactId\u003edirtyfx\u003c/artifactId\u003e\n    \u003cversion\u003e0.1.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n**Gradle**\n\n```groovy\ncompile 'org.nield:dirtyfx:0.1.2'\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasnield%2Fdirtyfx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomasnield%2Fdirtyfx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasnield%2Fdirtyfx/lists"}