Repository: kotcrab/VisEditor Branch: master Commit: 820300c86a1b Files: 301 Total size: 1.6 MB Directory structure: gitextract_uullo9ny/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ └── workflows/ │ ├── pr.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── ui/ │ ├── CHANGES.md │ ├── NOTICE │ ├── assets-raw/ │ │ ├── x1/ │ │ │ └── pack.json │ │ ├── x1-fonts/ │ │ │ ├── default.hiero │ │ │ └── font-small.hiero │ │ ├── x2/ │ │ │ └── pack.json │ │ └── x2-fonts/ │ │ ├── default.hiero │ │ └── font-small.hiero │ ├── build.gradle │ ├── gradle.properties │ ├── icons-license │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kotcrab/ │ │ │ └── vis/ │ │ │ └── ui/ │ │ │ ├── FocusManager.java │ │ │ ├── Focusable.java │ │ │ ├── Locales.java │ │ │ ├── Sizes.java │ │ │ ├── VisUI.java │ │ │ ├── building/ │ │ │ │ ├── CenteredTableBuilder.java │ │ │ │ ├── GridTableBuilder.java │ │ │ │ ├── OneColumnTableBuilder.java │ │ │ │ ├── OneRowTableBuilder.java │ │ │ │ ├── StandardTableBuilder.java │ │ │ │ ├── TableBuilder.java │ │ │ │ └── utilities/ │ │ │ │ ├── Alignment.java │ │ │ │ ├── CellWidget.java │ │ │ │ ├── Nullables.java │ │ │ │ ├── Padding.java │ │ │ │ └── layouts/ │ │ │ │ ├── ActorLayout.java │ │ │ │ ├── GridTableLayout.java │ │ │ │ └── TableLayout.java │ │ │ ├── i18n/ │ │ │ │ └── BundleText.java │ │ │ ├── layout/ │ │ │ │ ├── DragPane.java │ │ │ │ ├── FloatingGroup.java │ │ │ │ ├── FlowGroup.java │ │ │ │ ├── GridGroup.java │ │ │ │ ├── HorizontalFlowGroup.java │ │ │ │ └── VerticalFlowGroup.java │ │ │ ├── util/ │ │ │ │ ├── ActorUtils.java │ │ │ │ ├── BorderOwner.java │ │ │ │ ├── ColorUtils.java │ │ │ │ ├── CursorManager.java │ │ │ │ ├── FloatDigitsOnlyFilter.java │ │ │ │ ├── InputValidator.java │ │ │ │ ├── IntDigitsOnlyFilter.java │ │ │ │ ├── NumberDigitsTextFieldFilter.java │ │ │ │ ├── OsUtils.java │ │ │ │ ├── TableUtils.java │ │ │ │ ├── ToastManager.java │ │ │ │ ├── Validators.java │ │ │ │ ├── adapter/ │ │ │ │ │ ├── AbstractListAdapter.java │ │ │ │ │ ├── ArrayAdapter.java │ │ │ │ │ ├── ArrayListAdapter.java │ │ │ │ │ ├── CachedItemAdapter.java │ │ │ │ │ ├── ItemAdapter.java │ │ │ │ │ ├── ListAdapter.java │ │ │ │ │ ├── ListSelectionAdapter.java │ │ │ │ │ └── SimpleListAdapter.java │ │ │ │ ├── async/ │ │ │ │ │ ├── AsyncTask.java │ │ │ │ │ ├── AsyncTaskListener.java │ │ │ │ │ ├── AsyncTaskProgressDialog.java │ │ │ │ │ └── SteppedAsyncTask.java │ │ │ │ ├── dialog/ │ │ │ │ │ ├── ConfirmDialogListener.java │ │ │ │ │ ├── Dialogs.java │ │ │ │ │ ├── InputDialogAdapter.java │ │ │ │ │ ├── InputDialogListener.java │ │ │ │ │ ├── OptionDialogAdapter.java │ │ │ │ │ └── OptionDialogListener.java │ │ │ │ ├── form/ │ │ │ │ │ ├── FormInputValidator.java │ │ │ │ │ ├── FormValidator.java │ │ │ │ │ ├── SimpleFormValidator.java │ │ │ │ │ └── ValidatorWrapper.java │ │ │ │ ├── highlight/ │ │ │ │ │ ├── BaseHighlighter.java │ │ │ │ │ ├── Highlight.java │ │ │ │ │ ├── HighlightRule.java │ │ │ │ │ ├── Highlighter.java │ │ │ │ │ ├── RegexHighlightRule.java │ │ │ │ │ └── WordHighlightRule.java │ │ │ │ └── value/ │ │ │ │ ├── ConstantIfVisibleValue.java │ │ │ │ ├── PrefHeightIfVisibleValue.java │ │ │ │ ├── PrefWidthIfVisibleValue.java │ │ │ │ ├── VisValue.java │ │ │ │ └── VisWidgetValue.java │ │ │ └── widget/ │ │ │ ├── BusyBar.java │ │ │ ├── ButtonBar.java │ │ │ ├── CollapsibleWidget.java │ │ │ ├── Draggable.java │ │ │ ├── HighlightTextArea.java │ │ │ ├── HorizontalCollapsibleWidget.java │ │ │ ├── LinkLabel.java │ │ │ ├── ListView.java │ │ │ ├── ListViewStyle.java │ │ │ ├── Menu.java │ │ │ ├── MenuBar.java │ │ │ ├── MenuItem.java │ │ │ ├── MultiSplitPane.java │ │ │ ├── PopupMenu.java │ │ │ ├── ScrollableTextArea.java │ │ │ ├── Separator.java │ │ │ ├── Tooltip.java │ │ │ ├── VisCheckBox.java │ │ │ ├── VisDialog.java │ │ │ ├── VisImage.java │ │ │ ├── VisImageButton.java │ │ │ ├── VisImageTextButton.java │ │ │ ├── VisLabel.java │ │ │ ├── VisList.java │ │ │ ├── VisProgressBar.java │ │ │ ├── VisRadioButton.java │ │ │ ├── VisScrollPane.java │ │ │ ├── VisSelectBox.java │ │ │ ├── VisSlider.java │ │ │ ├── VisSplitPane.java │ │ │ ├── VisTable.java │ │ │ ├── VisTextArea.java │ │ │ ├── VisTextButton.java │ │ │ ├── VisTextField.java │ │ │ ├── VisTree.java │ │ │ ├── VisValidatableTextField.java │ │ │ ├── VisWindow.java │ │ │ ├── color/ │ │ │ │ ├── BasicColorPicker.java │ │ │ │ ├── ColorPicker.java │ │ │ │ ├── ColorPickerAdapter.java │ │ │ │ ├── ColorPickerListener.java │ │ │ │ ├── ColorPickerStyle.java │ │ │ │ ├── ColorPickerWidgetStyle.java │ │ │ │ ├── ExtendedColorPicker.java │ │ │ │ └── internal/ │ │ │ │ ├── AlphaChannelBar.java │ │ │ │ ├── AlphaImage.java │ │ │ │ ├── ChannelBar.java │ │ │ │ ├── ColorChannelWidget.java │ │ │ │ ├── ColorInputField.java │ │ │ │ ├── ColorPickerText.java │ │ │ │ ├── GridSubImage.java │ │ │ │ ├── Palette.java │ │ │ │ ├── PickerCommons.java │ │ │ │ ├── ShaderImage.java │ │ │ │ ├── VerticalChannelBar.java │ │ │ │ └── package-info.java │ │ │ ├── file/ │ │ │ │ ├── FileChooser.java │ │ │ │ ├── FileChooserAdapter.java │ │ │ │ ├── FileChooserListener.java │ │ │ │ ├── FileChooserStyle.java │ │ │ │ ├── FileTypeFilter.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── SingleFileChooserListener.java │ │ │ │ ├── StreamingFileChooserListener.java │ │ │ │ └── internal/ │ │ │ │ ├── AbstractSuggestionPopup.java │ │ │ │ ├── DirsSuggestionPopup.java │ │ │ │ ├── DriveCheckerService.java │ │ │ │ ├── FileChooserText.java │ │ │ │ ├── FileChooserWinService.java │ │ │ │ ├── FileHandleMetadata.java │ │ │ │ ├── FileHistoryManager.java │ │ │ │ ├── FileListAdapter.java │ │ │ │ ├── FilePopupMenu.java │ │ │ │ ├── FileSuggestionPopup.java │ │ │ │ ├── IconStack.java │ │ │ │ ├── PreferencesIO.java │ │ │ │ ├── ServiceThreadFactory.java │ │ │ │ ├── SortingPopupMenu.java │ │ │ │ └── package-info.java │ │ │ ├── internal/ │ │ │ │ └── SplitPaneCursorManager.java │ │ │ ├── spinner/ │ │ │ │ ├── AbstractSpinnerModel.java │ │ │ │ ├── ArraySpinnerModel.java │ │ │ │ ├── FloatSpinnerModel.java │ │ │ │ ├── IntSpinnerModel.java │ │ │ │ ├── SimpleFloatSpinnerModel.java │ │ │ │ ├── Spinner.java │ │ │ │ └── SpinnerModel.java │ │ │ ├── tabbedpane/ │ │ │ │ ├── Tab.java │ │ │ │ ├── TabbedPane.java │ │ │ │ ├── TabbedPaneAdapter.java │ │ │ │ └── TabbedPaneListener.java │ │ │ └── toast/ │ │ │ ├── MessageToast.java │ │ │ ├── Toast.java │ │ │ └── ToastTable.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── robovm/ │ │ │ └── ios/ │ │ │ └── robovm.xml │ │ └── com/ │ │ └── kotcrab/ │ │ └── vis/ │ │ ├── ui/ │ │ │ ├── i18n/ │ │ │ │ ├── ButtonBar.properties │ │ │ │ ├── ColorPicker.properties │ │ │ │ ├── Common.properties │ │ │ │ ├── Dialogs.properties │ │ │ │ ├── FileChooser.properties │ │ │ │ └── TabbedPane.properties │ │ │ ├── skin/ │ │ │ │ ├── x1/ │ │ │ │ │ ├── default.fnt │ │ │ │ │ ├── font-small.fnt │ │ │ │ │ ├── uiskin.atlas │ │ │ │ │ └── uiskin.json │ │ │ │ └── x2/ │ │ │ │ ├── default.fnt │ │ │ │ ├── font-small.fnt │ │ │ │ ├── uiskin.atlas │ │ │ │ └── uiskin.json │ │ │ └── widget/ │ │ │ └── color/ │ │ │ └── internal/ │ │ │ ├── checkerboard.frag │ │ │ ├── default.vert │ │ │ ├── hsv.frag │ │ │ ├── palette.frag │ │ │ ├── rgb.frag │ │ │ └── verticalBar.frag │ │ └── vis-ui.gwt.xml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── kotcrab/ │ │ └── vis/ │ │ └── ui/ │ │ └── test/ │ │ ├── GreaterThanValidatorTest.java │ │ ├── LesserThanValidatorTest.java │ │ ├── TestImageTextButtonOrientation.java │ │ └── manual/ │ │ ├── HighResFileChooserIconProvider.java │ │ ├── TestBuilders.java │ │ ├── TestBusyBar.java │ │ ├── TestButtonBar.java │ │ ├── TestCollapsible.java │ │ ├── TestColorPicker.java │ │ ├── TestDialogs.java │ │ ├── TestFileChooser.java │ │ ├── TestFloatingGroup.java │ │ ├── TestFlowGroup.java │ │ ├── TestFormValidator.java │ │ ├── TestGenerateDisabledImage.java │ │ ├── TestHighlightTextArea.java │ │ ├── TestIssue131.java │ │ ├── TestIssue326.java │ │ ├── TestLauncher.java │ │ ├── TestListView.java │ │ ├── TestMultiSplitPane.java │ │ ├── TestSplitPane.java │ │ ├── TestTabbedPane.java │ │ ├── TestTextAreaAndScroll.java │ │ ├── TestToasts.java │ │ ├── TestTree.java │ │ ├── TestValidator.java │ │ ├── TestVertical.java │ │ ├── TestWindow.java │ │ └── WindowResizeListener.java │ └── resources/ │ └── file-chooser-high-res.atlas ├── usl/ │ ├── CHANGES.md │ ├── build.gradle │ ├── gradle.properties │ ├── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── kotcrab/ │ │ │ └── vis/ │ │ │ └── usl/ │ │ │ ├── CollectionUtils.java │ │ │ ├── IncludeLoader.java │ │ │ ├── Lexer.java │ │ │ ├── LexerContext.java │ │ │ ├── Main.java │ │ │ ├── Parser.java │ │ │ ├── StyleMerger.java │ │ │ ├── Token.java │ │ │ ├── USL.java │ │ │ ├── USLException.java │ │ │ ├── USLJsonWriter.java │ │ │ ├── Utils.java │ │ │ └── lang/ │ │ │ ├── AliasIdentifier.java │ │ │ ├── BasicIdentifier.java │ │ │ ├── GroupIdentifier.java │ │ │ ├── Identifier.java │ │ │ ├── StyleBlock.java │ │ │ └── StyleIdentifier.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── kotcrab/ │ │ │ └── vis/ │ │ │ └── usl/ │ │ │ └── test/ │ │ │ ├── RemoteTest.java │ │ │ └── TemplateBasedParserTest.java │ │ └── resources/ │ │ ├── test-alias-expected.json │ │ ├── test-alias.usl │ │ ├── test-comments-expected.json │ │ ├── test-comments.usl │ │ ├── test-gdx-expected.json │ │ ├── test-gdx.usl │ │ ├── test-minus-expected.json │ │ ├── test-minus.usl │ │ ├── test-tinted-expected.json │ │ ├── test-tinted.usl │ │ ├── test-visui-expected.json │ │ └── test-visui.usl │ └── styles/ │ ├── gdx.usl │ ├── visui-1.0.2.usl │ ├── visui-1.1.6.usl │ ├── visui-1.2.3.usl │ ├── visui-1.2.4.usl │ ├── visui-1.2.5.usl │ ├── visui-1.3.0.usl │ ├── visui-1.4.0.usl │ ├── visui-1.4.1.usl │ ├── visui-1.4.10.usl │ ├── visui-1.4.11.usl │ ├── visui-1.4.2.usl │ ├── visui-1.4.3.usl │ ├── visui-1.4.4.usl │ ├── visui-1.4.5.usl │ ├── visui-1.4.6.usl │ ├── visui-1.4.7.usl │ ├── visui-1.4.8.usl │ └── visui-1.4.9.usl └── vis-intellij-formatter.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.svg filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/CODEOWNERS ================================================ * @kotcrab ================================================ FILE: .github/workflows/pr.yml ================================================ name: Build pull request on: pull_request: branches: [ master ] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - name: Repository checkout uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - name: Run tests run: ./gradlew check ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'visui-*' jobs: release: runs-on: ubuntu-latest environment: release steps: - name: Repository checkout uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - name: Run tests run: ./gradlew check - name: Upload to Maven Central run: ./gradlew :ui:publishToMavenCentral env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} ================================================ FILE: .github/workflows/snapshot.yml ================================================ name: Build and upload snapshot on: push: branches: [ master ] schedule: - cron: "40 16 10 * *" jobs: snapshot: runs-on: ubuntu-latest environment: release steps: - name: Repository checkout uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - name: Run tests run: ./gradlew check - name: Upload to Maven Central run: ./gradlew :ui:publishSnapshot env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.CENTRAL_PASSWORD }} ================================================ FILE: .gitignore ================================================ ## Maven target/ ## Java *.class *.war *.ear hs_err_pid* ## GWT war/gwt_bree/ gwt-unitCache/ .apt_generated/ war/WEB-INF/deploy/ war/WEB-INF/classes/ .gwt/ gwt-unitCache/ www-test/ .gwt-tmp/ ## Android Studio and Intellij .idea/ *.ipr *.iws *.iml out/ com_crashlytics_export_strings.xml ## Eclipse .metadata bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .settings/ .loadpath .pydevproject .project .classpath .externalToolBuilders/ *.launch ## NetBeans nbproject/private/ build/ nbbuild/ dist/ nbdist/ nbactions.xml nb-configuration.xml ## Gradle .gradle build/ /target /target /target ================================================ FILE: AUTHORS ================================================ # This is the official list of the AUTHORS of Vis Project # for copyright purposes. # This file is distinct from the CONTRIBUTORS files. # See the latter for an explanation. # Names should be added to this file as # Name or Organization # The email address is not required for organizations. Kotcrab ================================================ FILE: CONTRIBUTING.md ================================================ We are glad that you would like to contribute to the Vis Project. Here are some guidelines that you should follow when making your contributions. Start by forking this repository, then learn [how to run Vis Projects from source code](https://github.com/kotcrab/vis-ui/wiki/Building-Vis-From-Source). #### Git commits messages * Use sentence case (i.e. "Add feature" not "add feature") * Use imperative, present tense (i.e. "Add", not "Added" or "Adds") * Don't use dots, exclamation or question marks at the end of commit message #### Code Formatter We require you to use code formatter when making pull requests. Code formatter for IntelliJ IDEA can be found in root directory of this repository. If you are using Eclipse then you must use [libGDX Eclipse formatter](https://github.com/kotcrab/libgdx/blob/master/eclipse-formatter.xml). Remember to don't use Eclipse formatter on existing source code because it isn't fully compatible with the IntelliJ IDEA formatter used in this repository. It may change other irrelevant source code and if you decide to make pull request later it will be harder to review. To install formatter in Eclipse simply import xml file from settings window. To install formatter in IntelliJ IDEA copy xml to config directory, restart IDE, then select formatter from settings. Mac OS X: `~/Library/Preferences/.IdeaIC15/codestyles/` Linux: `~/.IdeaIC15/config/codestyles/` Windows: `\.IdeaIC15\config\codeStyles\` `.IdeaIC15` directory may be named different depending on your IDEA version #### Code Style Please follow [libGDX code style](https://github.com/libgdx/libgdx/blob/master/CONTRIBUTING.md#code-style). Thanks! ================================================ FILE: CONTRIBUTORS ================================================ # This is the official list of people who can contribute # (and who have contributed) code to the Vis Project # repository. # The AUTHORS file lists the copyright holders; this file # lists people. # kotcrab https://github.com/kotcrab Javier https://github.com/jdiazcano MJ https://github.com/czyzby/ StrongJoshua https://github.com/StrongJoshua code-disaster https://github.com/code-disaster piotr-j https://github.com/piotr-j intrigus https://github.com/intrigus StQuote https://github.com/StQuote Favorlock https://github.com/Favorlock strubelz https://github.com/strubelz Snehks https://github.com/Snehks ericnondahl https://github.com/ericnondahl metaphore https://github.com/metaphore cypherdare https://github.com/cypherdare fgnm https://github.com/fgnm ccmb2r https://github.com/ccmb2r bploeckelman https://github.com/bploeckelman ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================ # VisUI VisUI allows to create nice looking UI in libGDX using scene2d.ui. Library contains scene2d.ui skin, useful widgets like color picker and file chooser, it also contains modified scene2d.ui widgets to provide some extra functionality like focus borders, background change on over and click, etc. VisUI is licensed under Apache2 license meaning that you can use it for free in both commercial and non-commercial projects. ##### [CHANGES](https://github.com/kotcrab/vis-ui/blob/master/ui/CHANGES.md) file (definitely read before updating!) **[Web demo!](http://vis.kotcrab.com/demo/ui)** [(source code)](https://github.com/kotcrab/vis-ui/tree/master/ui/src/test/java/com/kotcrab/vis/ui/test/manual) ![VisUI screenshot](http://dl.kotcrab.com/github/vis/visui2.png) [Bigger screenshot](http://dl.kotcrab.com/github/vis/visui2.png) ## Adding VisUI to your project [![Maven Central](https://img.shields.io/maven-central/v/com.kotcrab.vis/vis-ui.svg)](https://search.maven.org/artifact/com.kotcrab.vis/vis-ui) Please refer to [libGDX documentation](https://libgdx.com/wiki/articles/dependency-management-with-gradle) if you don't know how to manage dependencies with Gradle. Alternatively JAR can be downloaded from [Maven repository](http://search.maven.org/#search|gav|1|g%3A%22com.kotcrab.vis%22%20AND%20a%3A%22vis-ui%22). If you are creating new project, you can use gdx-setup to automatically add VisUI for you. (press 'Show Third Party Extension' button) #### Manual Gradle setup: Open build.gradle in project root. In ``ext`` section under ``allprojects`` add: ```groovy visuiVersion = '1.X.X' ``` Look at [CHANGES](https://github.com/kotcrab/vis-ui/blob/master/ui/CHANGES.md) file to see what version of VisUI you can use for your version of libGDX. Note that using not matching versions is likely to cause runtime exceptions. **Core dependency** ```groovy api "com.kotcrab.vis:vis-ui:$visuiVersion" ``` **HTML dependency** (only if you are using GWT): ```groovy api "com.kotcrab.vis:vis-ui:$visuiVersion:sources" ``` ``GdxDefinition.gwt.xml`` and ``GdxDefinitionSuperdev.gwt.xml``: ```xml ``` Refresh Gradle dependencies. ## Usage Using VisUI is pretty simple, to load or unload the skin call: ```java VisUI.load(); VisUI.dispose(); ``` Create your UI like always, for extra skin features you have to use Vis widgets instead of standard scene2d.ui: | VisUI | Standard scene2d.ui | | ------------- | ------------------- | | VisLabel | Label | | [LinkLabel](https://github.com/kotcrab/vis-ui/wiki/LinkLabel) | - | | VisCheckBox | CheckBox | | VisList | List | | VisProgressBar| ProgressBar | | VisRadioButton| - | | VisScrollPane | ScrollPane | | VisSelectBox | SelectBox | | VisSlider | Slider | | VisSplitPane | SplitPane | | VisTextArea | TextArea | | VisTextButton | TextButton | | VisImageTextButton | ImageTextButton | | VisImageButton | ImageButton | | VisTextField | TextField | | [VisValidatableTextField](https://github.com/kotcrab/vis-ui/wiki/VisValidatableTextField) | - | | VisTree | Tree | | VisWindow | Window | | VisTable | Table | | [DragPane](https://github.com/kotcrab/vis-ui/wiki/DragPane) | - | | [GridGroup](https://github.com/kotcrab/vis-ui/wiki/GridGroup) | - | | [ListView](https://github.com/kotcrab/vis-ui/wiki/ListView) | - | | [TabbedPane](https://github.com/kotcrab/vis-ui/wiki/TabbedPane) | - | | [Spinner](https://github.com/kotcrab/vis-ui/wiki/Spinner) | - | | [CollapsibleWidget](https://github.com/kotcrab/vis-ui/wiki/CollapsibleWidget) | - | | [ButtonBar](https://github.com/kotcrab/vis-ui/wiki/ButtonBar) | - | | [FlowGroups](https://github.com/kotcrab/vis-ui/wiki/FlowGroups) | - | Using Vis widgets is necessary for proper focus border management. All VisUI widgets constructors do not have Skin argument, they are using VisUI.skin field. ### VisTable VisTable allows to easily set default spacing for vis components, construct it like this: ``` VisTable table = new VisTable(true); ``` VisTable also allows adding vertical and horizontal separators to table: ```java table.addSeparator() //horizontal table.addSeparator(true) //vertical ``` ### Using different `SkinScale`s Default VisUI skin can be too small for high resolution screens or mobile devices, in that case you can load a upscaled skin version simply by calling: ``` VisUI.load(SkinScale.X2); ``` ### Internal classes Classes inside `com.kotcrab.vis.[...].internal` packages are considered private and aren't part of public API. Changes to that classes won't be listed in change log. ### Default title align Default title align for VisWindow and VisDialog is `Align.left` this can be changed by calling: ```java VisUI.setDefaultTitleAlign(int align) ``` Calling this method does not affect windows that have been already created. ## Modifying skin [Raw skin files](https://github.com/kotcrab/vis-ui/tree/master/ui/assets-raw) are available if you would like to modify them. After you pack them using libGDX texture packer, add generated atlas to your project with [uiskin.json, default.fnt and font-small.fnt](https://github.com/kotcrab/vis-ui/tree/master/ui/src/main/resources/com/kotcrab/vis/ui/skin/x1) and load it by calling: ```java VisUI.load(Gdx.files.internal("path/to/your/modified/files/uiskin.json")) ``` Consider using USL if you want to extend existing VisUI styles. [Read more](https://github.com/kotcrab/vis-ui/wiki/USL) ## See also * [vis-ui-contrib](https://github.com/kotcrab/vis-ui-contrib) - Community driven extension, utilities and skins for VisUI * [ktx](https://github.com/czyzby/ktx) - Kotlin utilities for libGDX applications. The [ktx-vis](https://github.com/czyzby/ktx/tree/master/vis) and [ktx-style-vis](https://github.com/czyzby/ktx/tree/master/vis-style) modules provide Kotlin APIs for VisUI. ================================================ FILE: build.gradle ================================================ import com.vanniktech.maven.publish.JavaLibrary import com.vanniktech.maven.publish.JavadocJar buildscript { repositories { gradlePluginPortal() mavenCentral() } dependencies { classpath "com.kotcrab.vis:vis-usl:0.2.1" classpath "com.badlogicgames.gdx:gdx-tools:1.9.2" } } plugins { id("com.github.ben-manes.versions") version "0.52.0" id("com.vanniktech.maven.publish.base") version "0.33.0" } subprojects { apply plugin: "java" apply plugin: "idea" apply plugin: "signing" apply plugin: "com.vanniktech.maven.publish.base" group = 'com.kotcrab.vis' version = projectVersion ext { appName = 'vis' gdxVersion = '1.14.0' jnaVersion = '4.1.0' jnaPlatformVersion = '3.5.2' junitVersion = '4.13.2' imgscalrVersion = '4.2' isReleaseVersion = !version.endsWith("SNAPSHOT") } repositories { mavenCentral() } mavenPublishing { publishToMavenCentral(true) if (isReleaseVersion) { signAllPublications() } coordinates("com.kotcrab.vis", projectName, projectVersion) configure(new JavaLibrary(new JavadocJar.Javadoc(), true)) pom { name = projectName description = projectDesc url = 'https://github.com/kotcrab/vis-ui/' scm { connection = 'scm:git:git@github.com:kotcrab/vis-ui.git' developerConnection = 'scm:git:git@github.com:kotcrab/vis-ui.git' url = 'git@github.com:kotcrab/vis-ui.git' } licenses { license { name = 'The Apache License, Version 2.0' url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' } } developers { developer { id = 'kotcrab' name = 'Kotcrab' url = 'https://kotcrab.com' } } } } task publishSnapshot { if (!isReleaseVersion) { finalizedBy(tasks["publishToMavenCentral"]) } } javadoc { options.encoding = 'UTF-8' options.addStringOption('Xdoclint:none', '-quiet') } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.daemon=true org.gradle.jvmargs=-Xms256m -Xmx1024m org.gradle.configureondemand=true #required by deploy, change in your home gradle.properties file mavenCentralUsername= mavenCentralPassword= ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ rootProject.name = 'vis-ui' rootProject.buildFileName = 'build.gradle' include 'ui', 'usl' ================================================ FILE: ui/CHANGES.md ================================================ #### Version: 1.5.9-SNAPSHOT (libGDX 1.14.0) #### Version: 1.5.8 (libGDX 1.14.0) - Updated to libGDX 1.14.0 #### Version: 1.5.7 (libGDX 1.13.5) - Updated to libGDX 1.13.5 #### Version: 1.5.6 (libGDX 1.13.1) - Project is now published using Maven Central Portal instead of OSSRH - If you're using snapshots add new repository https://central.sonatype.com/repository/maven-snapshots/ - Source compatibility is now Java 1.8 - **Changed**: [#399](https://github.com/kotcrab/vis-ui/issues/399) - Font color markup is now disabled when drawing text field to prevent crashes #### Version: 1.5.5 (libGDX 1.13.1) - Updated to libGDX 1.13.1 - **Improved**: [#396](https://github.com/kotcrab/vis-ui/issues/396) - Added missing Czech diacritics characters - This required re-rendering the font, it was last done 8 years ago. I'm pretty sure I used same Hiero version as before, but I'm guessing newer JDK has some changes. Font baseline is affected by this, but I think it looks better now. Your UI will have slightly different paddings, see the issue for screenshots - **Added**: [#394](https://github.com/kotcrab/vis-ui/issues/394) - `VisImageTextButton` now supports focus font colors #### Version: 1.5.4 (libGDX 1.12.1) - **Changed**: [#393](https://github.com/kotcrab/vis-ui/pull/393) - Removed use of Apple Java extensions in `FileUtils` #### Version: 1.5.3 (libGDX 1.12.1) - Updated to libGDX 1.12.1 #### Version: 1.5.2 (libGDX 1.12.0) - Updated to libGDX 1.12.0 - **Added**: [#373](https://github.com/kotcrab/vis-ui/issues/373) - `VisImageTextButton` an optional `Orientation` value can be set to change how the button label is positioned relative to the button image - Defaults to existing behavior (label to the right of image in the same row), and orientation can be changed via `VisImageTextButton.setOrientation()` #### Version: 1.5.1 (libGDX 1.11.0) - Updated to libGDX 1.11.0 - **Added**: [#374](https://github.com/kotcrab/vis-ui/issues/374) - `ToastManager` now supports 'center' horizontal alignment for 'top' and 'bottom' alignments. - `ToastManager.updateToastsPositions` and `ToastManager` member variables are now protected, allowing further customization of the toast positions. #### Version: 1.5.0 (libGDX 1.10.0) - Updated to libGDX 1.10.0 - **Removed deprecated API**: `VisUI.VERSION` field - **Changed**: The project now uses Java 1.7 compatibility level, similarly to libGDX 1.10.0 - **Fixed**: [#355](https://github.com/kotcrab/vis-ui/issues/355) - Changing color of `VisImageButton` and `VisImageTextButton` did not work - **Fixed**: [#357](https://github.com/kotcrab/vis-ui/issues/357) - `CollapsibleWidget` rendering issues when placed inside a scroll pane - **Fixed**: [#358](https://github.com/kotcrab/vis-ui/issues/358) - `CollapsibleWidget` performance issue #### Version: 1.4.11 (libGDX 1.9.14) - Some changes have been made to simplify VisUI release process - **API Deprecated**: `VisUI.VERSION`, this field is no longer updated and will be removed in future versions - New VisUI USL file (in `usl/styles` directory) will be created only when breaking changes have to be made to the style definitions #### Version: 1.4.10 (libGDX 1.9.14) - Updated to libGDX 1.9.14 #### Version: 1.4.9 (libGDX 1.9.12) - **Changed**: [#350](https://github.com/kotcrab/vis-ui/pull/350) - Improve `CollapseAction` to support custom duration and interpolation - **Fixed**: `IllegalArgumentException` thrown by `MenuItem#getImageCell` when `MenuItem` was created without image ([reported indirectly here](https://github.com/crashinvaders/gdx-texture-packer-gui/issues/98)) #### Version: 1.4.8 (libGDX 1.9.12) - Updated to libGDX 1.9.12 #### Version: 1.4.7 (libGDX 1.9.11) - **Added**: [#335](https://github.com/kotcrab/vis-ui/issues/335) - `VisImageButton (Drawable imageUp, Drawable imageDown, Drawable imageChecked, String styleName)` constructor - **Added**: [#340](https://github.com/kotcrab/vis-ui/issues/340) - Option to disable color picker preview, added: `BasicColorPicker#setShowColorPreviews(boolean)`, `BasicColorPicker#isShowColorPreviews` - **Added**: [#333](https://github.com/kotcrab/vis-ui/issues/333) - `FlowGroup` a generalization of `HorizontalFlowGroup` and `VerticalFlowGroup` - `FlowGroup` fixes some layout issues, see linked pull request - `HorizontalFlowGroup` and `VerticalFlowGroup` are deprecated - To avoid breaking changes `DragPane` and `TabbedPane` implementation was not changed in this version - **Fixed**: [#331](https://github.com/kotcrab/vis-ui/issues/331) - `TabbedPane::removeAll` does not clear the active tab field - `TabbedPane::remove(Tab)` does not clear the active tab field when it removes last tab - **Fixed**: [#336](https://github.com/kotcrab/vis-ui/issues/336) - `VisTextField` crash when text has newlines - **Fixed**: [#339](https://github.com/kotcrab/vis-ui/issues/339) - `HighlightTextArea` was not disabling soft wrapping when using some constructors - **Fixed**: [#341](https://github.com/kotcrab/vis-ui/issues/341) - `HighlightTextArea` parent alpha was not used when drawing font #### Version: 1.4.6 (libGDX 1.9.11) - Updated to libGDX 1.9.11 #### Version: 1.4.5 (libGDX 1.9.10) - **Added**: [#328](https://github.com/kotcrab/vis-ui/issues/328) `BasicColorPicker#focusHexField` - **Fixed**: [#316](https://github.com/kotcrab/vis-ui/issues/316) `BusyBar` grows but does not shrink - **Fixed**: [#324](https://github.com/kotcrab/vis-ui/issues/324) `Spinner#getSelectorName` returns wrong field - **Fixed**: [#326](https://github.com/kotcrab/vis-ui/issues/326) Possible crash in `FocusManager` - **Changed**: [#315](https://github.com/kotcrab/vis-ui/issues/315) Generify `VisTree` to match libGDX implementation - **Changed**: [#314](https://github.com/kotcrab/vis-ui/issues/314) List `corner` Drawable wasn't set resulting in blank spot when both scrollbars were visible - **Changed**: [#325](https://github.com/kotcrab/vis-ui/issues/325) `VisTable#addSeparator(true)` will create `Separator` with `vertical` style - **Warning**: This might be a breaking change if you're using a custom skin - **Changed**: `FileChooser` will auto focus selected file text field when added to stage (use `FileChooser.focusSelectedFileTextFieldOnShow` to override this setting) - **Skin changes**: - **Added style**: `Separator`: `vertical` style #### Version: 1.4.4 (libGDX 1.9.10) - Updated to libGDX 1.9.10 #### Version: 1.4.3 (libGDX 1.9.9) - **Fixed**: Infinite loop in `PopupMenu` when trying to select next (or previous) `MenuItem` using keyboard and menu has no selectable `MenuItem`s. - **Fixed**: [#307](https://github.com/kotcrab/vis-ui/issues/307) `ArrayIndexOutOfBounds` exception in `VisTextArea` when trying to select text having empty first line - **Changed**: `PopupMenu` keyboard events will be now treated as handled by scene2d (they won't be passed to application under the stage) - **Changed**: [#302](https://github.com/kotcrab/vis-ui/issues/302) `ToastManager` now allows to specify X and Y screen padding separately - **Added**: `ToastManager#setScreenPaddingX(int)`, `ToastManager#setScreenPaddingY(int)`, `ToastManager#setScreenPadding(int, int)`, `ToastManager#getScreenPaddingX()` `ToastManager#getScreenPaddingY()` - **Deprecated**: `ToastManager#getScreenPadding()`, use either `ToastManager#getScreenPaddingX()` or `ToastManager#getScreenPaddingY()`. Now this method will throw `IllegalStateException` when padding X is different than padding Y. This should not cause any breaking changes until new API is used. This method will be removed in future versions. #### Version: 1.4.2 (libGDX 1.9.9) - Updated to libGDX 1.9.9 - **Fixed**: `TabbedPane` was not removing `Tab` from internal `ButtonGroup` thus preventing tab instance from being garbage collected #### Version: 1.4.1 (libGDX 1.9.8) - **Fixed**: `FileChooser`'s icon provider not working correctly when chooser's selection checkboxes were enabled - **Fixed**: [#292](https://github.com/kotcrab/vis-ui/issues/292) removing `Menu` from `MenuBar` causes `IllegalStateException` #### Version: 1.4.0 (libGDX 1.9.6) - **Added**: [#259](https://github.com/kotcrab/vis-ui/issues/259) `VisImageTextButton#setGenerateDisabledImage(boolean)` along with getter, added `VisImageButton#isGenerateDisabledImage` - **Added**: [#260](https://github.com/kotcrab/vis-ui/issues/260) `FileChooser#setDefaultFileName(String)` - **Added**: [#279](https://github.com/kotcrab/vis-ui/issues/279) `ToastManager(Group)` constructor - **Fixed**: [#255](https://github.com/kotcrab/vis-ui/issues/255) Custom cursor handling in `VisSplitPane` and `MultiSplitPane` - **Fixed**: [#262](https://github.com/kotcrab/vis-ui/issues/262) `FileChooser` was not updating selected file list when text was cut or pasted into file name field - **Fixed**: `FileChooser` file name suggestion menu was not updating file name field when navigating suggestion list using arrow keys - **Fixed**: [#273](https://github.com/kotcrab/vis-ui/issues/273) Fixed rare crash when doing undo in VisTextField can cause `IllegalStateException` - [#252](https://github.com/kotcrab/vis-ui/issues/252) `Spinner` now implements `Disableable` - **Improved**: [#264](https://github.com/kotcrab/vis-ui/issues/264) Added Turkish diacritics, added currency symbols: €, ¥ - **Changed**: [#272](https://github.com/kotcrab/vis-ui/pull/272) `VisTextField` and subclasses will now use `style.backgroundOver` when field has keyboard focus or mouse is over it (previously it was only shown on mouse over) #### Version: 1.3.0 (libGDX 1.9.6) - **Added**: `VisUI#dispose (boolean disposeSkin)` - Updated to libGDX 1.9.6 - Excluded `AsyncTask` API from GWT compilation #### Version: 1.2.5 (libGDX 1.9.4) - **Added**: `AsyncTask` API and `AsyncTaskProgressDialog` - **Added**: `PopupMenu.removeEveryMenu(Stage)` - **Added**: `FileChooser#setShowSelectionCheckboxes` - **Added**: `FileChooser#getIconProvider` - **Added**: `Spinner#setDisabled(boolean)`, `Spinner#isDisabled()` - **Added**: `HorizontalCollapsibleWidget` - **Fixed**: `MultiSplitPane#setSplit` not affecting split values - **Fixed**: `MultiSplitPane` and `VisSplitPane` default cursor not restored when mouse exited widget bounds when mouse was still on split handle bar - **Changed**: Selection of menu item is removed when mouse pointer leaves popup menu structure - **I18N Changes**: - Added `Common` bundle #### Version: 1.2.4 (libGDX 1.9.4) - **Added**: `ListSelection#setListener`, `#setProgrammaticChangeEvents` (with getters) - **Fixed**: `Spinner.TextFieldEventPolicy` is now public (was package-private) - **Fixed**: `HighlightTextArea` scroll pane not immediately updated after changing text using `setText()` - **Improved**: [#220](https://github.com/kotcrab/vis-ui/issues/220) when sub menu can't fit on the right side of parent menu, it will be shown on the side that has more available space (before in such case it was always shown on the left side) - **Improved**: When mouse is moved from sub-menu to parent menu, selection of menu item in sub-menu will be removed. - **Improved**: [#222](https://github.com/kotcrab/vis-ui/issues/222) Added clipping to BusyBar - **Skin changes**: - **Added style**: `ListViewStyle` - allows to customize `ListView` scroll pane style - **Added new icons**: `icon-maximize`, `icon-minimize`, `icon-restore`, `icon-close-titlebar` - **Added style**: `VisImageButtonStyle`: `close-titlebar` #### Version: 1.2.3 (libGDX 1.9.4) - **Added**: constructor `LinkLabel (CharSequence text, CharSequence url, LinkLabelStyle style)` - **Fixed**: Spinner could overflow Table cell bounds by 1 pixel - Removed `Sizes.spinnerButtonsWidth` and `Sizes.spinnerFieldRightPadding` (no longer needed) - Renamed `Sizes.spinnerButtonSize` to `Sizes.spinnerButtonHeight` - **Skin changes**: - Styles that used to reference other style by name (for example `FileChooserStyle` referencing `PopupMenu` style name) now embeds that style directly - Changed `String ToastStyle#closeButtonStyleName` to `VisImageButtonStyle ToastStyle#closeButtonStyle` - Changed `String FileChooserStyle#popupMenuStyleName` to `PopupMenuStyle FileChooserStyle#popupMenuStyle` - Changed `String MenuStyle#openButtonStyleName` to `VisTextButtonStyle MenuStyle#openButtonStyle` - For existing JSON files you only need to remove 'Name' postfix from field name, Skin loading mechanism can automatically resolve such references #### Version: 1.2.2 (libGDX 1.9.4) - **Fixed**: [#214](https://github.com/kotcrab/vis-ui/issues/214) minus sign not visible in Spinner when value was changed with text field focus - **Fixed**: When there was not enough space on the right to fully show sub-menu it was appearing in wrong position on the left side. #### Version: 1.2.1 (libGDX 1.9.4) - **Fixed**: When using libGDX 1.9.4 message was printed that libGDX version is incorrect. If your project is using 1.9.4 you could safely ignore it. #### Version: 1.2.0 (libGDX 1.9.4) - Updated to libGDX 1.9.4 #### Version: 1.1.6 (libGDX 1.9.3) - **Added**: `MenuBar#setMenuListener`, `MenuBarListener` - **Changed**: Spinner by default will fire change event after text field has lost focus, this can be changed. See `Spinner#setTextFieldEventPolicy` and `Spinner#TextFieldEventPolicy`. - Use `TextFieldEventPolicy.ON_ENTER_ONLY` to preserve old behaviour - **Changed**: `FileChooser` will auto focus file list scroll pane when added to stage (use `FileChooser.focusFileScrollPaneOnShow` to override this setting) - **Fixed**: [#207](https://github.com/kotcrab/vis-ui/issues/207) crash when user has placed text field cursor after last letter (possibly on LWJGL backend only) #### Version: 1.1.5 (libGDX 1.9.3) - **API Changed**: `VisTextField#setCurosrAtTextEnd` renamed to `setCursorAtTextEnd` (typo) - **Added**: `Tooltip#getTarget` - **Added**: `MenuItem` constructors taking style name - **Changed**: It's now impossible to create `FileTypeFilter` `Rule` without providing at least one extension - **Changed**: `FileTypeFilter` select box won't be shown when `FileChooser` `SelectionMode` is set to `DIRECTORIES` - **Changed**: `FileChooser` now can be closed by pressing enter when file name field has focus - **Changed**: `Dialogs#showOKDialog` can be closed using enter and escape key - **Changed**: [#176 (comment)](https://github.com/kotcrab/vis-ui/issues/176#issuecomment-237046516) - `FileChooser` path text field will now show end of the path when it's too long - **Changed**: `FileChooser` will fallback to default directory when `setDirectory` is called with invalid file handle (either non existing path or pointing to file) - Fixes possible crash when current directory is removed while it's open in file chooser - Removed protected `handleAsyncError`, no longer needed #### Version: 1.1.4 (libGDX 1.9.3) - **Added**: `BusyBar` - used to indicate that background work is going on - see `TestBusyBar` - **Added**: `MultiSplitPane` - similar to `VisSplitPane` but supports multiple widgets at once - **Added**: `Tooltip.Builder#width()`, `Tooltip#setText(String)`, `Tooltip#getContentCell()` - **Changed**: `FileChooser` directory listing is now performed on separate thread to prevent application hanging when accessing unresponsive drive - **Changed**: When `ColorPicker` is canceled previous color is restored after window fade out have been finished to avoid flickering (listeners are not affected by this change) - **Fixed**: `PopupMenu` with single item is now accessible using keyboard - **Fixed**: `TabbedPane` unable to move tab to the last position in pane - **Skin changes**: - **Added**: `TabbedPane` added style: `vertical` #### Version: 1.1.3 (libGDX 1.9.3) - **API Changed**: `FileChooser.setSaveLastDirectory` is now static and must be called before creating chooser to properly restore saved directory. - Last directory will not be saved when user has canceled selection dialog - **Changed**: `VisWindow#fadeOut` will reset alpha back to 1f after completing action. #### Version: 1.1.2 (libGDX 1.9.3) - **Added**: `CursorManager` - **Added**: `ScrollableTextArea` and `HighlightTextArea` with `Highlighter` API - **Added**: `VisTextField#setCurosrAtTextEnd()`, `#getProgrammaticChangeEvents()` - **Added**: `FileChooser` file sorting options available under right click menu - **Added**: `FileChooser#setSorting(FileSorting)` and `#setSortingOrderAscending` along with appropriate getters - **Added**: `FileChooser#setSaveLastDirectory` - allows to automatically remember last directory user browsed between app launches, disabled by default - **Fixed**: VisSplitPane was not restoring default cursor when user dragged pointer outside od pane area - **Fixed**: [#188](https://github.com/kotcrab/vis-ui/issues/188) - same instance of `VisDialog` couldn't be closed for the second time using close button - **Fixed**: `FileChooser` NPE when user right clicked last file item after deleting all others files - **Fixed**: `FileChooser` Duplicated instances of same disk could be visible on list when chooser was displayed right after creating - **Fixed**: [#196](https://github.com/kotcrab/vis-ui/issues/196) - `ColorPicker` sending old color to listener instead of new - **Changed**: `FileChooser` in save mode with active file type filter rule will automatically append rule extensions if user haven't typed extension or extension was wrong - **Changed**: `FileChooser` now shows files modified date when using details view mode - **Changed**: `FileChooser` will no longer show files when selection mode is `DIRECTORIES` - this behaviour can be changed in `DefaultFileFilter` - **Changed**: When `VisWindow#fadeOut()` is called then window touchable is set to disabled. Additionally keyboard focus is reset if any window child `Actor` owns keyboard focus. - This is done to prevent user input after fade out animation has started. - After fade out has finished window touchable property will be restored to previous value which was set before fade out started. - **Skin changes**: - **Added**: `VisTextArea` added style: `textArea` - no background drawable and focus border is disabled - **Added**: `FileChooserStyle` added `Drawable`: `contextMenuSelectedItem` - used to mark active item in context menu (by default `vis-radio-tick`) - **I18N Changes**: - **FileChooser**: added keys `contextMenuSortBy`, `sortByName`, `sortByDate`, `sortBySize`, `sortByAscending`, `sortByDescending` - **Misc**: Disabling Android Lint is no longer necessary #### Version: 1.1.1 (libGDX 1.9.3) - **Fixed**: NPE in FileChooser crash when navigating to other directory #### Version: 1.1.0 (libGDX 1.9.3) - **API Moved**: `JNAFileDeleter` was moved to [vis-ui-contrib](https://github.com/kotcrab/vis-ui-contrib) project - **API Deprecated**: `FileChooser.setFavoritesPrefsName()` replaced by `FileChooser.setDefaultPrefsName()` - **API Changed**: GridGroup is now taking float for item size instead of int. - **Warning:** There were two constructors `GridGroup (float spacing)` and `GridGroup (int itemSize)`. Constructor taking float spacing was removed. Constructor taking int item size now takes float. - **API Changed**: Refactored `FileChoose.FileIconProvider`, new methods added. `#provideIcon` takes `FileChooser.FileItem`, was `FileHandle` - **API Changed**: Refactored `VisCheckBox` - Style was refactored to separate checkbox background and tick drawable (see below for full skin drawables changes) - `VisCheckBoxStyle` now extends `TextButtonStyle`, was `CheckBox` (fields was renamed to properly communicate their functions) - `getImage()` removed, use `getBackgroundImage()` or `getTickImage()` - `getImageCell()` removed, use `getImageStackCell()` - protected `getCheckboxImage()` removed, override `getCheckboxBgImage()` or `getCheckboxTickImage()` - `getStyle()` returns `VisCheckBoxStyle`, was `CheckBoxStyle` - **Added**: default styles for `ImageButton` and `ImageTextButton`. Note: this is only applies to standard scene2d widgets. VisUI widgets equivalents (`VisImageButton`, `VisImageTextButton`) already had them. - **Added**: `SimpleFormValidator#validate` - **Added**: `ToastManager`, `Toast`, `ToastTable` - **Added**: VisTextField read-only mode (`VisTextField#setReadOnly(boolean)`) - **Added**: `TabbedPane#getUIOrderedTabs()` - **Added**: `FileChooser#setFavoriteFolderButtonVisible(true)` - FileChooser now can display 'add folder to favorites' button in the toolbar - **Added**: `FileChooser#setPrefsName()` - **Added**: `FileTypeFilter`, `FileChooser#setFileTypeFilter(...)` - **Added**: `MenuItem#getSubMenuIconCell()` and `MenuItem#getShortcutCell()` - **Added**: `VisTextField#setEnterKeyFocusTraversal(boolean)` - **Added**: `VisTextField#setCursorPercentHeight` - **Added**: `PopupMenuListener` - **Added**: `PopupMenu#showMenu (Stage stage, Actor actor)` - **Added**: `ConstantIfVisibleValue` - **Added**: `Sizes#borderSize` - **Added**: `Sizes#fileChooserViewModeBigIconsSize`, `fileChooserViewModeMediumIconsSize`, `fileChooserViewModeSmallIconsSize`, `fileChooserViewModeListWidthSize` - **Changed**: [#169](https://github.com/kotcrab/vis-ui/issues/169) - `TabbedPane#getTable()` returns `TabbedPaneTable` (holds reference to `TabbedPane` and allow to easily get its cells for customization) - **Changed**: `FileChooser` now tries to maintain selection while rebuilding file list - **Changed**: `FileChooser` will now select new folder after creating it - **Changed**: `FileChooser` will be automatically refreshed when added to `Stage` - **Changed**: `FileChooser` when typing file names manually suggestion will be showed - **Changed**: `TabbedPane`'s Tab now can't be dragged using it's close button - **Changed**: Synced `VisTextField` ans `VisTextArea` with equivalents of those classes libGDX - **Changed**: `PopupMenu` now support menu navigation using arrows keys - **Changed**: `PopupMenu` now optionally takes `Sizes` instance (added constructor `PopupMenu (Sizes sizes, PopupMenuStyle style)`) - **Removed deprecated API**: `NumberSelector` - replaced by `Spinner` - **Removed deprecated API**: `Sizes#numberSelectorButtonSize`, `numberSelectorButtonsWidth`, `numberSelectorFieldSize`, `numberSelectorFieldRightPadding` - **Fixed**: `Sizes.buttonBarSpacing` was ignored by `ButtonBar` - **Added**: constructors `ButtonBar(Sizes sizes, String order)` and `ButtonBar(Sizes sizes)` - **Fixed**: `TabbedPane` layout when no separator image was used. Fixed misc issue with close button style on touch down. - **Fixed**: `FileChooser` NPE when error occurred during directory deleting - **Fixed**: `FileChooser` non empty directories are now deleted correctly when using default `FileChooser` deleter - **Fixed**: `FileChooser` crash when user manually entered path to file instead of directory - **Fixed**: `FocusManager` calling `focusLost()` when the widget that was already focused tried to gain focus again - **Fixed**: `VisSplitPane` was not implementing `hit(...)` which could result in widget that was underneath split pane's handle get touch events - **Fixed**: Now it's not possible to call `VisWindow#fadeOut` multiple times - **Skin changes**: - **Changed**: `FileChooserStyle`: added drawable fields: `iconStar`, `iconStarOutline`, `iconRefresh`, `iconListSettings`, `expandDropdown` - **Added**: drawable `window-border-bg.9`, `icon-star`, `icon-star-outline`, `icon-refresh`, `icon-list-settings` - **Added**: style `BaseToastStyle` - **Added**: VisTextField `label` style - if combined with read-only mode allows to create selectable labels - **Updated**: `cursor` drawable (`cursor.9.png`) - **Removed**: `check-down-on`, `check-down`, `check-on-disabled`, `check-over-off`, `check-over-on`, `radio-down-on`, `radio-down`, `radio-on-disabled`, `radio-over-off`, `radio-over-on` - **Added**: `vis-check`, `vis-check-over`, `vis-check-down`, `vis-check-tick`, `vis-check-tick-disabled`, `vis-radio`, `vis-radio-over`, `vis-radio-down`, `vis-radio-tick`, `vis-radio-tick-disabled` - **I18N Changes**: - **FileChooser**: added keys `contextMenuRefresh`, `fileType`, `allFiles`, `changeViewMode`, `viewModeList`, `viewModeDetails`, `viewModeBigIcons`, `viewModeMediumIcons`, `viewModeSmallIcons` - **Misc**: Added Gradle tasks to package VisUI skin textures and compile USL into JSON (`gradlew :ui:compileSkin`) #### Version: 1.0.2 (libGDX 1.9.2) - **Changed**: [#163](https://github.com/kotcrab/vis-ui/issues/163) - When `VisCheckBox` or `VisTextField` is disabled and is marked as invalid then error border won't be drawn. - **Changed**: [#163](https://github.com/kotcrab/vis-ui/issues/163) - Added `SimpleFormValidator#setTreatDisabledFieldsAsValid` (and it's getter) - allow to control whether to mark form as invalid when invalid but disabled field is encountered. If set to true then all disabled fields are treated as valid, regardless of their state. - Defaults to true! Set to false to preserve old behaviour. - **API Changed**: `DragListener`: `Draggable` argument was added to each method - **API Deprecated**: `Sizes#numberSelectorButtonSize`, `numberSelectorButtonsWidth`, `numberSelectorFieldSize`, `numberSelectorFieldRightPadding` replaced by `spinnerButtonSize`. `spinnerButtonsWidth`, `spinnerFieldSize`, `spinnerFieldRightPadding` - **API Deprecated**: `NumberSelector` - replaced by `Spinner`, `NumberSelector` will be removed in future version - **Added**: `VisTextField#isTextSelected()` - **Added**: `VisTextField#clearText()` - **Added**: `FloatingGroup` - **Added**: `VisWindow#isKeepWithinParent` and `VisWindow#setKeepWithinParent` - **Added**: constructor `VisImage (String drawableName)` - **Added**: `VisUI.load(String internalVisSkinPath)` - **Added**: `VisTextField#setIgnoreEqualsTextChange(...)` - see [#165](https://github.com/kotcrab/vis-ui/issues/165) - **Fixed**: `OptionDialog#set(...)ButtonText` now updates dialog size - **Fixed**: [#131](https://github.com/kotcrab/vis-ui/issues/131) - fixed issue when copying numbers between `VisTextField`s with `FloatDigitsOnlyFilter` decimal point was lost - **Fixed**: `ListView#AbstractListAdapter` error on GWT - **Fixed**: `VisTextField` was changing system cursor when it was disabled - **Fixed**: [#165](https://github.com/kotcrab/vis-ui/issues/165) - fixed form not refreshed when text field content was changed to the same as before #### Version: 1.0.1 (libGDX 1.9.2) - **Added**: `ListView#getListAdapter()` - **Added**: `ListView#rebuildView()` and `UpdatePolicy.MANUAL` - **Added**: `Draggable#setDeadzoneRadius` - **Fixed**: Not being able to resize window with `TabbedPane` - **Fixed**: `OptionDialog` not modal by default - **Fixed**: `SimpleListAdapter` not working on GWT - **Fixed**: `VisCheckBox` focus border appeared was displayed in wrong place when using `Cell#growX()` - **Changed**: `DragPane`: `LimitChildren` listener now never rejects own children, even when max children amount is achieved. - **API Changed**: `ListView#getMainTable()` now returns `ListViewTable` instead of `VisTable` - **API Changed**: Added `ListAdapter.add(ItemT)` #### Version: 1.0.0 (libGDX 1.9.2) - **Changed**: `InputValidator` moved to `com.kotcrab.vis.ui.util` package - **Changed**: `LesserThanValidator#setEquals(boolean)` renamed to `setUseEquals` - **Changed**: `GreaterThanValidator#setEquals(boolean)` renamed to `setUseEquals` - **Changed**: `FormInputValidator#validateInput` is now final and can't be overridden - **Changed**: `FormInputValidator#getLastResult` is now package-private - **Changed**: `DialogUtils` renamed to `Dialogs` - **Changed**: `DialogUtils.properties` is now `Dialogs.properties` - **Changed**: `VisUI#setDialogUtilsBundle(...)` is now `VisUI#setDialogsBundle(...)` - **Changed**: `VisUI#getDialogUtilsBundle()` is now `VisUI#getDialogsBundle()` - **Added**: `showDetailsDialog (Stage stage, String text, String title, String details)` - **Added**: `showDetailsDialog (Stage stage, String text, String title, String details, boolean expandDetails)` - **Changed**: `ErrorDialog` renamed to `DetailsDialog` - **Changed**: Constructor `ErrorDialog (String text, String stacktrace)` changed to `DetailsDialog (String text, String title, String details)` - **Added**: `DetailsDialog#setDetailsVisible(...)` - **Added**: `DetailsDialog#setCopyDetailsButtonVisible(...)` - **Changed**: `FileChooserText`, `FilePopupMenu` and `ColorPickerText` moved to `internal` subpackages (were not part of public API) - **Changed**: `FileChooser#getFileDeleter` removed - **Changed**: `FileChooserListener` was refactored - `FileChooserListener#selected(FileHandle)` removed - If user can select single file use `SingleFileChooserListener` - If user can select multiple files use `StreamingFileChooserListener` or use `FileChooserListener` directly - **Changed**: `VisTextField#toString()` now returns field text - **Changed**: `OptionDialog` now extends `VisWindow` (was extending `VisDialog`) - **Changed**: `OptionDialog` and `InputDialog` now will show buttons in platform dependant order using `ButtonBar` - **Removed**: Removed all `Tooltip` constructors except those taking style - Use `new Tooltip.Builder(...)` eg. `new Tooltip.Builder("Tooltip Text").target(label).build()` - **Changed**: constructor `Tooltip (String text)` is now `Tooltip (String styleName)` - **Added**: constructor `Tooltip ()` - **Added**: constructor `Tooltip (TooltipStyle)` - **Removed**: `SeparatorStyle#vertical`, was not used - **Removed**: constructor `Separator (boolean vertical)` - **Added**: `ListView` and `ItemAdapter` API - **Added**: constructor `TabbedPane(TabbedPaneStyle style, Sizes sizes)` - **Added**: constructor `VisWindow(String title, String styleName)` - **Added**: `PrefWidthIfVisibleValue` - **Added**: `HorizontalFlowGroup` and `VerticalFlowGroup` - **Added**: `ButtonBar` - convenient class for creating button panels arranged in platform dependant order. - `FileChooser`, `ColorPicker` and `Dialogs` will now show buttons in platform dependant order - **Added**: `LinkLabel`, `VisTextField`, `VisTextArea` and `VisSplitPane` supports system cursors when using LWJGL3 or GWT backend - **Fixed**: `TabbedPane`: Tab close button too small when using `SkinScale.X2` - **Fixed**: `TabbedPane`: In vertical mode, tabs buttons were centered instead of being aligned at the top - **Removed deprecated API**: `ColumnGroup` (use libGDX's `VerticalGroup`) - **Skin**: - **Changed**: Color `menuitem-grey` renamed to `menuitem` - **Changed**: `TabbedPaneStyle#bottomBar` renamed to `separatorBar` - **Removed**: `FormValidatorStyle#colorTransition`, no longer needed. - If `colorTransitionDuration` is set to 0 then transition will be skipped. - **Removed**: `SeparatorStyle#vertical`, no longer needed - **Added**: Drawables: `grey`, `vis-blue`, `vis-red` - **Added**: New `Window` style: `resizable` - **I18N**: - **Changed** Bundle management moved to `Locales` class. Instead of calling `VisUI.setXYZBundle(...)` call `Locales.setXYZBundle(...)` - **Removed**: Dialogs bundle entries: yes, no, cancel, ok. Now handled by `ButtonBar` bundle. #### Version: 0.9.5 (libGDX 1.7.1) - **Added**: constructor `SimpleFormValidator(Disableable)`. - **Added**: `ActorUtils#keepWithinStage(Actor)`. - **Deprecated**: `ColumnGroup`. Will be removed in future versions. Use libGDX's `VerticalGroup` which supports more features. - **Fixed**: `BasicColorPicker` palette color selection were flipped. - **Fixed**: `BasicColorPicker` removed unnecessary right padding. - **Fixed**: `MenuItem` sub menu could appear outside screen. - **Fixed**: `DialogUtils`'s `ConfirmDialog` text label was not centered. #### Old changelog file: ``` [0.9.4] [libGDX 1.7.1] -Fixed GWT support [0.9.3] [libGDX 1.7.1] -API Change: FocusManager.getFocus(Stage) renamed to resetFocus -API Change: FocusManager.getFocus(Stage, Focusable) renamed to switchFocus -API Change: GridGroup#getItemSize() removed, use getItemWidth or getItemHeight -API Change: Moved FileChooser's FavouritesIO to 'com.kotcrab.vis.ui.widget.file.internal' package (isn't VisUI public API) -API Addition: FocusManager.resetFocus(Stage, Actor) -API Addition: FileChooser#getCurrentDirectory() -API Addition: GridGroup#setItemWidth(int), GridGroup#setItemWidth(int), GridGroup#getItemWidth(), GridGroup#getItemWidth() -API Addition: GridGroup#getItemSize(int width, int height) -API Addition: TabbedPane#disableTab(Tab tab, boolean disable), TabbedPane#isTabDisabled(Tab) -API Addition: IntDigitsOnlyFilter -API Addition: DragPane -API Addition: VisValue, VisWidgetValue - standard Table Values can be used as lambadas if you are using Java 1.8 -API Addition: PrefHeightIfVisibleValue -ColorPicker: -API Change: added ColorPickerListener#reset (Color previousColor, Color newColor) -Rewritten using shaders, huge performance boost, now usable on gwt and low end devices -Internal ColorPicker classes moved to `com.kotcrab.vis.ui.widget.color.internal` package (remember that those classes aren't considered as public api) -ColorPicker can now be used as embeddable widget, see ExtendedColorPicker and BasicColorPicker -Added ColorPickerWidgetStyle used by ExtendedColorPicker and BasicColorPicker -Changed ColorPickerStyle, now uses ColorPickerWidgetStyle as composition -I18N: Removed entries: "old", "new" (no longer needed) -Style: removed fields: alphaBar10px and alphaBar25px, white (no longer needed) -NumberSelector: -API Addition: setMaxLength(int), getMaxLength() -Fixed entering negative integer values -Fixed NumberSelector text field focus border when using SkinScale.X2 -When selector loses focus and entered value is bigger than max it will be set to max value, if it's smaller than min it will be set to min value -Previous behaviour was to restore last valid value -Trying to use PopupMenu.add(Actor) with MenuItem will throw an exception (MenuItems must be added using addItem method) -ColorPickerStyle now extends WindowStyle -Fixed GridGroup too high when total items width in single row was equals to group width -GridGroup now supports setting item width and height separately -TabbedPane tabs order can be changed by mouse dragging -Skin changes: -Removed drawables: alpha-bar-10px, alpha-bar-25px -VisUI is now supported by LML (templates for scene2d.ui with HTML-like syntax), https://github.com/czyzby/gdx-lml-vis [0.9.2] [libGDX 1.7.1] -API Change: VisValidatableTextField#getValidator() removed, use getValidators() instead -API Change: Removed constructors FileChooser taking I18NBundle -This way inconsistent with other widgets that did not support this, use VisUI class for setting global I18NBundles -API Change: NumberSelector now supports float values, methods taking and returning integers are now using floats -API Change: NumberSelectorListener#changed(int) is now NumberSelectorListener#changed(float) -API Change: VisProgressBar and VisSlider now extends standard scene2d.ui ProgressBar and Slider (should not affect existing code) -VisSliderStyle was removed, use SliderStyle which is fully compatible with VisSliderStyle -API Change: changed ColorPickerListener#canceled () to ColorPickerListener#canceled (Color oldColor) -API Change: added ColorPickerListener#changed (Color newColor) -If you are using ColorPickerAdapter this does not affect you -API Change: removed constructor LinkLabel (CharSequence text, String fontName, String colorName) because it was misleading with LinkLabel (CharSequence text, CharSequence url, String styleName) -API Addition: NumberSelector#setPrecision -API Addition: NumberSelector(String name, float initialValue, float min, float max, float step, int precision) -API Addition: NumberSelector(String styleName, String name, float initialValue, float min, float max, float step, int precision) -API Addition: NumberSelector(NumberSelectorStyle style, Sizes sizes, String name, float initialValue, float min, float max, float step, int precision) -API Addition: InputDialog#setText(String text), InputDialog#setText(String text, boolean selectAll) -API Addition: FileChooser#setSelectedFiles (FileHandle... files) -API Addition: FloatDigitsOnlyFilter -API Addition: VisUI.isLoaded() -API Addition: constructor VisValidatableTextField (String text, String styleName) -API Addition: constructor VisValidatableTextField (String text, VisTextFieldStyle style) -API Addition: CollapsibleWidget#setTable() -API Addition: VisWindow#setCenterOnAdd(boolean) -API Addition: ColorPicker#setAllowAlphaEdit (boolean allowAlphaEdit) -API Addition: ColorPicker#isDisposed() API Addition: VisCheckBox.setStateInvalid(boolean) and VisCheckBox.isStateInvalid() -VisCheckBox and VisRadioButton can now be marked as invalid (error border will be drawn around them) -CollapsibleWidget now supports creation without setting initial table -Added default style for standard scene2d.ui TextTooltip -Added VisTextField and TextArea "small" style with smaller font -LinkLabel now uses LinkLabelStyle -LinkLabel now has link underline on mouse over -SimpleFormValidator (note that following also applies to FormValidator): -API Addition: constructor SimpleFormValidator (Disableable targetToDisable, Label messageLabel, String styleName) -API Addition: constructor SimpleFormValidator (Disableable targetToDisable, Label messageLabel, FormValidatorStyle style) -API Addition: SimpleFormValidator#setMessageLabel(), and #setSuccessMessage -API Addition: SimpleFormValidator#addDisableTarget(Disableable) and SimpleFormValidator#removeDisableTarget(Disableable) -Now using FormValidatorStyle: allows to set color for message label when form is valid or invalid -Two built-in styles: 'default' and 'smooth' with smooth transition between colors -Any object implementing Disableable interface may be passed to FormInputValidator as target to disable if form is invalid -Multiple Disableable targets are supported -Added CheckBox support (checking if checkbox is checked/unchecked) -TextField/VisTextField.setMessageText(String text) now works properly. -DialogUtils methods now return dialogs objects (was void) -Focus border can now be disabled on all widgets having it -Fixed VisTree taking VisTextField focus when it was placed inside tree node -Fixed invalid key up/down events propagation when using multiple input processors -Fixed font kerning for character pairs: Ma, Me, Mi -Fixed TabbedPane listener called multiple times when user was switching current tab -Fixed TabbedPane close tab buttons styles -Fixed 'directory' validator in FormValidator -Added libGDX version check before loading VisUI, in case of version mismatch warning will be printed to console -Can be suppressed by VisUI.setSkipGdxVersionCheck(true); [0.9.1] [libGDX 1.7.0] -Updated to libGDX 1.7.0 [0.9.0] [libGDX 1.6.5] -API Change: Renamed VisValidableTextField to VisValidatableTextField (fixes typo in name) -API Change: MenuItem#getShortcut() returns CharSequence (was String) -API Addition: ColorPicker#setCloseAfterPickingFinished(boolean) -Now Tab#onHide() is called before Tab#dispose() -VisValidatableTextField ChangeEvent is now fired after input validation -Fixes bug with input dialog with validator, it was possible to enter invalid value -Fixed MenuItem not properly updated after changing menu shortcut when item was already added to PopupMenu -Fixed MenuItem not using bigger icons when using SkinScale.X2 -FileChooser: -Added tooltips for back, forward, and parent directory buttons -Added "New directory" button next to path field -Added popup menu icons -DefaultFileFilter class is now public so it's possible to extend it -If your project uses JNA library you can enable moving files to trash instead of deleting them permanently (chooser.setFileDeleter(new JNAFileDeleter())) -In file view added icons for common file types: text, images, audio and pdf. Custom icons can be supplied by setting FileIconProvider (see chooser.setIconProvider(...)) -I18N changes: added entries: back, forward, parentDirectory, newDirectory, popupDeleteFileFailed, contextMenuMoveToTrash, contextMenuMoveToTrashWarning -Skin changes: added iconFolderNew, iconFolderStar, iconTrash, iconFileText, iconFileImage, iconFilePdf, iconFileAudio [0.8.2] [libGDX 1.6.4] -API Addition: VisValidableTextField#restoreLastValidText() -API Addition: OsUtils.isAndroid(), OsUtils.getAndroidApiLevel(), OsUtils.isIos(), OsUtils.getShortcutFor(int... keycodes) -API Addition: MenuItem#setShortcut(int... keycodes) -API Addition: VisSplitPane#getFirstWidgetBounds(), VisSplitPane#getSecondWidgetBounds() -API Addition: NumberSelector#setProgrammaticChangeEvents(boolean), NumberSelector#setValue(int value, boolean fireEvent) -API Addition: NumberSelector#removeChangeListener(...) -API Change: Removed MenuItem#setShortcut(int modifier, int keycode) -API Change: FileUtils.isMac(), FileUtils.isUnix() and FileUtils.isWindows() moved to OsUtils -Added ColumnGroup -Fixed MenuItem shortcut label color when MenuItem is disabled -If user clicks mouse before Tooltip appears, Tooltip won't be showed -Fixed issue with GridGroup in ScrollPane - scroll bar appeared too late -Fixed GWT compilation issues -PopupMenu is now kept inside stage when displaying it -FileChooser -API Change: Removed FileChooser#setVisble(boolean) (typo in name), use FileChooser#setVisible(boolean) -API Change: Renamed: setGroupMultiselectKey to setGroupMultiSelectKey, getGroupMultiselectKey to getGroupMultiSelectKey, setMultiselectKey to setMultiSelectKey, getMultiselectKey to getMultiSelectKey setMultiselectionEnabled to setMultiSelectionEnabled, isMultiselectionEnabled to isMultiSelectionEnabled -Fixed issue with very slow chooser creation on computers with floppy disk drivers installed ( https://github.com/kotcrab/vis-ui/issues/11#issuecomment-136892177 ) -Fixed crash in when user tried to use history buttons for no longer existing directory -Fixed multiple selection when selection mode was set to FILES or DIRECTORIES -Added support for the back and forward mouse button for navigating in the history -I18N -added directoryNoLongerExists -added missing entries: newDirectoryDialogTitle, newDirectoryDialogText [0.8.1] [libGDX 1.6.4] -Updated libGDX to 1.6.4 -Error dialog from DialogUtils with exception will now show stacktrace from nested exceptions -Skin change: ColorPickerStyle alphaBar25pxShifted removed (no longer necessary) -Fixed VisSelectBox list elements padding -Added skin in higher resolution (can be loaded by new method: VisUI.load(SkinScale.X2)) -SVG file is also available thanks to piotr-j (https://github.com/piotr-j) -Removed VisUI.getDefaultSpacingTop/Bottom/Right/Left and VisUI.setDefaultSpacingTop/Bottom/Right/Left -Replaced by Sizes class ( VisUI.getSizes() ) -TableUtils.setSpacingDefault now properly uses all spacings if set -OptionDialog (DialogUtils.showOptionDialog(...)) message label is now by default center aligned. -Fixed bug in ColorPicker: pasting hex value was changing picker old color [0.8.0] [libGDX 1.6.3] -API Addition: Tooltip (Actor target, String text, int textAlign) -API Addition: Tooltip (String styleName, Actor target, String text, int textAlign) -API Addition: FormInputValidator#hideErrorOnEmptyInput (can be used with FromValidator to don't display error message if field is empty, field will be still marked with red border and accept button will be disabled) -API Addition: FormValidator#directory(...) -API Addition: FormValidator#directoryEmpty(...) -API Addition: FormValidator#directoryNotEmpty(...) -Optimized FileChooser ( https://github.com/kotcrab/vis-ui/issues/11 ) -Fixed issue when FileChooser confirm button text wasn't updated after changing mode ( https://github.com/kotcrab/vis-ui/pull/14 ) -Fixed issue when FileChooser would crash on file delete dialog -Added 'blue' button style -Added New Directory item in FileChooser popup menu ("contextMenuNewDirectory" was added to FileChooser I18N file) -FileChooser I18N file: added contextMenuNewDirectory, newDirectoryDialogIllegalCharacters, newDirectoryDialogAlreadyExists properties -Improved small font (some uppercase letters were missing 1px at the top) [0.7.7] [libGDX 1.6.1] -uiskin.json is now generated from USL (see USL page on GitHub Wiki, if you are not writing custom VisUI skins this does not affect you) -API Addition: various getters and setters in NumberSelector -API Addition: TabbedPane#getActiveTab -API Change: Menu#selectButton(TextButton) and deselectButton(TextButton) no longer public, they wasn't part of public API -Added 'navigate to parent directory' button in FileChooser -FileChooser now displays "Computer" instead of "/" in partitions list (also added 'computer' entry in FileChooser I18N file) -FileChooser: improved history (back and forward button) -FileChooser now can will automatically updates drives list after connecting/removing drive, usb stick etc. -FileChooser now will refresh files list after some files were changed in current directory -FileChooser I18N: added property: popupSelectedFileDoesNotExist. Removed: popupOk, popupYes, popupNo (replaced by DialogUtils) -Added some constructors that allows to use widgets without depending on VisUI.getSkin() -Fixed infinite key repeat bug on Android in VisTextField ( https://github.com/kotcrab/vis-ui/issues/9 ) -Fixed small gap with empty title in NumberSelector -Fixed issue where FileChooser file list wasn't rebuilt after setting new file filter -Fixed issue where NumberSelector won't allow to enter value if min is greater than 0 ( related to https://github.com/kotcrab/vis-ui/issues/7 ) -Fixed closeOnEscape() with multiple windows (windows were closed in improper order) ( https://github.com/kotcrab/vis-ui/issues/10 ) -Fixed invalid title align in VisWindow after adding close button when title align is not set to center -Fixed issue with disappearing MenuItem after opening PopupMenu while holding right mouse button and dragging down ( https://github.com/kotcrab/vis-ui/commit/a17e309b980b5d0db061a315685501e405811ff6 ) -FileChooser, ColorPicker, Tooltip, Menu and MenuBar now can use styles defined in skin file -ColorPicker now supports I18N (added VisUI.setColorPickerBundle(I18NBundle)) [0.7.6] [libGDX 1.6.0] -Updating to libGDX 1.6.0 [0.7.5] [libGDX 1.5.6] -Added VisImageTextButton [0.7.4] [libGDX 1.5.6] -API Addition: NumberSelector#setValue(int) -After adding close button to VisWindow, title label will be automatically centered if noting else was added to title table [0.7.3] [libGDX 1.5.6] [POM invalid, don't use] -Updating to libGDX 1.5.6 -Fixed input bug in VisTextField -Tooltip now can be created without setting target [0.7.2] [libGDX 1.5.5] [POM invalid, don't use] -API Addition: NumberSelector (String name, int initialValue, int min, int max) -API Addition: VisUI.load(Skin) -API Addition: GridGroup -API Addition: LinkLabel -API Addition: VisValidableTextField#setRestoreLastValid(boolean) -API Addition: VisTextButton (String, ChangeListener) -API Addition: VisTextButton (String, String, ChangeListener) -API Addition CollapsibleWidget.setCollapsed (boolean collapse, boolean applyAnimation) to change collapse state without animation -Fixed menu not closing after clicking it on MenuBar -Fixed submenu visible for disabled MenuItem -Fixing some Tooltip problems, Tooltip now won't fade away when user has it mosue over it -Better padding on VisList/List item (default list style selection drawable now uses 'padded-list-selection.9') -New default favoritesPrefsName is com.kotcrab.vis.ui.widget.file.filechooser_favorites (was pl.kotcrab.vis.ui.widget.file.filechooser_favorites) -Now waring will be printed to console if using default favorites preference name (see FileChooser.setFavoritesPrefsName(String)) -Improving text field (faster input while holding key) [0.7.1] [libGDX 1.5.5] -Fixing GWT compatibility [0.7.0] [libGDX 1.5.5] * Renaming: Validators.integers renamed to Validators.INTEGERS Validators.floats renamed to Validators.FLOAT PopupMenu#displayMenu renamed to PopupMenu#showMenu Skin Change: Separator 'height' renamed to 'thickness' * Moving classes / reorganizing: VisTable moved to com.kotcrab.vis.ui.widget package DialogUtils moved to com.kotcrab.vis.ui.util.dialog package OptionDialogListener and OptionDialogAdapter moved to com.kotcrab.vis.ui.util.dialog package InputDialogListener and InputDialogAdapter moved to com.kotcrab.vis.ui.util.dialog package FormValidator, SimpleFormValidator, FormInputValidator moved to com.kotcrab.vis.ui.util.form package BasicFormValidator renamed to SimpleFormValidator TableUtils.setSpaceDefaults renamed to TableUtils.setSpacingDefaults * Menu system changes: Submenus are now supported Removed PopupMenu constructors taking boolean, now auto remove is always enabled. Now when user has clicked MenuItem then PopupMenu will be removed from stage MenuBar constructor doesn't take Stage argument anymore Added: MenuItem#setSubMenu(PopupMenu subMenu) Added: MenuBar#addMenu(Menu menu) MenuBar#removeMenu(Menu menu) MenuBar#insertMenu(int index, Menu menu) Added: MenuBar#closeMenu() Skin Change: MenuItem widget now uses MenuItemStyle (used TextButtonStyle). MenuItemStyle extends TextButtonStyle. MenuItemStyle adds submenu icon. * Skin changes: Separator style now has 'vertical' property Added VisSplitPane handleOver property * Other API changes: API Change: Constructor Separator(boolean useMenuStyle) is now Separator(boolean vertical). Use 'new Separator("menu")' for menu styled separator API Change: FileChooserLocale removed, now using libGDX's I18NBundle, see FileChooserText class API Change: VisWindow#getButtonTable deprecated, instead use VisWindow#getTitleTable API Change: VisDialog#getButtonTable deprecated, instead use VisDialog#getButtonsTable API Change: Removed FileChooser.getFavoritesPrefsName() * Other API additions: Added New FormValidators: integerNumber, floatNumber, valueLesserThan, valueGreaterThan Added VisTable#addSeparator (boolean vertical) Added constructor VisLabel (CharSequence text, LabelStyle style) Added constructor VisLabel (CharSequence text, int alignment) Added TableBuilder and its implementations: StandardTableBuilder, CenteredTableBuilder, GridTableBuilder, OneColumnTableBuilder, OneRowTableBuilder * Other changes: VisTree/Tree now have default mouse over drawable Added shift selection for FileChooser (key can be changed by FielChooser#setGroupMultiselectKey(...)) Tooltip is now kept within Stage border Focus border is now optional for every widget that was using it Font support for Polish, German, Spanish, French, Greek and Russian characters FormInputValidator now uses validate(String) instead of validateInput(String) for input validation, calling setResult is no longer required (for examples on how to use it see SimpleFormValidator) Added ValidatorWrapper that allows standard validator to be used with (Simple)FormValidator#custom(...) Added DialogUtils.showConfirmDialog(...) Added TabbedPane [0.6.1] [libGDX 1.5.4] -Fixed FileChooser disappearing when removing favourite [0.6.0] [libGDX 1.5.4] -API Change: VisUI.skin is not private, use VisUI.getSkin() instead -API Addition: DialogUtils.showOptionDialog (Stage stage, String title, String text, OptionDialogType type, OptionDialogListener listener) -API Addition: FormValidator.fileExists (VisValidableTextField field, VisTextField relativeTo, String errorMsg, boolean errorIfRelativeEmpty) -API Addition: ColorPicker -API Addition: ColorUtils -API Addition: VisImage -API Addition: constructor VisLabel (CharSequence text, Color textColor) -Changed close button style name to 'close-window' (was 'close') -Added 'close' button style that matches other normal buttons -Fixed focus traversing when TAB pressed in VisTextField, doesn't change focus to invisible fields and doesn't leaves modal windows -Added built-in validators: IntegerValidator, FloatValidator, GreaterThanValidator, LesserThanValidator (see Validators class) -Added VERSION string constant in VisUI [0.5.1] [libGDX 1.5.3] -Added cancelable input dialog in DialogUtils [0.5.0] [libGDX 1.5.3] -API Addition: VisValidableTextField.setValidationEnabled(boolean) -API Addition: VisValidableTextField.isValidationEnabled() -API Addition: VisValidableTextField.setProgrammaticChangeEvents(boolean) -API Addition: constructor VisImageButton (String styleName) -API Addition: constructor VisCheckBox (String text, boolean checked) -API Addition: VisWindow.addCloseButton() -API Addition: VisWindow.closeOnEscape() -API Addition: VisTextField.focusField() -API Addition: MenuItem.getShortcut() -API Addition: DialogUtils.showInputDialog (Stage stage, String title, String fieldTitle, InputDialogListener listener) -API Addition: DialogUtils.showInputDialog (Stage stage, String title, String fieldTitle, InputValidator validator, InputDialogListener listener) -API Change: VisUI.setDefaultTitleAlign and VisUI.getDefaultTitleAlign (typo fixed) -API Change: Removed deprecated TableUtils.setColumnsDefaults(Table) -Separator style "menu" height changed to 3px (was 4px), that means PopupMenu separator height is now 3px as well -File chooser now have close button in top right corner -File chooser now closes when escape key has been pressed -Fixed bug where VisValidableTextField would loss focus if user type something and field don't have ChangeListener attached -Fixed focusing next field when TAB key is pressed inside VisTextField -Added Tooltips -Moved TableUtils to com.kotcrab.vis.util package (sorry!) [0.4.1] [libGDX 1.5.2] -Fixed FileChooser padding when scrollbar was showed (because libGDX scrollpane was probably fixed as well) [0.4.0] [libGDX 1.5.2] [Important] -Important: Moving everything to com.kotcrab.vis package, new Gradle definitions: in core: com.kotcrab.vis:vis-ui:$visuiVersion in html: com.kotcrab.vis:vis-ui:$visuiVersion:sources Also don't forget to update your GdxDefinition.gwt.xml and GdxDefinitionSuperdev.gwt.xml files! [0.3.1] [libGDX 1.5.0] -Added CollapsibleWidget -Fixed VisImageButton.setGenerateDisabledImage(boolean) -Fixed MenuBar not rendering Menu content if added Menu to MenuBar after adding items to it -VisTable.addSeparator() now sets expandX() and fillX() for separator instead of expand() and fill() -GWT compatibility for DialogUtils and FormValidator -Fixed VisCheckBox and VisRadioButton focus border padding [0.3.0] [libGDX 1.5.0] -libGDX dependency version changed to 1.5.0 -Font size changed to 15 -Separator width changed to 4px, split pane bar width/height changed to 4px -Removed markup font -API Change: Removed deprecated resize() from MenuBar -API Change: New MenuItem constructors which takes Image instead of Drawable, removed MenuItem (String text, Drawable image, String styleName) -API Change: FormValidator.fileExist(...) -> FormValidator.fileExists(...) -API Change: VisTable.addSeparator() returns Cell instead of void -API Addition: FileChooser(FileChooserLocale, Mode) -API Addition: MenuItem (String, ChangeListener) -API Addition: VisUI.setDefualtTitleAlign(int align) -API Addition: VisUI.getDefualtTitleAlign() -API Addition: FormValidator.fileExist(VisValidableTextField field, File relavtiveTo, String errorMsg) -API Addition: FormValidator can also take FileHandle when using file(Not)Exist relativeTo method -API Addition: Added VisTextField.isFocusBorderEnabled() and VisTextField.setFocusBorderEnabled(boolean) -API Addition: Added FormValidator.fileNotExist(...) methods -API Addition: Added FormValidator.custom (VisValidableTextField field, FormInputValidator customValidator) -API Addition: VisSplitPane.setWidgets (Actor firstWidget, Actor secondWidget) -Fixed bug when FormValidator doesn't updated all fields borders after changes in other field -FileChooser deselects all files when reopened -FileChooser: When clicked on drive shortcut file scroll pane table will get focus automaticly -FileChooser: When sorting file list chooser now ignores uppercase/lowercase -Fixed problem where Separator didn't set color before rendering -Fixed look of disabled MenuItem, if MenuItem has an image and it is disabled, image color will be set to Color.GRAY. This can be disabled by calling item.setGenerateDisabledImage(false) [0.2.0] [libGDX 1.4.1] -API change: removed Stage from VisWindow and VisDialog constructors -API change: removed VisWindow.setPositonToCenter() replaced with VisWindow.centerWindow() -API change: VisImageButton.setGeneateDisabledImage() -> VisImageButton.setGenerateDisabledImage() (typo) -Fixed findNextTextField in VisTextField -Increased default bottom padding from 6 to 8 -Calling MenuBar.resize() no longer required -When FileChooser is in SelectionMode.DIRECTORIES, none directory is selected, and finish button was clicked, current directory will be selected -VisValidableTextField will validate input on setText() and fire ChangeEvent -VisValidableTextField.validateInput() method is now public -Added SeparatorStyle class -Added PopupMenu.addSeparator() -Added FileUtils.toFileHandle(File file) -Added FormValidator -Added VisValidableTextField() -Added VisValidableTextField(InputValidator validator) -Added VisValidableTextField(String text) -Added VisLabel() -Added VisValidableTextField.getValidator() -Added VisValidableTextField.getValidators() -Added MenuItem(String text, Drawable image) -Added DialogUtils [0.1.1] [libGDX 1.4.1] -Updated menu bar look [0.1.0] [libGDX 1.4.1] -API change: VisValidableTextField#addValidable -> VisValidableTextField#addValidator -Added: VisTextField#isEmpty() -Added: VisTable#addSeparator() -Added: VisWindow(String title) -Added: VisTextButton(String text, VisTextButtonStyle buttonStyle) -Added FileChooser (Desktop only) -Added fadeOut(), fadeIn() to VisWindow -Added VisImageButton -Added VisDialog -Added PopupMenu -Disabling button will remove its focus -VisWindow can be created with border or without it -MenuItem can be created with icon -Fixed horizontal scroll pane slider not fully visible -Fixed fade out animation not worked on VisSplitPane [0.0.3] [libGDX 1.4.1] -API change: Renamed 'components' package to 'widget' (sorry!) -Added VisValidableTextField with InputValidator -Better CheckBox text padding [0.0.2] [libGDX 1.4.1] -Added GWT compatibility [0.0.1] [libGDX 1.4.1] -Initial release ``` ================================================ FILE: ui/NOTICE ================================================ VisUI uses icons licensed under CC BY-ND 3.0 https://github.com/Templarian/WindowsIcons/blob/master/WindowsPhone/license.txt (file: icons-license) ================================================ FILE: ui/assets-raw/x1/pack.json ================================================ { duplicatePadding: false, paddingX: 1, paddingY: 1, stripWhitespaceX: true, stripWhitespaceY: true } ================================================ FILE: ui/assets-raw/x1-fonts/default.hiero ================================================ font.name=Vis Open Sans font.size=15 font.bold=false font.italic=false font2.file=C:\Data\Git\vis-ui\ui\assets-raw\VisOpenSansKerned.ttf font2.use=true pad.top=0 pad.right=0 pad.bottom=0 pad.left=0 pad.advance.x=0 pad.advance.y=0 glyph.native.rendering=false glyph.page.width=256 glyph.page.height=256 glyph.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n1234567890•\n"!`?¿'.,;:()[]{}<>|/@\^$€¥-%+=#_&~*\nñÑąćęłńóśźżĄĆĘŁŃÓŚŹŻÄäÖöÜüßĞğŞşİıÇç\náéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ\nАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ\nабвгдеёжзийклмнопрстуфхцчшщъыьэюя\nΑΆΒΓΔΕΈΖΗΉΘΙΊΚΛΜΝΞΟΌΠΡΣΤΥΎΦΧΨΩΏ·\nαάβγδεέζηήθιίκλμνξοόπρστυύφχψωώ\nϊΐςϋΰ⌘⌥⇧čďěňřšťůýžČĎĚŇŘŠŤŮÝŽ effect.class=com.badlogic.gdx.tools.hiero.unicodefont.effects.ColorEffect effect.Color=ffffff ================================================ FILE: ui/assets-raw/x1-fonts/font-small.hiero ================================================ font.name=Vis Open Sans font.size=12 font.bold=false font.italic=false font2.file=C:\Data\Git\vis-ui\ui\assets-raw\VisOpenSansKerned.ttf font2.use=true pad.top=1 pad.right=0 pad.bottom=0 pad.left=0 pad.advance.x=0 pad.advance.y=0 glyph.native.rendering=false glyph.page.width=256 glyph.page.height=128 glyph.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n1234567890•\n"!`?¿'.,;:()[]{}<>|/@\^$€¥-%+=#_&~*\nñÑąćęłńóśźżĄĆĘŁŃÓŚŹŻÄäÖöÜüßĞğŞşİıÇç\náéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ\nАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ\nабвгдеёжзийклмнопрстуфхцчшщъыьэюя\nΑΆΒΓΔΕΈΖΗΉΘΙΊΚΛΜΝΞΟΌΠΡΣΤΥΎΦΧΨΩΏ·\nαάβγδεέζηήθιίκλμνξοόπρστυύφχψωώ\nϊΐςϋΰ⌘⌥⇧čďěňřšťůýžČĎĚŇŘŠŤŮÝŽ effect.class=com.badlogic.gdx.tools.hiero.unicodefont.effects.ColorEffect effect.Color=ffffff effect.class=com.badlogic.gdx.tools.hiero.unicodefont.effects.OutlineEffect effect.Color=ffffff effect.Width=0.3 effect.Join=2 ================================================ FILE: ui/assets-raw/x2/pack.json ================================================ { duplicatePadding: false, paddingX: 1, paddingY: 1, stripWhitespaceX: true, stripWhitespaceY: true } ================================================ FILE: ui/assets-raw/x2-fonts/default.hiero ================================================ font.name=Vis Open Sans font.size=30 font.bold=false font.italic=false font2.file=C:\Data\Git\vis-ui\ui\assets-raw\VisOpenSansKerned.ttf font2.use=true pad.top=0 pad.right=0 pad.bottom=0 pad.left=0 pad.advance.x=0 pad.advance.y=0 glyph.native.rendering=false glyph.page.width=512 glyph.page.height=256 glyph.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n1234567890•\n"!`?¿'.,;:()[]{}<>|/@\^$€¥-%+=#_&~*\nñÑąćęłńóśźżĄĆĘŁŃÓŚŹŻÄäÖöÜüßĞğŞşİıÇç\náéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ\nАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ\nабвгдеёжзийклмнопрстуфхцчшщъыьэюя\nΑΆΒΓΔΕΈΖΗΉΘΙΊΚΛΜΝΞΟΌΠΡΣΤΥΎΦΧΨΩΏ·\nαάβγδεέζηήθιίκλμνξοόπρστυύφχψωώ\nϊΐςϋΰ⌘⌥⇧čďěňřšťůýžČĎĚŇŘŠŤŮÝŽ effect.class=com.badlogic.gdx.tools.hiero.unicodefont.effects.ColorEffect effect.Color=ffffff ================================================ FILE: ui/assets-raw/x2-fonts/font-small.hiero ================================================ font.name=Vis Open Sans font.size=24 font.bold=false font.italic=false font2.file=C:\Data\Git\vis-ui\ui\assets-raw\VisOpenSansKerned.ttf font2.use=true pad.top=1 pad.right=0 pad.bottom=0 pad.left=0 pad.advance.x=0 pad.advance.y=0 glyph.native.rendering=false glyph.page.width=512 glyph.page.height=256 glyph.text=ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n1234567890•\n"!`?¿'.,;:()[]{}<>|/@\^$€¥-%+=#_&~*\nñÑąćęłńóśźżĄĆĘŁŃÓŚŹŻÄäÖöÜüßĞğŞşİıÇç\náéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ\nАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ\nабвгдеёжзийклмнопрстуфхцчшщъыьэюя\nΑΆΒΓΔΕΈΖΗΉΘΙΊΚΛΜΝΞΟΌΠΡΣΤΥΎΦΧΨΩΏ·\nαάβγδεέζηήθιίκλμνξοόπρστυύφχψωώ\nϊΐςϋΰ⌘⌥⇧čďěňřšťůýžČĎĚŇŘŠŤŮÝŽ effect.class=com.badlogic.gdx.tools.hiero.unicodefont.effects.ColorEffect effect.Color=ffffff ================================================ FILE: ui/build.gradle ================================================ archivesBaseName = "vis-ui" sourceCompatibility = 1.8 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' dependencies { implementation "com.badlogicgames.gdx:gdx:$gdxVersion" testImplementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" testImplementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" testImplementation "junit:junit:$junitVersion" //testImplementation "com.kotcrab.vne:vne-runtime:1.0.1" //testImplementation "com.kotcrab.vne:vne-win-thumbnails:1.0.1" //testImplementation group: 'com.kotcrab.vis', name: 'vis-ui-contrib', version: '1.3.0' //testImplementation "org.imgscalr:imgscalr-lib:4.2" } compileTestJava { targetCompatibility = 1.8 } test { useJUnit { include "**/*Test.**" exclude 'com.kotcrab.vis.ui.test.manual.**' } } task run(dependsOn: jar, type: JavaExec) { main = 'com.kotcrab.vis.ui.test.manual.TestLauncher' classpath = sourceSets.test.runtimeClasspath ignoreExitValue = true if (System.getProperty("os.name").toLowerCase().contains("mac")) { // Required to run lwjgl java apps on Mac OSX jvmArgs = ["-XstartOnFirstThread"] } } task packSkin { doLast { description 'Compiles VisUI USL skin to JSON' def basePath = rootProject.projectDir.absolutePath + "/ui/" def x1Input = new File(basePath + "assets-raw/x1").absolutePath def x1Output = new File(basePath + "src/main/resources/com/kotcrab/vis/ui/skin/x1/").absolutePath com.badlogic.gdx.tools.texturepacker.TexturePacker.process(x1Input, x1Output, "uiskin") def x2Input = new File(basePath + "assets-raw/x2").absolutePath def x2Output = new File(basePath + "src/main/resources/com/kotcrab/vis/ui/skin/x2/").absolutePath com.badlogic.gdx.tools.texturepacker.TexturePacker.process(x2Input, x2Output, "uiskin") } } task compileUsl { doLast { description 'Pack skin textures into texture atlases' def basePath = rootProject.projectDir.absolutePath + "/ui/src/main/resources/com/kotcrab/vis/ui/skin/" def x1File = new File(basePath + "x1/uiskin.json") def x2File = new File(basePath + "x2/uiskin.json") com.kotcrab.vis.usl.Lexer.addIncludeSource(rootProject.projectDir.absolutePath + "/usl/styles") def json = com.kotcrab.vis.usl.USL.parse(null, "include ") x1File.text = json x2File.text = json } } task compileSkin(dependsOn: ['compileUsl', 'packSkin']) { description 'Pack skin textures into texture atlases and compile USL into JSON' } ================================================ FILE: ui/gradle.properties ================================================ projectName=vis-ui projectDesc=UI toolkit and flat design skin for scene2d.ui projectVersion=1.5.9-SNAPSHOT ================================================ FILE: ui/icons-license ================================================ # License Please carefully understand the license and download the latest icons at ModernUIIcons.com. ## Understand Your Rights No Attribution and No Derived Works http://creativecommons.org/licenses/by-nd/3.0/ * - If your project is open source include this license file in the source. - Nothing is needed in the front facing project (UNLESS you are using any of the icons listed below in the attribution section). - Commercial use is not only allowed but encouraged. If it is an icon in the attribution list below, you still need to attribute those! - Do not distribute the entire package (I've allowed this dozens of times for open source projects, but email me first). ## Creator - Austin Andrews (@templarian) ## Contributor** - Jay Zawrotny (@JayZawrotny) - A Bunch - Oren Nachman - appbar.chevron.down - appbar.chevron.up - appbar.chevron.left - appbar.chevron.right ## Derived Works - Alex Peattie - Social: http://www.alexpeattie.com/projects/justvector_icons/ ## Attribution*** - Kris Vandermotten (@kvandermotten) - appbar.medical.pulse - Constantin Kichinsky (@kichinsky) - appbar.currency.rubles - appbar.currency.grivna - Massimo Savazzi (@msavazzi) - List of missing exported icons - Proletkult Graphik, from The Noun Project - appbar.draw.pen (inspired) - Olivier Guin, from The Noun Project - appbar.draw.marker - Gibran Bisio, from The Noun Project - appbar.draw.bucket Andrew Forrester, from The Noun Project - appbar.fingerprint * The license is for attribution, but this is not required. ** Developers and designers that emailed Templarian the source .design icons to be added into the package. PNGs also accepted, but may take longer to be added. *** Icons I've copied so closely you want to attribute them and are also under the CC license. Contact - http://templarian.com/ - admin[@]templarian[.]com * Does not apply to copyrighted logos - Skype - Facebook - Twitter - etc... ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/FocusManager.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; /** * Manages focus of VisUI components. This is different from stage2d.ui focus management. In scene2d widgets can only * acquire keyboard and scroll focus. VisUI focus managers allows any widget to acquire general user focus, this is used * mainly to manage rendering focus borders around widgets. Generally there is no need to call those method manually. * @author Kotcrab * @see Focusable */ public class FocusManager { private static Focusable focusedWidget; /** * Takes focus from current focused widget (if any), and sets focus to provided widget * @param stage if passed stage is not null then stage keyboard focus will be set to null * @param widget that will acquire focus */ public static void switchFocus (Stage stage, Focusable widget) { if (focusedWidget == widget) return; if (focusedWidget != null) focusedWidget.focusLost(); focusedWidget = null; if (stage != null) stage.setKeyboardFocus(null); focusedWidget = widget; focusedWidget.focusGained(); } /** * Takes focus from current focused widget (if any), and sets current focused widget to null. If widgets owns * keyboard focus {@link #resetFocus(Stage, Actor)} should be always preferred. * @param stage if passed stage is not null then stage keyboard focus will be set to null */ public static void resetFocus (Stage stage) { if (focusedWidget != null) focusedWidget.focusLost(); if (stage != null) stage.setKeyboardFocus(null); focusedWidget = null; } /** * Takes focus from current focused widget (if any), and sets current focused widget to null * @param stage if passed stage is not null then stage keyboard focus will be set to null only if current * focus owner is passed actor */ public static void resetFocus (Stage stage, Actor caller) { if (focusedWidget != null) focusedWidget.focusLost(); if (stage != null && stage.getKeyboardFocus() == caller) stage.setKeyboardFocus(null); focusedWidget = null; } public static Focusable getFocusedWidget () { return focusedWidget; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/Focusable.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui; /** * Implemented by objects that can acquire VisUI focus. * @author Kotcrab * @see FocusManager */ public interface Focusable { /** Called by VisUI when object lost focus. Don not cally manually, see {@link FocusManager}. */ void focusLost (); /** Called by VisUI when object gained focus. Don not cally manually, see {@link FocusManager}. */ void focusGained (); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/Locales.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.I18NBundle; import com.kotcrab.vis.ui.i18n.BundleText; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.widget.ButtonBar; import com.kotcrab.vis.ui.widget.color.ColorPicker; import com.kotcrab.vis.ui.widget.file.FileChooser; import com.kotcrab.vis.ui.widget.tabbedpane.TabbedPane; import java.util.Locale; /** * Manages VisUI's I18N bundles. * @author Kotcrab * @since 1.0.0 */ public class Locales { private static Locale locale = new Locale("en"); private static I18NBundle commonBundle; private static I18NBundle buttonBarBundle; private static I18NBundle fileChooserBundle; private static I18NBundle dialogsBundle; private static I18NBundle tabbedPaneBundle; private static I18NBundle colorPickerBundle; /** Returns common I18N bundle. If current bundle is null, a default bundle is set and returned */ public static I18NBundle getCommonBundle () { if (commonBundle == null) commonBundle = getBundle("com/kotcrab/vis/ui/i18n/Common"); return commonBundle; } /** * Changes common bundle. Since this bundle may be used by multiple VisUI parts it should be changed before loading VisUI. * If set to null then {@link #getCommonBundle()} will return default bundle. */ public static void setCommonBundle (I18NBundle commonBundle) { Locales.commonBundle = commonBundle; } /** Returns I18N bundle used by {@link FileChooser}, if current bundle is null, a default bundle is set and returned */ public static I18NBundle getFileChooserBundle () { if (fileChooserBundle == null) fileChooserBundle = getBundle("com/kotcrab/vis/ui/i18n/FileChooser"); return fileChooserBundle; } /** * Changes bundle used by {@link FileChooser}, will not affect already created FileChoosers. * If set to null then {@link #getFileChooserBundle()} will return default bundle. */ public static void setFileChooserBundle (I18NBundle fileChooserBundle) { Locales.fileChooserBundle = fileChooserBundle; } /** Returns I18N bundle used by {@link Dialogs}, if current bundle is null, a default bundle is set and returned */ public static I18NBundle getDialogsBundle () { if (dialogsBundle == null) dialogsBundle = getBundle("com/kotcrab/vis/ui/i18n/Dialogs"); return dialogsBundle; } /** * Changes bundle used by {@link Dialogs}, will not affect already created dialogs. * If set to null then {@link #getDialogsBundle()} will return default bundle. */ public static void setDialogsBundle (I18NBundle dialogsBundle) { Locales.dialogsBundle = dialogsBundle; } /** Returns I18N bundle used by {@link TabbedPane}, if current bundle is null, a default bundle is set and returned */ public static I18NBundle getTabbedPaneBundle () { if (tabbedPaneBundle == null) tabbedPaneBundle = getBundle("com/kotcrab/vis/ui/i18n/TabbedPane"); return tabbedPaneBundle; } /** * Changes bundle used by {@link TabbedPane}, will not affect already created TabbedPane. * If set to null then {@link #getTabbedPaneBundle()} will return default bundle. */ public static void setTabbedPaneBundle (I18NBundle tabbedPaneBundle) { Locales.tabbedPaneBundle = tabbedPaneBundle; } /** Returns I18N bundle used by {@link ColorPicker}, if current bundle is null, a default bundle is set and returned */ public static I18NBundle getColorPickerBundle () { if (colorPickerBundle == null) colorPickerBundle = getBundle("com/kotcrab/vis/ui/i18n/ColorPicker"); return colorPickerBundle; } /** * Changes bundle used by {@link ColorPicker}, will not affect already created pickers. * If set to null then {@link #getColorPickerBundle()} will return default bundle. */ public static void setColorPickerBundle (I18NBundle colorPickerBundle) { Locales.colorPickerBundle = colorPickerBundle; } /** Returns I18N bundle used by {@link ButtonBar}, if current bundle is null, a default bundle is set and returned */ public static I18NBundle getButtonBarBundle () { if (buttonBarBundle == null) buttonBarBundle = getBundle("com/kotcrab/vis/ui/i18n/ButtonBar"); return buttonBarBundle; } /** * Changes bundle used by {@link ButtonBar}, will not affect already created bars. * If set to null then {@link #getButtonBarBundle()} ()} will return default bundle. */ public static void setButtonBarBundle (I18NBundle buttonBarBundle) { Locales.buttonBarBundle = buttonBarBundle; } /** * Changes current locale, this should be done when VisUI isn't loaded yet because changing this won't affect bundles * that are already loaded. */ public static void setLocale (Locale locale) { Locales.locale = locale; } private static I18NBundle getBundle (String path) { FileHandle bundleFile = Gdx.files.classpath(path); return I18NBundle.createBundle(bundleFile, locale); } public enum CommonText implements BundleText { PLEASE_WAIT("pleaseWait"), UNKNOWN_ERROR_OCCURRED("unknownErrorOccurred"); private final String name; CommonText (final String name) { this.name = name; } private static I18NBundle getBundle () { return Locales.getCommonBundle(); } @Override public final String getName () { return name; } @Override public final String get () { return getBundle().get(name); } @Override public final String format () { return getBundle().format(name); } @Override public final String format (final Object... arguments) { return getBundle().format(name, arguments); } @Override public final String toString () { return get(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/Sizes.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui; /** * VisUI padding, spacings and sizes. Loaded from skin, will differ between different skin sizes. If you are using * custom skin it must contain "default" definition of Sizes values. * @author Kotcrab * @since 0.8.1 */ public class Sizes { public float scaleFactor; public float spacingTop; public float spacingBottom; public float spacingRight; public float spacingLeft; public float buttonBarSpacing; public float menuItemIconSize; /** * Size of focus border. 1 for standard Vis skin. This is used to avoid showing overlapping borders when two widgets * have borders (for example button can have it's own focus border which without this padding would overlap with menu border) */ public float borderSize; public float spinnerButtonHeight; public float spinnerFieldSize; public float fileChooserViewModeBigIconsSize; public float fileChooserViewModeMediumIconsSize; public float fileChooserViewModeSmallIconsSize; public float fileChooserViewModeListWidthSize; public Sizes () { } public Sizes (Sizes other) { this.scaleFactor = other.scaleFactor; this.spacingTop = other.spacingTop; this.spacingBottom = other.spacingBottom; this.spacingRight = other.spacingRight; this.spacingLeft = other.spacingLeft; this.buttonBarSpacing = other.buttonBarSpacing; this.menuItemIconSize = other.menuItemIconSize; this.borderSize = other.borderSize; this.spinnerButtonHeight = other.spinnerButtonHeight; this.spinnerFieldSize = other.spinnerFieldSize; this.fileChooserViewModeBigIconsSize = other.fileChooserViewModeBigIconsSize; this.fileChooserViewModeMediumIconsSize = other.fileChooserViewModeMediumIconsSize; this.fileChooserViewModeSmallIconsSize = other.fileChooserViewModeSmallIconsSize; this.fileChooserViewModeListWidthSize = other.fileChooserViewModeListWidthSize; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/VisUI.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Version; import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.GdxRuntimeException; /** * Allows to easily load VisUI skin and change default title alignment and I18N bundles. * Contains static field with VisUI version. * @author Kotcrab */ public class VisUI { private static final String TARGET_GDX_VERSION = "1.14.0"; private static boolean skipGdxVersionCheck = false; private static int defaultTitleAlign = Align.left; private static SkinScale scale; private static Skin skin; /** Defines possible built-in skin scales. */ public enum SkinScale { /** Standard VisUI skin */ X1("com/kotcrab/vis/ui/skin/x1/uiskin.json", "default"), /** VisUI skin 2x upscaled */ X2("com/kotcrab/vis/ui/skin/x2/uiskin.json", "x2"); private final String classpath; private final String sizesName; SkinScale (String classpath, String sizesName) { this.classpath = classpath; this.sizesName = sizesName; } public FileHandle getSkinFile () { return Gdx.files.classpath(classpath); } public String getSizesName () { return sizesName; } } /** Loads default VisUI skin with {@link SkinScale#X1}. */ public static void load () { load(SkinScale.X1); } /** Loads default VisUI skin for given {@link SkinScale}. */ public static void load (SkinScale scale) { VisUI.scale = scale; load(scale.getSkinFile()); } /** Loads skin from provided internal file path. Skin must be compatible with default VisUI skin. */ public static void load (String internalVisSkinPath) { load(Gdx.files.internal(internalVisSkinPath)); } /** Loads skin from provided file. Skin must be compatible with default VisUI skin. */ public static void load (FileHandle visSkinFile) { checkBeforeLoad(); VisUI.skin = new Skin(visSkinFile); } /** * Sets provided skin as default for every VisUI widget. Skin must be compatible with default VisUI skin. This * can be used if you prefer to load skin manually for example by using {@link AssetManager}. */ public static void load (Skin skin) { checkBeforeLoad(); VisUI.skin = skin; } private static void checkBeforeLoad () { if (skin != null) throw new GdxRuntimeException("VisUI cannot be loaded twice"); if (!skipGdxVersionCheck && !Version.VERSION.equals(TARGET_GDX_VERSION)) { Gdx.app.log("VisUI", "Warning, using invalid libGDX version.\n" + "You are using libGDX " + Version.VERSION + " but you need " + TARGET_GDX_VERSION + ". This may cause " + "unexpected problems and runtime exceptions."); } } /** Unloads VisUI. */ public static void dispose () { dispose(true); } /** * Unloads VisUI. * @param disposeSkin if true then internal skin instance will be disposed */ public static void dispose (boolean disposeSkin) { if (skin != null) { if (disposeSkin) skin.dispose(); skin = null; } } public static Skin getSkin () { if (skin == null) throw new IllegalStateException("VisUI is not loaded!"); return skin; } public static boolean isLoaded () { return skin != null; } public static Sizes getSizes () { if (scale == null) return getSkin().get(Sizes.class); else return getSkin().get(scale.getSizesName(), Sizes.class); } /** @return int value from {@link Align} */ public static int getDefaultTitleAlign () { return defaultTitleAlign; } /** * Sets default title align user for VisWindow and VisDialog * @param defaultTitleAlign int value from {@link Align} */ public static void setDefaultTitleAlign (int defaultTitleAlign) { VisUI.defaultTitleAlign = defaultTitleAlign; } /** * @param setSkipGdxVersionCheck if true VisUI won't check if provided libGDX version is compatible for current version of VisUI. * If false, before loading VisUI, a libGDX version check will be performed, in case of version mismatch warning * will be printed to console * @see Version compatiblity table (online) */ public static void setSkipGdxVersionCheck (boolean setSkipGdxVersionCheck) { VisUI.skipGdxVersionCheck = setSkipGdxVersionCheck; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/CenteredTableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.IntArray; import com.kotcrab.vis.ui.building.utilities.Padding; /** * Builds a table with the appended widgets, trying to keep them centered. Expands X axis for first and last * widget in each row and overrides their alignments to right and left, keeping the widgets centered. Each * table's row will have the same colspan. While useful, StandardTableBuilder might be more appropriate for * complex tables. * @author MJ */ public class CenteredTableBuilder extends TableBuilder { public CenteredTableBuilder () { super(); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public CenteredTableBuilder (final Padding defaultWidgetPadding) { super(defaultWidgetPadding); } public CenteredTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount) { super(estimatedWidgetsAmount, estimatedRowsAmount); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public CenteredTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { super(estimatedWidgetsAmount, estimatedRowsAmount, defaultWidgetPadding); } @Override protected void fillTable (final Table table) { final IntArray rowSizes = getRowSizes(); final int widgetsInRow = getLowestCommonMultiple(rowSizes); for (int rowIndex = 0, widgetIndex = 0; rowIndex < rowSizes.size; rowIndex++) { final int rowSize = rowSizes.get(rowIndex); final int currentWidgetColspan = widgetsInRow / rowSize; boolean isFirst = shouldExpand(rowSize); for (final int totalWidgetsBeforeRowEnd = widgetIndex + rowSize; widgetIndex < totalWidgetsBeforeRowEnd; widgetIndex++) { final Cell cell = getWidget(widgetIndex).buildCell(table, getDefaultWidgetPadding()).colspan( currentWidgetColspan); // Keeping widgets together - expanding X for first and last widget, setting alignments: if (isFirst) { isFirst = false; cell.expandX().right(); } else if (isLast(widgetIndex, rowSize, totalWidgetsBeforeRowEnd)) { cell.expandX().left(); } } table.row(); } } /** * When table is trying to keep widgets together and widget is not alone in the row (in which case it * should be centered instead), it has to expand on X and be aligned right. * @param rowSize current row size. * @return true if row size is bigger than 1. */ private boolean shouldExpand (final int rowSize) { return rowSize != 1; } /** * @return true if the widget is last. It is used to determine if the widget has to be left-aligned and * expand on X axis. */ private boolean isLast (final int widgetIndex, final int rowSize, final int totalWidgetsInRow) { return shouldExpand(rowSize) && widgetIndex == totalWidgetsInRow - 1; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/GridTableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.kotcrab.vis.ui.building.utilities.CellWidget; import com.kotcrab.vis.ui.building.utilities.Padding; /** * Ignores row() calls and builds table with all widgets put into rows of given size. Note that this builder * will not center or in any way try to "repair" the last row if too few widgets are given to create a true * grid. * @author MJ */ public class GridTableBuilder extends TableBuilder { private final int rowSize; public GridTableBuilder (final int rowSize) { super(); this.rowSize = rowSize; } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public GridTableBuilder (final Padding defaultWidgetPadding, final int rowSize) { super(defaultWidgetPadding); this.rowSize = rowSize; } public GridTableBuilder (final int rowSize, final int estimatedWidgetsAmount, final int estimatedRowsAmount) { super(estimatedWidgetsAmount, estimatedRowsAmount); this.rowSize = rowSize; } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public GridTableBuilder (final int rowSize, final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { super(estimatedWidgetsAmount, estimatedRowsAmount, defaultWidgetPadding); this.rowSize = rowSize; } @Override protected void fillTable (final Table table) { int widgetsCounter = 0; for (final CellWidget widget : getWidgets()) { widget.buildCell(table, getDefaultWidgetPadding()); if (++widgetsCounter == rowSize) { widgetsCounter -= rowSize; table.row(); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/OneColumnTableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.kotcrab.vis.ui.building.utilities.CellWidget; import com.kotcrab.vis.ui.building.utilities.Padding; /** * Ignores row() calls and builds table with all widgets put into one column. * @author MJ */ public class OneColumnTableBuilder extends TableBuilder { public OneColumnTableBuilder () { super(); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public OneColumnTableBuilder (final Padding defaultWidgetPadding) { super(defaultWidgetPadding); } public OneColumnTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount) { super(estimatedWidgetsAmount, estimatedRowsAmount); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public OneColumnTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { super(estimatedWidgetsAmount, estimatedRowsAmount, defaultWidgetPadding); } @Override protected void fillTable (final Table table) { for (final CellWidget widget : getWidgets()) { widget.buildCell(table, getDefaultWidgetPadding()).row(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/OneRowTableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.kotcrab.vis.ui.building.utilities.CellWidget; import com.kotcrab.vis.ui.building.utilities.Padding; /** * Ignores row() calls and builds table with all widgets put into one row. Works like a StandardTableBuilder * if row() is never used, but keeps the code clearer, as the name pretty much tells what you are trying to * do. * @author MJ */ public class OneRowTableBuilder extends TableBuilder { public OneRowTableBuilder () { super(); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public OneRowTableBuilder (final Padding defaultWidgetPadding) { super(defaultWidgetPadding); } public OneRowTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount) { super(estimatedWidgetsAmount, estimatedRowsAmount); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public OneRowTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { super(estimatedWidgetsAmount, estimatedRowsAmount, defaultWidgetPadding); } @Override protected void fillTable (final Table table) { for (final CellWidget widget : getWidgets()) { widget.buildCell(table, getDefaultWidgetPadding()); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/StandardTableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.IntArray; import com.kotcrab.vis.ui.building.utilities.Padding; /** * Builds a standard table with the appended widgets. Each table's row will have the same colspan. Honors all * CellWidget settings and TableBuilder commands, making it the most flexible TableBuilder and the best one * for complex, custom tables. * @author MJ */ public class StandardTableBuilder extends TableBuilder { public StandardTableBuilder () { super(); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public StandardTableBuilder (final Padding defaultWidgetPadding) { super(defaultWidgetPadding); } public StandardTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount) { super(estimatedWidgetsAmount, estimatedRowsAmount); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public StandardTableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { super(estimatedWidgetsAmount, estimatedRowsAmount, defaultWidgetPadding); } @Override protected void fillTable (final Table table) { final IntArray rowSizes = getRowSizes(); final int widgetsInRow = getLowestCommonMultiple(rowSizes); for (int rowIndex = 0, widgetIndex = 0; rowIndex < rowSizes.size; rowIndex++) { final int rowSize = rowSizes.get(rowIndex); final int currentWidgetColspan = widgetsInRow / rowSize; for (final int totalWidgets = widgetIndex + rowSize; widgetIndex < totalWidgets; widgetIndex++) { getWidget(widgetIndex).buildCell(table, getDefaultWidgetPadding()).colspan( currentWidgetColspan); } table.row(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/TableBuilder.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.IntArray; import com.kotcrab.vis.ui.building.utilities.CellWidget; import com.kotcrab.vis.ui.building.utilities.CellWidget.CellWidgetBuilder; import com.kotcrab.vis.ui.building.utilities.Padding; import com.kotcrab.vis.ui.building.utilities.layouts.ActorLayout; import com.kotcrab.vis.ui.building.utilities.layouts.TableLayout; /** * Allows to easily build Scene2D tables, without having to worry about different colspans of table's rows. * Table built using this helper class will have the same amount of cells in each row. CellWidget class allows * to store cell's settings, and thanks to that - even the most complex tables can be built using one of the * TableBuilders. * @author MJ */ public abstract class TableBuilder { private final static int DEFAULT_WIDGETS_AMOUNT = 10, DEFAULT_ROWS_AMOUNT = 3; private final Array> widgets; private final IntArray rowSizes; // Control variables. private int currentRowSize; // Settings. private final Padding widgetPadding; private Padding tablePadding; public TableBuilder () { this(DEFAULT_WIDGETS_AMOUNT, DEFAULT_ROWS_AMOUNT, Padding.PAD_0); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public TableBuilder (final Padding defaultWidgetPadding) { this(DEFAULT_WIDGETS_AMOUNT, DEFAULT_ROWS_AMOUNT, defaultWidgetPadding); } public TableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount) { this(estimatedWidgetsAmount, estimatedRowsAmount, Padding.PAD_0); } /** @param defaultWidgetPadding will be applied to all added widgets if no specific padding is given. */ public TableBuilder (final int estimatedWidgetsAmount, final int estimatedRowsAmount, final Padding defaultWidgetPadding) { widgets = new Array>(estimatedWidgetsAmount); rowSizes = new IntArray(estimatedRowsAmount); widgetPadding = defaultWidgetPadding; } /** @return the greatest common denominator of two values. */ public static int getGreatestCommonDenominator (final int valueA, final int valueB) { return valueB == 0 ? valueA : getGreatestCommonDenominator(valueB, valueA % valueB); } /** @return lowest common multiple for the given two values. */ public static int getLowestCommonMultiple (final int valueA, final int valueB) { return valueA * (valueB / getGreatestCommonDenominator(valueA, valueB)); } /** * @param values cannot be empty or null. * @return lowest common multiple for the given values. */ public static int getLowestCommonMultiple (final IntArray values) { int lowestCommonMultiple = values.first(); for (int index = 1; index < values.size; index++) { lowestCommonMultiple = getLowestCommonMultiple(lowestCommonMultiple, values.get(index)); } return lowestCommonMultiple; } /** * @param tablePadding will define the amount of pixels separating widgets from the table's borders. Can be * null - nulled padding will be ignored. */ public TableBuilder setTablePadding (final Padding tablePadding) { this.tablePadding = tablePadding; return this; } /** * @return default widgets' padding. Should be applied to cells that have not specified custom padding * setting. */ protected Padding getDefaultWidgetPadding () { return widgetPadding; } /** @param widget will be added to the table with current default table's padding. */ public TableBuilder append (final Actor widget) { return append(CellWidget.of(widget).padding(widgetPadding).wrap()); } /** @param widget will be added to the table with custom provided data. */ public TableBuilder append (final CellWidget widget) { widgets.add(widget); currentRowSize++; return this; } /** @param widgets will be converted into one cell, with widgets appended into one row. */ public TableBuilder append (final Actor... widgets) { return append(TableLayout.HORIZONTAL, widgets); } /** * @param widgets will be converted into one cell, with widgets appended into one row. Note that these * CellWidgets' settings are local to the merging widget and additional data might have to be * passed. See methods that consume CellWidgetBuilder. */ public TableBuilder append (final CellWidget... widgets) { return append(TableLayout.HORIZONTAL, widgets); } /** * @param layout will determine how widgets are converted into one cell. See TableLayout for default * implementations. * @param widgets will be converted into one cell using passed layout. */ public TableBuilder append (final ActorLayout layout, final Actor... widgets) { return append(layout.convertToActor(widgets)); } /** * @param layout will determine how widgets are converted into one cell. See TableLayout for default * implementations. * @param widgets will be converted into one cell using passed layout. Note that some (or all) CellWidget * settings might be ignored, depending on the implementation of ActorLayout. Default layouts * use TableBuilders, so they do not ignore (most of) passed data. Also, these CellWidgets' * settings are local to the merging widget and additional data might have to be passed. See * methods that consume CellWidgetBuilder. */ public TableBuilder append (final ActorLayout layout, final CellWidget... widgets) { return append(layout.convertToActor(widgets)); } /** * @param mergedCellSettings its data will be applied to the cell that will contain passed widgets merged * into one actor. * @param widgets will be converted into one cell, with widgets appended into one row. */ public TableBuilder append (final CellWidgetBuilder mergedCellSettings, final Actor... widgets) { return append(TableLayout.HORIZONTAL, mergedCellSettings, widgets); } /** * @param mergedCellSettings its data will be applied to the cell that will contain passed widgets merged * into one actor. * @param widgets will be converted into one cell, with widgets appended into one row. */ public TableBuilder append (final CellWidgetBuilder mergedCellSettings, final CellWidget... widgets) { return append(TableLayout.HORIZONTAL, mergedCellSettings, widgets); } /** * @param layout will determine how widgets are converted into one cell. See TableLayout for default * implementations. * @param mergedCellSettings its data will be applied to the cell that will contain passed widgets merged * into one actor. * @param widgets will be converted into one cell using passed layout. */ public TableBuilder append (final ActorLayout layout, final CellWidgetBuilder mergedCellSettings, final Actor... widgets) { return append(mergedCellSettings.widget(layout.convertToActor(widgets)).wrap()); } /** * @param layout will determine how widgets are converted into one cell. See TableLayout for default * implementations. * @param mergedCellSettings its data will be applied to the cell that will contain passed widgets merged * into one actor. * @param widgets will be converted into one cell using passed layout. Note that some (or all) CellWidget * settings might be ignored, depending on the implementation of ActorLayout. Default layouts * use TableBuilders, so they do not ignore (most of) passed data. */ public TableBuilder append (final ActorLayout layout, final CellWidgetBuilder mergedCellSettings, final CellWidget... widgets) { return append(mergedCellSettings.widget(layout.convertToActor(widgets)).wrap()); } /** * Appends an empty cell to the table. Equivalent to passing null to append methods or appending * CellWidget.EMPTY/CellWidget.empty(). */ public TableBuilder append () { return append(CellWidget.EMPTY); } /** * Changes the current row, starts another. If no widgets were appended since the last call, row() will be * ignored. */ public TableBuilder row () { if (currentRowSize != 0) { rowSizes.add(currentRowSize); currentRowSize = 0; } return this; } /** @return a new table with the appended widgets, with widgets added depending on the chosen builder type. */ public Table build () { return build(new Table()); } /** * @return passed table with the appended widgets, with widgets added depending on the chosen builder type. * Note that if the passed table is not empty, builder implementations do not have to ensure that * the widgets are actually correctly appended. */ public T build (final T table) { prepareNewTable(table); if (widgets.size == 0) { // Table is empty; avoiding unnecessary operations. return table; } else { fillTable(table); return prepareBuiltTable(table); } } private Table prepareNewTable (final Table table) { validateRowSize(); if (tablePadding != null) { return tablePadding.applyPadding(table); } return table; } /** * Should fill the given table with the widgets appended to the builder. Widgets can be accessed with * getWidget(index) and getWidgets() methods. Row sizes are already validated. There is at least one * widget. * @param table is a properly created Scene2D table. Will be packed and return after filling. */ protected abstract void fillTable (Table table); private T prepareBuiltTable (final T table) { table.pack(); return table; } /** Will append a new row if any new widgets were passed to make sure that all widgets are honored. */ private void validateRowSize () { if (currentRowSize != 0) { row(); } } /** @return array with sizes of each row. */ protected IntArray getRowSizes () { return rowSizes; } /** @return CellWidget with the given index in the widgets array. */ protected CellWidget getWidget (final int index) { return widgets.get(index); } /** @return all CellWidgets appended to the builder. */ protected Array> getWidgets () { return widgets; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/Alignment.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.utils.Align; /** * libGDX alignments are simple integers and it's rather easy to make a mistake while using the align methods. * This enums wraps all default alignments, allowing to validate if the alignment value is actually correct. * @author MJ */ public enum Alignment { CENTER(Align.center), TOP(Align.top), BOTTOM(Align.bottom), LEFT(Align.left), RIGHT(Align.right), TOP_LEFT(Align.topLeft), TOP_RIGHT(Align.topRight), BOTTOM_LEFT(Align.bottomLeft), BOTTOM_RIGHT(Align.bottomRight); private final int alignment; private Alignment (final int alignment) { this.alignment = alignment; } public int getAlignment () { return alignment; } public void apply (final Cell cell) { cell.align(alignment); } /** @return true for TOP, TOP_LEFT and TOP_RIGHT. */ public boolean isAlignedWithTop () { return (alignment & Align.top) != 0; } /** @return true for BOTTOM, BOTTOM_LEFT and BOTTOM_RIGHT. */ public boolean isAlignedWithBottom () { return (alignment & Align.bottom) != 0; } /** @return true for LEFT, BOTTOM_LEFT and TOP_LEFT. */ public boolean isAlignedWithLeft () { return (alignment & Align.left) != 0; } /** @return true for RIGHT, BOTTOM_RIGHT and TOP_RIGHT. */ public boolean isAlignedWithRight () { return (alignment & Align.right) != 0; } /** @return true for CENTER. */ public boolean isCentered () { return alignment == Align.center; } /** * @param index ordinal of an enum constant. * @return optional value of enum constant. Will be null for invalid index. */ public static Alignment getByIndex (final int index) { return isIndexValid(index) ? values()[index] : null; } /** * @param index a valid ordinal of an enum constant. * @return enum constant with the selected index. * @throws ArrayIndexOutOfBoundsException for invalid index. */ public static Alignment getByValidIndex (final int index) { return values()[index]; } /** @return true if the index is connected with an enum constant. */ public static boolean isIndexValid (final int index) { return index >= 0 && index < values().length; } /** @return true if the index is connected with the last enum constant. */ public static boolean isIndexLast (final int index) { return index == values().length - 1; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/CellWidget.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Table; /** * Wraps a Scene2D widget, allowing to store cell data for delayed Table creation. Note that some filling data * (like expanding on X axis or alignment) might be overridden by some TableBuilders (like the * CenteredTableBuilder, which tries to keep all widgets centered by setting expansion and alignment of some * cells). * @author MJ */ public class CellWidget { private final static int IGNORED_SIZE = 0; /** Contains nulled actor. */ public final static CellWidget EMPTY = empty(); private final Widget widget; private final Padding padding; private final boolean expandX, expandY, fillX, fillY, useSpacing; private final Alignment alignment; private final int width, height, minWidth, minHeight; // Cast is safe - builder will have the same type as the cell widget. @SuppressWarnings("unchecked") private CellWidget (final CellWidgetBuilder cellWidgetBuilder) { widget = (Widget) cellWidgetBuilder.widget; padding = cellWidgetBuilder.padding; expandX = cellWidgetBuilder.expandX; expandY = cellWidgetBuilder.expandY; fillX = cellWidgetBuilder.fillX; fillY = cellWidgetBuilder.fillY; useSpacing = cellWidgetBuilder.useSpacing; alignment = cellWidgetBuilder.alignment; width = cellWidgetBuilder.width; height = cellWidgetBuilder.height; minWidth = cellWidgetBuilder.minWidth; minHeight = cellWidgetBuilder.minHeight; } /** * @param widget will be wrapped with CellWidget. * @return a new CellWidgetBuilder, allowing to specify the cell's settings. */ public static CellWidgetBuilder of (final Widget widget) { return new CellWidgetBuilder(widget); } /** * @param widget will be used to set initial data of builder, allowing to "modify" a prepared CellWidget. * @return a new CellWidgetBuilder, allowing to respecify the cell's settings. */ public static CellWidgetBuilder using (final CellWidget widget) { return new CellWidgetBuilder(widget); } /** * @param widget will be immediately wrapped into a CellWidget with no specific settings. * @return wrapped widget. */ public static CellWidget wrap (final Widget widget) { return of(widget).wrap(); } /** * @param widgets will be converted to CellWidgets without any specific settings. * @return wrapped widgets. */ public static CellWidget[] wrap (final Actor... widgets) { final CellWidget[] wrappedWidgets = new CellWidget[widgets.length]; for (int index = 0; index < widgets.length; index++) { wrappedWidgets[index] = CellWidget.of(widgets[index]).wrap(); } return wrappedWidgets; } /** @return a new empty, non-null CellWidget with no actor. */ public static CellWidget empty () { return builder().wrap(); } /** @return an empty builder with no widget that can be used as data container. */ public static CellWidgetBuilder builder () { return of(null); } /** @return widget wrapped with the CellWidget object. */ public Widget getWidget () { return widget; } /** * @param table will contain a cell with the object's widget with specified cell settings. * @return a reference to the built cell. */ public Cell buildCell (final Table table) { return buildCell(table, null); } /** * @param table will contain a cell with the object's widget with specified cell settings. * @param defaultWidgetPadding will be applied to the cell if padding was not specified. Can be null. * @return a reference to the built cell. */ public Cell buildCell (final Table table, final Padding defaultWidgetPadding) { final Cell cell = table.add(widget); applyPadding(cell, defaultWidgetPadding); applySizeData(cell); applyFillingData(cell); return cell; } private void applyPadding (final Cell cell, final Padding defaultWidgetPadding) { final Padding appliedPadding = Nullables.getOrElse(padding, defaultWidgetPadding); if (appliedPadding != null) { if (useSpacing) { appliedPadding.applySpacing(cell); } else { appliedPadding.applyPadding(cell); } } } private void applySizeData (final Cell cell) { if (width > IGNORED_SIZE) { cell.width(width); } if (height > IGNORED_SIZE) { cell.height(height); } if (minWidth > IGNORED_SIZE) { cell.minWidth(minWidth); } if (minHeight > IGNORED_SIZE) { cell.minHeight(minHeight); } } private void applyFillingData (final Cell cell) { if (alignment != null) { alignment.apply(cell); } cell.expand(expandX, expandY); cell.fill(fillX, fillY); } /** * Allows to set the CellWidget's data. All setter methods return this for chaining. * @author MJ */ public static class CellWidgetBuilder { private Actor widget; private Padding padding; private boolean expandX, expandY, fillX, fillY, useSpacing; private Alignment alignment; private int width = IGNORED_SIZE, height = IGNORED_SIZE, minWidth = IGNORED_SIZE, minHeight = IGNORED_SIZE; private CellWidgetBuilder (final Actor widget) { this.widget = widget; } private CellWidgetBuilder (final CellWidget widget) { this.widget = widget.widget; padding = widget.padding; expandX = widget.expandX; expandY = widget.expandY; fillX = widget.fillX; fillY = widget.fillY; useSpacing = widget.useSpacing; alignment = widget.alignment; width = widget.width; height = widget.height; minWidth = widget.minWidth; minHeight = widget.minHeight; } /** @return widget passed to factory method wrapped with CellWidget with the applied data. */ public CellWidget wrap () { return new CellWidget(this); } /** @param widget will replace the original widget wrapped by the builder. */ public CellWidgetBuilder widget (final Widget widget) { this.widget = widget; return this; } /** @param padding can be also applied as spacing after calling useSpacing(). */ public CellWidgetBuilder padding (final Padding padding) { this.padding = padding; return this; } /** * Forces the given padding object to work as spacing data. Spacing will be applied to the cell * instead of padding. */ public CellWidgetBuilder useSpacing () { useSpacing = true; return this; } /** Widget will expand on X axis. */ public CellWidgetBuilder expandX () { expandX = true; return this; } /** Widget will expand on Y axis. */ public CellWidgetBuilder expandY () { expandY = true; return this; } /** Widget will fill X axis. Often used with expansion or fixed width setting. */ public CellWidgetBuilder fillX () { fillX = true; return this; } /** Widget will fill Y axis. Often used with expansion or fixed width setting. */ public CellWidgetBuilder fillY () { fillY = true; return this; } /** * @param alignment will be used to align the widget in the cell area it has. May need expansion to * take any effect. */ public CellWidgetBuilder align (final Alignment alignment) { this.alignment = alignment; return this; } /** @param width set as min, preffered and max width. Has to be higher than 0 or it will be ignored. */ public CellWidgetBuilder width (final int width) { this.width = width; return this; } /** @param height set as min, preffered and max height. Has to be higher than 0 or it will be ignored. */ public CellWidgetBuilder height (final int height) { this.height = height; return this; } /** * @param minWidth forces the minimum width of the widget. Note that it overrides width() setting for * the min width. Has to be higher than 0 or it will be ignored. */ public CellWidgetBuilder minWidth (final int minWidth) { this.minWidth = minWidth; return this; } /** * @param minHeight forces the minimum height of the widget. Note that it overrides height() setting * for the min height. Has to be higher than 0 or it will be ignored. */ public CellWidgetBuilder minHeight (final int minHeight) { this.minHeight = minHeight; return this; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/Nullables.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities; /** * Provides static utilities for nullable objects to avoid NullPointerExceptions. Java 6 compatible, although * some methods might be quite useful for lambdas. * @author MJ */ public class Nullables { private Nullables () { } /** A simple null-check. */ public static boolean isNull (final Object nullable) { return nullable == null; } /** A simple not-null-check. */ public static boolean isNotNull (final Object nullable) { return nullable != null; } /** * @param nullable probable null. * @param alternative will be return if nullable is null. */ public static Type getOrElse (final Type nullable, final Type alternative) { return nullable == null ? alternative : nullable; } /** * @param nullable probable null. * @param command will be executed only if nullable object exists. */ public static void executeIfNotNull (final Object nullable, final Runnable command) { if (nullable != null) { command.run(); } } /** @return true if objects are equal (using equals method) or if both are null. */ public static boolean areEqual (final Object first, final Object second) { return first == second || first != null && first.equals(second); } /** * @param nullables nullable objects. * @return true if any of the objects is null. */ public static boolean isAnyNull (final Object... nullables) { for (final Object object : nullables) { if (object == null) { return true; } } return false; } /** * @param nullables nullable objects. * @return true if all passed objects are null. */ public static boolean areAllNull (final Object... nullables) { for (final Object object : nullables) { if (object != null) { return false; } } return true; } /** * @param nullables nullable objects. * @return true if any of the objects is not null. */ public static boolean isAnyNotNull (final Object... nullables) { for (final Object object : nullables) { if (object != null) { return true; } } return false; } /** * @param nullables nullable objects. * @return true if all passed objects are not null. */ public static boolean areAllNotNull (final Object... nullables) { for (final Object object : nullables) { if (object == null) { return false; } } return true; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/Padding.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Table; /** * A simple helper class that holds informations about padding on each side of an object. Static methods can * be used to quickly set padding or spacing of a cell or a table using this class' object. *

* When padding is set for a table, its cells will be separated from its borders by the given value. When * padding is set for a window, additionally to the table's padding effect, top padding will be a draggable * area, allowing to move the window. *

* When padding is set for a cell, it will be separated from other cells and table's border by the given * value. When spacing is set for a cell, it will be separated by at least as many pixels from other cells as * specified. If spacing or padding with the same values is used on every cell in a table, padding will * provide twice as big distances between cells, since spacings can overlap. * @author MJ */ public class Padding { /** Common padding sizes. */ public static final Padding PAD_0 = of(0f), PAD_2 = of(2f), PAD_4 = of(4f), PAD_8 = of(8f); private final float top, left, bottom, right; /** @param padding will be set as padding for all directions. */ public Padding (final float padding) { this(padding, padding, padding, padding); } /** * @param horizontal will be set as left and right padding. * @param vertical will be set as top and bottom padding. */ public Padding (final float horizontal, final float vertical) { this(vertical, horizontal, vertical, horizontal); } /** * @param top top padding value. * @param left left padding value. * @param bottom bottom padding value. * @param right right padding value. */ public Padding (final float top, final float left, final float bottom, final float right) { this.top = top; this.left = left; this.bottom = bottom; this.right = right; } /** @param padding will be set as padding for all directions. */ public static Padding of (final float padding) { return new Padding(padding, padding, padding, padding); } /** * @param horizontal will be set as left and right padding. * @param vertical will be set as top and bottom padding. */ public static Padding of (final float horizontal, final float vertical) { return new Padding(vertical, horizontal, vertical, horizontal); } /** * @param top top padding value. * @param left left padding value. * @param bottom bottom padding value. * @param right right padding value. */ public static Padding of (final float top, final float left, final float bottom, final float right) { return new Padding(top, left, bottom, right); } /** @return top padding value. */ public float getTop () { return top; } /** @return left padding value. */ public float getLeft () { return left; } /** @return bottom padding value. */ public float getBottom () { return bottom; } /** @return right padding value. */ public float getRight () { return right; } /** * @param padding will be added to the given padding. * @return new Padding object with summed pad values. */ public Padding add (final Padding padding) { return new Padding(top + padding.getTop(), left + padding.getLeft(), bottom + padding.getBottom(), right + padding.getRight()); } /** * @param padding will be subtracted from the given padding. * @return new Padding object with subtracted pad values. */ public Padding subtract (final Padding padding) { return new Padding(top - padding.getTop(), left - padding.getLeft(), bottom - padding.getBottom(), right - padding.getRight()); } /** @return new Padding object with reversed pad values. */ public Padding reverse () { return new Padding(-top, -left, -bottom, -right); } /** * Allows to set Table's padding with the Padding object, which has be done externally, as it's not part * of the standard libGDX API. * @param table will have the padding set according to the this object's data. * @return the given table for chaining. */ public Table applyPadding (final Table table) { table.pad(top, left, bottom, right); return table; } /** * Allows to set Cell's padding with the Padding object, which has be done externally, as it's not part of * the standard libGDX API. * @param cell will have the padding set according to the this object's data. * @return the given cell for chaining. */ public Cell applyPadding (final Cell cell) { cell.pad(top, left, bottom, right); return cell; } /** * Allows to set Cell's spacing with the Padding object, which has be done externally, as it's not part of * the standard libGDX API. Padding holds 4 floats for each direction, so it's compatible with both * padding and spacing settings without any additional changes. * @param cell will have the padding set according to the this object's data. * @return the given cell for chaining. */ public Cell applySpacing (final Cell cell) { cell.space(top, left, bottom, right); return cell; } // Padding utilities: /** * Allows to set Table's padding with the Padding object, which has be done externally, as it's not part * of the standard libGDX API. * @param padding contains data of padding sizes. * @param table will have the padding set according to the given data. * @return the given table for chaining. */ public static Table setPadding (final Padding padding, final Table table) { table.pad(padding.getTop(), padding.getLeft(), padding.getBottom(), padding.getRight()); return table; } /** * Allows to set Cell's padding with the Padding object, which has be done externally, as it's not part of * the standard libGDX API. * @param padding contains data of padding sizes. * @param cell will have the padding set according to the given data. * @return the given cell for chaining. */ public static Cell setPadding (final Padding padding, final Cell cell) { return cell.pad(padding.getTop(), padding.getLeft(), padding.getBottom(), padding.getRight()); } /** * Allows to set Cell's spacing with the Padding object, which has be done externally, as it's not part of * the standard libGDX API. Padding holds 4 floats for each direction, so it's compatible with both * padding and spacing settings without any additional changes. * @param spacing contains data of spacing sizes. * @param cell will have the padding set according to the given data. * @return the given cell for chaining. */ public static Cell setSpacing (final Padding spacing, final Cell cell) { return cell.space(spacing.getTop(), spacing.getLeft(), spacing.getBottom(), spacing.getRight()); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/layouts/ActorLayout.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities.layouts; import com.badlogic.gdx.scenes.scene2d.Actor; import com.kotcrab.vis.ui.building.utilities.CellWidget; /** * An interface that allows to convert multiple widgets into one, providing utilities for complex tables * building. For sample implementations, see TableLayout class. * @author MJ */ public interface ActorLayout { /** @return passed actors merged into one widget. */ public Actor convertToActor (Actor... widgets); /** @return passed wrapped actors merged into one widget. */ public Actor convertToActor (CellWidget... widgets); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/layouts/GridTableLayout.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities.layouts; import com.badlogic.gdx.scenes.scene2d.Actor; import com.kotcrab.vis.ui.building.GridTableBuilder; import com.kotcrab.vis.ui.building.utilities.CellWidget; /** * Additional TableLayout with customizable variables. Converts passed widgets into a table using * GridTableBuilder. * @author MJ */ public class GridTableLayout implements ActorLayout { private final int rowSize; public GridTableLayout (final int rowSize) { this.rowSize = rowSize; } /** * Default factory method. * @return new GridTableLayout, building grid with the passed row size. */ public static GridTableLayout withRowSize (final int rowSize) { return new GridTableLayout(rowSize); } @Override public Actor convertToActor (final Actor... widgets) { return convertToActor(CellWidget.wrap(widgets)); } @Override public Actor convertToActor (final CellWidget... widgets) { return TableLayout.convertToTable(new GridTableBuilder(rowSize), widgets); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/building/utilities/layouts/TableLayout.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.building.utilities.layouts; import com.badlogic.gdx.scenes.scene2d.Actor; import com.kotcrab.vis.ui.building.OneColumnTableBuilder; import com.kotcrab.vis.ui.building.OneRowTableBuilder; import com.kotcrab.vis.ui.building.TableBuilder; import com.kotcrab.vis.ui.building.utilities.CellWidget; /** * Default ActorLayout implementations, using table builders that don't require row() calls to convert * multiple actors into one cell. *

* Beside VERTICAL and HORIZONTAL, there's also grid layout available. Since it's customizable, an instance of * grid table layout must be manually initiated using grid() method. * @author MJ */ public enum TableLayout implements ActorLayout { /** Converts passed widgets into a single column. */ VERTICAL { @Override public Actor convertToActor (final CellWidget... widgets) { return convertToTable(new OneColumnTableBuilder(), widgets); } }, /** Converts passed widgets into a single row. */ HORIZONTAL { @Override public Actor convertToActor (final CellWidget... widgets) { return convertToTable(new OneRowTableBuilder(), widgets); } }; @Override public Actor convertToActor (final Actor... widgets) { return convertToActor(CellWidget.wrap(widgets)); } /** * Utility method. Appends all widgets into the passed builder and creates a table with no additional * settings. */ public static Actor convertToTable (final TableBuilder usingBuilder, final CellWidget... widgets) { for (final CellWidget widget : widgets) { usingBuilder.append(widget); } return usingBuilder.build(); } /** @return a new instance of GridTableLayout that creates tables as grids with the specified row size. */ public static GridTableLayout grid (final int rowSize) { return GridTableLayout.withRowSize(rowSize); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/i18n/BundleText.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.i18n; /** * A simple interface for one text line of the bundle file. * @author MJ */ public interface BundleText { /** @return name of the bundle text in the bundle file. */ String getName (); /** @return text's unformatted message as it appears in the bundle. */ String get (); /** @return text's formatted message without any arguments. */ String format (); /** @return text's formatted message with the passes arguments filling bundle placeholders. */ String format (Object... arguments); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/DragPane.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.Container; import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.SnapshotArray; import com.kotcrab.vis.ui.widget.Draggable; import com.kotcrab.vis.ui.widget.Draggable.DragListener; /** * Stores actors in an internally managed {@link WidgetGroup}. Allows actors with specialized {@link Draggable} listener attached * to be dropped and added into its group's content. *

* Note that unless {@link Draggable} with appropriate listener (preferably {@link DefaultDragListener}) is attached to dragged * actors, this widget will act like a regular group with no extra functionalities. It's usually a good idea to use * {@link #setDraggable(Draggable)} method, as it will attach the listener to all its children, making them all draggable. If you * want to filter widgets accepted by this pane, use {@link #setListener(DragPaneListener)} method. * @author MJ * @see #setDraggable(Draggable) * @see #setListener(DragPaneListener) * @since 0.9.3 */ public class DragPane extends Container { private Draggable draggable; private DragPaneListener listener; /** Creates a new horizontal drag pane. */ public DragPane () { this(false); } /** @param vertical if true, actors will be stored vertically, if false - horizontally. */ public DragPane (final boolean vertical) { this(vertical ? new VerticalGroup() : new HorizontalGroup()); } /** * @param group must append its actors through standard {@link WidgetGroup#addActor(Actor)} method. Must support * {@link WidgetGroup#addActorAfter(Actor, Actor)} and {@link WidgetGroup#addActorBefore(Actor, Actor)} methods. Note * that {@link com.badlogic.gdx.scenes.scene2d.ui.Table} does not meet these requirements. * @see VerticalGroup * @see HorizontalGroup * @see GridGroup */ public DragPane (final WidgetGroup group) { if (group == null) { throw new IllegalArgumentException("Group cannot be null."); } super.setActor(group); setTouchable(Touchable.enabled); } /** * @return true if children are displayed vertically in a {@link VerticalGroup}. * @see #getVerticalGroup() */ public boolean isVertical () { return getActor() instanceof VerticalGroup; } /** * @return true if children are displayed horizontally in a {@link HorizontalGroup}. * @see #getHorizontalGroup() */ public boolean isHorizontal () { return getActor() instanceof HorizontalGroup; } /** * @return true if children are displayed as a grid in a {@link GridGroup}. * @see #getGridGroup() */ public boolean isGrid () { return getActor() instanceof GridGroup; } /** * @return true if children are displayed with a {@link VerticalFlowGroup}. * @see #getVerticalFlowGroup() */ public boolean isVerticalFlow () { return getActor() instanceof VerticalFlowGroup; } /** * @return true if children are displayed with a {@link HorizontalFlowGroup}. * @see #getHorizontalFlowGroup() */ public boolean isHorizontalFlow () { return getActor() instanceof HorizontalFlowGroup; } /** * @return true if children are displayed with a {@link FloatingGroup}. * @see #getFloatingGroup() */ public boolean isFloating () { return getActor() instanceof FloatingGroup; } @Override public SnapshotArray getChildren () { return getActor().getChildren(); } /** @return internally managed group of actors. */ public WidgetGroup getGroup () { return getActor(); } /** @param group will replace the internally managed group. All current children will be moved to this group. */ public void setGroup (final WidgetGroup group) { setActor(group); } /** @param group will replace the internally managed group. All current children will be moved to this group. */ @Override public void setActor (final WidgetGroup group) { if (group == null) { throw new IllegalArgumentException("Group cannot be null."); } final Group previousGroup = getActor(); super.setActor(group); attachListener(); // Attaches draggable to all previous group children. for (final Actor child : previousGroup.getChildren()) { group.addActor(child); // No need to attach draggable, child was already in pane. } } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not horizontal. * @see #isHorizontal() */ public HorizontalGroup getHorizontalGroup () { return (HorizontalGroup) getActor(); } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not vertical. * @see #isVertical() */ public VerticalGroup getVerticalGroup () { return (VerticalGroup) getActor(); } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not a grid. * @see #isGrid() */ public GridGroup getGridGroup () { return (GridGroup) getActor(); } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not horizontal flow. * @see #isHorizontalFlow() */ public HorizontalFlowGroup getHorizontalFlowGroup () { return (HorizontalFlowGroup) getActor(); } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not vertical flow. * @see #isVerticalFlow() */ public VerticalFlowGroup getVerticalFlowGroup () { return (VerticalFlowGroup) getActor(); } /** * @return internally managed group of actors. * @throws ClassCastException if drag pane is not floating. * @see #isFloating() */ public FloatingGroup getFloatingGroup () { return (FloatingGroup) getActor(); } /** @return dragging listener automatically added to all panes' children. */ public Draggable getDraggable () { return draggable; } /** @param draggable will be automatically added to all children. */ public void setDraggable (final Draggable draggable) { removeListener(); this.draggable = draggable; attachListener(); } @Override public void setBounds (final float x, final float y, final float width, final float height) { super.setBounds(x, y, width, height); getActor().setWidth(width); getActor().setHeight(height); // Child position omitted on purpose. } @Override public void setWidth (final float width) { super.setWidth(width); getActor().setWidth(width); } @Override public void setHeight (final float height) { super.setHeight(height); getActor().setHeight(height); } private void removeListener () { if (draggable == null) { return; } for (final Actor actor : getChildren()) { actor.removeListener(draggable); } } private void attachListener () { if (draggable == null) { return; } for (final Actor actor : getChildren()) { draggable.attachTo(actor); } } /** * @param actor might be in the drag pane. * @return true if actor is added to the pane's internal group. */ public boolean contains (final Actor actor) { return actor.getParent() == getActor(); } /** * Removes an actor from this group. If the actor will not be used again and has actions, they should be * {@link Actor#clearActions() cleared} so the actions will be returned to their * {@link Action#setPool(com.badlogic.gdx.utils.Pool) pool}, if any. This is not done automatically. *

* Note that the direct parent of {@link DragPane}'s children is the internal pane's group accessible through * {@link #getGroup()} - and since this removal method is overridden and extended, pane's children should be deleted with * {@code dragPane.removeActor(child)} rather than {@link Actor#remove()} method. * @param actor will be removed, if present in the internal {@link WidgetGroup}. * @return true if the actor was removed from this group. */ @Override public boolean removeActor (final Actor actor) { return removeActor(actor, true); } /** * Removes an actor from this group. If the actor will not be used again and has actions, they should be * {@link Actor#clearActions() cleared} so the actions will be returned to their * {@link Action#setPool(com.badlogic.gdx.utils.Pool) pool}, if any. This is not done automatically. *

* Note that the direct parent of {@link DragPane}'s children is the internal pane's group accessible through * {@link #getGroup()} - and since this removal method is overridden and extended, pane's children should be deleted with * {@code dragPane.removeActor(child, true)} rather than {@link Actor#remove()} method. * @param unfocus if true, {@link Stage#unfocus(Actor)} is called. * @param actor will be removed, if present in the internal {@link WidgetGroup}. * @return true if the actor was removed from this group. */ @Override public boolean removeActor (final Actor actor, final boolean unfocus) { if (getActor().getChildren().contains(actor, true)) { // Stage input focus causes problems, as touchUp is called in Draggable. Reproducing input unfocus after stage removed. Stage stage = actor.getStage(); getActor().removeActor(actor, false); // Stage is cleared. if (unfocus && stage != null) { stage.unfocus(actor); } return true; } return false; } @Override public void clear () { getActor().clear(); } @Override public void addActor (final Actor actor) { getActor().addActor(actor); doOnAdd(actor); } @Override public void addActorAfter (final Actor actorAfter, final Actor actor) { getActor().addActorAfter(actorAfter, actor); doOnAdd(actor); } @Override public void addActorAt (final int index, final Actor actor) { getActor().addActorAt(index, actor); doOnAdd(actor); } @Override public void addActorBefore (final Actor actorBefore, final Actor actor) { getActor().addActorBefore(actorBefore, actor); doOnAdd(actor); } /** @param actor was just added to the group. */ protected void doOnAdd (final Actor actor) { if (draggable != null) { draggable.attachTo(actor); } } @Override public T findActor (final String name) { return getActor().findActor(name); } @Override public void invalidate () { super.invalidate(); getActor().invalidate(); } @Override public void validate () { super.validate(); getActor().validate(); } /** @param listener manages children appended to the drag pane. */ public void setListener (final DragPaneListener listener) { this.listener = listener; } /** * @param actor is dragged over the pane. * @return true if actor can be added to the pane. */ protected boolean accept (final Actor actor) { return listener == null || listener.accept(this, actor); } /** * Default {@link DragListener} implementation. Implements {@link DragPane} behavior. * @author MJ * @since 0.9.3 */ public static class DefaultDragListener implements DragListener { /** Contains stage drag end position, which might be changed to local widget coordinates by some methods. */ protected static final Vector2 DRAG_POSITION = new Vector2(); private Policy policy; /** Creates a new drag listener with default policy. */ public DefaultDragListener () { this(DefaultPolicy.ALLOW_REMOVAL); } /** * @param policy determines behavior of dragged actors. Allows to prohibit actors from being added to a {@link DragPane}. * Cannot be null. * @see #setPolicy(Policy) */ public DefaultDragListener (final Policy policy) { setPolicy(policy); } /** * @param policy determines behavior of dragged actors. Allows to prohibit actors from being added to a {@link DragPane}. * Cannot be null. * @see DefaultPolicy */ public void setPolicy (final Policy policy) { if (policy == null) { throw new IllegalArgumentException("Policy cannot be null."); } this.policy = policy; } @Override public boolean onStart (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { return APPROVE; } @Override public void onDrag (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { } @Override public boolean onEnd (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { if (actor == null || actor.getStage() == null) { return CANCEL; } final Actor overActor = actor.getStage().hit(stageX, stageY, true); if (overActor == null || overActor == actor) { return CANCEL; } else if (overActor.isAscendantOf(actor)) { final DragPane dragPane = getDragPane(actor); if (dragPane != null && dragPane.isFloating()) { DRAG_POSITION.set(stageX, stageY); return addToFloatingGroup(draggable, actor, dragPane); } return CANCEL; } DRAG_POSITION.set(stageX, stageY); if (overActor instanceof DragPane) { return addDirectlyToPane(draggable, actor, (DragPane) overActor); } final DragPane dragPane = getDragPane(overActor); if (accept(actor, dragPane)) { return addActor(draggable, actor, overActor, dragPane); } return CANCEL; } /** * @param draggable is attached to the actor. * @param actor dragged actor. * @param dragPane is directly under the dragged actor. If accepts the actor, it should be added to its content. * @return true if actor was accepted. */ protected boolean addDirectlyToPane (final Draggable draggable, final Actor actor, final DragPane dragPane) { if (accept(actor, dragPane)) { if (dragPane.isFloating()) { return addToFloatingGroup(draggable, actor, dragPane); } // Dragged directly to a pane. Assuming no padding, adding last: dragPane.addActor(actor); return APPROVE; } return CANCEL; } /** * @param actor has just been dragged. * @param dragPane is under the dragged actor (if exists). Can be null. * @return true if the actor can be added to the dragPane. */ protected boolean accept (final Actor actor, final DragPane dragPane) { return dragPane != null && dragPane.accept(actor) && policy.accept(dragPane, actor); } /** * @param draggable is attached to the actor. * @param actor is being dragged. * @param overActor is directly under the dragged actor. * @param dragPane contains the actor under dragged widget. * @return true if actor is accepted and added to the group. */ protected boolean addActor (final Draggable draggable, final Actor actor, final Actor overActor, final DragPane dragPane) { final Actor directPaneChild = getActorInDragPane(overActor, dragPane); directPaneChild.stageToLocalCoordinates(DRAG_POSITION); if (dragPane.isVertical() || dragPane.isVerticalFlow()) { return addToVerticalGroup(actor, dragPane, directPaneChild); } else if (dragPane.isHorizontal() || dragPane.isHorizontalFlow()) { return addToHorizontalGroup(actor, dragPane, directPaneChild); } else if (dragPane.isFloating()) { return addToFloatingGroup(draggable, actor, dragPane); } // This is the default behavior for grid and unknown groups: return addToOtherGroup(actor, dragPane, directPaneChild); } /** * @param actor is being dragged. * @param dragPane is under the actor. Stores a {@link HorizontalGroup}. * @param directPaneChild actor under the cursor. * @return true if actor was accepted by the group. */ protected boolean addToHorizontalGroup (final Actor actor, final DragPane dragPane, final Actor directPaneChild) { final Array children = dragPane.getChildren(); final int indexOfDraggedActor = children.indexOf(actor, true); actor.remove(); if (indexOfDraggedActor >= 0) { final int indexOfDirectChild = children.indexOf(directPaneChild, true); if (indexOfDirectChild > indexOfDraggedActor) { dragPane.addActorAfter(directPaneChild, actor); } else { dragPane.addActorBefore(directPaneChild, actor); } } else if (DRAG_POSITION.x > directPaneChild.getWidth() / 2f) { dragPane.addActorAfter(directPaneChild, actor); } else { dragPane.addActorBefore(directPaneChild, actor); } return APPROVE; } /** * @param actor is being dragged. * @param dragPane is under the actor. Stores a {@link VerticalGroup}. * @param directPaneChild actor under the cursor. * @return true if actor was accepted by the group. */ protected boolean addToVerticalGroup (final Actor actor, final DragPane dragPane, final Actor directPaneChild) { final Array children = dragPane.getChildren(); final int indexOfDraggedActor = children.indexOf(actor, true); actor.remove(); if (indexOfDraggedActor >= 0) { final int indexOfDirectChild = children.indexOf(directPaneChild, true); if (indexOfDirectChild > indexOfDraggedActor) { dragPane.addActorAfter(directPaneChild, actor); } else { dragPane.addActorBefore(directPaneChild, actor); } } else if (DRAG_POSITION.y < directPaneChild.getHeight() / 2f) { // Y inverted. dragPane.addActorAfter(directPaneChild, actor); } else { dragPane.addActorBefore(directPaneChild, actor); } return APPROVE; } /** * @param draggable attached to dragged actor. * @param actor is being dragged. * @param dragPane is under the actor. Stores a {@link FloatingGroup}. * @return true if actor was accepted by the group. */ protected boolean addToFloatingGroup (final Draggable draggable, final Actor actor, final DragPane dragPane) { final FloatingGroup group = dragPane.getFloatingGroup(); dragPane.stageToLocalCoordinates(DRAG_POSITION); float x = DRAG_POSITION.x + draggable.getOffsetX(); if (x < 0f || x + actor.getWidth() > dragPane.getWidth()) { // Normalizing value if set to keep within parent's bounds: if (draggable.isKeptWithinParent()) { x = x < 0f ? 0f : dragPane.getWidth() - actor.getWidth() - 1f; } else { return CANCEL; } } float y = DRAG_POSITION.y + draggable.getOffsetY(); if (y < 0f || y + actor.getHeight() > dragPane.getHeight()) { if (draggable.isKeptWithinParent()) { y = y < 0f ? 0f : dragPane.getHeight() - actor.getHeight() - 1f; } else { return CANCEL; } } actor.remove(); actor.setPosition(x, y); group.addActor(actor); return APPROVE; } /** * @param actor is being dragged. * @param dragPane is under the actor. Stores a {@link GridGroup} or unknown group. * @param directPaneChild actor under the cursor. * @return true if actor was accepted by the group. */ protected boolean addToOtherGroup (final Actor actor, final DragPane dragPane, final Actor directPaneChild) { final Array children = dragPane.getChildren(); final int indexOfDirectChild = children.indexOf(directPaneChild, true); final int indexOfDraggedActor = children.indexOf(actor, true); actor.remove(); if (indexOfDraggedActor >= 0) { // Dragging own actor. if (indexOfDraggedActor > indexOfDirectChild) { // Dropped after current position. dragPane.addActorBefore(directPaneChild, actor); } else { // Dropped before current position. dragPane.addActorAfter(directPaneChild, actor); } } else if (indexOfDirectChild == children.size - 1) { // Dragged into last element. if (DRAG_POSITION.y < directPaneChild.getHeight() / 2f || DRAG_POSITION.x > directPaneChild.getWidth() / 2f) { // Adding last: // last: dragPane.addActor(actor); } else { dragPane.addActorBefore(directPaneChild, actor); } } else if (indexOfDirectChild == 0) { // Dragged into first element. if (DRAG_POSITION.y < directPaneChild.getHeight() / 2f || DRAG_POSITION.x > directPaneChild.getWidth() / 2f) { dragPane.addActorAfter(directPaneChild, actor); } else { // Adding first: dragPane.addActorBefore(directPaneChild, actor); } } else { // Replacing hovered actor: dragPane.addActorBefore(directPaneChild, actor); } return APPROVE; } /** * @param actor if in the drag pane, but does not have to be added directly. * @param dragPane contains the actor. * @return passed actor or the parent of the actor added directly to the pane. */ protected Actor getActorInDragPane (Actor actor, final DragPane dragPane) { while (actor != dragPane && actor != null) { if (dragPane.contains(actor)) { return actor; } // Actor might not be added directly to the drag pane. Trying out the parent: actor = actor.getParent(); } return null; } /** * @param fromActor might be in a drag pane. * @return drag pane parent or null. */ protected DragPane getDragPane (Actor fromActor) { while (fromActor != null) { if (fromActor instanceof DragPane) { return (DragPane) fromActor; } fromActor = fromActor.getParent(); } return null; } /** * Determines behavior of {@link DefaultDragListener}. * @author MJ * @since 0.9.3 */ public static interface Policy { /** * @param dragPane is under the actor. * @param actor was dragged into the drag pane. * @return true if the actor can be added to the {@link DragPane} */ boolean accept (DragPane dragPane, Actor actor); } /** * Contains basic {@link DefaultDragListener} behaviors, allowing to modify the listener without extending it. * @author MJ * @since 0.9.3 */ public static enum DefaultPolicy implements Policy { /** Allows children to be moved to different {@link DragPane}s. */ ALLOW_REMOVAL { @Override public boolean accept (final DragPane dragPane, final Actor actor) { return APPROVE; } }, /** Prohibits from removing children from the {@link DragPane}, allowing them only to be moved within their own group. */ KEEP_CHILDREN { @Override public boolean accept (final DragPane dragPane, final Actor actor) { return dragPane.contains(actor); // dragPane must be the direct parent of actor. } }; } } /** * Allows to select children added to the group. * @author MJ * @since 0.9.3 */ public static interface DragPaneListener { /** Return in {@link #accept(DragPane, Actor)} method for code clarity. */ boolean ACCEPT = true, REFUSE = false; /** * @param dragPane has this listener attached. * @param actor if being dragged over the {@link DragPane}. * @return true if actor can be added to the drag pane. False if it cannot. */ boolean accept (DragPane dragPane, Actor actor); /** * When actors are dragged into the {@link DragPane}, they are accepted and added into the pane only if their direct parent * is the pane itself. * @author MJ * @since 0.9.3 */ public static class AcceptOwnChildren implements DragPaneListener { @Override public boolean accept (final DragPane dragPane, final Actor actor) { return dragPane.contains(actor); } } /** * Limits {@link DragPane} children amount to a certain number. Never rejects own children. * @author MJ * @since 0.9.3 */ public static class LimitChildren implements DragPaneListener { private final int max; /** * @param max if {@link DragPane}'s children amount equals (or is greater than) this value, other children will not be * accepted. */ public LimitChildren (final int max) { this.max = max; } @Override public boolean accept (final DragPane dragPane, final Actor actor) { return dragPane.contains(actor) || dragPane.getChildren().size < max; } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/FloatingGroup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.SnapshotArray; import com.kotcrab.vis.ui.widget.VisWindow; /** * Does not arranges actors in specific layout, instead it uses their position and preferred sizes to place them inside * group. Does not check position values and allows widgets to be placed outside group. If combined with * {@link VisWindow#setKeepWithinParent(boolean)} allows to create windows that can only moved within this group area. * @author Kotcrab * @since 1.0.2 */ public class FloatingGroup extends WidgetGroup { private boolean useChildrenPreferredSize = false; private float prefWidth = 0f; private float prefHeight = 0f; /** * Creates floating group without preferred sizes set. Group size should be controlled by parent, note that * most group relies on correctness of preferred width and height so not all groups can be applied. *

* This can be useful for adding group to {@link Table}. For example: add(floatingGroup).grow() to fill all available * space. */ public FloatingGroup () { setTouchable(Touchable.childrenOnly); } /** * Creates floating group with fixed area size. * @param prefHeight preferred height of group. If set to to lower than 0 then {@link #getHeight()} is used as preferred height. * @param prefWidth preferred width of group. If set to to lower than 0 then {@link #getWidth()} is used as preferred width. */ public FloatingGroup (float prefWidth, float prefHeight) { setTouchable(Touchable.childrenOnly); setPrefWidth(prefWidth); setPrefHeight(prefHeight); } @Override public void layout () { if (useChildrenPreferredSize == false) return; SnapshotArray children = getChildren(); for (int i = 0; i < children.size; i++) { Actor child = children.get(i); float width = child.getWidth(); float height = child.getHeight(); if (child instanceof Layout) { Layout layout = (Layout) child; width = layout.getPrefWidth(); height = layout.getPrefHeight(); } child.setBounds(child.getX(), child.getY(), width, height); } } public boolean isUseChildrenPreferredSize () { return useChildrenPreferredSize; } /** * If true then children preferred size will be used whenever possible when doing group layout. If set to true * changing widget and height using {@link Actor#getWidth()} and {@link Actor#getHeight()} may not have effect * for instances of {@link Layout} (depending on implementation). Default is false. */ public void setUseChildrenPreferredSize (boolean useChildrenPreferredSize) { this.useChildrenPreferredSize = useChildrenPreferredSize; invalidate(); } @Override public float getPrefWidth () { return prefWidth < 0f ? getWidth() : prefWidth; } @Override public float getPrefHeight () { return prefHeight < 0f ? getHeight() : prefHeight; } public void setPrefWidth (float prefWidth) { this.prefWidth = prefWidth; invalidate(); } public void setPrefHeight (float prefHeight) { this.prefHeight = prefHeight; invalidate(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/FlowGroup.java ================================================ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.SnapshotArray; /** * Arranges actors to flow in a specified layout direction using up available space and, if sensible, expanding in that direction. *
* For horizontal layout direction ({@code vertical=false}), attempts to expand to the desired width, creates new rows and expands vertically as necessary. * Children automatically overflow to the next row when necessary. *
* For vertical layout direction ({@code vertical=true}), attempts to expand to the desired height, creates new columns and expands horizontally as necessary. * Children automatically overflow to the next column when necessary. *
*
* Can be embedded in a scroll pane. To ensure proper flowing (instead of expanding in the layout direction), scrolling in the layout direction should be disabled, i.e., horizontal layout direction should have scrolling in x direction disabled and vertical layout direction should have scrolling in y direction disabled. *
*
* This is a more versatile implementation of a flow group subsuming * {@link com.kotcrab.vis.ui.layout.HorizontalFlowGroup HorizontalFlowGroup} and {@link com.kotcrab.vis.ui.layout.VerticalFlowGroup VerticalFlowGroup}. *
*
* Key differences: *

    *
  • Can be configured to have a layout direction as either horizontal ({@code vertical=false}) or vertical ({@code vertical=true}).
  • *
  • Layout direction can be changed programmatically during runtime.
  • *
  • If available, uses up all necessary space in the layout direction, i.e., when {@code vertical=false}, attempts to expand horizontally and, when {@code vertical=true}, attempts to expand vertically (instead of using the specified width/height as before).
  • *
  • Adds spacing only between children, but not after the last element.
  • *
  • When even the first child does not fit its row/column, space is no longer placed before it.
  • *
* @author ccmb2r * @since 1.4.7 */ public class FlowGroup extends WidgetGroup { private static final float DEFAULT_SPACING = 0; private boolean vertical; private float spacing; private boolean sizeInvalid = true; private float minWidth; private float minHeight; private float layoutedWidth; private float layoutedHeight; private float relaxedWidth; private float relaxedHeight; public FlowGroup (boolean vertical) { this(vertical, DEFAULT_SPACING); } public FlowGroup (boolean vertical, float spacing) { this.vertical = vertical; this.spacing = spacing; setTouchable(Touchable.childrenOnly); } public boolean isVertical () { return vertical; } public void setVertical (boolean vertical) { if (this.vertical == vertical) { return; } this.vertical = vertical; invalidate(); } protected void computeSize () { if (vertical) { computeSizeVertical(); } else { computeSizeHorizontal(); } } protected void computeSizeHorizontal () { final float targetWidth = getWidth(); float maxChildWidth = 0; float maxChildHeight = 0; float x = 0; float currentRowHeight = 0; float totalWidth = 0; float totalHeight = 0; SnapshotArray children = getChildren(); boolean wasChildProcessed = false; for (Actor child : children) { float childWidth; float childHeight; if (child instanceof Layout) { Layout layout = (Layout) child; childWidth = layout.getPrefWidth(); childHeight = layout.getPrefHeight(); } else { childWidth = child.getWidth(); childHeight = child.getHeight(); } //See if it fits this row but place at least one child in the first row! if (x + childWidth <= targetWidth || !wasChildProcessed) { //Fits into this row. currentRowHeight = Math.max(childHeight, currentRowHeight); } else { //Start new row. totalHeight += currentRowHeight + spacing; x = 0; currentRowHeight = childHeight; } float widthIncrement = childWidth + spacing; x += widthIncrement; totalWidth += widthIncrement; maxChildWidth = Math.max(maxChildWidth, childWidth); maxChildHeight = Math.max(maxChildHeight, childHeight); wasChildProcessed = true; } //Handle last column (if at least one column exists). if (wasChildProcessed) { //Remove the last spacing that was added excessively. totalWidth -= spacing; } //Handle last row (no final spacing). totalHeight += currentRowHeight; //Store results. minWidth = maxChildWidth; minHeight = maxChildHeight; layoutedWidth = targetWidth; layoutedHeight = totalHeight; relaxedWidth = totalWidth; sizeInvalid = false; } protected void computeSizeVertical () { final float targetHeight = getHeight(); float maxChildWidth = 0; float maxChildHeight = 0; float y = targetHeight; float currentColumnWidth = 0; float totalWidth = 0; float totalHeight = 0; SnapshotArray children = getChildren(); boolean wasChildProcessed = false; for (Actor child : children) { float childWidth; float childHeight; if (child instanceof Layout) { Layout layout = (Layout) child; childWidth = layout.getPrefWidth(); childHeight = layout.getPrefHeight(); } else { childWidth = child.getWidth(); childHeight = child.getHeight(); } //See if it fits this column but place at least one child in the first column! if (y - childHeight >= 0 || !wasChildProcessed) { //Fits into this column. currentColumnWidth = Math.max(childWidth, currentColumnWidth); } else { //Start new column. totalWidth += currentColumnWidth + spacing; y = targetHeight; currentColumnWidth = childWidth; } float heightIncrement = childHeight + spacing; y -= heightIncrement; totalHeight += heightIncrement; maxChildWidth = Math.max(maxChildWidth, childWidth); maxChildHeight = Math.max(maxChildHeight, childHeight); wasChildProcessed = true; } //Handle last row (if at least one row exists). if (wasChildProcessed) { //Remove the last spacing that was added excessively. totalHeight -= spacing; } //Handle last column (no final spacing). totalWidth += currentColumnWidth; //Store results. minWidth = maxChildWidth; minHeight = maxChildHeight; layoutedWidth = totalWidth; layoutedHeight = targetHeight; relaxedHeight = totalHeight; sizeInvalid = false; } @Override public void layout () { if (vertical) { layoutVertical(); } else { layoutHorizontal(); } } protected void layoutHorizontal () { //NOTE: Should children invalidate/validate as per contract? computeSizeIfNeeded(); final float targetWidth = getWidth(); final float targetHeight = getHeight(); float x = 0; float y = targetHeight; float rowHeight = 0; SnapshotArray children = getChildren(); boolean wasChildProcessed = false; //Layout the child; first upside down as total height is, as of yet, unknown //(due to the dynamic width used) and will be determined during this run. for (Actor child : children) { float childWidth; float childHeight; if (child instanceof Layout) { Layout layout = (Layout) child; childWidth = layout.getPrefWidth(); childHeight = layout.getPrefHeight(); //Need to update size. child.setSize(childWidth, childHeight); } else { childWidth = child.getWidth(); childHeight = child.getHeight(); //Child already at size. } //See if it fits this row but place at least one child in the first row! if (x + childWidth <= targetWidth || !wasChildProcessed) { //Fits into this row. rowHeight = Math.max(childHeight, rowHeight); } else { //Start new row. x = 0; y -= rowHeight + spacing; rowHeight = childHeight; } child.setPosition(x, y - childHeight); x += childWidth + spacing; wasChildProcessed = true; } //Did a best effort to fit into the specified size but it still did not work. //Let the ancestors know in hopes that they resize the group. if (getHeight() != layoutedHeight) { invalidateHierarchy(); } } protected void layoutVertical () { //NOTE: Should children invalidate/validate as per contract? computeSizeIfNeeded(); final float targetHeight = getHeight(); float x = 0; float y = targetHeight; float columnWidth = 0; SnapshotArray children = getChildren(); boolean wasChildProcessed = false; //Layout the child; first upside down as total height is, as of yet, unknown //(due to the dynamic width used) and will be determined during this run. for (Actor child : children) { float childWidth; float childHeight; if (child instanceof Layout) { Layout layout = (Layout) child; childWidth = layout.getPrefWidth(); childHeight = layout.getPrefHeight(); //Need to update size. child.setSize(childWidth, childHeight); } else { childWidth = child.getWidth(); childHeight = child.getHeight(); //Child already at size. } //See if it fits this column but place at least one child in the first column! if (y - childHeight >= 0 || !wasChildProcessed) { //Fits into this column. columnWidth = Math.max(childWidth, columnWidth); } else { //Start new column. x += columnWidth + spacing; y = targetHeight; columnWidth = childWidth; } child.setPosition(x, y - childHeight); y -= childHeight + spacing; wasChildProcessed = true; } //Did a best effort to fit into the specified size but it still did not work. //Let the ancestors know in hopes that they resize the group. if (getWidth() != layoutedWidth) { invalidateHierarchy(); } } public float getSpacing () { return spacing; } public void setSpacing (float spacing) { this.spacing = spacing; invalidateHierarchy(); } @Override public void invalidate () { sizeInvalid = true; super.invalidate(); } @Override public float getMinWidth () { computeSizeIfNeeded(); return minWidth; } @Override public float getMinHeight () { computeSizeIfNeeded(); return minHeight; } @Override public float getPrefWidth () { computeSizeIfNeeded(); return vertical ? layoutedWidth : relaxedWidth; } @Override public float getPrefHeight () { computeSizeIfNeeded(); return vertical ? relaxedHeight : layoutedHeight; } protected void computeSizeIfNeeded () { if (sizeInvalid) { computeSize(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/GridGroup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.SnapshotArray; /** * Arrange actors in grid layout. You can set item width, height and spacing between items. *

* Grid group can be embedded in scroll pane. However in such case scrolling in X direction must be disabled. * @author Kotcrab * @since 0.7.2 */ public class GridGroup extends WidgetGroup { private float prefWidth; private float prefHeight; private float lastPrefHeight; private boolean sizeInvalid = true; private float itemWidth = 256; private float itemHeight = 256; private float spacing = 8; public GridGroup () { setTouchable(Touchable.childrenOnly); } public GridGroup (float itemSize) { this.itemWidth = itemSize; this.itemHeight = itemSize; setTouchable(Touchable.childrenOnly); } public GridGroup (float itemSize, float spacing) { this.spacing = spacing; this.itemWidth = itemSize; this.itemHeight = itemSize; setTouchable(Touchable.childrenOnly); } private void computeSize () { prefWidth = getWidth(); prefHeight = 0; sizeInvalid = false; SnapshotArray children = getChildren(); if (children.size == 0) { prefWidth = 0; prefHeight = 0; return; } float width = getWidth(); float maxHeight = 0; float tempX = spacing; for (int i = 0; i < children.size; i++) { if (tempX + itemWidth + spacing > width) { tempX = spacing; maxHeight += itemHeight + spacing; } tempX += itemWidth + spacing; } if (itemWidth + spacing * 2 > prefWidth) maxHeight += spacing; else maxHeight += itemHeight + spacing * 2; prefHeight = maxHeight; } @Override public void layout () { if (sizeInvalid) { computeSize(); if (lastPrefHeight != prefHeight) { lastPrefHeight = prefHeight; invalidateHierarchy(); } } SnapshotArray children = getChildren(); float width = getWidth(); boolean notEnoughSpace = itemWidth + spacing * 2 > width; float x = spacing; float y = notEnoughSpace ? (getHeight()) : (getHeight() - itemHeight - spacing); for (int i = 0; i < children.size; i++) { Actor child = children.get(i); if (x + itemWidth + spacing > width) { x = spacing; y -= itemHeight + spacing; } child.setBounds(x, y, itemWidth, itemHeight); x += itemWidth + spacing; } } public float getSpacing () { return spacing; } public void setSpacing (float spacing) { this.spacing = spacing; invalidateHierarchy(); } public void setItemSize (float itemSize) { this.itemWidth = itemSize; this.itemHeight = itemSize; invalidateHierarchy(); } public void setItemSize (float itemWidth, float itemHeight) { this.itemWidth = itemWidth; this.itemHeight = itemHeight; invalidateHierarchy(); } public float getItemWidth () { return itemWidth; } public void setItemWidth (float itemWidth) { this.itemWidth = itemWidth; } public float getItemHeight () { return itemHeight; } public void setItemHeight (float itemHeight) { this.itemHeight = itemHeight; } @Override public void invalidate () { super.invalidate(); sizeInvalid = true; } @Override public float getPrefWidth () { if (sizeInvalid) computeSize(); return prefWidth; } @Override public float getPrefHeight () { if (sizeInvalid) computeSize(); return prefHeight; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/HorizontalFlowGroup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.SnapshotArray; /** * Arranges actors in rows filling available horizontal space. Creates new rows and expands vertically as necessary. * Children automatically overflow to next row when necessary. *

* Can be embedded in scroll pane however in that case scrolling in X direction must be disabled. * @author Kotcrab * @since 1.0.0 * @deprecated Deprecated since 1.4.7. Use {@link com.kotcrab.vis.ui.layout.FlowGroup} instead. */ @Deprecated public class HorizontalFlowGroup extends WidgetGroup { private float prefWidth; private float prefHeight; private float lastPrefHeight; private boolean sizeInvalid = true; private float spacing = 0; public HorizontalFlowGroup () { setTouchable(Touchable.childrenOnly); } public HorizontalFlowGroup (float spacing) { this.spacing = spacing; setTouchable(Touchable.childrenOnly); } private void computeSize () { prefWidth = getWidth(); prefHeight = 0; sizeInvalid = false; SnapshotArray children = getChildren(); float x = 0; float rowHeight = 0; for (int i = 0; i < children.size; i++) { Actor child = children.get(i); float width = child.getWidth(); float height = child.getHeight(); if (child instanceof Layout) { Layout layout = (Layout) child; width = layout.getPrefWidth(); height = layout.getPrefHeight(); } if (x + width > getWidth()) { x = 0; prefHeight += rowHeight + spacing; rowHeight = height; } else { rowHeight = Math.max(height, rowHeight); } x += width + spacing; } //handle last row height prefHeight += rowHeight + spacing; } @Override public void layout () { if (sizeInvalid) { computeSize(); if (lastPrefHeight != prefHeight) { lastPrefHeight = prefHeight; invalidateHierarchy(); } } SnapshotArray children = getChildren(); float x = 0; float y = getHeight(); float rowHeight = 0; for (int i = 0; i < children.size; i++) { Actor child = children.get(i); float width = child.getWidth(); float height = child.getHeight(); if (child instanceof Layout) { Layout layout = (Layout) child; width = layout.getPrefWidth(); height = layout.getPrefHeight(); } if (x + width > getWidth()) { x = 0; y -= rowHeight + spacing; rowHeight = height; } else { rowHeight = Math.max(height, rowHeight); } child.setBounds(x, y - height, width, height); x += width + spacing; } } public float getSpacing () { return spacing; } public void setSpacing (float spacing) { this.spacing = spacing; invalidateHierarchy(); } @Override public void invalidate () { super.invalidate(); sizeInvalid = true; } @Override public float getPrefWidth () { if (sizeInvalid) computeSize(); return prefWidth; } @Override public float getPrefHeight () { if (sizeInvalid) computeSize(); return prefHeight; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/layout/VerticalFlowGroup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.layout; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.SnapshotArray; /** * Arranges actors in single column filling available vertical space. Creates new columns and expands horizontally as * necessary. * Children automatically overflow to next column when necessary. *

* Can be embedded in scroll pane however in that case scrolling in Y direction must be disabled. * @author Kotcrab * @since 1.0.0 * @deprecated Deprecated since 1.4.7. Use {@link com.kotcrab.vis.ui.layout.FlowGroup} instead. */ @Deprecated public class VerticalFlowGroup extends WidgetGroup { private float prefWidth; private float prefHeight; private float lastPrefHeight; private boolean sizeInvalid = true; private float spacing = 0; public VerticalFlowGroup () { setTouchable(Touchable.childrenOnly); } public VerticalFlowGroup (float spacing) { this.spacing = spacing; setTouchable(Touchable.childrenOnly); } private void computeSize () { prefWidth = 0; prefHeight = getHeight(); sizeInvalid = false; SnapshotArray children = getChildren(); float y = 0; float columnWidth = 0; for (int i = 0; i < children.size; i++) { Actor child = children.get(i); float width = child.getWidth(); float height = child.getHeight(); if (child instanceof Layout) { Layout layout = (Layout) child; width = layout.getPrefWidth(); height = layout.getPrefHeight(); } if (y + height > getHeight()) { y = 0; prefWidth += columnWidth + spacing; columnWidth = width; } else { columnWidth = Math.max(width, columnWidth); } y += height + spacing; } //handle last column width prefWidth += columnWidth + spacing; } @Override public void layout () { if (sizeInvalid) { computeSize(); if (lastPrefHeight != prefHeight) { lastPrefHeight = prefHeight; invalidateHierarchy(); } } SnapshotArray children = getChildren(); float x = 0; float y = getHeight(); float columnWidth = 0; for (int i = 0; i < children.size; i++) { Actor child = children.get(i); float width = child.getWidth(); float height = child.getHeight(); if (child instanceof Layout) { Layout layout = (Layout) child; width = layout.getPrefWidth(); height = layout.getPrefHeight(); } if (y - height < 0) { y = getHeight(); x += columnWidth + spacing; columnWidth = width; } else { columnWidth = Math.max(width, columnWidth); } child.setBounds(x, y - height, width, height); y -= height + spacing; } } public float getSpacing () { return spacing; } public void setSpacing (float spacing) { this.spacing = spacing; invalidateHierarchy(); } @Override public void invalidate () { super.invalidate(); sizeInvalid = true; } @Override public float getPrefWidth () { if (sizeInvalid) computeSize(); return prefWidth; } @Override public float getPrefHeight () { if (sizeInvalid) computeSize(); return prefHeight; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/ActorUtils.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.utils.Align; /** * {@link Actor} related utils. * @author Kotcrab */ public class ActorUtils { /** * Makes sures that actor will be fully visible in stage. If it's necessary actor position will be changed to fit it * on screen. * @throws IllegalStateException if actor does not belong to any stage. */ public static void keepWithinStage (Actor actor) { Stage stage = actor.getStage(); if (stage == null) { throw new IllegalStateException("keepWithinStage cannot be used on Actor that doesn't belong to any stage. "); } keepWithinStage(actor.getStage(), actor); } /** * Makes sures that actor will be fully visible in stage. If it's necessary actor position will be changed to fit it * on screen. */ public static void keepWithinStage (Stage stage, Actor actor) { //taken from scene2d.ui Window Camera camera = stage.getCamera(); if (camera instanceof OrthographicCamera) { OrthographicCamera orthographicCamera = (OrthographicCamera) camera; float parentWidth = stage.getWidth(); float parentHeight = stage.getHeight(); if (actor.getX(Align.right) - camera.position.x > parentWidth / 2 / orthographicCamera.zoom) actor.setPosition(camera.position.x + parentWidth / 2 / orthographicCamera.zoom, actor.getY(Align.right), Align.right); if (actor.getX(Align.left) - camera.position.x < -parentWidth / 2 / orthographicCamera.zoom) actor.setPosition(camera.position.x - parentWidth / 2 / orthographicCamera.zoom, actor.getY(Align.left), Align.left); if (actor.getY(Align.top) - camera.position.y > parentHeight / 2 / orthographicCamera.zoom) actor.setPosition(actor.getX(Align.top), camera.position.y + parentHeight / 2 / orthographicCamera.zoom, Align.top); if (actor.getY(Align.bottom) - camera.position.y < -parentHeight / 2 / orthographicCamera.zoom) actor.setPosition(actor.getX(Align.bottom), camera.position.y - parentHeight / 2 / orthographicCamera.zoom, Align.bottom); } else if (actor.getParent() == stage.getRoot()) { float parentWidth = stage.getWidth(); float parentHeight = stage.getHeight(); if (actor.getX() < 0) actor.setX(0); if (actor.getRight() > parentWidth) actor.setX(parentWidth - actor.getWidth()); if (actor.getY() < 0) actor.setY(0); if (actor.getTop() > parentHeight) actor.setY(parentHeight - actor.getHeight()); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/BorderOwner.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; /** * Implemented by actors that has VisUI focus border, actor implementing this interface must support disabling its border. * @author Kotcrab */ public interface BorderOwner { boolean isFocusBorderEnabled (); void setFocusBorderEnabled (boolean focusBorderEnabled); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/ColorUtils.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.MathUtils; /** * Utilities for converting between RGB and HSV color systems. * @author Kotcrab * @since 0.6.0 */ public class ColorUtils { /** * Converts HSV to RGB * @param h hue 0-360 * @param s saturation 0-100 * @param v value 0-100 * @param alpha 0-1 * @return RGB values in libGDX {@link Color} class */ public static Color HSVtoRGB (float h, float s, float v, float alpha) { Color c = HSVtoRGB(h, s, v); c.a = alpha; return c; } /** * Converts HSV color system to RGB * @param h hue 0-360 * @param s saturation 0-100 * @param v value 0-100 * @return RGB values in libGDX {@link Color} class */ public static Color HSVtoRGB (float h, float s, float v) { Color c = new Color(1, 1, 1, 1); HSVtoRGB(h, s, v, c); return c; } /** * Converts HSV color system to RGB * @param h hue 0-360 * @param s saturation 0-100 * @param v value 0-100 * @param targetColor color that result will be stored in * @return targetColor */ public static Color HSVtoRGB (float h, float s, float v, Color targetColor) { if (h == 360) h = 359; int r, g, b; int i; float f, p, q, t; h = (float) Math.max(0.0, Math.min(360.0, h)); s = (float) Math.max(0.0, Math.min(100.0, s)); v = (float) Math.max(0.0, Math.min(100.0, v)); s /= 100; v /= 100; h /= 60; i = MathUtils.floor(h); f = h - i; p = v * (1 - s); q = v * (1 - s * f); t = v * (1 - s * (1 - f)); switch (i) { case 0: r = MathUtils.round(255 * v); g = MathUtils.round(255 * t); b = MathUtils.round(255 * p); break; case 1: r = MathUtils.round(255 * q); g = MathUtils.round(255 * v); b = MathUtils.round(255 * p); break; case 2: r = MathUtils.round(255 * p); g = MathUtils.round(255 * v); b = MathUtils.round(255 * t); break; case 3: r = MathUtils.round(255 * p); g = MathUtils.round(255 * q); b = MathUtils.round(255 * v); break; case 4: r = MathUtils.round(255 * t); g = MathUtils.round(255 * p); b = MathUtils.round(255 * v); break; default: r = MathUtils.round(255 * v); g = MathUtils.round(255 * p); b = MathUtils.round(255 * q); } targetColor.set(r / 255.0f, g / 255.0f, b / 255.0f, targetColor.a); return targetColor; } /** * Converts {@link Color} to HSV color system * @return 3 element int array with hue (0-360), saturation (0-100) and value (0-100) */ public static int[] RGBtoHSV (Color c) { return RGBtoHSV(c.r, c.g, c.b); } /** * Converts RGB to HSV color system * @param r red 0-1 * @param g green 0-1 * @param b blue 0-1 * @return 3 element int array with hue (0-360), saturation (0-100) and value (0-100) */ public static int[] RGBtoHSV (float r, float g, float b) { float h, s, v; float min, max, delta; min = Math.min(Math.min(r, g), b); max = Math.max(Math.max(r, g), b); v = max; delta = max - min; if (max != 0) s = delta / max; else { s = 0; h = 0; return new int[]{MathUtils.round(h), MathUtils.round(s), MathUtils.round(v)}; } if (delta == 0) h = 0; else { if (r == max) h = (g - b) / delta; else if (g == max) h = 2 + (b - r) / delta; else h = 4 + (r - g) / delta; } h *= 60; if (h < 0) h += 360; s *= 100; v *= 100; return new int[]{MathUtils.round(h), MathUtils.round(s), MathUtils.round(v)}; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/CursorManager.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Cursor; /** * Manages default cursor of VisUI app. If you are using custom cursor you must set it here otherwise some VisUI widget * that changes cursors will reset it to default system cursor. * @author Kotcrab * @since 1.1.2 */ public class CursorManager { private static Cursor defaultCursor; private static Cursor.SystemCursor defaultSystemCursor = Cursor.SystemCursor.Arrow; private static boolean systemCursorAsDefault = true; /** * Sets cursor that will be used as default when {@link #restoreDefaultCursor()} is called, for example by VisUI widget. * @param defaultCursor default cursor, can't be null */ public static void setDefaultCursor (Cursor defaultCursor) { if (defaultCursor == null) throw new IllegalArgumentException("defaultCursor can't be null"); CursorManager.defaultCursor = defaultCursor; CursorManager.systemCursorAsDefault = false; } /** * Sets cursor that will be used as default when {@link #restoreDefaultCursor()} is called, for example by Vis widget. * @param defaultCursor default cursor from {@link Cursor.SystemCursor}, can't be null */ public static void setDefaultCursor (Cursor.SystemCursor defaultCursor) { if (defaultCursor == null) throw new IllegalArgumentException("defaultCursor can't be null"); CursorManager.defaultSystemCursor = defaultCursor; CursorManager.systemCursorAsDefault = true; } /** Restores currently used cursor to default one. */ public static void restoreDefaultCursor () { if (systemCursorAsDefault) { Gdx.graphics.setSystemCursor(defaultSystemCursor); } else { Gdx.graphics.setCursor(defaultCursor); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/FloatDigitsOnlyFilter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisTextField.TextFieldFilter; /** * {@link TextFieldFilter} that only allows digits for float values. * @author Kotcrab */ public class FloatDigitsOnlyFilter extends IntDigitsOnlyFilter { public FloatDigitsOnlyFilter (boolean acceptNegativeValues) { super(acceptNegativeValues); } @Override public boolean acceptChar (VisTextField field, char c) { int selectionStart = field.getSelectionStart(); int cursorPos = field.getCursorPosition(); String text; if (field.isTextSelected()) { //issue #131 String beforeSelection = field.getText().substring(0, Math.min(selectionStart, cursorPos)); String afterSelection = field.getText().substring(Math.max(selectionStart, cursorPos)); text = beforeSelection + afterSelection; } else { text = field.getText(); } if (c == '.' && text.contains(".") == false) return true; return super.acceptChar(field, c); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/InputValidator.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.util.form.SimpleFormValidator; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Interface implemented by classes that can validate whether user input is right or wrong, typically used by {@link VisValidatableTextField} * {@link SimpleFormValidator} and {@link Dialogs} input dialogs. * @author Kotcrab * @since 0.0.3 */ public interface InputValidator { /** * Called when input must be validated. * @param input text that should be validated * @return true if input is valid, false otherwise */ boolean validateInput (String input); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/IntDigitsOnlyFilter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisTextField.TextFieldFilter; /** * {@link TextFieldFilter} that only allows digits for integer values. * @author Kotcrab */ public class IntDigitsOnlyFilter extends NumberDigitsTextFieldFilter { public IntDigitsOnlyFilter (boolean acceptNegativeValues) { super(acceptNegativeValues); } @Override public boolean acceptChar (VisTextField field, char c) { if (isAcceptNegativeValues()) { if (isUseFieldCursorPosition()) { if (c == '-' && (field.getCursorPosition() > 0 || field.getText().startsWith("-"))) return false; } else { if (c == '-' && field.getText().startsWith("-")) return false; } if (c == '-') return true; } return Character.isDigit(c); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/NumberDigitsTextFieldFilter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.kotcrab.vis.ui.widget.VisTextField.TextFieldFilter; /** * Base class for number digits text field filters. Filters extending this class must handle disabling entering * negative number values and using cursor position to prevent typing minus in wrong place. * @author Kotcrab * @see IntDigitsOnlyFilter * @see FloatDigitsOnlyFilter */ public abstract class NumberDigitsTextFieldFilter implements TextFieldFilter { private boolean acceptNegativeValues; private boolean useFieldCursorPosition; public NumberDigitsTextFieldFilter (boolean acceptNegativeValues) { this.acceptNegativeValues = acceptNegativeValues; } public boolean isAcceptNegativeValues () { return acceptNegativeValues; } public void setAcceptNegativeValues (boolean acceptNegativeValues) { this.acceptNegativeValues = acceptNegativeValues; } public boolean isUseFieldCursorPosition () { return useFieldCursorPosition; } /** * @param useFieldCursorPosition if true this filter will use current field cursor position to prevent typing minus sign * in wrong place. This is disabled by default. If you enable this feature you must ensure that field cursor position is * set to 0 when you change text programmatically. Non zero cursor position can happen when you are changing text when * field still has user focus. */ public void setUseFieldCursorPosition (boolean useFieldCursorPosition) { this.useFieldCursorPosition = useFieldCursorPosition; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/OsUtils.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; /** * Operating system related utils. * @author Kotcrab * @author Simon Gerst */ public class OsUtils { private static final String OS = System.getProperty("os.name", "").toLowerCase(); private static final boolean WINDOWS = OS.contains("win"); private static final boolean MAC = OS.contains("mac"); private static final boolean UNIX = OS.contains("nix") || OS.contains("nux") || OS.contains("aix"); /** @return {@code true} if the current OS is Windows */ public static boolean isWindows () { return WINDOWS; } /** @return {@code true} if the current OS is Mac */ public static boolean isMac () { return MAC; } /** @return {@code true} if the current OS is Unix */ public static boolean isUnix () { return UNIX; } /** @return {@code true} if the current OS is iOS */ public static boolean isIos () { return Gdx.app.getType() == ApplicationType.iOS; } /** @return {@code true} if the current OS is Android */ public static boolean isAndroid () { return Gdx.app.getType() == ApplicationType.Android; } /** * Returns the Android API level it's basically the same as android.os.Build.VERSION.SDK_INT * @return the API level. Returns 0 if the current OS isn't Android */ public static int getAndroidApiLevel () { if (isAndroid()) { return Gdx.app.getVersion(); } else { return 0; } } /** * Creates platform dependant shortcut text. Converts int keycodes to String text. Eg. Keys.CONTROL_LEFT, * Keys.SHIFT_LEFT, Keys.F5 will be converted to Ctrl+Shift+F5 on Windows and Linux, and to ⌘⇧F5 on Mac. *

* CONTROL_LEFT and CONTROL_RIGHT and SYM are mapped to Ctrl. The same goes for Alt (ALT_LEFT, ALT_RIGHT) and Shift (SHIFT_LEFT, SHIFT_RIGHT). *

* Keycodes equal to {@link Integer#MIN_VALUE} will be ignored. * @param keycodes keycodes from {@link Keys} that are used to create shortcut text * @return the platform dependent shortcut text */ public static String getShortcutFor (int... keycodes) { StringBuilder builder = new StringBuilder(); String separatorString = "+"; String ctrlKey = "Ctrl"; String altKey = "Alt"; String shiftKey = "Shift"; if (OsUtils.isMac()) { separatorString = ""; ctrlKey = "\u2318"; altKey = "\u2325"; shiftKey = "\u21E7"; } for (int i = 0; i < keycodes.length; i++) { if (keycodes[i] == Integer.MIN_VALUE) { continue; } if (keycodes[i] == Keys.CONTROL_LEFT || keycodes[i] == Keys.CONTROL_RIGHT || keycodes[i] == Keys.SYM) { builder.append(ctrlKey); } else if (keycodes[i] == Keys.SHIFT_LEFT || keycodes[i] == Keys.SHIFT_RIGHT) { builder.append(shiftKey); } else if (keycodes[i] == Keys.ALT_LEFT || keycodes[i] == Keys.ALT_RIGHT) { builder.append(altKey); } else { builder.append(Keys.toString(keycodes[i])); } if (i < keycodes.length - 1) { // Is this NOT the last key builder.append(separatorString); } } return builder.toString(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/TableUtils.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; /** * Utilities for VisTable/Table. * @author Kotcrab */ public class TableUtils { /** Sets default table spacing for VisUI skin. Uses values from current skin {@link Sizes} class obtained from {@link VisUI#getSizes()}. */ public static void setSpacingDefaults (Table table) { Sizes sizes = VisUI.getSizes(); if (sizes.spacingTop != 0) table.defaults().spaceTop(sizes.spacingTop); if (sizes.spacingBottom != 0) table.defaults().spaceBottom(sizes.spacingBottom); if (sizes.spacingRight != 0) table.defaults().spaceRight(sizes.spacingRight); if (sizes.spacingLeft != 0) table.defaults().spaceLeft(sizes.spacingLeft); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/ToastManager.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.Timer; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.toast.MessageToast; import com.kotcrab.vis.ui.widget.toast.Toast; import com.kotcrab.vis.ui.widget.toast.ToastTable; /** * Utility for displaying toast messages at corner of application screen (by default top right). Toasts can be closed by users or they * can automatically disappear after a period of time. Typically only one instance of ToastManager is used per * application window. *

* To properly support window resize {@link #resize()} must be called when application resize has occurred. *

* Most show methods are taking {@link VisTable} however {@link ToastTable} should be preferred because it's provides * access to enclosing {@link Toast} instance. * @author Kotcrab * @see Toast * @see ToastTable * @see MessageToast * @since 1.1.0 */ public class ToastManager { public static final int UNTIL_CLOSED = -1; protected final Group root; protected int screenPaddingX = 20; protected int screenPaddingY = 20; protected int messagePadding = 5; protected int alignment = Align.topRight; protected Array toasts = new Array(); protected ObjectMap timersTasks = new ObjectMap(); /** Toast manager will create own group to host toasts and put it into the stage root. */ public ToastManager (Stage stage) { WidgetGroup widgetGroup = new WidgetGroup(); widgetGroup.setFillParent(true); widgetGroup.setTouchable(Touchable.childrenOnly); stage.addActor(widgetGroup); this.root = widgetGroup; } /** @param root Toast manager will use this group as a host for toast actors. */ public ToastManager (Group root) { this.root = root; } /** Displays basic toast with provided text as message. Toast will be displayed until it is closed by user. */ public void show (String text) { show(text, UNTIL_CLOSED); } /** Displays basic toast with provided text as message. Toast will be displayed for given amount of seconds. */ public void show (String text, float timeSec) { VisTable table = new VisTable(); table.add(text).grow(); show(table, timeSec); } /** Displays toast with provided table as toast's content. Toast will be displayed until it is closed by user. */ public void show (Table table) { show(table, UNTIL_CLOSED); } /** Displays toast with provided table as toast's content. Toast will be displayed for given amount of seconds. */ public void show (Table table, float timeSec) { show(new Toast(table), timeSec); } /** * Displays toast with provided table as toast's content. If this toast was already displayed then it reuses * stored {@link Toast} instance. * Toast will be displayed until it is closed by user. */ public void show (ToastTable toastTable) { show(toastTable, UNTIL_CLOSED); } /** * Displays toast with provided table as toast's content. If this toast was already displayed then it reuses * stored {@link Toast} instance. * Toast will be displayed for given amount of seconds. */ public void show (ToastTable toastTable, float timeSec) { Toast toast = toastTable.getToast(); if (toast != null) { show(toast, timeSec); } else { show(new Toast(toastTable), timeSec); } } /** Displays toast. Toast will be displayed until it is closed by user. */ public void show (Toast toast) { show(toast, UNTIL_CLOSED); } /** Displays toast. Toast will be displayed for given amount of seconds. */ public void show (final Toast toast, float timeSec) { Table toastMainTable = toast.getMainTable(); if (toastMainTable.getStage() != null) { remove(toast); } toasts.add(toast); toast.setToastManager(this); toast.fadeIn(); toastMainTable.pack(); root.addActor(toastMainTable); updateToastsPositions(); if (timeSec > 0) { Timer.Task fadeOutTask = new Timer.Task() { @Override public void run () { toast.fadeOut(); timersTasks.remove(toast); } }; timersTasks.put(toast, fadeOutTask); Timer.schedule(fadeOutTask, timeSec); } } /** Must be called after application window resize to properly update toast positions on screen. */ public void resize () { updateToastsPositions(); } /** * Removes toast from screen. * @return true when toast was removed, false otherwise */ public boolean remove (Toast toast) { boolean removed = toasts.removeValue(toast, true); if (removed) { toast.getMainTable().remove(); Timer.Task timerTask = timersTasks.remove(toast); if (timerTask != null) timerTask.cancel(); updateToastsPositions(); } return removed; } public void clear () { for (Toast toast : toasts) { toast.getMainTable().remove(); } toasts.clear(); for (Timer.Task task : timersTasks.values()) { task.cancel(); } timersTasks.clear(); updateToastsPositions(); } public void toFront () { root.toFront(); } protected void updateToastsPositions () { boolean bottom = (alignment & Align.bottom) != 0; boolean left = (alignment & Align.left) != 0; boolean center = (alignment & Align.center) != 0; float y = bottom ? screenPaddingY : root.getHeight() - screenPaddingY; for (Toast toast : toasts) { Table table = toast.getMainTable(); float x = left ? screenPaddingX : center ? (root.getWidth() - table.getWidth() - screenPaddingX) / 2f : /*right*/ root.getWidth() - table.getWidth() - screenPaddingX; table.setPosition(x, bottom ? y : y - table.getHeight()); y += (table.getHeight() + messagePadding) * (bottom ? 1 : -1); } } /** * @return returns current screen padding only if padding X is equals to padding Y. * @throws IllegalStateException when current screen padding X is different than screen padding Y * @deprecated this method was deprecated in VisUI 1.4.3. Use either {@link #getScreenPaddingX()} or * {@link #getScreenPaddingY()}. This method will be removed in future versions. */ @Deprecated public int getScreenPadding () { if (screenPaddingX != screenPaddingY) { throw new IllegalStateException("Value of screen padding X is different than padding Y. " + "Use either getScreenPaddingX or getScreenPaddingY."); } return screenPaddingX; } public int getScreenPaddingX () { return screenPaddingX; } public int getScreenPaddingY () { return screenPaddingY; } /** Sets padding of a message from window corner (actual corner used depends on current alignment settings). */ public void setScreenPadding (int screenPadding) { this.screenPaddingX = screenPadding; this.screenPaddingY = screenPadding; updateToastsPositions(); } /** Sets padding of a message from window corner (actual corner used depends on current alignment settings). */ public void setScreenPadding (int screenPaddingX, int screenPaddingY) { this.screenPaddingX = screenPaddingX; this.screenPaddingY = screenPaddingY; updateToastsPositions(); } /** Sets padding of a message from window vertical edge (actual edge used depends on current alignment settings). */ public void setScreenPaddingX (int screenPaddingX) { this.screenPaddingX = screenPaddingX; updateToastsPositions(); } /** Sets padding of a message from window horizontal edge (actual edge used depends on current alignment settings). */ public void setScreenPaddingY (int screenPaddingY) { this.screenPaddingY = screenPaddingY; updateToastsPositions(); } public int getMessagePadding () { return messagePadding; } /** Sets padding between messages */ public void setMessagePadding (int messagePadding) { this.messagePadding = messagePadding; updateToastsPositions(); } public int getAlignment () { return alignment; } /** * Sets toast messages screen alignment. By default toasts are displayed in application top right corner * @param alignment one of {@link Align#topLeft}, {@link Align#topRight}, {@link Align#bottomLeft} or {@link Align#bottomRight}, */ public void setAlignment (int alignment) { this.alignment = alignment; updateToastsPositions(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/Validators.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Provides premade validators that can be used with for example with {@link VisValidatableTextField} or {@link Dialogs} * when displaying input dialogs. * @author Kotcrab */ public class Validators { /** Shared static instance of {@link IntegerValidator}. Can be safely reused. */ public static final IntegerValidator INTEGERS = new IntegerValidator(); /** Shared static instance of {@link FloatValidator}. Can be safely reused. */ public static final FloatValidator FLOATS = new FloatValidator(); /** Validates whether input is an integer number. You should use shared instance {@link Validators#INTEGERS}. */ public static class IntegerValidator implements InputValidator { @Override public boolean validateInput (String input) { try { Integer.parseInt(input); return true; } catch (NumberFormatException e) { return false; } } } /** Validates whether input is a float number. You should use shared instance {@link Validators#FLOATS}. */ public static class FloatValidator implements InputValidator { @Override public boolean validateInput (String input) { try { Float.parseFloat(input); return true; } catch (NumberFormatException e) { return false; } } } /** Validates whether input is lesser (alternatively lesser or equal) than provided number. */ public static class LesserThanValidator implements InputValidator { private float lesserThan; private boolean equals; public LesserThanValidator (float lesserThan) { this.lesserThan = lesserThan; } /** @param inputCanBeEqual if true <= comparison will be used, if false < will be used. */ public LesserThanValidator (float lesserThan, boolean inputCanBeEqual) { this.lesserThan = lesserThan; this.equals = inputCanBeEqual; } @Override public boolean validateInput (String input) { try { float value = Float.valueOf(input); return equals ? value <= lesserThan : value < lesserThan; } catch (NumberFormatException ex) { return false; } } /*** @param equals if true <= comparison will be used, if false < will be used. */ public void setUseEquals (boolean equals) { this.equals = equals; } public void setLesserThan (float lesserThan) { this.lesserThan = lesserThan; } } /** Validates whether input is greater (alternatively greater or equal) than provided number. */ public static class GreaterThanValidator implements InputValidator { private float greaterThan; private boolean useEquals; public GreaterThanValidator (float greaterThan) { this.greaterThan = greaterThan; } /** @param inputCanBeEqual if true >= comparison will be used, if false > will be used. */ public GreaterThanValidator (float greaterThan, boolean inputCanBeEqual) { this.greaterThan = greaterThan; this.useEquals = inputCanBeEqual; } @Override public boolean validateInput (String input) { try { float value = Float.valueOf(input); return useEquals ? value >= greaterThan : value > greaterThan; } catch (NumberFormatException ex) { return false; } } /*** @param useEquals if true >= comparison will be used, if false > will be used. */ public void setUseEquals (boolean useEquals) { this.useEquals = useEquals; } public void setGreaterThan (float greaterThan) { this.greaterThan = greaterThan; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/AbstractListAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.EventListener; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.ListView; import com.kotcrab.vis.ui.widget.ListView.ItemClickListener; import com.kotcrab.vis.ui.widget.ListView.ListAdapterListener; import com.kotcrab.vis.ui.widget.VisTable; import java.util.Comparator; /** * Basic {@link ListAdapter} implementation using {@link CachedItemAdapter}. Supports item selection. Classes * extending this should store provided list and provide delegates for all common methods that change array state. * Those delegates should call {@link #itemAdded(Object)} or {@link #itemRemoved(Object)} in order to properly update * view cache. When changes to array are too big to be handled by those two methods {@link #itemsChanged()} should be * called. When only items fields has changed, and no new item were added or removed you should call * {@link #itemsDataChanged()}. *

* When view does not existed in cache and must be created {@link #createView(Object)} is called. When item view exists * in cache {@link #updateView(Actor, Object)} will be called. *

* Enabling item selection requires calling {@link #setSelectionMode(SelectionMode)} and overriding * {@link #selectView(Actor)} and {@link #deselectView(Actor)}. * @author Kotcrab * @see ArrayAdapter * @see ArrayListAdapter * @since 1.0.0 */ public abstract class AbstractListAdapter extends CachedItemAdapter implements ListAdapter { protected ListView view; protected ListAdapterListener viewListener; private ItemClickListener clickListener; private SelectionMode selectionMode = SelectionMode.DISABLED; private ListSelection selection = new ListSelection(this); private Comparator itemsComparator; @Override public void fillTable (VisTable itemsTable) { if (itemsComparator != null) sort(itemsComparator); for (final ItemT item : iterable()) { final ViewT view = getView(item); prepareViewBeforeAddingToTable(item, view); itemsTable.add(view).growX(); itemsTable.row(); } } protected void prepareViewBeforeAddingToTable (ItemT item, ViewT view) { boolean listenerMissing = true; for (EventListener listener : view.getListeners()) { if (listener instanceof AbstractListAdapter.ListClickListener) { listenerMissing = false; break; } } if (listenerMissing) { view.setTouchable(Touchable.enabled); view.addListener(new ListClickListener(view, item)); } } @Override public void setListView (ListView view, ListAdapterListener viewListener) { if (this.view != null) throw new IllegalStateException("Adapter was already assigned to ListView"); this.view = view; this.viewListener = viewListener; } @Override public void setItemClickListener (ItemClickListener listener) { clickListener = listener; } protected void itemAdded (ItemT item) { viewListener.invalidateDataSet(); } protected void itemRemoved (ItemT item) { selection.deselect(item); getViews().remove(item); viewListener.invalidateDataSet(); } /** * Notifies adapter that underlying collection has changed, ie. some items were added or removed. This does not need to * be called when only the fields of stored objects changed see {@link #itemsDataChanged()}. *

* WARNING: When using {@link ListView.UpdatePolicy#MANUAL} this won't cause to rebuild {@link ListView}. This method * only notifies ListView that it needs rebuilding however when using {@link ListView.UpdatePolicy#MANUAL} mode it * will be ignored. */ public void itemsChanged () { selection.deselectAll(); getViews().clear(); viewListener.invalidateDataSet(); } /** * Notifies adapter that data of items has changed. This means that objects fields in underlying collection has changed * and views needs updating. This must not be called if some items were removed or added from collection for that * {@link #itemsChanged()} *

* WARNING: When using {@link ListView.UpdatePolicy#MANUAL} this won't cause to rebuild {@link ListView}. This method * only notifies ListView that it needs rebuilding however when using {@link ListView.UpdatePolicy#MANUAL} mode it * will be ignored. */ public void itemsDataChanged () { viewListener.invalidateDataSet(); } @Override protected void updateView (ViewT view, ItemT item) { } public SelectionMode getSelectionMode () { return selectionMode; } public void setSelectionMode (SelectionMode selectionMode) { if (selectionMode == null) throw new IllegalArgumentException("selectionMode can't be null"); this.selectionMode = selectionMode; } /** * Sets items comparator allowing to define order in which items will be displayed in list view. This will sort * underlying array before building views. * @param comparator that will be used to compare items */ public void setItemsSorter (Comparator comparator) { this.itemsComparator = comparator; } public Comparator getItemsSorter () { return itemsComparator; } /** @return selected items, must not be modified */ public Array getSelection () { return selection.getSelection(); } public ListSelection getSelectionManager () { return selection; } protected void selectView (ViewT view) { if (selectionMode == SelectionMode.DISABLED) return; throw new UnsupportedOperationException("selectView must be implemented when `selectionMode` is different than SelectionMode.DISABLED"); } protected void deselectView (ViewT view) { if (selectionMode == SelectionMode.DISABLED) return; throw new UnsupportedOperationException("deselectView must be implemented when `selectionMode` is different than SelectionMode.DISABLED"); } private class ListClickListener extends ClickListener { private ViewT view; private ItemT item; public ListClickListener (ViewT view, ItemT item) { this.view = view; this.item = item; } @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { super.touchDown(event, x, y, pointer, button); selection.touchDown(view, item); return true; } @Override public void clicked (InputEvent event, float x, float y) { if (clickListener != null) clickListener.clicked(item); } } protected abstract void sort (Comparator comparator); public enum SelectionMode { /** Selecting items is not possible. */ DISABLED, /** * Only one element can be selected. {@link AbstractListAdapter#selectView(Actor)} and * {@link AbstractListAdapter#deselectView(Actor)} must be implemented. */ SINGLE, /** * Multiple elements can be selected. {@link AbstractListAdapter#selectView(Actor)} and * {@link AbstractListAdapter#deselectView(Actor)} must be implemented. */ MULTIPLE } /** * Manages selection of {@link AbstractListAdapter} items. * @author Kotcrab */ public static class ListSelection { private AbstractListAdapter adapter; public static final int DEFAULT_KEY = -1; private int groupMultiSelectKey = DEFAULT_KEY; //shift by default private int multiSelectKey = DEFAULT_KEY; //ctrl (or command on mac) by default private Array selection = new Array(); private boolean programmaticChangeEvents = true; private ListSelectionListener listener = new ListSelectionAdapter(); private ListSelection (AbstractListAdapter adapter) { this.adapter = adapter; } public void select (ItemT item) { select(item, adapter.getViews().get(item), true); } void select (ItemT item, ViewT view, boolean programmaticChange) { if (adapter.getSelectionMode() == SelectionMode.DISABLED) return; if (adapter.getSelectionMode() == SelectionMode.SINGLE) deselectAll(programmaticChange); if (adapter.getSelectionMode() == SelectionMode.MULTIPLE && selection.size >= 1 && isGroupMultiSelectKeyPressed()) { selectGroup(item); } doSelect(item, view, programmaticChange); } private void doSelect (ItemT item, ViewT view, boolean programmaticChange) { if (selection.contains(item, true) == false) { adapter.selectView(view); selection.add(item); if (programmaticChange == false || programmaticChangeEvents) listener.selected(item, view); } } public void deselect (ItemT item) { deselect(item, adapter.getViews().get(item), true); } public void deselectAll () { deselectAll(true); } private void selectGroup (ItemT newItem) { int thisSelectionIndex = adapter.indexOf(newItem); int lastSelectionIndex = adapter.indexOf(selection.peek()); if (lastSelectionIndex == -1) return; int start; int end; if (thisSelectionIndex > lastSelectionIndex) { start = lastSelectionIndex; end = thisSelectionIndex; } else { start = thisSelectionIndex; end = lastSelectionIndex; } for (int i = start; i < end; i++) { ItemT item = adapter.get(i); doSelect(item, adapter.getViews().get(item), false); } } void deselect (ItemT item, ViewT view, boolean programmaticChange) { if (selection.contains(item, true) == false) return; adapter.deselectView(view); selection.removeValue(item, true); if (programmaticChange == false || programmaticChangeEvents) listener.deselected(item, view); } void deselectAll (boolean programmaticChange) { Array items = new Array(selection); for (ItemT item : items) { deselect(item, adapter.getViews().get(item), programmaticChange); } } /** @return internal array, MUST NOT be modified directly */ public Array getSelection () { return selection; } void touchDown (ViewT view, ItemT item) { if (adapter.getSelectionMode() == SelectionMode.DISABLED) return; if (isMultiSelectKeyPressed() == false && isGroupMultiSelectKeyPressed() == false) { deselectAll(false); } if (selection.contains(item, true) == false) { select(item, view, false); } else { deselect(item, view, false); } } public int getMultiSelectKey () { return multiSelectKey; } /** @param multiSelectKey from {@link Keys} or {@link ListSelection#DEFAULT_KEY} to restore default */ public void setMultiSelectKey (int multiSelectKey) { this.multiSelectKey = multiSelectKey; } public int getGroupMultiSelectKey () { return groupMultiSelectKey; } /** @param groupMultiSelectKey from {@link Keys} or {@link ListSelection#DEFAULT_KEY} to restore default */ public void setGroupMultiSelectKey (int groupMultiSelectKey) { this.groupMultiSelectKey = groupMultiSelectKey; } public void setListener (ListSelectionListener listener) { if (listener == null) listener = new ListSelectionAdapter(); this.listener = listener; } public ListSelectionListener getListener () { return listener; } public boolean isProgrammaticChangeEvents () { return programmaticChangeEvents; } public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) { this.programmaticChangeEvents = programmaticChangeEvents; } private boolean isMultiSelectKeyPressed () { if (multiSelectKey == DEFAULT_KEY) return UIUtils.ctrl(); else return Gdx.input.isKeyPressed(multiSelectKey); } private boolean isGroupMultiSelectKeyPressed () { if (groupMultiSelectKey == DEFAULT_KEY) return UIUtils.shift(); else return Gdx.input.isKeyPressed(groupMultiSelectKey); } } public interface ListSelectionListener { void selected (ItemT item, ViewT view); void deselected (ItemT item, ViewT view); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/ArrayAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.utils.Array; import java.util.Comparator; /** * Built-in adapter implementation for {@link Array}. * @author Kotcrab * @since 1.0.0 */ public abstract class ArrayAdapter extends AbstractListAdapter { private Array array; public ArrayAdapter (Array array) { this.array = array; } @Override public int indexOf (ItemT item) { return array.indexOf(item, true); } @Override public int size () { return array.size; } @Override public ItemT get (int index) { return array.get(index); } @Override public void add (ItemT element) { array.add(element); itemAdded(element); } @Override protected void sort (Comparator comparator) { array.sort(comparator); } @Override public Iterable iterable () { return array; } //Delegates public void addAll (Array array) { this.array.addAll(array); itemsChanged(); } public void addAll (Array array, int start, int count) { this.array.addAll(array, start, count); itemsChanged(); } public void addAll (ItemT... array) { this.array.addAll(array); itemsChanged(); } public void addAll (ItemT[] array, int start, int count) { this.array.addAll(array, start, count); itemsChanged(); } public void set (int index, ItemT value) { array.set(index, value); itemsChanged(); } public void insert (int index, ItemT value) { array.insert(index, value); itemsChanged(); } public void swap (int first, int second) { array.swap(first, second); itemsChanged(); } public boolean removeValue (ItemT value, boolean identity) { boolean res = array.removeValue(value, identity); if (res) itemRemoved(value); return res; } public ItemT removeIndex (int index) { ItemT item = array.removeIndex(index); if (item != null) itemRemoved(item); return item; } public void removeRange (int start, int end) { array.removeRange(start, end); itemsChanged(); } public boolean removeAll (Array array, boolean identity) { boolean res = this.array.removeAll(array, identity); itemsChanged(); return res; } public void clear () { array.clear(); itemsChanged(); } public void shuffle () { array.shuffle(); itemsChanged(); } public void reverse () { array.reverse(); itemsChanged(); } public ItemT pop () { ItemT item = array.pop(); itemsChanged(); return item; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/ArrayListAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.scenes.scene2d.Actor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; /** * Built-in adapter implementation for {@link ArrayList}. * @author Kotcrab * @since 1.0.0 */ public abstract class ArrayListAdapter extends AbstractListAdapter { private ArrayList array; public ArrayListAdapter (ArrayList array) { this.array = array; } @Override public Iterable iterable () { return array; } @Override public int size () { return array.size(); } @Override public int indexOf (ItemT item) { return array.indexOf(item); } @Override public void add (ItemT element) { array.add(element); itemAdded(element); } @Override public ItemT get (int index) { return array.get(index); } @Override protected void sort (Comparator comparator) { Collections.sort(array, comparator); } // Delegates public ItemT set (int index, ItemT element) { ItemT res = array.set(index, element); itemsChanged(); return res; } public void add (int index, ItemT element) { array.add(index, element); itemAdded(element); } public ItemT remove (int index) { ItemT res = array.remove(index); if (res != null) itemRemoved(res); return res; } public boolean remove (ItemT item) { boolean res = array.remove(item); if (res) itemRemoved(item); return res; } public void clear () { array.clear(); itemsChanged(); } public boolean addAll (Collection c) { boolean res = array.addAll(c); itemsChanged(); return res; } public boolean addAll (int index, Collection c) { boolean res = array.addAll(index, c); itemsChanged(); return res; } public boolean removeAll (Collection c) { boolean res = array.removeAll(c); itemsChanged(); return res; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/CachedItemAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.utils.ObjectMap; /** * Implementation of {@link ItemAdapter} that caches created views. Provides two methods that are called when new view * should be created and when old view should be updated (see {@link #createView(Object)} and {@link #updateView(Actor, Object)}). * Internal cache is not cleared automatically and obsolete entries must be removed manually. * @author Kotcrab * @since 1.0.0 */ public abstract class CachedItemAdapter implements ItemAdapter { private ObjectMap views = new ObjectMap(); @Override public final ViewT getView (ItemT item) { ViewT view = views.get(item); if (view == null) { view = createView(item); if (view == null) throw new IllegalStateException("Returned view view can't be null"); views.put(item, view); } else { updateView(view, item); } return view; } /** @return internal views cache map */ protected ObjectMap getViews () { return views; } protected abstract ViewT createView (ItemT item); protected abstract void updateView (ViewT view, ItemT item); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/ItemAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.scenes.scene2d.Actor; /** * Generic use adapter used to create views for given objects. * @author Kotcrab * @since 1.0.0 */ public interface ItemAdapter { Actor getView (ItemT item); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/ListAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.kotcrab.vis.ui.widget.ListView; import com.kotcrab.vis.ui.widget.ListView.ItemClickListener; import com.kotcrab.vis.ui.widget.ListView.ListAdapterListener; import com.kotcrab.vis.ui.widget.VisTable; /** * Adapter used to display items list in {@link ListView}. Classes implementing this interface should store array and * provide delegates to methods that change array state, such as add/remove etc. Those delegates should call * {@link ListAdapterListener#invalidateDataSet()}. Single instance of ListAdapter can only be used for one ListView. * Implementations must support setting item click listener. * @author Kotcrab * @see ArrayAdapter * @see ArrayListAdapter * @since 1.0.0 */ public interface ListAdapter { /** Called by {@link ListView} when this adapter is assigned to it. */ void setListView (ListView view, ListAdapterListener viewListener); /** Called by {@link ListView} when this adapter should create and add all views to provided itemsTable. */ void fillTable (VisTable itemsTable); /** Called by {@link ListView} when it's item click listener changed. */ void setItemClickListener (ItemClickListener listener); /** @return iterable for internal collection */ Iterable iterable (); /** @return size of internal collection */ int size (); /** @return index of element in internal collection */ int indexOf (ItemT item); /** Adds item to internal collection */ void add (ItemT item); /** @return element for given index */ ItemT get (int index); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/ListSelectionAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.kotcrab.vis.ui.util.adapter.AbstractListAdapter.ListSelectionListener; /** * Empty {@link ListSelectionListener} implementation. * @author Kotcrab */ public class ListSelectionAdapter implements ListSelectionListener { @Override public void selected (ItemT item, ViewT view) { } @Override public void deselected (ItemT item, ViewT view) { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/adapter/SimpleListAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.adapter; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.ListView; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisTable; /** * Very simple default implementation of adapter for {@link ListView}. Uses {@link Object#toString()} to create text * view for item. * @author Kotcrab */ public class SimpleListAdapter extends ArrayAdapter { private final SimpleListAdapterStyle style; public SimpleListAdapter (Array array) { this(array, "default"); } public SimpleListAdapter (Array array, String styleName) { this(array, VisUI.getSkin().get(styleName, SimpleListAdapterStyle.class)); } public SimpleListAdapter (Array array, SimpleListAdapterStyle style) { super(array); this.style = style; } @Override protected VisTable createView (ItemT item) { VisTable table = new VisTable(); table.left(); table.add(new VisLabel(item.toString())); return table; } @Override protected void selectView (VisTable view) { view.setBackground(style.selection); } @Override protected void deselectView (VisTable view) { view.setBackground(style.background); } public static class SimpleListAdapterStyle { public Drawable background; public Drawable selection; public SimpleListAdapterStyle () { } public SimpleListAdapterStyle (Drawable background, Drawable selection) { this.background = background; this.selection = selection; } public SimpleListAdapterStyle (SimpleListAdapterStyle style) { this.background = style.background; this.selection = style.selection; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/async/AsyncTask.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.async; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.Array; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; /** * Represents task that is executed asynchronously in another thread. AsyncTask and related classes are not available * on GWT. * @author Kotcrab * @see AsyncTaskListener * @see SteppedAsyncTask * @see AsyncTaskProgressDialog */ public abstract class AsyncTask { private String threadName; private Status status = Status.PENDING; private Array listeners = new Array(); public AsyncTask (String threadName) { this.threadName = threadName; } public void execute () { if (status == Status.RUNNING) throw new IllegalStateException("Task is already running."); if (status == Status.FINISHED) throw new IllegalStateException("Task has been already executed and can't be reused."); status = Status.RUNNING; new Thread(new Runnable() { @Override public void run () { executeInBackground(); } }, threadName).start(); } private void executeInBackground () { try { doInBackground(); } catch (Exception e) { failed(e); } Gdx.app.postRunnable(new Runnable() { @Override public void run () { for (AsyncTaskListener listener : listeners) { listener.finished(); } status = Status.FINISHED; } }); } /** * Called when this task should execute some action in background. This is always called from non-main thread. * From this method only {@link #setProgressPercent(int)}, {@link #setMessage(String)}, {@link #failed(String)}, * {@link #failed(Exception)}, {@link #failed(String, Exception)} should be called. */ protected abstract void doInBackground () throws Exception; protected void failed (String message) { failed(message, new IllegalStateException(message)); } protected void failed (Exception exception) { failed(exception.getMessage(), exception); } protected void failed (final String message, final Exception exception) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { for (AsyncTaskListener listener : listeners) { listener.failed(message, exception); } } }); } protected void setProgressPercent (final int progressPercent) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { for (AsyncTaskListener listener : listeners) { listener.progressChanged(progressPercent); } } }); } protected void setMessage (final String message) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { for (AsyncTaskListener listener : listeners) { listener.messageChanged(message); } } }); } /** * Executes runnable on main GDX thread. This methods blocks until runnable has finished executing. Note that this * runnable will also block main render thread. */ protected void executeOnGdx (final Runnable runnable) { final CountDownLatch latch = new CountDownLatch(1); final AtomicReference exceptionAt = new AtomicReference(); Gdx.app.postRunnable(new Runnable() { @Override public void run () { try { runnable.run(); } catch (Exception e) { exceptionAt.set(e); } finally { latch.countDown(); } } }); try { latch.await(); final Exception e = exceptionAt.get(); if (e != null) { failed(e); } } catch (InterruptedException e) { failed(e); } } public void addListener (AsyncTaskListener listener) { listeners.add(listener); } public boolean removeListener (AsyncTaskListener listener) { return listeners.removeValue(listener, true); } public String getThreadName () { return threadName; } public Status getStatus () { return status; } enum Status { PENDING, RUNNING, FINISHED } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/async/AsyncTaskListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.async; /** * Allows to listen to events occurring in {@link AsyncTask}. * @author Kotcrab */ public interface AsyncTaskListener { /** Called when task status message has changed. */ void messageChanged (String message); /** Called when task progress has changed. */ void progressChanged (int newProgressPercent); /** * Called when task has finished executing. Finished will always called, even if some exception occurred during task * execution. */ void finished (); /** Called when some error occurred during task execution. */ void failed (String message, Exception exception); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/async/AsyncTaskProgressDialog.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.async; import com.kotcrab.vis.ui.Locales.CommonText; import com.kotcrab.vis.ui.util.TableUtils; import com.kotcrab.vis.ui.util.async.AsyncTask.Status; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisProgressBar; import com.kotcrab.vis.ui.widget.VisWindow; /** * Dialog used to display progress of {@link AsyncTask} as standard VisUI window. Shows progress bar and status * of currently executed task. * @author Kotcrab */ public class AsyncTaskProgressDialog extends VisWindow { private AsyncTask task; /** * Creates new dialog, note that task will be automatically started. Created dialog must be manually added to stage, * preferably with {@link VisWindow#fadeIn()} animation. * @param title title used as window title * @param task task to be executed */ public AsyncTaskProgressDialog (String title, AsyncTask task) { super(title); this.task = task; setModal(true); TableUtils.setSpacingDefaults(this); final VisLabel statusLabel = new VisLabel(CommonText.PLEASE_WAIT.get()); final VisProgressBar progressBar = new VisProgressBar(0, 100, 1, false); defaults().padLeft(6).padRight(6); add(statusLabel).padTop(6).left().row(); add(progressBar).width(300).padTop(6).padBottom(6); task.addListener(new AsyncTaskListener() { @Override public void progressChanged (int newProgressPercent) { progressBar.setValue(newProgressPercent); } @Override public void messageChanged (String message) { statusLabel.setText(message); } @Override public void finished () { fadeOut(); } @Override public void failed (String message, Exception exception) { Dialogs.showErrorDialog(getStage(), exception.getMessage() == null ? CommonText.UNKNOWN_ERROR_OCCURRED.get() : exception.getMessage(), exception); } }); pack(); centerWindow(); task.execute(); } public AsyncTask getTask () { return task; } public void addListener (AsyncTaskListener listener) { task.addListener(listener); } public Status getStatus () { return task.getStatus(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/async/SteppedAsyncTask.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.async; /** * {@link AsyncTask} that performs fixed numbers of steps, provides convenient methods to calculate and update task progress. * @author Kotcrab */ public abstract class SteppedAsyncTask extends AsyncTask { private int step; private int totalSteps; public SteppedAsyncTask (String threadName) { super(threadName); } /** * Sets total numbers ot steps this task will have to perform, usually called at the beginning of {@link #doInBackground()}. * @see #nextStep() */ protected void setTotalSteps (int totalSteps) { this.totalSteps = totalSteps; this.step = 0; setProgressPercent(0); } /** Advances task to next step and updates its percent progress. */ protected void nextStep () { setProgressPercent(++step * 100 / totalSteps); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/ConfirmDialogListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; /** * Used to get events from {@link Dialogs} confirm dialog. * @author Kotcrab */ public interface ConfirmDialogListener { /** Called when dialog button was pressed, type of results is generic and depends on created dialog. */ void result (T result); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/Dialogs.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.CharArray; import com.badlogic.gdx.utils.I18NBundle; import com.kotcrab.vis.ui.Locales; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.i18n.BundleText; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.util.TableUtils; import com.kotcrab.vis.ui.util.Validators; import com.kotcrab.vis.ui.widget.*; import com.kotcrab.vis.ui.widget.ButtonBar.ButtonType; /** * Utilities for displaying various type of dialogs. Equivalent of JOptionPane from Swing. * @author Kotcrab * @since 0.2.0 */ public class Dialogs { private static final int BUTTON_OK = 1; private static final int BUTTON_DETAILS = 2; /** * Dialog with given text and single OK button. * @param title dialog title */ public static VisDialog showOKDialog (Stage stage, String title, String text) { final VisDialog dialog = new VisDialog(title); dialog.closeOnEscape(); dialog.text(text); dialog.button(ButtonType.OK.getText()).padBottom(3); dialog.pack(); dialog.centerWindow(); dialog.addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ENTER) { dialog.fadeOut(); return true; } return false; } }); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with text and buttons like Yes, No, Cancel. * @param title dialog title * @param type specifies what types of buttons will this dialog have * @param listener dialog buttons listener. * @return dialog for the purpose of changing buttons text. * @see OptionDialog * @since 0.6.0 */ public static OptionDialog showOptionDialog (Stage stage, String title, String text, OptionDialogType type, OptionDialogListener listener) { OptionDialog dialog = new OptionDialog(title, text, type, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with title, text and n amount of buttons. If you need dialog with only buttons like Yes, No, Cancel then * see {@link #showOptionDialog(Stage, String, String, OptionDialogType, OptionDialogListener)}. *

* @param title dialog title. * @param listener button listener for this dialog. This dialog is generic, listener type will depend on * 'returns' param type. * @since 0.7.0 */ public static ConfirmDialog showConfirmDialog (Stage stage, String title, String text, String[] buttons, T[] returns, ConfirmDialogListener listener) { ConfirmDialog dialog = new ConfirmDialog(title, text, buttons, returns, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with text and text field for user input. Cannot be canceled. * @param title dialog title. * @param fieldTitle displayed before input field, may be null. * @param listener dialog buttons listener. */ public static InputDialog showInputDialog (Stage stage, String title, String fieldTitle, InputDialogListener listener) { InputDialog dialog = new InputDialog(title, fieldTitle, true, null, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with text and text field for user input. Cannot be canceled. * @param title dialog title. * @param fieldTitle displayed before input field, may be null. * @param validator used to validate user input. Eg. limit input to integers only. See {@link Validators} for built-in validators. * @param listener dialog buttons listener. */ public static InputDialog showInputDialog (Stage stage, String title, String fieldTitle, InputValidator validator, InputDialogListener listener) { InputDialog dialog = new InputDialog(title, fieldTitle, true, validator, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with text and text field for user input. * @param title dialog title. * @param cancelable if true dialog may be canceled by user. * @param fieldTitle displayed before input field, may be null. * @param listener dialog buttons listener. */ public static InputDialog showInputDialog (Stage stage, String title, String fieldTitle, boolean cancelable, InputDialogListener listener) { InputDialog dialog = new InputDialog(title, fieldTitle, cancelable, null, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** * Dialog with text and text field for user input. * @param title dialog title * @param validator used to validate user input, can be used to easily limit input to int etc. See {@link Validators} for premade validators. * @param cancelable if true dialog may be canceled. * @param fieldTitle displayed before input field, may be null. */ public static InputDialog showInputDialog (Stage stage, String title, String fieldTitle, boolean cancelable, InputValidator validator, InputDialogListener listener) { InputDialog dialog = new InputDialog(title, fieldTitle, cancelable, validator, listener); stage.addActor(dialog.fadeIn()); return dialog; } /** Dialog with title "Error" and provided text. */ public static DetailsDialog showErrorDialog (Stage stage, String text) { return showErrorDialog(stage, text, (String) null); } /** Dialog with title "Error", provided text and exception stacktrace available after pressing 'Details' button. */ public static DetailsDialog showErrorDialog (Stage stage, String text, Throwable exception) { if (exception == null) return showErrorDialog(stage, text, (String) null); else return showErrorDialog(stage, text, getStackTrace(exception)); } /** Dialog with title "Error", provided text, and provided details available after pressing 'Details' button. */ public static DetailsDialog showErrorDialog (Stage stage, String text, String details) { DetailsDialog dialog = new DetailsDialog(text, Text.ERROR.get(), details); stage.addActor(dialog.fadeIn()); return dialog; } /** Dialog with given title, provided text, and more details available after pressing 'Details' button. */ public static DetailsDialog showDetailsDialog (Stage stage, String text, String title, String details) { return showDetailsDialog(stage, text, title, details, false); } /** * Dialog with given title, provided text, and more details available after pressing 'Details' button. * @param expandDetails if true details will be visible without need to press 'Details' button */ public static DetailsDialog showDetailsDialog (Stage stage, String text, String title, String details, boolean expandDetails) { DetailsDialog dialog = new DetailsDialog(text, title, details); dialog.setDetailsVisible(expandDetails); stage.addActor(dialog.fadeIn()); return dialog; } private static VisScrollPane createScrollPane (Actor widget) { VisScrollPane scrollPane = new VisScrollPane(widget); scrollPane.setOverscroll(false, true); scrollPane.setFadeScrollBars(false); return scrollPane; } private static String getStackTrace (Throwable throwable) { CharArray builder = new CharArray(); getStackTrace(throwable, builder); return builder.toString(); } private static void getStackTrace (Throwable throwable, CharArray builder) { String msg = throwable.getMessage(); if (msg != null) { builder.append(msg); builder.append("\n\n"); } for (StackTraceElement element : throwable.getStackTrace()) { builder.append(element); builder.append("\n"); } if (throwable.getCause() != null) { builder.append("\nCaused by: "); getStackTrace(throwable.getCause(), builder); } } public enum OptionDialogType { YES_NO, YES_NO_CANCEL, YES_CANCEL } /** * Dialog with input field and optional {@link InputValidator}. Can be used directly although you should use {@link Dialogs} * showInputDialog methods. */ public static class InputDialog extends VisWindow { private InputDialogListener listener; private VisTextField field; private VisTextButton okButton; private VisTextButton cancelButton; public InputDialog (String title, String fieldTitle, boolean cancelable, InputValidator validator, InputDialogListener listener) { super(title); this.listener = listener; TableUtils.setSpacingDefaults(this); setModal(true); if (cancelable) { addCloseButton(); closeOnEscape(); } ButtonBar buttonBar = new ButtonBar(); buttonBar.setIgnoreSpacing(true); buttonBar.setButton(ButtonType.CANCEL, cancelButton = new VisTextButton(ButtonType.CANCEL.getText())); buttonBar.setButton(ButtonType.OK, okButton = new VisTextButton(ButtonType.OK.getText())); VisTable fieldTable = new VisTable(true); if (validator == null) field = new VisTextField(); else field = new VisValidatableTextField(validator); if (fieldTitle != null) fieldTable.add(new VisLabel(fieldTitle)); fieldTable.add(field).expand().fill(); add(fieldTable).padTop(3).spaceBottom(4); row(); add(buttonBar.createTable()).padBottom(3); addListeners(); if (validator != null) { addValidatableFieldListener(field); okButton.setDisabled(!field.isInputValid()); } pack(); centerWindow(); } @Override protected void close () { super.close(); listener.canceled(); } @Override protected void setStage (Stage stage) { super.setStage(stage); if (stage != null) field.focusField(); } public InputDialog setText (String text) { return setText(text, false); } /** @param selectText if true text will be selected (this can be useful if you want to allow user quickly erase all text). */ public InputDialog setText (String text, boolean selectText) { field.setText(text); field.setCursorPosition(text.length()); if (selectText) { field.selectAll(); } return this; } private InputDialog addValidatableFieldListener (final VisTextField field) { field.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (field.isInputValid()) { okButton.setDisabled(false); } else { okButton.setDisabled(true); } } }); return this; } private void addListeners () { okButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { listener.finished(field.getText()); fadeOut(); } }); cancelButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { close(); } }); field.addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ENTER && okButton.isDisabled() == false) { listener.finished(field.getText()); fadeOut(); } return super.keyDown(event, keycode); } }); } } /** * Dialog with text and buttons like Yes, No, Cancel. Can be used directly although you should use {@link Dialogs} * showOptionDialog methods. */ public static class OptionDialog extends VisWindow { //NOTE: when updating this class, don't forget about Editor's DisableableOptionDialog private final ButtonBar buttonBar; public OptionDialog (String title, String text, OptionDialogType type, final OptionDialogListener listener) { super(title); setModal(true); add(new VisLabel(text, Align.center)); row(); defaults().space(6); defaults().padBottom(3); buttonBar = new ButtonBar(); buttonBar.setIgnoreSpacing(true); ChangeListener yesBtnListener = new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { listener.yes(); fadeOut(); } }; ChangeListener noBtnListener = new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { listener.no(); fadeOut(); } }; ChangeListener cancelBtnListener = new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { listener.cancel(); fadeOut(); } }; switch (type) { case YES_NO: buttonBar.setButton(ButtonType.YES, yesBtnListener); buttonBar.setButton(ButtonType.NO, noBtnListener); break; case YES_CANCEL: buttonBar.setButton(ButtonType.YES, yesBtnListener); buttonBar.setButton(ButtonType.CANCEL, cancelBtnListener); break; case YES_NO_CANCEL: buttonBar.setButton(ButtonType.YES, yesBtnListener); buttonBar.setButton(ButtonType.NO, noBtnListener); buttonBar.setButton(ButtonType.CANCEL, cancelBtnListener); break; } add(buttonBar.createTable()); pack(); centerWindow(); } public OptionDialog setNoButtonText (String text) { buttonBar.getTextButton(ButtonType.NO).setText(text); pack(); return this; } public OptionDialog setYesButtonText (String text) { buttonBar.getTextButton(ButtonType.YES).setText(text); pack(); return this; } public OptionDialog setCancelButtonText (String text) { buttonBar.getTextButton(ButtonType.CANCEL).setText(text); pack(); return this; } } /** * Dialog with text and exception stacktrace available after pressing Details button. * Can be used directly although you should use {@link Dialogs} showErrorDialog methods. */ public static class DetailsDialog extends VisDialog { private VisTable detailsTable = new VisTable(true); private Cell detailsCell; private boolean detailsVisible; private VisTextButton copyButton; private VisLabel detailsLabel; public DetailsDialog (String text, String title, String details) { super(title); text(text); if (details != null) { copyButton = new VisTextButton(Text.COPY.get()); detailsLabel = new VisLabel(details); Sizes sizes = VisUI.getSizes(); copyButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { Gdx.app.getClipboard().setContents((detailsLabel.getText().toString())); copyButton.setText(Text.COPIED.get()); } }); detailsTable.add(new VisLabel(Text.DETAILS_COLON.get())).left().expand().padTop(6); detailsTable.add(copyButton); detailsTable.row(); VisTable detailsTable = new VisTable(); detailsTable.add(detailsLabel).top().expand().fillX(); this.detailsTable.add(createScrollPane(detailsTable)).colspan(2).minWidth(600 * sizes.scaleFactor).height(300 * sizes.scaleFactor); getContentTable().row(); detailsCell = getContentTable().add(this.detailsTable); detailsCell.setActor(null); button(Text.DETAILS.get(), BUTTON_DETAILS); } button(ButtonType.OK.getText(), BUTTON_OK).padBottom(3); pack(); centerWindow(); } @Override protected void result (Object object) { int result = (Integer) object; if (result == BUTTON_DETAILS) { setDetailsVisible(!detailsVisible); cancel(); } } public void setWrapDetails (boolean wrap) { detailsLabel.setWrap(wrap); } public void setCopyDetailsButtonVisible (boolean visible) { copyButton.setVisible(visible); } public boolean isCopyDetailsButtonVisible () { return copyButton.isVisible(); } /** * Changes visibility of details pane. Note that Window must be added to Stage or Window won't be packed properly and * it's size will be wrong. If Window is not added to Stage packing will be performed next frame, if it is still * not added at that point, Window size will be incorrect. */ public void setDetailsVisible (boolean visible) { if (detailsVisible == visible) return; detailsVisible = visible; detailsCell.setActor(detailsCell.hasActor() ? null : detailsTable); //looks like Stage is required to properly pack window //if it's null do packing next frame and hope that window have been already added to Stage at that point if (getStage() == null) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { pack(); centerWindow(); } }); } else { pack(); centerWindow(); } } public boolean isDetailsVisible () { return detailsVisible; } } /** * Dialog with title, text and n amount of buttons. Can be used directly although you should use {@link Dialogs} * showConfirmDialog methods. * @author Javier * @author Kotcrab */ public static class ConfirmDialog extends VisDialog { private ConfirmDialogListener listener; public ConfirmDialog (String title, String text, String[] buttons, T[] returns, ConfirmDialogListener listener) { super(title); if (buttons.length != returns.length) { throw new IllegalStateException("buttons.length must be equal to returns.length"); } this.listener = listener; text(new VisLabel(text, Align.center)); defaults().padBottom(3); for (int i = 0; i < buttons.length; i++) { button(buttons[i], returns[i]); } padBottom(3); pack(); centerWindow(); } @Override protected void result (Object object) { listener.result((T) object); } } /** {@link Dialogs} I18N properties. */ private enum Text implements BundleText { DETAILS("details"), DETAILS_COLON("detailsColon"), COPY("copy"), COPIED("copied"), ERROR("error"); private final String name; Text (final String name) { this.name = name; } private static I18NBundle getBundle () { return Locales.getDialogsBundle(); } @Override public final String getName () { return name; } @Override public final String get () { return getBundle().get(name); } @Override public final String format () { return getBundle().format(name); } @Override public final String format (final Object... arguments) { return getBundle().format(name, arguments); } @Override public final String toString () { return get(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/InputDialogAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; /** * Empty implementation of {@link InputDialogListener}. * @author Kotcrab */ public class InputDialogAdapter implements InputDialogListener { @Override public void finished (String input) { } @Override public void canceled () { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/InputDialogListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; /** * Used to get events from {@link Dialogs} input dialog. * @author Kotcrab */ public interface InputDialogListener { /** * Called when input dialog has finished. * @param input text entered by user. */ void finished (String input); /** * Called when user canceled dialog or pressed 'close' button. This won't be ever called if dialog is not * cancelable. */ void canceled (); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/OptionDialogAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; /** * Empty implementation of {@link OptionDialogListener}. * @author Kotcrab */ public class OptionDialogAdapter implements OptionDialogListener { @Override public void yes () { } @Override public void no () { } @Override public void cancel () { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/dialog/OptionDialogListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.dialog; /** * Used to get events from {@link Dialogs} option dialog. * @author Kotcrab */ public interface OptionDialogListener { /** Called when 'yes' button was pressed. */ void yes (); /** Called when 'no' button was pressed. */ void no (); /** Called when 'cancel' button was pressed. */ void cancel (); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/form/FormInputValidator.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.form; import com.kotcrab.vis.ui.util.InputValidator; /** * Base class for all validators used in {@link SimpleFormValidator}. Implementing custom {@link FormInputValidator} doesn't * differ from creating standard {@link InputValidator}. You just need to supply error message which will be displayed * when form validation failed on this validator. Because implementing custom {@link FormInputValidator} does not require * any more changes you can use {@link ValidatorWrapper} for existing {@link InputValidator}s. * @author Kotcrab * @see InputValidator * @see ValidatorWrapper */ public abstract class FormInputValidator implements InputValidator { private String errorMsg; private boolean result; private boolean hideErrorOnEmptyInput = false; public FormInputValidator (String errorMsg) { this.errorMsg = errorMsg; } @Override public final boolean validateInput (String input) { result = validate(input); return result; } /** * Called by FormInputValidator when input should be validated, for proper validator behaviour this must be used * instead of {@link #validateInput(String)}. * Last result of this function will be stored because it is required by FromValidator. * @param input that should be validated. * @return if input is valid, false otherwise. */ protected abstract boolean validate (String input); /** * When called, error message of this validator won't be displayed if input field is empty, however from still will * be treated as invalid (confirm button won't be enabled). This is UX improvement feature, simply don't display * error before user typed in something. */ public FormInputValidator hideErrorOnEmptyInput () { hideErrorOnEmptyInput = true; return this; } /** @see #hideErrorOnEmptyInput() */ public void setHideErrorOnEmptyInput (boolean hideErrorOnEmptyInput) { this.hideErrorOnEmptyInput = hideErrorOnEmptyInput; } public boolean isHideErrorOnEmptyInput () { return hideErrorOnEmptyInput; } public void setErrorMsg (String errorMsg) { this.errorMsg = errorMsg; } public String getErrorMsg () { return errorMsg; } boolean getLastResult () { return result; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/form/FormValidator.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.form; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.utils.Disableable; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisValidatableTextField; import java.io.File; /** * Utility class made for creating input forms that requires inputting various information and that information cannot be wrong. * For example user registration form. *

* FromValidator is not GWT compatible, if you need that see {@link SimpleFormValidator}. * @author Kotcrab */ public class FormValidator extends SimpleFormValidator { /** @see SimpleFormValidator#SimpleFormValidator(Disableable) */ public FormValidator (Disableable targetToDisable) { super(targetToDisable); } /** @see SimpleFormValidator#SimpleFormValidator(Disableable, Label) */ public FormValidator (Disableable targetToDisable, Label messageLabel) { super(targetToDisable, messageLabel); } /** @see SimpleFormValidator#SimpleFormValidator(Disableable, Label, String) */ public FormValidator (Disableable targetToDisable, Label messageLabel, String styleName) { super(targetToDisable, messageLabel, styleName); } /** @see SimpleFormValidator#SimpleFormValidator(Disableable, Label, FormValidatorStyle) */ public FormValidator (Disableable targetToDisable, Label messageLabel, FormValidatorStyle style) { super(targetToDisable, messageLabel, style); } /** Validates if absolute path entered in text field points to an existing file. */ public FormInputValidator fileExists (VisValidatableTextField field, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(errorMsg); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an existing file. * @param relativeTo path entered in this field is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileExists (VisValidatableTextField field, VisTextField relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo, errorMsg); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an existing file. * @param relativeTo path entered in this field is used to create absolute path from entered in field (see {@link FileExistsValidator}). * @param errorIfRelativeEmpty if true field input will be valid if 'relativeTo' field is empty, usually used with notEmpty validator on 'relativeTo' field to * avoid form errors. Settings this to true improves UX, errors are not displayed until user types something in 'relativeTo' field. */ public FormInputValidator fileExists (VisValidatableTextField field, VisTextField relativeTo, String errorMsg, boolean errorIfRelativeEmpty) { FileExistsValidator validator = new FileExistsValidator(relativeTo, errorMsg, false, errorIfRelativeEmpty); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an existing file. * @param relativeTo path of this file is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileExists (VisValidatableTextField field, File relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo, errorMsg); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an existing file. * @param relativeTo path of this file is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileExists (VisValidatableTextField field, FileHandle relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo.file(), errorMsg); field.addValidator(validator); add(field); return validator; } /** Validates if relative path entered in text field points to an non existing file. */ public FormInputValidator fileNotExists (VisValidatableTextField field, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(errorMsg, true); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an non existing file. * @param relativeTo path entered in this field is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileNotExists (VisValidatableTextField field, VisTextField relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo, errorMsg, true); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an non existing file. * @param relativeTo path of this file is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileNotExists (VisValidatableTextField field, File relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo, errorMsg, true); field.addValidator(validator); add(field); return validator; } /** * Validates if relative path entered in text field points to an non existing file. * @param relativeTo path of this file is used to create absolute path from entered in field (see {@link FileExistsValidator}). */ public FormInputValidator fileNotExists (VisValidatableTextField field, FileHandle relativeTo, String errorMsg) { FileExistsValidator validator = new FileExistsValidator(relativeTo.file(), errorMsg, true); field.addValidator(validator); add(field); return validator; } /** Validates if relative path entered in text field points to an existing directory. */ public FormInputValidator directory (VisValidatableTextField field, String errorMsg) { DirectoryValidator validator = new DirectoryValidator(errorMsg); field.addValidator(validator); add(field); return validator; } /** Validates if relative path entered in text field points to an existing and empty directory. */ public FormInputValidator directoryEmpty (VisValidatableTextField field, String errorMsg) { DirectoryContentValidator validator = new DirectoryContentValidator(errorMsg, true); field.addValidator(validator); add(field); return validator; } /** Validates if relative path entered in text field points to an existing and non empty directory. */ public FormInputValidator directoryNotEmpty (VisValidatableTextField field, String errorMsg) { DirectoryContentValidator validator = new DirectoryContentValidator(errorMsg, false); field.addValidator(validator); add(field); return validator; } /** Validates if entered absolute path points to existing directory. */ public static class DirectoryValidator extends FormInputValidator { public DirectoryValidator (String errorMsg) { super(errorMsg); } @Override protected boolean validate (String input) { FileHandle file = Gdx.files.absolute(input); return file.exists() && file.isDirectory(); } } /** * Validates if entered path (absolute) points to an existing directory. Then checks if this directory is empty or if * it has files in it. * @see DirectoryValidator */ public static class DirectoryContentValidator extends FormInputValidator { private boolean mustBeEmpty; /** @param mustBeEmpty if true validated directory must be empty, if false that directory must not be empty. */ public DirectoryContentValidator (String errorMsg, boolean mustBeEmpty) { super(errorMsg); this.mustBeEmpty = mustBeEmpty; } @Override protected boolean validate (String input) { FileHandle file = Gdx.files.absolute(input); if (file.exists() == false || file.isDirectory() == false) return false; if (mustBeEmpty) { return file.list().length == 0; } else { return file.list().length != 0; } } public void setMustBeEmpty (boolean mustBeEmpty) { this.mustBeEmpty = mustBeEmpty; } public boolean isMustBeEmpty () { return mustBeEmpty; } } /** * Validates if entered path points to an existing or non existing file. *

* Additionally you can specify relativePath that entered path will be checked against. Relative path can be * either supplied as File or some other VisTextField. In that case path entered in that relative text field is used to check * if file exist in that directory. Eg. if relativePath points to "C:\directory\" and field that * has this validator contains "test.txt" then this validator will check if file ""C:\directory\text.txt" exists (or not). */ public static class FileExistsValidator extends FormInputValidator { VisTextField relativeTo; File relativeToFile; boolean mustNotExist; boolean errorIfRelativeEmpty; public FileExistsValidator (String errorMsg) { this(errorMsg, false); } public FileExistsValidator (String errorMsg, boolean mustNotExist) { super(errorMsg); this.mustNotExist = mustNotExist; } public FileExistsValidator (File relativeTo, String errorMsg) { this(relativeTo, errorMsg, false); } public FileExistsValidator (File relativeTo, String errorMsg, boolean mustNotExist) { super(errorMsg); this.relativeToFile = relativeTo; this.mustNotExist = mustNotExist; } public FileExistsValidator (VisTextField relativeTo, String errorMsg) { this(relativeTo, errorMsg, false); } public FileExistsValidator (VisTextField relativeTo, String errorMsg, boolean mustNotExist) { super(errorMsg); this.relativeTo = relativeTo; this.mustNotExist = mustNotExist; } /** @see FormValidator#fileExists(VisValidatableTextField, VisTextField, String, boolean) */ public FileExistsValidator (VisTextField relativeTo, String errorMsg, boolean mustNotExist, boolean errorIfRelativeEmpty) { super(errorMsg); this.relativeTo = relativeTo; this.mustNotExist = mustNotExist; this.errorIfRelativeEmpty = errorIfRelativeEmpty; } @Override public boolean validate (String input) { File file; if (relativeTo != null) { if (relativeTo.getText().length() == 0 && errorIfRelativeEmpty == false) { return true; } file = new File(relativeTo.getText(), input); } else if (relativeToFile != null) { file = new File(relativeToFile, input); } else { file = new File(input); } if (mustNotExist) return !file.exists(); else return file.exists(); } public void setRelativeToFile (File relativeToFile) { if (relativeTo != null) throw new IllegalStateException("This validator already has relativeToTextField set"); this.relativeToFile = relativeToFile; } public void setRelativeToTextField (VisTextField relativeTo) { if (relativeToFile != null) throw new IllegalStateException("This validator already has relativeToFile set."); this.relativeTo = relativeTo; } public void setMustNotExist (boolean notExist) { this.mustNotExist = notExist; } public void setErrorIfRelativeEmpty (boolean errorIfRelativeEmpty) { this.errorIfRelativeEmpty = errorIfRelativeEmpty; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/form/SimpleFormValidator.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.form; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.Disableable; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.util.Validators; import com.kotcrab.vis.ui.util.Validators.GreaterThanValidator; import com.kotcrab.vis.ui.util.Validators.LesserThanValidator; import com.kotcrab.vis.ui.widget.VisCheckBox; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Utility class made for creating input forms that requires inputting various information and that information cannot be wrong. * For example user registration form. *

* SimpleFormValidator is GWT compatible and does not provide fileExists methods, if you are not using GWT use * {@link FormValidator}. * @author Kotcrab */ public class SimpleFormValidator { private FormValidatorStyle style; private ChangeSharedListener changeListener = new ChangeSharedListener(); private Array fields = new Array(); private Array buttons = new Array(); private String successMsg; private boolean formInvalid = false; private String errorMsgText = ""; private Array disableTargets = new Array(); private Label messageLabel; private boolean treatDisabledFieldsAsValid = true; /** * @param targetToDisable target actor that will be disabled if form is invalid. Eg. you can pass form Confirm button. * May be null. */ public SimpleFormValidator (Disableable targetToDisable) { this(targetToDisable, null, "default"); } /** * @param targetToDisable target actor that will be disabled if form is invalid. Eg. you can pass form Confirm button. * May be null. * @param messageLabel label that text will be changed if from is valid or invalid. May be null. */ public SimpleFormValidator (Disableable targetToDisable, Label messageLabel) { this(targetToDisable, messageLabel, "default"); } /** * @param targetToDisable target actor that will be disabled if form is invalid. Eg. you can pass form Confirm button. * May be null. * @param messageLabel label that text will be changed if from is valid or invalid. May be null. */ public SimpleFormValidator (Disableable targetToDisable, Label messageLabel, String styleName) { this(targetToDisable, messageLabel, VisUI.getSkin().get(styleName, FormValidatorStyle.class)); } /** * @param targetToDisable target actor that will be disabled if form is invalid. Eg. you can pass form Confirm button. * May be null. * @param messageLabel label that text will be changed if from is valid or invalid. May be null. */ public SimpleFormValidator (Disableable targetToDisable, Label messageLabel, FormValidatorStyle style) { this.style = style; if (targetToDisable != null) disableTargets.add(targetToDisable); this.messageLabel = messageLabel; } /** Validates if file is not empty */ public FormInputValidator notEmpty (VisValidatableTextField field, String errorMsg) { EmptyInputValidator validator = new EmptyInputValidator(errorMsg); field.addValidator(validator); add(field); return validator; } /** Validates if entered text is integer number */ public FormInputValidator integerNumber (VisValidatableTextField field, String errorMsg) { ValidatorWrapper wrapper = new ValidatorWrapper(errorMsg, Validators.INTEGERS); field.addValidator(wrapper); add(field); return wrapper; } /** Validates if entered text is float number */ public FormInputValidator floatNumber (VisValidatableTextField field, String errorMsg) { ValidatorWrapper wrapper = new ValidatorWrapper(errorMsg, Validators.FLOATS); field.addValidator(wrapper); add(field); return wrapper; } /** * Validates if entered text is greater than entered number

* Can be used in combination with {@link #integerNumber(VisValidatableTextField, String)} to only allows integers. */ public FormInputValidator valueGreaterThan (VisValidatableTextField field, String errorMsg, float value) { return valueGreaterThan(field, errorMsg, value, false); } /** * Validates if entered text is lesser than entered number

* Can be used in combination with {@link #integerNumber(VisValidatableTextField, String)} to only allows integers. */ public FormInputValidator valueLesserThan (VisValidatableTextField field, String errorMsg, float value) { return valueLesserThan(field, errorMsg, value, false); } /** * Validates if entered text is greater than (or equal) entered number

* Can be used in combination with {@link #integerNumber(VisValidatableTextField, String)} to only allows integers. */ public FormInputValidator valueGreaterThan (VisValidatableTextField field, String errorMsg, float value, boolean validIfEqualsValue) { ValidatorWrapper wrapper = new ValidatorWrapper(errorMsg, new GreaterThanValidator(value, validIfEqualsValue)); field.addValidator(wrapper); add(field); return wrapper; } /** * Validates if entered text is lesser (or equal) than entered number

* Can be used in combination with {@link #integerNumber(VisValidatableTextField, String)} to only allows integers. */ public FormInputValidator valueLesserThan (VisValidatableTextField field, String errorMsg, float value, boolean validIfEqualsValue) { ValidatorWrapper wrapper = new ValidatorWrapper(errorMsg, new LesserThanValidator(value, validIfEqualsValue)); field.addValidator(wrapper); add(field); return wrapper; } /** Allows to add custom validator to field */ public FormInputValidator custom (VisValidatableTextField field, FormInputValidator customValidator) { field.addValidator(customValidator); add(field); return customValidator; } /** Validates if given button (usually checkbox) is checked. Use VisCheckBox to additionally support error border around it. */ public void checked (Button button, String errorMsg) { buttons.add(new CheckedButtonWrapper(button, true, errorMsg)); button.addListener(changeListener); validate(); } /** Validates if given button (usually checkbox) is unchecked. Use VisCheckBox to additionally support error border around it. */ public void unchecked (Button button, String errorMsg) { buttons.add(new CheckedButtonWrapper(button, false, errorMsg)); button.addListener(changeListener); validate(); } /** * Adds field to this form without attaching any {@link FormInputValidator} to it. This can be used when field * already has added all required validators. */ public void add (VisValidatableTextField field) { if (fields.contains(field, true) == false) fields.add(field); field.addListener(changeListener); //addListener won't allow to add same listener twice validate(); } public void addDisableTarget (Disableable disableable) { disableTargets.add(disableable); updateWidgets(); } public boolean removeDisableTarget (Disableable disableable) { boolean result = disableTargets.removeValue(disableable, true); updateWidgets(); return result; } public void setMessageLabel (Label messageLabel) { this.messageLabel = messageLabel; updateWidgets(); } /** @param successMsg message that will be displayed on {@link #messageLabel} if all fields were valid. May be null. */ public void setSuccessMessage (String successMsg) { this.successMsg = successMsg; updateWidgets(); } public boolean isTreatDisabledFieldsAsValid () { return treatDisabledFieldsAsValid; } /** * If true then this FormValidator will treat disabled fields as valid regardless of their validator states. * Default is true. Changing this cause form to be revalidated. * @since 1.0.2 */ public void setTreatDisabledFieldsAsValid (boolean treatDisabledFieldAsValid) { this.treatDisabledFieldsAsValid = treatDisabledFieldAsValid; validate(); } /** * Performs full check of this form, typically there is no need to call this method manually since form will be automatically * validated upon field content change. However calling this might be required when change made to field state does not * cause change event to be fired. For example disabling or enabling field. */ public void validate () { formInvalid = false; errorMsgText = null; for (CheckedButtonWrapper wrapper : buttons) { if (wrapper.button.isChecked() != wrapper.mustBeChecked) { wrapper.setButtonStateInvalid(true); } else { wrapper.setButtonStateInvalid(false); } } for (CheckedButtonWrapper wrapper : buttons) { if (treatDisabledFieldsAsValid && wrapper.button.isDisabled()) { continue; } if (wrapper.button.isChecked() != wrapper.mustBeChecked) { errorMsgText = wrapper.errorMsg; formInvalid = true; break; } } for (VisValidatableTextField field : fields) { field.validateInput(); } for (VisValidatableTextField field : fields) { if (treatDisabledFieldsAsValid && field.isDisabled()) { continue; } if (field.isInputValid() == false) { Array validators = field.getValidators(); for (InputValidator v : validators) { if (v instanceof FormInputValidator == false) { throw new IllegalStateException("Fields validated by FormValidator cannot have validators not added using FormValidator methods. " + "Are you adding validators to field manually?"); } FormInputValidator validator = (FormInputValidator) v; if (validator.getLastResult() == false) { if (!(validator.isHideErrorOnEmptyInput() && field.getText().equals(""))) { errorMsgText = validator.getErrorMsg(); } formInvalid = true; break; } } break; } } updateWidgets(); } private void updateWidgets () { for (Disableable disableable : disableTargets) { disableable.setDisabled(formInvalid); } if (messageLabel != null) { if (errorMsgText != null) { messageLabel.setText(errorMsgText); } else { messageLabel.setText(successMsg); //setText will default to "" if successMsg is null } Color targetColor = errorMsgText != null ? style.errorLabelColor : style.validLabelColor; if (targetColor != null && style.colorTransitionDuration != 0) { messageLabel.addAction(Actions.color(targetColor, style.colorTransitionDuration)); } else { messageLabel.setColor(targetColor); } } } private class ChangeSharedListener extends ChangeListener { @Override public void changed (ChangeEvent event, Actor actor) { validate(); } } private static class CheckedButtonWrapper { public Button button; public boolean mustBeChecked; public String errorMsg; public CheckedButtonWrapper (Button button, boolean mustBeChecked, String errorMsg) { this.button = button; this.mustBeChecked = mustBeChecked; this.errorMsg = errorMsg; } public void setButtonStateInvalid (boolean state) { if (button instanceof VisCheckBox) { ((VisCheckBox) button).setStateInvalid(state); } } } public static class EmptyInputValidator extends FormInputValidator { public EmptyInputValidator (String errorMsg) { super(errorMsg); } @Override public boolean validate (String input) { return !input.isEmpty(); } } public static class FormValidatorStyle { /** Optional */ public Color errorLabelColor; /** Optional */ public Color validLabelColor; public float colorTransitionDuration; public FormValidatorStyle () { } public FormValidatorStyle (Color errorLabelColor, Color validLabelColor) { this.errorLabelColor = errorLabelColor; this.validLabelColor = validLabelColor; } public FormValidatorStyle (FormValidatorStyle style) { this.errorLabelColor = style.errorLabelColor; this.validLabelColor = style.validLabelColor; this.colorTransitionDuration = style.colorTransitionDuration; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/form/ValidatorWrapper.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.form; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Allows standard {@link InputValidator} to be used with {@link SimpleFormValidator#custom(VisValidatableTextField, FormInputValidator)} * Wraps standard input validator and adds error message. * @author Kotcrab */ public class ValidatorWrapper extends FormInputValidator { private InputValidator validator; public ValidatorWrapper (String errorMsg, InputValidator validator) { super(errorMsg); this.validator = validator; } @Override protected boolean validate (String input) { return validator.validateInput(input); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/BaseHighlighter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.HighlightTextArea; /** * Highlighter aggregates multiple {@link HighlightRule} into single collection. Highlighter is used by {@link HighlightTextArea} * to get information about which parts of text should be highlighted. Compared to {@link Highlighter} this class is GWT compatible. * @author Kotcrab * @see Highlighter * @since 1.1.2 */ public class BaseHighlighter { private Array rules = new Array(); /** Adds highlighter rule. What is highlighted depends on rule implementation. */ public void addRule (HighlightRule rule) { rules.add(rule); } /** * Adds word based highlighter rule. Note that for most uses, word based rules are not sophisticated enough - for example * using regex rule for programming language keywords detection is far more robust. * @see WordHighlightRule */ public void word (Color color, String word) { addRule(new WordHighlightRule(color, word)); } /** * Adds word based highlighter rule. Utility method allowing to add many words at once. * @see #word(Color, String) * @see WordHighlightRule */ public void word (Color color, String... words) { for (String word : words) { addRule(new WordHighlightRule(color, word)); } } /** * Process all rules in this highlighter. * @param highlights current highlights, new highlights can be added to this list however it should not be modified in any other ways */ public void process (HighlightTextArea textArea, Array highlights) { for (HighlightRule rule : rules) { rule.process(textArea, highlights); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/Highlight.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.graphics.Color; /** * Represents single highlight. * @author Kotcrab * @since 1.1.2 */ public class Highlight implements Comparable { private Color color; private int start; private int end; public Highlight (Color color, int start, int end) { if (color == null) throw new IllegalArgumentException("color can't be null"); if (start >= end) throw new IllegalArgumentException("start can't be >= end: " + start + " >= " + end); this.color = color; this.start = start; this.end = end; } public Color getColor () { return color; } public int getStart () { return start; } public int getEnd () { return end; } @Override public int compareTo (Highlight o) { return getStart() - o.getStart(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/HighlightRule.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.HighlightTextArea; /** * @author Kotcrab * @since 1.1.2 */ public interface HighlightRule { /** * Process this rule. This method should detect matches in text area text, create {@link Highlight} instances and add them to provided * highlights array. * @param textArea text area * @param highlights current highlights, new highlights can be added to this list however it should not be modified in any other ways */ void process (HighlightTextArea textArea, Array highlights); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/Highlighter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.graphics.Color; import com.kotcrab.vis.ui.widget.HighlightTextArea; /** * Highlighter aggregates multiple {@link HighlightRule} into single collection. Highlighter is used by {@link HighlightTextArea} * to get information about which parts of text should be highlighted. If you need GWT compatibility, you need to use {@link BaseHighlighter}. * @author Kotcrab * @see BaseHighlighter * @since 1.1.2 */ public class Highlighter extends BaseHighlighter { /** Adds regex based highlighter rule. */ public void regex (Color color, String regex) { addRule(new RegexHighlightRule(color, regex)); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/RegexHighlightRule.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.HighlightTextArea; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Highlighter rule using regex to detect text matches. Regexes and thus this rule can't be used on GWT. * @author Kotcrab * @since 1.1.2 */ public class RegexHighlightRule implements HighlightRule { private Color color; private Pattern pattern; public RegexHighlightRule (Color color, String regex) { this.color = color; pattern = Pattern.compile(regex); } @Override public void process (HighlightTextArea textArea, Array highlights) { Matcher matcher = pattern.matcher(textArea.getText()); while (matcher.find()) { highlights.add(new Highlight(color, matcher.start(), matcher.end())); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/highlight/WordHighlightRule.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.highlight; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.HighlightTextArea; /** * Highlighter rule using {@link String#indexOf(String)} to detect text matches. * @author Kotcrab * @since 1.1.2 */ public class WordHighlightRule implements HighlightRule { private Color color; private String word; public WordHighlightRule (Color color, String word) { this.color = color; this.word = word; } @Override public void process (HighlightTextArea textArea, Array highlights) { String text = textArea.getText(); int index = text.indexOf(word); while (index >= 0) { highlights.add(new Highlight(color, index, index += word.length())); index = text.indexOf(word, index); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/value/ConstantIfVisibleValue.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.value; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Value; /** * Value that returns given fixed constant value if widget is visible. If actor is invisible then returns 0. * @author Kotcrab * @since 1.1.0 */ public class ConstantIfVisibleValue extends Value { private Actor actor; private float constant; public ConstantIfVisibleValue (float constant) { this.constant = constant; } public ConstantIfVisibleValue (Actor actor, float constant) { this.actor = actor; this.constant = constant; } @Override public float get (Actor context) { if (actor != null) context = actor; return context.isVisible() ? constant : 0; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/value/PrefHeightIfVisibleValue.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.value; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.Value; import com.badlogic.gdx.scenes.scene2d.ui.Widget; /** * Value that returns widget preferred height if it's visible. If widget is invisible then returns 0. * This can be only added to classes extending {@link Widget} or {@link Table}, if you try to add it to any other class * you will get {@link IllegalStateException} during runtime. * @author Kotcrab * @since 0.9.3 */ public class PrefHeightIfVisibleValue extends Value { public static final PrefHeightIfVisibleValue INSTANCE = new PrefHeightIfVisibleValue(); @Override public float get (Actor actor) { if (actor instanceof Widget) { Widget widget = (Widget) actor; return widget.isVisible() ? widget.getPrefHeight() : 0; } if (actor instanceof Table) { Table table = (Table) actor; return table.isVisible() ? table.getPrefHeight() : 0; } throw new IllegalStateException("Unsupported actor type for PrefHeightIfVisibleValue: " + actor.getClass()); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/value/PrefWidthIfVisibleValue.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.value; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.Value; import com.badlogic.gdx.scenes.scene2d.ui.Widget; /** * Value that returns widget preferred width if it's visible. If widget is invisible then returns 0. * This can be only added to classes extending {@link Widget} or {@link Table}, if you try to add it to any other class * you will get {@link IllegalStateException} during runtime. * @author Kotcrab * @since 1.0.0 */ public class PrefWidthIfVisibleValue extends Value { public static final PrefWidthIfVisibleValue INSTANCE = new PrefWidthIfVisibleValue(); @Override public float get (Actor actor) { if (actor instanceof Widget) { Widget widget = (Widget) actor; return widget.isVisible() ? widget.getPrefWidth() : 0; } if (actor instanceof Table) { Table table = (Table) actor; return table.isVisible() ? table.getPrefWidth() : 0; } throw new IllegalStateException("Unsupported actor type for PrefWidthIfVisibleValue: " + actor.getClass()); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/value/VisValue.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.value; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Value; /** * Allows to use libGDX {@link Value} with lambdas. Using this on Java lower than 1.8 is pointless because lambadas are * not supported. * @author Kotcrab * @see VisWidgetValue * @since 0.9.3 */ public class VisValue extends Value { private ValueGetter getter; public VisValue (ValueGetter getter) { this.getter = getter; } @Override public float get (Actor context) { return getter.get(context); } public interface ValueGetter { float get (Actor context); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/util/value/VisWidgetValue.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.util.value; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Value; import com.badlogic.gdx.scenes.scene2d.ui.Widget; /** * Allows to use libGDX {@link Value} with lambdas for scene2d.ui widgets. Note that this cannot be added to actors, * only widgets are supported, if you try to do so you will get {@link ClassCastException} when this Value has been invoked. * Using this on Java lower than 1.8 is pointless because lambadas are not supported. * @author Kotcrab * @see VisValue * @see PrefHeightIfVisibleValue * @since 0.9.3 */ public class VisWidgetValue extends Value { protected WidgetValueGetter getter; public VisWidgetValue (WidgetValueGetter getter) { this.getter = getter; } @Override public float get (Actor context) { return getter.get((Widget) context); } public interface WidgetValueGetter { float get (Widget context); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/BusyBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.ui.Widget; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.VisUI; /** * BusyBar is a type of indeterminate progress bar. This widget is usually added at the top of table and is shown * to indicate that some background work is going on. This widget should span across full width of table that is added to. * Default style of widget is blue rectangle that moves from left to right edge of screen in loop. For example you can * see it in FileChooser when opening directory containing thousand of files or checkout TestBusyBar in VisUI test application. * @author Kotcrab * @since 1.1.4 */ public class BusyBar extends Widget { private BusyBarStyle style; private float segmentX; public BusyBar () { style = VisUI.getSkin().get(BusyBarStyle.class); } public BusyBar (String styleName) { style = VisUI.getSkin().get(styleName, BusyBarStyle.class); } public BusyBar (BusyBarStyle style) { this.style = style; } @Override public float getPrefHeight () { return style.height; } @Override public float getPrefWidth () { return style.segmentWidth; } @Override public void draw (Batch batch, float parentAlpha) { batch.flush(); if (clipBegin()) { Color c = getColor(); batch.setColor(c.r, c.g, c.b, c.a * parentAlpha); segmentX += getSegmentDeltaX(); style.segment.draw(batch, getX() + segmentX, getY(), style.segmentWidth, style.height); if (segmentX > getWidth() + style.segmentOverflow) { resetSegment(); } if (isVisible()) Gdx.graphics.requestRendering(); batch.flush(); clipEnd(); } } public void resetSegment () { segmentX = -style.segmentWidth - style.segmentOverflow; } protected float getSegmentDeltaX () { return Gdx.graphics.getDeltaTime() * getWidth(); } public BusyBarStyle getStyle () { return style; } static public class BusyBarStyle { public Drawable segment; public int segmentOverflow; public int segmentWidth; public int height; public BusyBarStyle () { } public BusyBarStyle (BusyBarStyle style) { this.segment = style.segment; this.segmentOverflow = style.segmentOverflow; this.segmentWidth = style.segmentWidth; this.height = style.height; } public BusyBarStyle (Drawable segment, int segmentOverflow, int segmentWidth, int height) { this.segment = segment; this.segmentOverflow = segmentOverflow; this.segmentWidth = segmentWidth; this.height = height; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/ButtonBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.ObjectMap; import com.kotcrab.vis.ui.Locales; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.OsUtils; /** * Convenient class for creating button panels with buttons such as "Ok", "Cancel", "Yes" etc. Buttons are arranged in * platform dependent order. Built-in orders support Windows, Mac, and Linux. When no platform matches ButtonBar * defaults to Linux order. * User may specify custom order, see {@link ButtonType} for buttons ids. * @author Kotcrab * @since 1.0.0 */ public class ButtonBar { public static final String WINDOWS_ORDER = "L H BEF YNOCA R"; public static final String OSX_ORDER = "L H BEF NYCOA R"; public static final String LINUX_ORDER = "L H NYACBEFO R"; private Sizes sizes; private ObjectMap buttons = new ObjectMap(); private boolean ignoreSpacing; private String order; public ButtonBar () { this(VisUI.getSizes(), getDefaultOrder()); } public ButtonBar (String order) { this(VisUI.getSizes(), order); } public ButtonBar (Sizes sizes) { this(sizes, getDefaultOrder()); } public ButtonBar (Sizes sizes, String order) { if (sizes == null) throw new IllegalArgumentException("sizes can't be null"); this.sizes = sizes; setOrder(order); } private static String getDefaultOrder () { if (OsUtils.isWindows()) { return WINDOWS_ORDER; } else if (OsUtils.isMac()) { return OSX_ORDER; } else //default to linux order return LINUX_ORDER; } public boolean isIgnoreSpacing () { return ignoreSpacing; } /** @param ignoreSpacing if true spacing symbols in order will be ignored */ public void setIgnoreSpacing (boolean ignoreSpacing) { this.ignoreSpacing = ignoreSpacing; } public String getOrder () { return order; } public void setOrder (String order) { if (order == null) throw new IllegalArgumentException("order can't be null"); this.order = order; } public void setButton (ButtonType type, ChangeListener listener) { setButton(type, type.getText(), listener); } public void setButton (ButtonType type, String text, ChangeListener listener) { setButton(type, new VisTextButton(text), listener); } public void setButton (ButtonType type, Button button) { setButton(type, button, null); } public void setButton (ButtonType type, Button button, ChangeListener listener) { if (type == null) throw new IllegalArgumentException("type can't be null"); if (button == null) throw new IllegalArgumentException("button can't be null"); if (buttons.containsKey(type.id)) buttons.remove(type.id); buttons.put(type.id, button); if (listener != null) button.addListener(listener); } public Button getButton (ButtonType type) { return buttons.get(type.getId()); } /** * @return stored button casted to {@link VisTextButton}. Will throw {@link ClassCastException} in case stored button * type is wrong. This may be safely used when button was created using {@link #setButton(ButtonType, String, ChangeListener)}. */ public VisTextButton getTextButton (ButtonType type) { return (VisTextButton) getButton(type); } /** * Builds and returns {@link VisTable} containing buttons in platform dependant order. Note that calling this multiple * times will remove buttons from previous tables. */ public VisTable createTable () { VisTable table = new VisTable(true); table.left(); boolean spacingValid = false; for (int i = 0; i < order.length(); i++) { char ch = order.charAt(i); if (ignoreSpacing == false && ch == ' ' && spacingValid) { table.add().width(sizes.buttonBarSpacing); spacingValid = false; } Button button = buttons.get(ch); if (button != null) { table.add(button); spacingValid = true; } } return table; } /** Defines possible button types for {@link ButtonBar} */ public enum ButtonType { LEFT("left", 'L'), RIGHT("right", 'R'), HELP("help", 'H'), NO("no", 'N'), YES("yes", 'Y'), CANCEL("cancel", 'C'), BACK("back", 'B'), NEXT("next", 'E'), APPLY("apply", 'A'), FINISH("finish", 'F'), OK("ok", 'O'); private final String key; private final char id; ButtonType (String key, char id) { this.key = key; this.id = id; } public char getId () { return id; } public final String getText () { return Locales.getButtonBarBundle().get(key); } @Override public final String toString () { return getText(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/CollapsibleWidget.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.actions.FloatAction; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.GdxRuntimeException; /** * Widget containing table that can be vertically collapsed. * * @author Kotcrab * @see HorizontalCollapsibleWidget * @since 0.3.1 */ public class CollapsibleWidget extends WidgetGroup { private Table table; private CollapseAction collapseAction = new CollapseAction(); private float collapseDuration = 0.3f; private Interpolation collapseInterpolation = Interpolation.pow3Out; private boolean collapsed; private boolean actionRunning; private float currentHeight; public CollapsibleWidget () { } public CollapsibleWidget (Table table) { this(table, false); } public CollapsibleWidget (Table table, boolean collapsed) { this.collapsed = collapsed; this.table = table; updateTouchable(); if (table != null) addActor(table); } public void setCollapsed (boolean collapse, boolean withAnimation) { this.collapsed = collapse; updateTouchable(); if (table == null) return; actionRunning = true; if (withAnimation) { collapseAction.reset(); collapseAction.setStart(currentHeight); collapseAction.setEnd(collapse ? 0f : table.getPrefHeight()); collapseAction.setDuration(collapseDuration); collapseAction.setInterpolation(collapseInterpolation); addAction(collapseAction); } else { if (collapse) { currentHeight = 0; collapsed = true; } else { currentHeight = table.getPrefHeight(); collapsed = false; } actionRunning = false; invalidateHierarchy(); } } public void setCollapsed (boolean collapse) { setCollapsed(collapse, true); } public boolean isCollapsed () { return collapsed; } private void updateTouchable () { if (collapsed) setTouchable(Touchable.disabled); else setTouchable(Touchable.enabled); } public void setCollapseDuration (float collapseDuration) { this.collapseDuration = collapseDuration; } public void setCollapseInterpolation (Interpolation collapseInterpolation) { this.collapseInterpolation = collapseInterpolation; } @Override public void draw (Batch batch, float parentAlpha) { if (currentHeight > 1 && getY() + currentHeight > 1) { if (actionRunning) { batch.flush(); boolean clipEnabled = clipBegin(getX(), getY(), getWidth(), currentHeight); super.draw(batch, parentAlpha); batch.flush(); if (clipEnabled) clipEnd(); } else { super.draw(batch, parentAlpha); } } } @Override public void layout () { if (table == null) return; table.setBounds(0, 0, table.getPrefWidth(), table.getPrefHeight()); if (actionRunning == false) { if (collapsed) currentHeight = 0; else currentHeight = table.getPrefHeight(); } } @Override public float getPrefWidth () { return table == null ? 0 : table.getPrefWidth(); } @Override public float getPrefHeight () { if (table == null) return 0; if (actionRunning == false) { if (collapsed) return 0; else return table.getPrefHeight(); } return currentHeight; } public void setTable (Table table) { this.table = table; clearChildren(); addActor(table); } @Override protected void childrenChanged () { super.childrenChanged(); if (getChildren().size > 1) throw new GdxRuntimeException("Only one actor can be added to CollapsibleWidget"); } private class CollapseAction extends FloatAction { @Override protected void update (float percent) { super.update(percent); currentHeight = getValue(); if (percent == 1) { actionRunning = false; collapsed = currentHeight == 0; } invalidateHierarchy(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/Draggable.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.utils.Disableable; import com.kotcrab.vis.ui.layout.DragPane; import java.util.Iterator; /** * Draws copies of dragged actors which have this listener attached. * @author MJ * @since 0.9.3 */ public class Draggable extends InputListener { private static final Vector2 MIMIC_COORDINATES = new Vector2(); private static final Vector2 STAGE_COORDINATES = new Vector2(); /** * Initial fading time value of dragged actors. * @see #setFadingTime(float) */ public static float DEFAULT_FADING_TIME = 0.1f; /** * Initial moving time value of dragged actors. * @see #setMovingTime(float) */ public static float DEFAULT_MOVING_TIME = 0.1f; /** * Initial invisibility setting of dragged actors. * @see #setInvisibleWhenDragged(boolean) */ public static boolean INVISIBLE_ON_DRAG = false; /** * Initial setting of keeping the dragged widget within its parent's bounds. * @see #setKeepWithinParent(boolean) */ public static boolean KEEP_WITHIN_PARENT = false; /** * Initial alpha setting of dragged actors. * @see #setAlpha(float) */ public static float DEFAULT_ALPHA = 1f; /** * Initial listener of draggables, unless a different listener is specified in the constructor. By default, * {@link DragPane.DefaultDragListener} is used, which allows to drag actors into {@link DragPane} widgets. * @see #setListener(DragListener) * @see DragListener */ public static DragListener DEFAULT_LISTENER = new DragPane.DefaultDragListener(); /** * If true, other actors will not receive mouse events while the actor is dragged. * @see #setBlockInput(boolean) */ public static boolean BLOCK_INPUT = true; /*** Blocks mouse input during dragging. */ private static final Actor BLOCKER = new Actor(); // Settings. private DragListener listener; private boolean blockInput = BLOCK_INPUT; private boolean invisibleWhenDragged = INVISIBLE_ON_DRAG; private boolean keepWithinParent = KEEP_WITHIN_PARENT; private float deadzoneRadius; private float fadingTime = DEFAULT_FADING_TIME; private float movingTime = DEFAULT_FADING_TIME; private float alpha = DEFAULT_ALPHA; private Interpolation fadingInterpolation = Interpolation.fade; private Interpolation movingInterpolation = Interpolation.sineOut; // Control variables. private final MimicActor mimic = new MimicActor(); private float dragStartX; private float dragStartY; private float offsetX; private float offsetY; /** Creates a new draggable with default listener. */ public Draggable () { this(DEFAULT_LISTENER); } /** @param listener is being notified of draggable events and can change its behavior. Can be null. */ public Draggable (final DragListener listener) { this.listener = listener; mimic.setTouchable(Touchable.disabled); } static { // Blocks mouse input. BLOCKER.addListener(new InputListener() { @Override public boolean mouseMoved (final InputEvent event, final float x, final float y) { return true; } @Override public boolean touchDown (final InputEvent event, final float x, final float y, final int pointer, final int button) { return true; } @Override public boolean scrolled (final InputEvent event, final float x, final float y, final float amountX, final float amountY) { return true; } }); } /** * @param actor will have this listener attached and all other {@link Draggable} listeners removed. If you want multiple * {@link Draggable} listeners or you are sure that the widget has no other {@link Draggable}s attached, you can add * the listener using the standard method: {@link Actor#addListener(EventListener)} - avoiding validation and * iteration over actor's listeners. */ public void attachTo (final Actor actor) { for (final Iterator listeners = actor.getListeners().iterator(); listeners.hasNext(); ) { final EventListener listener = listeners.next(); if (listener instanceof Draggable) { listeners.remove(); } } actor.addListener(this); } /** @return during dragging, this is the offset value on X axis from the start of the dragged widget. */ public float getOffsetX () { return offsetX; } /** @return during dragging, this is the offset value on Y axis from the start of the dragged widget. */ public float getOffsetY () { return offsetY; } /** @return alpha color value of dragged actor copy. */ public float getAlpha () { return alpha; } /** @param alpha alpha color value of dragged actor copy. */ public void setAlpha (final float alpha) { this.alpha = alpha; } /** @return true if mouse input is blocked during dragging. */ public boolean isBlockingInput () { return blockInput; } /** * @param blockInput true if mouse input should be blocked during actors dragging. If false, other actors might still receive * mouse events (for example, buttons might switch to "over" style). */ public void setBlockInput (final boolean blockInput) { this.blockInput = blockInput; } /** @return if true, original actor is invisible while it's being dragged. */ public boolean isInvisibleWhenDragged () { return invisibleWhenDragged; } /** @param invisibleWhenDragged if true, original actor is invisible while it's being dragged. */ public void setInvisibleWhenDragged (final boolean invisibleWhenDragged) { this.invisibleWhenDragged = invisibleWhenDragged; } /** @return if true, widget cannot be dragged out of the bounds of its parent. */ public boolean isKeptWithinParent () { return keepWithinParent; } /** * @param keepWithinParent if true, widget cannot be dragged out of the bounds of its parent. Stage coordinates in listener * will always be inside the parent. Note that for this setting to work properly, both actor and its parent have to * correctly return their sizes with {@link Actor#getWidth()} and {@link Actor#getHeight()} methods. */ public void setKeepWithinParent (final boolean keepWithinParent) { this.keepWithinParent = keepWithinParent; } /** @return distance from the widget's parent in which the actor is not dragged out of parent bounds. */ public float getDeadzoneRadius () { return deadzoneRadius; } /** * @param deadzoneRadius distance from the widget's parent in which the actor is not dragged out of parent bounds. Defaults to * 0f. Values lower or equal to 0 are ignored during dragging. If {@link #isKeptWithinParent()} returns true, this * value is ignored and actor is always kept within parent. */ public void setDeadzoneRadius (float deadzoneRadius) { this.deadzoneRadius = deadzoneRadius; } /** @return time after which the dragged actor copy disappears. */ public float getFadingTime () { return fadingTime; } /** @param fadingTime time after which the dragged actor copy disappears. */ public void setFadingTime (final float fadingTime) { this.fadingTime = fadingTime; } /** @return time after which the dragged actor copy returns to its origin. */ public float getMovingTime () { return movingTime; } /** @param movingTime time after which the dragged actor copy returns to its origin. */ public void setMovingTime (final float movingTime) { this.movingTime = movingTime; } /** @param movingInterpolation used to move the dragged widgets to the original position when their drag was cancelled. */ public void setMovingInterpolation (final Interpolation movingInterpolation) { this.movingInterpolation = movingInterpolation; } /** @param fadingInterpolation used to fade out dragged widgets after their drag was accepted. */ public void setFadingInterpolation (final Interpolation fadingInterpolation) { this.fadingInterpolation = fadingInterpolation; } /** * @param listener is being notified of draggable events and can change its behavior. Can be null. * @see DragAdapter */ public void setListener (final DragListener listener) { this.listener = listener; } /** @return listener notified of draggable events. Can be null. */ public DragListener getListener () { return listener; } @Override public boolean touchDown (final InputEvent event, final float x, final float y, final int pointer, final int button) { final Actor actor = event.getListenerActor(); if (!isValid(actor) || isDisabled(actor)) { return false; } if (listener == null || listener.onStart(this, actor, event.getStageX(), event.getStageY())) { attachMimic(actor, event, x, y); return true; } return false; } /** * @param actor might be already removed. * @return true if actor is not null and has a {@link Stage}. */ protected boolean isValid (final Actor actor) { return actor != null && actor.getStage() != null; } /** * @param actor might be a {@link Disableable}. * @return true if actor is disabled. */ protected boolean isDisabled (final Actor actor) { return actor instanceof Disableable && ((Disableable) actor).isDisabled(); } /** * @param actor has the listener attached. * @param event touch down event which triggered mimic spawning. * @param x actor's relative X event position. * @param y actor's relative Y event position. */ protected void attachMimic (final Actor actor, final InputEvent event, final float x, final float y) { mimic.clearActions(); mimic.getColor().a = alpha; mimic.setActor(actor); offsetX = -x; offsetY = -y; getStageCoordinates(event); dragStartX = MIMIC_COORDINATES.x; dragStartY = MIMIC_COORDINATES.y; mimic.setPosition(dragStartX, dragStartY); actor.getStage().addActor(mimic); mimic.toFront(); actor.setVisible(!invisibleWhenDragged); if (blockInput) { addBlocker(actor.getStage()); } } /** @param stage will contain a mock-up blocker actor, which blocks all mouse input. */ protected static void addBlocker (final Stage stage) { stage.addActor(BLOCKER); BLOCKER.setBounds(0f, 0f, stage.getWidth(), stage.getHeight()); BLOCKER.toFront(); } /** Removes mock-up blocker actor from the stage. */ protected static void removeBlocker () { BLOCKER.remove(); } /** @param event will extract stage coordinates from the event, respecting mimic offset and other dragging settings. */ protected void getStageCoordinates (final InputEvent event) { if (keepWithinParent) { getStageCoordinatesWithinParent(event); } else if (deadzoneRadius > 0f) { getStageCoordinatesWithDeadzone(event); } else { getStageCoordinatesWithOffset(event); } } private void getStageCoordinatesWithDeadzone (final InputEvent event) { final Actor parent = mimic.getActor().getParent(); if (parent != null) { MIMIC_COORDINATES.set(Vector2.Zero); parent.localToStageCoordinates(MIMIC_COORDINATES); final float parentX = MIMIC_COORDINATES.x; final float parentY = MIMIC_COORDINATES.y; final float parentEndX = parentX + parent.getWidth(); final float parentEndY = parentY + parent.getHeight(); if (isWithinDeadzone(event, parentX, parentY, parentEndX, parentEndY)) { // Keeping within parent bounds: MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY); if (MIMIC_COORDINATES.x < parentX) { MIMIC_COORDINATES.x = parentX; } else if (MIMIC_COORDINATES.x + mimic.getWidth() > parentEndX) { MIMIC_COORDINATES.x = parentEndX - mimic.getWidth(); } if (MIMIC_COORDINATES.y < parentY) { MIMIC_COORDINATES.y = parentY; } else if (MIMIC_COORDINATES.y + mimic.getHeight() > parentEndY) { MIMIC_COORDINATES.y = parentEndY - mimic.getHeight(); } STAGE_COORDINATES.set(MathUtils.clamp(event.getStageX(), parentX, parentEndX - 1f), MathUtils.clamp(event.getStageY(), parentY, parentEndY - 1f)); } else { getStageCoordinatesWithOffset(event); } } else { getStageCoordinatesWithOffset(event); } } private boolean isWithinDeadzone (InputEvent event, float parentX, float parentY, float parentEndX, float parentEndY) { return parentX - deadzoneRadius <= event.getStageX() && parentEndX + deadzoneRadius >= event.getStageX() && parentY - deadzoneRadius <= event.getStageY() && parentEndY + deadzoneRadius >= event.getStageY(); } private void getStageCoordinatesWithinParent (final InputEvent event) { final Actor parent = mimic.getActor().getParent(); if (parent != null) { MIMIC_COORDINATES.set(Vector2.Zero); parent.localToStageCoordinates(MIMIC_COORDINATES); final float parentX = MIMIC_COORDINATES.x; final float parentY = MIMIC_COORDINATES.y; final float parentEndX = parentX + parent.getWidth(); final float parentEndY = parentY + parent.getHeight(); MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY); if (MIMIC_COORDINATES.x < parentX) { MIMIC_COORDINATES.x = parentX; } else if (MIMIC_COORDINATES.x + mimic.getWidth() > parentEndX) { MIMIC_COORDINATES.x = parentEndX - mimic.getWidth(); } if (MIMIC_COORDINATES.y < parentY) { MIMIC_COORDINATES.y = parentY; } else if (MIMIC_COORDINATES.y + mimic.getHeight() > parentEndY) { MIMIC_COORDINATES.y = parentEndY - mimic.getHeight(); } STAGE_COORDINATES.set(MathUtils.clamp(event.getStageX(), parentX, parentEndX - 1f), MathUtils.clamp(event.getStageY(), parentY, parentEndY - 1f)); } else { getStageCoordinatesWithOffset(event); } } private void getStageCoordinatesWithOffset (final InputEvent event) { MIMIC_COORDINATES.set(event.getStageX() + offsetX, event.getStageY() + offsetY); STAGE_COORDINATES.set(event.getStageX(), event.getStageY()); } @Override public void touchDragged (final InputEvent event, final float x, final float y, final int pointer) { if (isDragged()) { getStageCoordinates(event); mimic.setPosition(MIMIC_COORDINATES.x, MIMIC_COORDINATES.y); if (listener != null) { listener.onDrag(this, mimic.getActor(), STAGE_COORDINATES.x, STAGE_COORDINATES.y); } } } @Override public void touchUp (final InputEvent event, final float x, final float y, final int pointer, final int button) { if (isDragged()) { removeBlocker(); getStageCoordinates(event); mimic.setPosition(MIMIC_COORDINATES.x, MIMIC_COORDINATES.y); if (listener == null || mimic.getActor().getStage() != null && listener.onEnd(this, mimic.getActor(), STAGE_COORDINATES.x, STAGE_COORDINATES.y)) { // Drag end approved - fading out. addMimicHidingAction(Actions.fadeOut(fadingTime, fadingInterpolation), fadingTime); } else { // Drag end cancelled - returning to the original position. addMimicHidingAction(Actions.moveTo(dragStartX, dragStartY, movingTime, movingInterpolation), movingTime); } } } /** @return true if some actor with this listener attached is currently dragged. */ public boolean isDragged () { return mimic.getActor() != null; } /** @param hidingAction will be attached to the mimic actor. */ protected void addMimicHidingAction (final Action hidingAction, final float delay) { mimic.addAction(Actions.sequence(hidingAction, Actions.removeActor())); mimic.getActor().addAction(Actions.delay(delay, Actions.visible(true))); } /** * Allows to control {@link Draggable} behavior. * @author MJ * @since 0.9.3 */ public static interface DragListener { /** Use in listner's method for code clarity. */ boolean CANCEL = false, APPROVE = true; /** * @param draggable source of event. * @param actor is about to be dragged. * @param stageX stage coordinate on X axis where the drag started. * @param stageY stage coordinate on Y axis where the drag started. * @return if true, actor will not be dragged. */ boolean onStart (Draggable draggable, Actor actor, float stageX, float stageY); /** * @param draggable source of event. * @param actor is being dragged. * @param stageX stage coordinate on X axis with current cursor position. * @param stageY stage coordinate on Y axis with current cursor position. */ void onDrag (Draggable draggable, Actor actor, float stageX, float stageY); /** * @param draggable source of event. * @param actor is about to stop being dragged. * @param stageX stage coordinate on X axis where the drag ends. * @param stageY stage coordinate on X axis where the drag ends. * @return if true, "mirror" of the actor will quickly fade out. If false, mirror will return to the original actor's * position. */ boolean onEnd (Draggable draggable, Actor actor, float stageX, float stageY); } /** * Default, empty implementation of {@link DragListener}. Approves all drag requests. * @author MJ * @since 0.9.3 */ public static class DragAdapter implements DragListener { @Override public boolean onStart (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { return APPROVE; } @Override public void onDrag (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { } @Override public boolean onEnd (final Draggable draggable, final Actor actor, final float stageX, final float stageY) { return APPROVE; } } /** * Draws the chosen actor with modified alpha value in a custom position. Clears mimicked actor upon removing from the stage. * @author MJ * @since 0.9.3 */ public static class MimicActor extends Actor { private static final Vector2 LAST_POSITION = new Vector2(); private Actor actor; /** Has no actor to mimic. See {@link #setActor(Actor)}. */ public MimicActor () { } /** @param actor will be mimicked. */ public MimicActor (final Actor actor) { this.actor = actor; } @Override public boolean remove () { actor = null; return super.remove(); } /** @return mimicked actor. */ public Actor getActor () { return actor; } /** @param actor will be mimicked. */ public void setActor (final Actor actor) { this.actor = actor; } @Override public float getWidth () { return actor == null ? 0f : actor.getWidth(); } @Override public float getHeight () { return actor == null ? 0f : actor.getHeight(); } @Override public void draw (final Batch batch, final float parentAlpha) { if (actor != null) { LAST_POSITION.set(actor.getX(), actor.getY()); actor.setPosition(getX(), getY()); actor.draw(batch, getColor().a * parentAlpha); actor.setPosition(LAST_POSITION.x, LAST_POSITION.y); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/HighlightTextArea.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.GlyphLayout; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pools; import com.kotcrab.vis.ui.util.highlight.BaseHighlighter; import com.kotcrab.vis.ui.util.highlight.Highlight; import com.kotcrab.vis.ui.util.highlight.Highlighter; /** * Text area implementation supporting highlighting words and scrolling in both X and Y directions. *

* For best scroll pane settings you should create scroll pane using {@link #createCompatibleScrollPane()}. *

* Note about overlapping highlights: this text area can handle overlapping highlights, highlights that starts earlier * have higher priority. If two highlights have the exactly the same start point, then it is undefined which highlight * will be used and depends on how array containing highlights will be sorted. * @author Kotcrab * @see Highlighter * @since 1.1.2 */ public class HighlightTextArea extends ScrollableTextArea { private Array highlights = new Array(); private Array renderChunks = new Array(); private boolean chunkUpdateScheduled = true; private Color defaultColor = Color.WHITE; private BaseHighlighter highlighter; private float maxAreaWidth = 0; private float maxAreaHeight = 0; public HighlightTextArea (String text) { super(text); init(); } public HighlightTextArea (String text, String styleName) { super(text, styleName); init(); } public HighlightTextArea (String text, VisTextFieldStyle style) { super(text, style); init(); } private void init() { softwrap = false; } @Override void updateDisplayText () { super.updateDisplayText(); processHighlighter(); } @Override protected void calculateOffsets () { super.calculateOffsets(); if (chunkUpdateScheduled == false) return; chunkUpdateScheduled = false; highlights.sort(); renderChunks.clear(); String text = getText(); Pool layoutPool = Pools.get(GlyphLayout.class); GlyphLayout layout = layoutPool.obtain(); boolean carryHighlight = false; for (int lineIdx = 0, highlightIdx = 0; lineIdx < linesBreak.size; lineIdx += 2) { int lineStart = linesBreak.items[lineIdx]; int lineEnd = linesBreak.items[lineIdx + 1]; int lineProgress = lineStart; float chunkOffset = 0; for (; highlightIdx < highlights.size; ) { Highlight highlight = highlights.get(highlightIdx); if (highlight.getStart() > lineEnd) { break; } if (highlight.getStart() == lineProgress || carryHighlight) { renderChunks.add(new Chunk(text.substring(lineProgress, Math.min(highlight.getEnd(), lineEnd)), highlight.getColor(), chunkOffset, lineIdx)); lineProgress = Math.min(highlight.getEnd(), lineEnd); if (highlight.getEnd() > lineEnd) { carryHighlight = true; } else { carryHighlight = false; highlightIdx++; } } else { //protect against overlapping highlights boolean noMatch = false; while (highlight.getStart() <= lineProgress) { highlightIdx++; if (highlightIdx >= highlights.size) { noMatch = true; break; } highlight = highlights.get(highlightIdx); if (highlight.getStart() > lineEnd) { noMatch = true; break; } } if (noMatch) break; renderChunks.add(new Chunk(text.substring(lineProgress, highlight.getStart()), defaultColor, chunkOffset, lineIdx)); lineProgress = highlight.getStart(); } Chunk chunk = renderChunks.peek(); layout.setText(style.font, chunk.text); chunkOffset += layout.width; //current highlight needs to be applied to next line meaning that there is no other highlights that can be applied to currently parsed line if (carryHighlight) break; } if (lineProgress < lineEnd) { renderChunks.add(new Chunk(text.substring(lineProgress, lineEnd), defaultColor, chunkOffset, lineIdx)); } } maxAreaWidth = 0; for (String line : text.split("\\n")) { layout.setText(style.font, line); maxAreaWidth = Math.max(maxAreaWidth, layout.width + 30); } layoutPool.free(layout); updateScrollLayout(); } @Override protected void drawText (Batch batch, BitmapFont font, float x, float y) { maxAreaHeight = 0; float offsetY = 0; float parentAlpha = font.getColor().a; for (int i = firstLineShowing * 2; i < (firstLineShowing + linesShowing) * 2 && i < linesBreak.size; i += 2) { for (Chunk chunk : renderChunks) { if (chunk.lineIndex == i) { font.setColor(chunk.color); font.getColor().a *= parentAlpha; font.draw(batch, chunk.text, x + chunk.offsetX, y + offsetY); } } offsetY -= font.getLineHeight(); maxAreaHeight += font.getLineHeight(); } maxAreaHeight += 30; } /** * Processes highlighter rules, collects created highlights and schedules text area displayed text update. This should be called * after highlighter rules has changed to update highlights. */ public void processHighlighter () { if (highlights == null) return; highlights.clear(); if (highlighter != null) highlighter.process(this, highlights); chunkUpdateScheduled = true; } /** * Changes highlighter of text area. Note that you don't have to call {@link #processHighlighter()} after changing * highlighter - you only have to call it when highlighter rules has changed. */ public void setHighlighter (BaseHighlighter highlighter) { this.highlighter = highlighter; processHighlighter(); } public BaseHighlighter getHighlighter () { return highlighter; } @Override public float getPrefWidth () { return maxAreaWidth + 5; } @Override public float getPrefHeight () { return maxAreaHeight + 5; } @Override public ScrollPane createCompatibleScrollPane () { ScrollPane scrollPane = super.createCompatibleScrollPane(); scrollPane.setScrollingDisabled(false, false); return scrollPane; } private static class Chunk { String text; Color color; float offsetX; int lineIndex; public Chunk (String text, Color color, float offsetX, int lineIndex) { this.text = text; this.color = color; this.offsetX = offsetX; this.lineIndex = lineIndex; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/HorizontalCollapsibleWidget.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.actions.FloatAction; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.utils.GdxRuntimeException; /** * Widget containing table that can be horizontally collapsed. * @author Kotcrab * @see CollapsibleWidget * @since 1.2.5 */ public class HorizontalCollapsibleWidget extends WidgetGroup { private Table table; private CollapseAction collapseAction = new CollapseAction(); private float collapseDuration = 0.3f; private Interpolation collapseInterpolation = Interpolation.pow3Out; private boolean collapsed; private boolean actionRunning; private float currentWidth; public HorizontalCollapsibleWidget () { } public HorizontalCollapsibleWidget (Table table) { this(table, false); } public HorizontalCollapsibleWidget (Table table, boolean collapsed) { this.collapsed = collapsed; this.table = table; updateTouchable(); if (table != null) addActor(table); } public void setCollapsed (boolean collapse, boolean withAnimation) { this.collapsed = collapse; updateTouchable(); if (table == null) return; actionRunning = true; if (withAnimation) { collapseAction.reset(); collapseAction.setStart(currentWidth); collapseAction.setEnd(collapse ? 0f : table.getPrefWidth()); collapseAction.setDuration(collapseDuration); collapseAction.setInterpolation(collapseInterpolation); addAction(collapseAction); } else { if (collapse) { currentWidth = 0; collapsed = true; } else { currentWidth = table.getPrefWidth(); collapsed = false; } actionRunning = false; invalidateHierarchy(); } } public void setCollapsed (boolean collapse) { setCollapsed(collapse, true); } public boolean isCollapsed () { return collapsed; } private void updateTouchable () { if (collapsed) setTouchable(Touchable.disabled); else setTouchable(Touchable.enabled); } @Override public void draw (Batch batch, float parentAlpha) { if (currentWidth > 1 && getX() + currentWidth > 1) { if (actionRunning) { batch.flush(); boolean clipEnabled = clipBegin(getX(), getY(), currentWidth, getHeight()); super.draw(batch, parentAlpha); batch.flush(); if (clipEnabled) clipEnd(); } else { super.draw(batch, parentAlpha); } } } @Override public void layout () { if (table == null) return; table.setBounds(0, 0, table.getPrefWidth(), table.getPrefHeight()); if (actionRunning == false) { if (collapsed) currentWidth = 0; else currentWidth = table.getPrefWidth(); } } @Override public float getPrefHeight () { return table == null ? 0 : table.getPrefHeight(); } @Override public float getPrefWidth () { if (table == null) return 0; if (actionRunning == false) { if (collapsed) return 0; else return table.getPrefWidth(); } return currentWidth; } public void setTable (Table table) { this.table = table; clearChildren(); addActor(table); } @Override protected void childrenChanged () { super.childrenChanged(); if (getChildren().size > 1) throw new GdxRuntimeException("Only one actor can be added to CollapsibleWidget"); } private class CollapseAction extends FloatAction { @Override protected void update (float percent) { super.update(percent); currentWidth = getValue(); if (percent == 1) { actionRunning = false; collapsed = currentWidth == 0; } invalidateHierarchy(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/LinkLabel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Cursor.SystemCursor; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.CursorManager; /** * Simple LinkLabel allows to create label with clickable link and underline on mouse over. Link can have custom text. * By default clicking link will open it in default browser, this can be changed by settings label listener. * @author Kotcrab * @since 0.7.2 */ public class LinkLabel extends VisLabel { static private final Color tempColor = new Color(); private LinkLabelStyle style; private ClickListener clickListener; private LinkLabelListener listener; private CharSequence url; public LinkLabel (CharSequence url) { super(url, VisUI.getSkin().get(LinkLabelStyle.class)); init(url); } public LinkLabel (CharSequence text, CharSequence url) { super(text, VisUI.getSkin().get(LinkLabelStyle.class)); init(url); } public LinkLabel (CharSequence text, int alignment) { super(text, VisUI.getSkin().get(LinkLabelStyle.class)); setAlignment(alignment); init(text); } public LinkLabel (CharSequence text, Color textColor) { super(text, VisUI.getSkin().get(LinkLabelStyle.class)); setColor(textColor); init(text); } public LinkLabel (CharSequence text, LinkLabelStyle style) { super(text, style); init(text); } public LinkLabel (CharSequence text, CharSequence url, String styleName) { super(text, VisUI.getSkin().get(styleName, LinkLabelStyle.class)); init(url); } public LinkLabel (CharSequence text, CharSequence url, LinkLabelStyle style) { super(text, style); init(url); } public LinkLabel (CharSequence text, String fontName, Color color) { super(text, new LinkLabelStyle(VisUI.getSkin().getFont(fontName), color, VisUI.getSkin().getDrawable("white"))); init(text); } @Override public LinkLabelStyle getStyle () { return (LinkLabelStyle) super.getStyle(); } private void init (CharSequence linkUrl) { this.url = linkUrl; style = getStyle(); addListener(clickListener = new ClickListener(Buttons.LEFT) { @Override public void clicked (InputEvent event, float x, float y) { super.clicked(event, x, y); if (listener == null) { Gdx.net.openURI(url.toString()); } else { listener.clicked(url.toString()); } } @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { super.enter(event, x, y, pointer, fromActor); Gdx.graphics.setSystemCursor(SystemCursor.Hand); } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { super.exit(event, x, y, pointer, toActor); CursorManager.restoreDefaultCursor(); } }); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); Drawable underline = style.underline; if (underline != null && clickListener.isOver()) { Color color = tempColor.set(getColor()); color.a *= parentAlpha; if (style.fontColor != null) color.mul(style.fontColor); batch.setColor(color); underline.draw(batch, getX(), getY(), getWidth(), 1); } } public CharSequence getUrl () { return url; } public void setUrl (CharSequence url) { this.url = url; } public LinkLabelListener getListener () { return listener; } public void setListener (LinkLabelListener listener) { this.listener = listener; } public interface LinkLabelListener { void clicked (String url); } public static class LinkLabelStyle extends LabelStyle { /** Optional */ public Drawable underline; public LinkLabelStyle () { } public LinkLabelStyle (BitmapFont font, Color fontColor, Drawable underline) { super(font, fontColor); this.underline = underline; } public LinkLabelStyle (LinkLabelStyle style) { super(style); this.underline = style.underline; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/ListView.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.adapter.AbstractListAdapter; import com.kotcrab.vis.ui.util.adapter.ArrayAdapter; import com.kotcrab.vis.ui.util.adapter.ArrayListAdapter; import com.kotcrab.vis.ui.util.adapter.ListAdapter; /** * ListView displays list of scrollable items. Item views are created by using {@link ListAdapter}s. * @author Kotcrab * @see ListAdapter * @see ArrayAdapter * @see ArrayListAdapter * @since 1.0.0 */ public class ListView { private ListAdapter adapter; private ItemClickListener clickListener; private UpdatePolicy updatePolicy = UpdatePolicy.IMMEDIATELY; private boolean dataInvalidated = false; private ListViewTable mainTable; private VisScrollPane scrollPane; private VisTable scrollTable; private VisTable itemsTable; private Actor header; private Actor footer; public ListView (ListAdapter adapter) { this(adapter, "default"); } public ListView (ListAdapter adapter, String styleName) { this(adapter, VisUI.getSkin().get(styleName, ListViewStyle.class)); } public ListView (ListAdapter adapter, ListViewStyle style) { if (style == null) throw new IllegalArgumentException("style can't be null"); if (adapter == null) throw new IllegalArgumentException("adapter can't be null"); this.adapter = adapter; mainTable = new ListViewTable(this); scrollTable = new VisTable(); itemsTable = new VisTable(); scrollPane = new VisScrollPane(scrollTable, style.scrollPaneStyle); scrollPane.setOverscroll(false, true); scrollPane.setFlickScroll(false); scrollPane.setFadeScrollBars(false); mainTable.add(scrollPane).grow(); adapter.setListView(this, new ListAdapterListener()); rebuildView(true); } public void rebuildView () { rebuildView(true); } private void rebuildView (boolean full) { scrollTable.clearChildren(); scrollTable.top(); if (header != null) { scrollTable.add(header).growX(); scrollTable.row(); } if (full) { itemsTable.clearChildren(); adapter.fillTable(itemsTable); } scrollTable.add(itemsTable).growX(); scrollTable.row(); if (footer != null) { scrollTable.add(footer).growX(); scrollTable.row(); } } public ListAdapter getAdapter () { return adapter; } /** @return main table containing scroll pane and all items view */ public ListViewTable getMainTable () { return mainTable; } /** * @return internal {@link VisScrollPane}. Do NOT add this scroll pane directly to {@link Stage} * use {@link #getMainTable()} instead. Use this only for changing scroll pane properties. */ public VisScrollPane getScrollPane () { return scrollPane; } public void setItemClickListener (ItemClickListener listener) { this.clickListener = listener; adapter.setItemClickListener(listener); } public ItemClickListener getClickListener () { return clickListener; } public Actor getHeader () { return header; } public void setHeader (Actor header) { this.header = header; rebuildView(false); } public Actor getFooter () { return footer; } public void setFooter (Actor footer) { this.footer = footer; rebuildView(false); } public void setUpdatePolicy (UpdatePolicy updatePolicy) { this.updatePolicy = updatePolicy; } public UpdatePolicy getUpdatePolicy () { return updatePolicy; } public interface ItemClickListener { void clicked (ItemT item); } public class ListAdapterListener { public void invalidateDataSet () { if (updatePolicy == UpdatePolicy.IMMEDIATELY) rebuildView(true); if (updatePolicy == UpdatePolicy.ON_DRAW) dataInvalidated = true; } } /** Controls when list view's views are updated after underlying data was invalidated. */ public enum UpdatePolicy { /** If list data was was invalidated then views are updated before drawing list. */ ON_DRAW, /** If list data was was invalidated then views are updated immediately after data invalidation. */ IMMEDIATELY, /** * In manual mode ListView must be rebuild manually by calling {@link #rebuildView()}. Notification from adapter * about need to rebuild view will be ignored (see {@link AbstractListAdapter#itemsChanged()}). This mode should * be only used when it's really required to have manual control over how {@link ListView} rebuilds. Note that * ListView should be rebuild before user interacts with it. */ MANUAL } /** ListView main table. */ public static class ListViewTable extends VisTable { private ListView listView; private ListViewTable (ListView listView) { this.listView = listView; } @Override public void draw (Batch batch, float parentAlpha) { if (listView.updatePolicy == UpdatePolicy.ON_DRAW && listView.dataInvalidated) listView.rebuildView(true); super.draw(batch, parentAlpha); } public ListView getListView () { return listView; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/ListViewStyle.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; /** @author Kotcrab */ public class ListViewStyle { public ScrollPane.ScrollPaneStyle scrollPaneStyle; public ListViewStyle () { } public ListViewStyle (ListViewStyle style) { if (style.scrollPaneStyle != null) this.scrollPaneStyle = new ScrollPane.ScrollPaneStyle(style.scrollPaneStyle); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/Menu.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.VisTextButton.VisTextButtonStyle; /** * Menu used in {@link MenuBar}, it is a standard {@link PopupMenu} with tittle displayed in MenuBar. * @author Kotcrab */ public class Menu extends PopupMenu { private MenuBar menuBar; public VisTextButton openButton; public Drawable buttonDefault; private String title; public Menu (String title) { this(title, "default"); } public Menu (String title, String styleName) { this(title, VisUI.getSkin().get(styleName, MenuStyle.class)); } public Menu (String title, MenuStyle style) { super(style); this.title = title; openButton = new VisTextButton(title, new VisTextButtonStyle(style.openButtonStyle)); buttonDefault = openButton.getStyle().up; openButton.addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (menuBar.getCurrentMenu() == Menu.this) { menuBar.closeMenu(); return true; } switchMenu(); event.stop(); return true; } @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { if (menuBar.getCurrentMenu() != null && menuBar.getCurrentMenu() != Menu.this) switchMenu(); } }); } public String getTitle () { return title; } private void switchMenu () { menuBar.closeMenu(); showMenu(); } private void showMenu () { Vector2 pos = openButton.localToStageCoordinates(new Vector2(0, 0)); setPosition(pos.x, pos.y - getHeight()); openButton.getStage().addActor(this); menuBar.setCurrentMenu(this); } @Override public boolean remove () { boolean result = super.remove(); menuBar.setCurrentMenu(null); return result; } /** Called by MenuBar when this menu is added to it */ void setMenuBar (MenuBar menuBar) { if (this.menuBar != null && menuBar != null) throw new IllegalStateException("Menu was already added to MenuBar"); this.menuBar = menuBar; } TextButton getOpenButton () { return openButton; } void selectButton () { openButton.getStyle().up = openButton.getStyle().over; } void deselectButton () { openButton.getStyle().up = buttonDefault; } public static class MenuStyle extends PopupMenuStyle { public VisTextButtonStyle openButtonStyle; public MenuStyle () { } public MenuStyle (MenuStyle style) { super(style); this.openButtonStyle = style.openButtonStyle; } public MenuStyle (Drawable background, Drawable border, VisTextButtonStyle openButtonStyle) { super(background, border); this.openButtonStyle = openButtonStyle; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/MenuBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.VisUI; /** * Bar with expandable menus available after pressing button, usually displayed on top of the stage. * @author Kotcrab */ public class MenuBar { private Table mainTable; private Table menuItems; private Menu currentMenu; private Array

menus = new Array(); private MenuBarListener menuListener; public MenuBar () { this("default"); } public MenuBar (String styleName) { this(VisUI.getSkin().get(styleName, MenuBarStyle.class)); } public MenuBar (MenuBarStyle style) { menuItems = new VisTable(); mainTable = new VisTable() { @Override protected void sizeChanged () { super.sizeChanged(); closeMenu(); } }; mainTable.left(); mainTable.add(menuItems); mainTable.setBackground(style.background); } public void addMenu (Menu menu) { menus.add(menu); menu.setMenuBar(this); menuItems.add(menu.getOpenButton()); } public boolean removeMenu (Menu menu) { boolean removed = menus.removeValue(menu, true); if (removed) { menu.setMenuBar(null); menuItems.removeActor(menu.getOpenButton()); } return removed; } public void insertMenu (int index, Menu menu) { menus.insert(index, menu); menu.setMenuBar(this); rebuild(); } private void rebuild () { menuItems.clear(); for (Menu menu : menus) menuItems.add(menu.getOpenButton()); } /** Closes currently opened menu (if any). Used by framework and typically there is no need to call this manually */ public void closeMenu () { if (currentMenu != null) { currentMenu.deselectButton(); currentMenu.remove(); currentMenu = null; } } Menu getCurrentMenu () { return currentMenu; } void setCurrentMenu (Menu newMenu) { if (currentMenu == newMenu) return; if (currentMenu != null) { currentMenu.deselectButton(); if (menuListener != null) menuListener.menuClosed(currentMenu); } if (newMenu != null) { newMenu.selectButton(); if (menuListener != null) menuListener.menuOpened(newMenu); } currentMenu = newMenu; } public void setMenuListener (MenuBarListener menuListener) { this.menuListener = menuListener; } /** Returns table containing all menus that should be added to Stage, typically with expandX and fillX properties. */ public Table getTable () { return mainTable; } public static class MenuBarStyle { public Drawable background; public MenuBarStyle () { } public MenuBarStyle (MenuBarStyle style) { this.background = style.background; } public MenuBarStyle (Drawable background) { this.background = background; } } public interface MenuBarListener { void menuOpened (Menu menu); void menuClosed (Menu menu); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/MenuItem.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Pools; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.OsUtils; /** * MenuItem displayed in {@link Menu} and {@link PopupMenu}. MenuItem contains text or text with icon. * Best icon size is 22px. MenuItem can also have a hotkey text. *

* When listening for menu item press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling menu item and will still report item clicks. * @author Kotcrab */ public class MenuItem extends Button { private static final Vector2 tmpVector = new Vector2(); //MenuItem is modified version of TextButton private MenuItemStyle style; private Image image; private Cell imageCell; private boolean generateDisabledImage = true; private Label label; private Color shortcutLabelColor; private VisLabel shortcutLabel; private Image subMenuImage; private Cell subMenuIconCell; private PopupMenu subMenu; /** Menu that this item belongs to */ PopupMenu containerMenu; public MenuItem (String text) { this(text, (Image) null, VisUI.getSkin().get(MenuItemStyle.class)); } public MenuItem (String text, String styleName) { this(text, (Image) null, VisUI.getSkin().get(styleName, MenuItemStyle.class)); } public MenuItem (String text, ChangeListener changeListener) { this(text, (Image) null, VisUI.getSkin().get(MenuItemStyle.class)); addListener(changeListener); } public MenuItem (String text, Drawable drawable) { this(text, drawable, VisUI.getSkin().get(MenuItemStyle.class)); } public MenuItem (String text, Drawable drawable, ChangeListener changeListener) { this(text, drawable, VisUI.getSkin().get(MenuItemStyle.class)); addListener(changeListener); } public MenuItem (String text, Drawable drawable, String styleName) { this(text, drawable, VisUI.getSkin().get(styleName, MenuItemStyle.class)); } public MenuItem (String text, Image image) { this(text, image, VisUI.getSkin().get(MenuItemStyle.class)); } public MenuItem (String text, Image image, ChangeListener changeListener) { this(text, image, VisUI.getSkin().get(MenuItemStyle.class)); addListener(changeListener); } public MenuItem (String text, Image image, String styleName) { this(text, image, VisUI.getSkin().get(styleName, MenuItemStyle.class)); } // Base constructors public MenuItem (String text, Image image, MenuItemStyle style) { super(style); init(text, image, style); } public MenuItem (String text, Drawable drawable, MenuItemStyle style) { super(style); init(text, new Image(drawable), style); } private void init (String text, Image image, MenuItemStyle style) { this.style = style; this.image = image; setSkin(VisUI.getSkin()); Sizes sizes = VisUI.getSizes(); defaults().space(3); if (image != null) image.setScaling(Scaling.fit); imageCell = add(image).size(sizes.menuItemIconSize); label = new Label(text, new LabelStyle(style.font, style.fontColor)); label.setAlignment(Align.left); add(label).expand().fill(); add(shortcutLabel = new VisLabel("", "menuitem-shortcut")).padLeft(10).right(); shortcutLabelColor = shortcutLabel.getStyle().fontColor; subMenuIconCell = add(subMenuImage = new Image(style.subMenu)).padLeft(3).padRight(3) .size(style.subMenu.getMinWidth(), style.subMenu.getMinHeight()); subMenuIconCell.setActor(null); addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (subMenu != null) { //makes submenu item not clickable event.stop(); } } }); addListener(new InputListener() { @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { if (subMenu != null) { //removes selection of child submenu if mouse moved to parent submenu subMenu.setActiveItem(null, false); subMenu.setActiveSubMenu(null); } if (subMenu == null || isDisabled()) { //hides last visible submenu (if any) hideSubMenu(); } else { showSubMenu(); } } }); } public void setSubMenu (final PopupMenu subMenu) { this.subMenu = subMenu; if (subMenu == null) { subMenuIconCell.setActor(null); } else { subMenuIconCell.setActor(subMenuImage); } } public PopupMenu getSubMenu () { return subMenu; } void packContainerMenu () { if (containerMenu != null) containerMenu.pack(); } @Override protected void setParent (Group parent) { super.setParent(parent); if (parent instanceof PopupMenu) containerMenu = (PopupMenu) parent; else containerMenu = null; } void hideSubMenu () { if (containerMenu != null) { containerMenu.setActiveSubMenu(null); } } void showSubMenu () { Stage stage = getStage(); Vector2 pos = localToStageCoordinates(tmpVector.setZero()); float availableSpaceLeft = pos.x; float availableSpaceRight = stage.getWidth() - (pos.x + getWidth()); boolean canFitOnTheRight = pos.x + getWidth() + subMenu.getWidth() <= stage.getWidth(); float subMenuX; if (canFitOnTheRight || availableSpaceRight > availableSpaceLeft) { subMenuX = pos.x + getWidth() - 1; } else { subMenuX = pos.x - subMenu.getWidth() + 1; } if (containerMenu.getActiveSubMenu() != subMenu) { boolean hasEnoughBottomSpace = stage.getHeight() - (pos.y + getHeight()) + subMenu.getHeight() <= stage.getHeight(); float heightCorrection = hasEnoughBottomSpace ? getHeight() : 0; subMenu.showMenu(stage, subMenuX, pos.y + heightCorrection); containerMenu.setActiveSubMenu(subMenu); } } void fireChangeEvent () { ChangeListener.ChangeEvent changeEvent = Pools.obtain(ChangeListener.ChangeEvent.class); fire(changeEvent); Pools.free(changeEvent); } @Override public MenuItemStyle getStyle () { return style; } @Override public void setStyle (ButtonStyle style) { if (!(style instanceof MenuItemStyle)) throw new IllegalArgumentException("style must be a MenuItemStyle."); super.setStyle(style); this.style = (MenuItemStyle) style; if (label != null) { TextButtonStyle textButtonStyle = (TextButtonStyle) style; LabelStyle labelStyle = label.getStyle(); labelStyle.font = textButtonStyle.font; labelStyle.fontColor = textButtonStyle.fontColor; label.setStyle(labelStyle); } } @Override public void draw (Batch batch, float parentAlpha) { Color fontColor; if (isDisabled() && style.disabledFontColor != null) fontColor = style.disabledFontColor; else if (isPressed() && style.downFontColor != null) fontColor = style.downFontColor; else if (isChecked() && style.checkedFontColor != null) fontColor = (isOver() && style.checkedOverFontColor != null) ? style.checkedOverFontColor : style.checkedFontColor; else if (isOver() && style.overFontColor != null) fontColor = style.overFontColor; else fontColor = style.fontColor; if (fontColor != null) label.getStyle().fontColor = fontColor; if (isDisabled()) shortcutLabel.getStyle().fontColor = style.disabledFontColor; else shortcutLabel.getStyle().fontColor = shortcutLabelColor; if (generateDisabledImage && image != null) { if (isDisabled()) { image.setColor(Color.GRAY); } else { image.setColor(Color.WHITE); } } super.draw(batch, parentAlpha); } @Override public boolean isOver () { if (containerMenu == null || containerMenu.getActiveItem() == null) { return super.isOver(); } else { return containerMenu.getActiveItem() == this; } } public boolean isGenerateDisabledImage () { return generateDisabledImage; } /** * Changes generateDisabledImage property, when true that function is enabled. When it is enabled and this MenuItem * is disabled then image color will be changed to gray meaning that it is disabled, by default it is enabled. */ public void setGenerateDisabledImage (boolean generateDisabledImage) { this.generateDisabledImage = generateDisabledImage; } /** * Set shortcuts text displayed in this menu item. * This DOES NOT set actual hot key for this menu item, it only makes shortcut text visible in item. * @param keycode from {@link Keys}. */ public MenuItem setShortcut (int keycode) { return setShortcut(Keys.toString(keycode)); } public CharSequence getShortcut () { return shortcutLabel.getText(); } /** * Set shortcuts text displayed in this menu item. This DOES NOT set actual hot key for this menu item, * it only makes shortcut text visible in item. * @param text text that will be displayed * @return this object for the purpose of chaining methods */ public MenuItem setShortcut (String text) { shortcutLabel.setText(text); packContainerMenu(); return this; } /** * Creates platform dependant shortcut text. Converts int keycodes to String text. Eg. Keys.CONTROL_LEFT, * Keys.SHIFT_LEFT, Keys.F5 will be converted to Ctrl+Shift+F5 on Windows and Linux, and to ⌘⇧F5 on Mac. *

* CONTROL_LEFT and CONTROL_RIGHT are mapped to Ctrl. The same goes for Alt (ALT_LEFT, ALT_RIGHT) and Shift (SHIFT_LEFT, SHIFT_RIGHT). *

* This DOES NOT set actual hot key for this menu item, it only makes shortcut text visible in item. * @param keycodes keycodes from {@link Keys} that are used to create shortcut text * @return this object for the purpose of chaining methods */ public MenuItem setShortcut (int... keycodes) { shortcutLabel.setText(OsUtils.getShortcutFor(keycodes)); packContainerMenu(); return this; } @Override protected void setStage (Stage stage) { super.setStage(stage); label.invalidate(); //fixes issue with disappearing menu item after holding right mouse button and dragging down while opening menu } public Image getImage () { return image; } public Cell getImageCell () { return imageCell; } public Label getLabel () { return label; } public Cell getLabelCell () { return getCell(label); } public CharSequence getText () { return label.getText(); } public void setText (CharSequence text) { label.setText(text); } public Cell getSubMenuIconCell () { return subMenuIconCell; } public Cell getShortcutCell () { return getCell(shortcutLabel); } static public class MenuItemStyle extends TextButtonStyle { public Drawable subMenu; public MenuItemStyle () { } public MenuItemStyle (Drawable subMenu) { this.subMenu = subMenu; } public MenuItemStyle (MenuItemStyle style) { super(style); this.subMenu = style.subMenu; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/MultiSplitPane.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.FloatArray; import com.badlogic.gdx.utils.SnapshotArray; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.internal.SplitPaneCursorManager; import java.util.Arrays; /** * Similar to {@link VisSplitPane} but supports multiple widgets with multiple split bars at once. Use {@link #setWidgets(Actor...)} * after creating to set pane widgets. * @author Kotcrab * @see VisSplitPane * @since 1.1.4 */ public class MultiSplitPane extends WidgetGroup { private MultiSplitPaneStyle style; private boolean vertical; private Array widgetBounds = new Array(); private Array scissors = new Array(); private Array handleBounds = new Array(); private FloatArray splits = new FloatArray(); private Vector2 handlePosition = new Vector2(); private Vector2 lastPoint = new Vector2(); private Rectangle handleOver; private int handleOverIndex; public MultiSplitPane (boolean vertical) { this(vertical, "default-" + (vertical ? "vertical" : "horizontal")); } public MultiSplitPane (boolean vertical, String styleName) { this(vertical, VisUI.getSkin().get(styleName, MultiSplitPaneStyle.class)); } public MultiSplitPane (boolean vertical, MultiSplitPaneStyle style) { this.vertical = vertical; setStyle(style); setSize(getPrefWidth(), getPrefHeight()); initialize(); } private void initialize () { addListener(new SplitPaneCursorManager(this, vertical) { @Override protected boolean handleBoundsContains (float x, float y) { return getHandleContaining(x, y) != null; } @Override protected boolean contains (float x, float y) { for (Rectangle bound : widgetBounds) { if (bound.contains(x, y)) return true; } return getHandleContaining(x, y) != null; } }); addListener(new InputListener() { int draggingPointer = -1; @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (isTouchable() == false) return false; if (draggingPointer != -1) return false; if (pointer == 0 && button != 0) return false; Rectangle containingHandle = getHandleContaining(x, y); if (containingHandle != null) { handleOverIndex = handleBounds.indexOf(containingHandle, true); FocusManager.resetFocus(getStage()); draggingPointer = pointer; lastPoint.set(x, y); handlePosition.set(containingHandle.x, containingHandle.y); return true; } return false; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (pointer == draggingPointer) draggingPointer = -1; handleOver = getHandleContaining(x, y); } @Override public boolean mouseMoved (InputEvent event, float x, float y) { handleOver = getHandleContaining(x, y); return false; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { if (pointer != draggingPointer) return; Drawable handle = style.handle; if (!vertical) { float delta = x - lastPoint.x; float availWidth = getWidth() - handle.getMinWidth(); float dragX = handlePosition.x + delta; handlePosition.x = dragX; dragX = Math.max(0, dragX); dragX = Math.min(availWidth, dragX); float targetSplit = dragX / availWidth; setSplit(handleOverIndex, targetSplit); lastPoint.set(x, y); } else { float delta = y - lastPoint.y; float availHeight = getHeight() - handle.getMinHeight(); float dragY = handlePosition.y + delta; handlePosition.y = dragY; dragY = Math.max(0, dragY); dragY = Math.min(availHeight, dragY); float targetSplit = 1 - (dragY / availHeight); setSplit(handleOverIndex, targetSplit); lastPoint.set(x, y); } invalidate(); } }); } private Rectangle getHandleContaining (float x, float y) { for (Rectangle rect : handleBounds) { if (rect.contains(x, y)) { return rect; } } return null; } /** * Returns the split pane's style. Modifying the returned style may not have an effect until {@link #setStyle(MultiSplitPaneStyle)} * is called. */ public MultiSplitPaneStyle getStyle () { return style; } public void setStyle (MultiSplitPaneStyle style) { this.style = style; invalidateHierarchy(); } @Override public void layout () { if (!vertical) calculateHorizBoundsAndPositions(); else calculateVertBoundsAndPositions(); SnapshotArray actors = getChildren(); for (int i = 0; i < actors.size; i++) { Actor actor = actors.get(i); Rectangle bounds = widgetBounds.get(i); actor.setBounds(bounds.x, bounds.y, bounds.width, bounds.height); if (actor instanceof Layout) ((Layout) actor).validate(); } } @Override public float getPrefWidth () { float width = 0; for (Actor actor : getChildren()) { width = actor instanceof Layout ? ((Layout) actor).getPrefWidth() : actor.getWidth(); } if (!vertical) width += handleBounds.size * style.handle.getMinWidth(); return width; } @Override public float getPrefHeight () { float height = 0; for (Actor actor : getChildren()) { height = actor instanceof Layout ? ((Layout) actor).getPrefHeight() : actor.getHeight(); } if (vertical) height += handleBounds.size * style.handle.getMinHeight(); return height; } @Override public float getMinWidth () { return 0; } @Override public float getMinHeight () { return 0; } public void setVertical (boolean vertical) { this.vertical = vertical; } private void calculateHorizBoundsAndPositions () { float height = getHeight(); float width = getWidth(); float handleWidth = style.handle.getMinWidth(); float availWidth = width - (handleBounds.size * handleWidth); float areaUsed = 0; float currentX = 0; for (int i = 0; i < splits.size; i++) { float areaWidthFromLeft = (int) (availWidth * splits.get(i)); float areaWidth = areaWidthFromLeft - areaUsed; areaUsed += areaWidth; widgetBounds.get(i).set(currentX, 0, areaWidth, height); currentX += areaWidth; handleBounds.get(i).set(currentX, 0, handleWidth, height); currentX += handleWidth; } if (widgetBounds.size != 0) widgetBounds.peek().set(currentX, 0, availWidth - areaUsed, height); } private void calculateVertBoundsAndPositions () { float width = getWidth(); float height = getHeight(); float handleHeight = style.handle.getMinHeight(); float availHeight = height - (handleBounds.size * handleHeight); float areaUsed = 0; float currentY = height; for (int i = 0; i < splits.size; i++) { float areaHeightFromTop = (int) (availHeight * splits.get(i)); float areaHeight = areaHeightFromTop - areaUsed; areaUsed += areaHeight; widgetBounds.get(i).set(0, currentY - areaHeight, width, areaHeight); currentY -= areaHeight; handleBounds.get(i).set(0, currentY - handleHeight, width, handleHeight); currentY -= handleHeight; } if (widgetBounds.size != 0) widgetBounds.peek().set(0, 0, width, availHeight - areaUsed); } @Override public void draw (Batch batch, float parentAlpha) { validate(); Color color = getColor(); applyTransform(batch, computeTransform()); SnapshotArray actors = getChildren(); for (int i = 0; i < actors.size; i++) { Actor actor = actors.get(i); Rectangle bounds = widgetBounds.get(i); Rectangle scissor = scissors.get(i); getStage().calculateScissors(bounds, scissor); if (ScissorStack.pushScissors(scissor)) { if (actor.isVisible()) actor.draw(batch, parentAlpha * color.a); batch.flush(); ScissorStack.popScissors(); } } batch.setColor(color.r, color.g, color.b, parentAlpha * color.a); Drawable handle = style.handle; Drawable handleOver = style.handle; if (isTouchable() && style.handleOver != null) handleOver = style.handleOver; for (Rectangle rect : handleBounds) { if (this.handleOver == rect) { handleOver.draw(batch, rect.x, rect.y, rect.width, rect.height); } else { handle.draw(batch, rect.x, rect.y, rect.width, rect.height); } } resetTransform(batch); } @Override public Actor hit (float x, float y, boolean touchable) { if (touchable && getTouchable() == Touchable.disabled) return null; if (getHandleContaining(x, y) != null) { return this; } else { return super.hit(x, y, touchable); } } /** Changes widgets of this split pane. You can pass any number of actors even 1 or 0. Actors can't be null. */ public void setWidgets (Actor... actors) { setWidgets(Arrays.asList(actors)); } /** Changes widgets of this split pane. You can pass any number of actors even 1 or 0. Actors can't be null. */ public void setWidgets (Iterable actors) { clearChildren(); widgetBounds.clear(); scissors.clear(); handleBounds.clear(); splits.clear(); for (Actor actor : actors) { super.addActor(actor); widgetBounds.add(new Rectangle()); scissors.add(new Rectangle()); } float currentSplit = 0; float splitAdvance = 1f / getChildren().size; for (int i = 0; i < getChildren().size - 1; i++) { handleBounds.add(new Rectangle()); currentSplit += splitAdvance; splits.add(currentSplit); } invalidate(); } /** * @param handleBarIndex index of handle bar starting from zero, max index is number of widgets - 1 * @param split new value of split, must be greater than 0 and lesser than 1 and must be smaller and bigger than * previous and next split value. Invalid values will be clamped to closest valid one. */ public void setSplit (int handleBarIndex, float split) { if (handleBarIndex < 0) throw new IllegalStateException("handleBarIndex can't be < 0"); if (handleBarIndex >= splits.size) throw new IllegalStateException("handleBarIndex can't be >= splits size"); float minSplit = handleBarIndex == 0 ? 0 : splits.get(handleBarIndex - 1); float maxSplit = handleBarIndex == splits.size - 1 ? 1 : splits.get(handleBarIndex + 1); split = MathUtils.clamp(split, minSplit, maxSplit); splits.set(handleBarIndex, split); } @Override public void addActorAfter (Actor actorAfter, Actor actor) { throw new UnsupportedOperationException("Use MultiSplitPane#setWidgets"); } @Override public void addActor (Actor actor) { throw new UnsupportedOperationException("Use MultiSplitPane#setWidgets"); } @Override public void addActorAt (int index, Actor actor) { throw new UnsupportedOperationException("Use MultiSplitPane#setWidgets"); } @Override public void addActorBefore (Actor actorBefore, Actor actor) { throw new UnsupportedOperationException("Use MultiSplitPane#setWidgets"); } @Override public boolean removeActor (Actor actor) { throw new UnsupportedOperationException("Use MultiSplitPane#setWidgets"); } public static class MultiSplitPaneStyle extends VisSplitPane.VisSplitPaneStyle { public MultiSplitPaneStyle () { } public MultiSplitPaneStyle (VisSplitPane.VisSplitPaneStyle style) { super(style); } public MultiSplitPaneStyle (Drawable handle, Drawable handleOver) { super(handle, handleOver); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/PopupMenu.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Input; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.SnapshotArray; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.ActorUtils; /** * Standard popup menu that can be displayed anywhere on stage. Menu is automatically removed when user clicked outside menu, * or clicked menu item. For proper behaviour menu should be displayed in touchUp event. If you want to display * menu from touchDown you have to call event.stop() otherwise menu will by immediately closed. *

* If you want to add right click menu to actor you can use getDefaultInputListener() to get premade default listener. *

* Since 1.0.2 arrow keys can be used to navigate menu hierarchy. * @author Kotcrab */ public class PopupMenu extends Table { private static final Vector2 tmpVector = new Vector2(); private Sizes sizes; private PopupMenuStyle style; private PopupMenuListener listener; private InputListener stageListener; private InputListener sharedMenuItemInputListener; private ChangeListener sharedMenuItemChangeListener; private InputListener defaultInputListener; /** The parent sub-menu, that this popup menu belongs to or null if this sub menu is root */ private PopupMenu parentSubMenu; /** The current sub-menu, set by MenuItem */ private PopupMenu activeSubMenu; private MenuItem activeItem; public PopupMenu () { this("default"); } public PopupMenu (String styleName) { this(VisUI.getSkin().get(styleName, PopupMenuStyle.class)); } public PopupMenu (PopupMenuStyle style) { this(VisUI.getSizes(), style); } public PopupMenu (Sizes sizes, PopupMenuStyle style) { this.sizes = sizes; this.style = style; setTouchable(Touchable.enabled); pad(0); setBackground(style.background); createListeners(); } /** * Removes every instance of {@link PopupMenu} form {@link Stage} actors. *

* Generally called from {@link ApplicationListener#resize(int, int)} to remove menus on resize event. */ public static void removeEveryMenu (Stage stage) { for (Actor actor : stage.getActors()) { if (actor instanceof PopupMenu) { PopupMenu menu = (PopupMenu) actor; menu.removeHierarchy(); } } } private void createListeners () { stageListener = new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (getRootMenu().subMenuStructureContains(x, y) == false) { remove(); } return true; } @Override public boolean keyDown (InputEvent event, int keycode) { SnapshotArray children = getChildren(); if (children.size == 0 || activeSubMenu != null) return false; if (keycode == Input.Keys.DOWN) { selectNextItem(); return true; } if (keycode == Input.Keys.UP) { selectPreviousItem(); return true; } if (activeItem == null) return false; if (keycode == Input.Keys.LEFT && activeItem.containerMenu.parentSubMenu != null) { activeItem.containerMenu.parentSubMenu.setActiveSubMenu(null); return true; } if (keycode == Input.Keys.RIGHT && activeItem.getSubMenu() != null) { activeItem.showSubMenu(); activeSubMenu.selectNextItem(); return true; } if (keycode == Input.Keys.ENTER) { activeItem.fireChangeEvent(); return true; } return false; } }; sharedMenuItemInputListener = new InputListener() { @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { if (pointer == -1 && event.getListenerActor() instanceof MenuItem) { MenuItem item = (MenuItem) event.getListenerActor(); if (item.isDisabled() == false) { setActiveItem(item, false); } } } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { if (pointer == -1 && event.getListenerActor() instanceof MenuItem) { if (activeSubMenu != null) return; MenuItem item = (MenuItem) event.getListenerActor(); if (item == activeItem) { setActiveItem(null, false); } } } }; sharedMenuItemChangeListener = new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (event.isStopped() == false) removeHierarchy(); } }; } private PopupMenu getRootMenu () { if (parentSubMenu != null) return parentSubMenu.getRootMenu(); return this; } private boolean subMenuStructureContains (float x, float y) { if (contains(x, y)) return true; if (activeSubMenu != null) return activeSubMenu.subMenuStructureContains(x, y); return false; } private void removeHierarchy () { if (activeItem != null && activeItem.containerMenu != null && activeItem.containerMenu.parentSubMenu != null) { activeItem.containerMenu.parentSubMenu.removeHierarchy(); } remove(); } private void selectNextItem () { SnapshotArray children = getChildren(); if (!hasSelectableMenuItems()) return; int startIndex = activeItem == null ? 0 : children.indexOf(activeItem, true) + 1; for (int i = startIndex; ; i++) { if (i >= children.size) i = 0; Actor actor = children.get(i); if (actor instanceof MenuItem && ((MenuItem) actor).isDisabled() == false) { setActiveItem((MenuItem) actor, true); break; } } } private void selectPreviousItem () { SnapshotArray children = getChildren(); if (!hasSelectableMenuItems()) return; int startIndex = activeItem == null ? children.size - 1 : children.indexOf(activeItem, true) - 1; for (int i = startIndex; ; i--) { if (i <= -1) i = children.size - 1; Actor actor = children.get(i); if (actor instanceof MenuItem && ((MenuItem) actor).isDisabled() == false) { setActiveItem((MenuItem) actor, true); break; } } } private boolean hasSelectableMenuItems () { SnapshotArray children = getChildren(); for (Actor actor : children) { if (actor instanceof MenuItem && ((MenuItem) actor).isDisabled() == false) return true; } return false; } @Override public Cell add (T actor) { if (actor instanceof MenuItem) { throw new IllegalArgumentException("MenuItems can be only added to PopupMenu by using addItem(MenuItem) method"); } return super.add(actor); } public void addItem (MenuItem item) { super.add(item).fillX().expandX().row(); pack(); item.addListener(sharedMenuItemChangeListener); item.addListener(sharedMenuItemInputListener); } public void addSeparator () { add(new Separator("menu")).padTop(2).padBottom(2).fill().expand().row(); } /** * Returns input listener that can be added to scene2d actor. When right mouse button is pressed on that actor, * menu will be displayed */ public InputListener getDefaultInputListener () { return getDefaultInputListener(Buttons.RIGHT); } /** * Returns input listener that can be added to scene2d actor. When mouse button is pressed on that actor, * menu will be displayed * @param mouseButton from {@link Buttons} */ public InputListener getDefaultInputListener (final int mouseButton) { if (defaultInputListener == null) { defaultInputListener = new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (event.getButton() == mouseButton) showMenu(event.getStage(), event.getStageX(), event.getStageY()); } }; } return defaultInputListener; } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); if (style.border != null) style.border.draw(batch, getX(), getY(), getWidth(), getHeight()); } /** * Shows menu as given stage coordinates * @param stage stage instance that this menu is being added to * @param x stage x position * @param y stage y position */ public void showMenu (Stage stage, float x, float y) { setPosition(x, y - getHeight()); if (stage.getHeight() - getY() > stage.getHeight()) setY(getY() + getHeight()); ActorUtils.keepWithinStage(stage, this); stage.addActor(this); } /** * Shows menu below (or above if not enough space) given actor. * @param stage stage instance that this menu is being added to * @param actor used to get calculate menu position in stage, menu will be displayed above or below it */ public void showMenu (Stage stage, Actor actor) { Vector2 pos = actor.localToStageCoordinates(tmpVector.setZero()); float menuY; if (pos.y - getHeight() <= 0) { menuY = pos.y + actor.getHeight() + getHeight() - sizes.borderSize; } else { menuY = pos.y + sizes.borderSize; } showMenu(stage, pos.x, menuY); } public boolean contains (float x, float y) { return getX() < x && getX() + getWidth() > x && getY() < y && getY() + getHeight() > y; } /** Called by framework, when PopupMenu is added to MenuItem as submenu */ void setActiveSubMenu (PopupMenu newSubMenu) { if (activeSubMenu == newSubMenu) return; if (activeSubMenu != null) activeSubMenu.remove(); activeSubMenu = newSubMenu; if (newSubMenu != null) { newSubMenu.setParentMenu(this); } } public PopupMenu getActiveSubMenu () { return activeSubMenu; } @Override protected void setStage (Stage stage) { super.setStage(stage); if (stage != null) stage.addListener(stageListener); } @Override public boolean remove () { if (getStage() != null) getStage().removeListener(stageListener); if (activeSubMenu != null) activeSubMenu.remove(); setActiveItem(null, false); parentSubMenu = null; activeSubMenu = null; return super.remove(); } void setActiveItem (MenuItem newItem, boolean keyboardChange) { activeItem = newItem; if (listener != null) listener.activeItemChanged(newItem, keyboardChange); } public MenuItem getActiveItem () { return activeItem; } void setParentMenu (PopupMenu parentSubMenu) { this.parentSubMenu = parentSubMenu; } public PopupMenuListener getListener () { return listener; } public void setListener (PopupMenuListener listener) { this.listener = listener; } /** * Listener used to get events from {@link PopupMenu}. * @since 1.0.2 */ public interface PopupMenuListener { /** * Called when active menu item (the highlighted one) has changed. This can't be used to listen when * {@link MenuItem} was pressed, add {@link ChangeListener} to {@link MenuItem} directly to achieve this. * @param newActiveItem new item that is now active. May be null. * @param changedByKeyboard whether the change occurred by keyboard (arrows keys) or by mouse. */ void activeItemChanged (MenuItem newActiveItem, boolean changedByKeyboard); } static public class PopupMenuStyle { public Drawable background; public Drawable border; public PopupMenuStyle () { } public PopupMenuStyle (Drawable background, Drawable border) { this.background = background; this.border = border; } public PopupMenuStyle (PopupMenuStyle style) { this.background = style.background; this.border = style.border; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/ScrollableTextArea.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.badlogic.gdx.scenes.scene2d.utils.Cullable; /** * Custom {@link VisTextArea} supporting embedding in scroll pane by calculating required space needed for current text. *

* Warning: By default this can only support vertical scrolling. Scrolling in X direction MUST be disabled. It is NOT possible * to use vertical scrolling without child class properly implementing {@link #getPrefWidth()} and disabling soft wraps. * Example of such class is {@link HighlightTextArea}. *

* For best scroll pane settings you should create scroll pane using {@link #createCompatibleScrollPane()}. * @author Kotcrab * @since 1.1.2 */ public class ScrollableTextArea extends VisTextArea implements Cullable { private Rectangle cullingArea; public ScrollableTextArea (String text) { super(text, "textArea"); } public ScrollableTextArea (String text, String styleName) { super(text, styleName); } public ScrollableTextArea (String text, VisTextFieldStyle style) { super(text, style); } @Override protected InputListener createInputListener () { return new ScrollTextAreaListener(); } @Override protected void setParent (Group parent) { super.setParent(parent); if (parent instanceof ScrollPane) { calculateOffsets(); } } private void updateScrollPosition () { if (cullingArea == null || getParent() instanceof ScrollPane == false) return; ScrollPane scrollPane = (ScrollPane) getParent(); if (cullingArea.contains(getCursorX(), cullingArea.y) == false) { scrollPane.setScrollPercentX(getCursorX() / getWidth()); } if (cullingArea.contains(cullingArea.x, (getHeight() - getCursorY())) == false) { scrollPane.setScrollPercentY(getCursorY() / getHeight()); } } @Override public void setCullingArea (Rectangle cullingArea) { this.cullingArea = cullingArea; } /** * Creates scroll pane for this scrolling text area with best possible default settings. Note that text area * can belong to only one scroll pane, calling this multiple times will break previously created scroll pane. * The scroll pane should be embedded in container with fixed size or optionally grow property. * @return newly created scroll pane which can be added directly to container. */ public ScrollPane createCompatibleScrollPane () { VisScrollPane scrollPane = new VisScrollPane(this); scrollPane.setOverscroll(false, false); scrollPane.setFlickScroll(false); scrollPane.setFadeScrollBars(false); scrollPane.setScrollbarsOnTop(true); scrollPane.setScrollingDisabled(true, false); return scrollPane; } @Override protected void sizeChanged () { super.sizeChanged(); linesShowing = 1000000000; //aka a lot, forces text area not to use its stupid 'scrolling' } @Override public float getPrefHeight () { return getLines() * style.font.getLineHeight(); } @Override public void setText (String str) { super.setText(str); if (programmaticChangeEvents == false) { //changeText WILL NOT be called when programmaticChangeEvents are disabled updateScrollLayout(); } } @Override boolean changeText (String oldText, String newText) { boolean changed = super.changeText(oldText, newText); updateScrollLayout(); return changed; } void updateScrollLayout () { invalidateHierarchy(); layout(); if (getParent() instanceof ScrollPane) ((ScrollPane) getParent()).layout(); updateScrollPosition(); } public class ScrollTextAreaListener extends TextAreaListener { @Override public boolean keyDown (InputEvent event, int keycode) { updateScrollPosition(); return super.keyDown(event, keycode); } @Override public boolean keyTyped (InputEvent event, char character) { updateScrollPosition(); return super.keyTyped(event, character); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/Separator.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.ui.Widget; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.VisUI; /** * A separator widget (horizontal or vertical bar) that can be used in menus, tables or other widgets, typically added * to new row with growX() (if creating horizontal separator) OR growY() (if creating vertical separator) * {@link PopupMenu} and {@link VisTable} provides utilities addSeparator() methods that adds new separator. * @author Kotcrab * @since 0.1.0 */ public class Separator extends Widget { private SeparatorStyle style; /** New separator with default style */ public Separator () { style = VisUI.getSkin().get(SeparatorStyle.class); } public Separator (String styleName) { style = VisUI.getSkin().get(styleName, SeparatorStyle.class); } public Separator (SeparatorStyle style) { this.style = style; } @Override public float getPrefHeight () { return style.thickness; } @Override public float getPrefWidth () { return style.thickness; } @Override public void draw (Batch batch, float parentAlpha) { Color c = getColor(); batch.setColor(c.r, c.g, c.b, c.a * parentAlpha); style.background.draw(batch, getX(), getY(), getWidth(), getHeight()); } public SeparatorStyle getStyle () { return style; } static public class SeparatorStyle { public Drawable background; public int thickness; public SeparatorStyle () { } public SeparatorStyle (SeparatorStyle style) { this.background = style.background; this.thickness = style.thickness; } public SeparatorStyle (Drawable bg, int thickness) { this.background = bg; this.thickness = thickness; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/Tooltip.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.EventListener; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.Timer.Task; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.ActorUtils; /** * Tooltips are widgets that appear below other widget on mouse pointer hover. Each actor can have only one tooltip. *

* libGDX 1.6.4 introduced it's own systems of tooltips. VisUI tooltips existed before that and are unrelated and * incompatible with libGDX tooltips. VisUI tooltips will remain supported. * @author Kotcrab * @since 0.5.0 */ public class Tooltip extends VisTable { public static float DEFAULT_FADE_TIME = 0.3f; public static float DEFAULT_APPEAR_DELAY_TIME = 0.6f; /** * Controls whether to fade out tooltip when mouse was moved. Changing this will not affect already existing tooltips. * @see #setMouseMoveFadeOut(boolean) */ public static boolean MOUSE_MOVED_FADEOUT = false; private Actor target; private Actor content; private Cell contentCell; private boolean mouseMoveFadeOut = MOUSE_MOVED_FADEOUT; private TooltipInputListener listener; private DisplayTask displayTask; private float fadeTime = DEFAULT_FADE_TIME; private float appearDelayTime = DEFAULT_APPEAR_DELAY_TIME; private Tooltip (Builder builder) { super(true); TooltipStyle style = builder.style; if (style == null) style = VisUI.getSkin().get("default", TooltipStyle.class); init(style, builder.target, builder.content); if (builder.width != -1) { contentCell.width(builder.width); pack(); } } public Tooltip () { this("default"); } public Tooltip (String styleName) { super(true); init(VisUI.getSkin().get(styleName, TooltipStyle.class), null, null); } public Tooltip (TooltipStyle style) { super(true); init(style, null, null); } /** * Remove any attached tooltip from target actor * @param target that tooltips will be removed */ public static void removeTooltip (Actor target) { Array listeners = target.getListeners(); for (EventListener listener : listeners) { if (listener instanceof TooltipInputListener) target.removeListener(listener); } } private void init (TooltipStyle style, Actor target, Actor content) { this.target = target; this.content = content; this.listener = new TooltipInputListener(); this.displayTask = new DisplayTask(); setBackground(style.background); contentCell = add(content).padLeft(3).padRight(3).padBottom(2); pack(); if (target != null) attach(); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { toFront(); return true; } @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { if (pointer == -1) { clearActions(); addAction(Actions.sequence(Actions.fadeIn(fadeTime, Interpolation.fade))); } } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { if (pointer == -1) { fadeOut(); } } }); } /** * Attaches tooltip to current target, must be called if tooltip listener was removed from target (for example by * calling target.clearListeners() ) */ public void attach () { if (target == null) return; Array listeners = target.getListeners(); for (EventListener listener : listeners) { if (listener instanceof TooltipInputListener) { throw new IllegalStateException("More than one tooltip cannot be added to the same target!"); } } target.addListener(listener); } /** * Detaches tooltip form current target, does not change tooltip target meaning that this tooltip can be reattached to * same target by calling {@link Tooltip#attach()} */ public void detach () { if (target == null) return; target.removeListener(listener); } /** Sets new target for this tooltip, tooltip will be automatically detached from old target. */ public void setTarget (Actor newTarget) { detach(); target = newTarget; attach(); } public Actor getTarget () { return target; } private void fadeOut () { clearActions(); addAction(Actions.sequence(Actions.fadeOut(fadeTime, Interpolation.fade), Actions.removeActor())); } private VisTable fadeIn () { clearActions(); setColor(1, 1, 1, 0); addAction(Actions.sequence(Actions.fadeIn(fadeTime, Interpolation.fade))); return this; } public Actor getContent () { return content; } public void setContent (Actor content) { this.content = content; contentCell.setActor(content); pack(); } public Cell getContentCell () { return contentCell; } /** * Changes text tooltip to specified text. If tooltip content is not instance of VisLabel then previous tooltip content * will be replaced by VisLabel instance. * @param text next tooltip text */ public void setText (String text) { if (content instanceof VisLabel) { ((VisLabel) content).setText(text); } else { setContent(new VisLabel(text)); } pack(); } @Override public void setPosition (float x, float y) { super.setPosition((int) x, (int) y); } public float getAppearDelayTime () { return appearDelayTime; } public void setAppearDelayTime (float appearDelayTime) { this.appearDelayTime = appearDelayTime; } public float getFadeTime () { return fadeTime; } public void setFadeTime (float fadeTime) { this.fadeTime = fadeTime; } public boolean isMouseMoveFadeOut () { return mouseMoveFadeOut; } /** * @param mouseMoveFadeOut if true tooltip fill fade out when mouse was moved. If false tooltip will only fadeout on * mouse click or when mouse has exited target widget. Default is {@link Tooltip#MOUSE_MOVED_FADEOUT}. */ public void setMouseMoveFadeOut (boolean mouseMoveFadeOut) { this.mouseMoveFadeOut = mouseMoveFadeOut; } private class DisplayTask extends Task { @Override public void run () { if (target.getStage() == null) return; target.getStage().addActor(fadeIn()); ActorUtils.keepWithinStage(getStage(), Tooltip.this); } } private class TooltipInputListener extends InputListener { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { displayTask.cancel(); Tooltip.this.toFront(); fadeOut(); return true; } @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { if (pointer == -1) { Vector2 targetPos = target.localToStageCoordinates(new Vector2()); setX(targetPos.x + (target.getWidth() - getWidth()) / 2); float tooltipY = targetPos.y - getHeight() - 6; float stageHeight = target.getStage().getHeight(); //is there enough space to display above widget? if (stageHeight - tooltipY > stageHeight) setY(targetPos.y + target.getHeight() + 6); //display above widget else setY(tooltipY); //display below displayTask.cancel(); Timer.schedule(displayTask, appearDelayTime); } } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { if (pointer == -1) { displayTask.cancel(); fadeOut(); } } @Override public boolean mouseMoved (InputEvent event, float x, float y) { if (mouseMoveFadeOut && isVisible() && getActions().size == 0) fadeOut(); return false; } } public static class TooltipStyle { public Drawable background; public TooltipStyle () { } public TooltipStyle (TooltipStyle style) { this.background = style.background; } public TooltipStyle (Drawable background) { this.background = background; } } public static class Builder { private final Actor content; private Actor target = null; private TooltipStyle style = null; private float width = -1; public Builder (Actor content) { this.content = content; } public Builder (String text) { this(text, Align.center); } public Builder (String text, int textAlign) { VisLabel label = new VisLabel(text); label.setAlignment(textAlign); this.content = label; } public Builder target (Actor target) { this.target = target; return this; } public Builder style (String styleName) { return style(VisUI.getSkin().get(styleName, TooltipStyle.class)); } public Builder style (TooltipStyle style) { this.style = style; return this; } /** Sets tooltip width. If tooltip content is text only then calling this will automatically enable label wrapping. */ public Builder width (float width) { if (width < 0) throw new IllegalArgumentException("width must be > 0"); this.width = width; if (content instanceof VisLabel) { ((VisLabel) content).setWrap(true); } return this; } public Tooltip build () { return new Tooltip(this); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisCheckBox.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Align; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.BorderOwner; /** * A checkbox is a button that contains an image indicating the checked or unchecked state and a label. * This widget is different than scene2d.ui's {@link CheckBox}. Style supports more checkbox states, focus and error border. * {@link VisCheckBoxStyle} is significantly different than {@link CheckBox.CheckBoxStyle} here background and tick are * stored as separate drawables. Due to scope of changes made this widget is not compatible with {@link CheckBox}. *

* When listening for checkbox press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling checkbox and will still report checkbox presses. * @author Nathan Sweet * @author Kotcrab * @see CheckBox */ public class VisCheckBox extends TextButton implements Focusable, BorderOwner { private VisCheckBoxStyle style; private Image bgImage; private Image tickImage; private Stack imageStack; private Cell imageStackCell; private boolean drawBorder; private boolean stateInvalid; private boolean focusBorderEnabled = true; public VisCheckBox (String text) { this(text, VisUI.getSkin().get(VisCheckBoxStyle.class)); } public VisCheckBox (String text, boolean checked) { this(text, VisUI.getSkin().get(VisCheckBoxStyle.class)); setChecked(checked); } public VisCheckBox (String text, String styleName) { this(text, VisUI.getSkin().get(styleName, VisCheckBoxStyle.class)); } public VisCheckBox (String text, VisCheckBoxStyle style) { super(text, style); clearChildren(); bgImage = new Image(style.checkBackground); tickImage = new Image(style.tick); imageStackCell = add(imageStack = new Stack(bgImage, tickImage)); Label label = getLabel(); add(label).padLeft(5); label.setAlignment(Align.left); setSize(getPrefWidth(), getPrefHeight()); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (isDisabled() == false) FocusManager.switchFocus(getStage(), VisCheckBox.this); return false; } }); } /** * Returns the checkbox's style. Modifying the returned style may not have an effect until {@link #setStyle(ButtonStyle)} is * called. */ @Override public VisCheckBoxStyle getStyle () { return style; } @Override public void setStyle (ButtonStyle style) { if (style instanceof VisCheckBoxStyle == false) throw new IllegalArgumentException("style must be a VisCheckBoxStyle."); super.setStyle(style); this.style = (VisCheckBoxStyle) style; } @Override public void draw (Batch batch, float parentAlpha) { bgImage.setDrawable(getCheckboxBgImage()); tickImage.setDrawable(getCheckboxTickImage()); super.draw(batch, parentAlpha); if (isDisabled() == false && stateInvalid && style.errorBorder != null) { style.errorBorder.draw(batch, getX() + imageStack.getX(), getY() + imageStack.getY(), imageStack.getWidth(), imageStack.getHeight()); } else if (focusBorderEnabled && drawBorder && style.focusBorder != null) { style.focusBorder.draw(batch, getX() + imageStack.getX(), getY() + imageStack.getY(), imageStack.getWidth(), imageStack.getHeight()); } } protected Drawable getCheckboxBgImage () { if (isDisabled()) return style.checkBackground; if (isPressed()) return style.checkBackgroundDown; if (isOver()) return style.checkBackgroundOver; return style.checkBackground; } protected Drawable getCheckboxTickImage () { if (isChecked()) { return isDisabled() ? style.tickDisabled : style.tick; } return null; } public Image getBackgroundImage () { return bgImage; } public Image getTickImage () { return tickImage; } public Stack getImageStack () { return imageStack; } public Cell getImageStackCell () { return imageStackCell; } /** @param stateInvalid if true error border around this checkbox will be drawn. Does not affect any other properties */ public void setStateInvalid (boolean stateInvalid) { this.stateInvalid = stateInvalid; } public boolean setStateInvalid () { return stateInvalid; } @Override public void focusLost () { drawBorder = false; } @Override public void focusGained () { drawBorder = true; } @Override public boolean isFocusBorderEnabled () { return focusBorderEnabled; } @Override public void setFocusBorderEnabled (boolean focusBorderEnabled) { this.focusBorderEnabled = focusBorderEnabled; } static public class VisCheckBoxStyle extends TextButtonStyle { public Drawable focusBorder; public Drawable errorBorder; public Drawable checkBackground; public Drawable checkBackgroundOver; public Drawable checkBackgroundDown; public Drawable tick; public Drawable tickDisabled; public VisCheckBoxStyle () { super(); } public VisCheckBoxStyle (Drawable checkBackground, Drawable tick, BitmapFont font, Color fontColor) { this.checkBackground = checkBackground; this.tick = tick; this.font = font; this.fontColor = fontColor; } public VisCheckBoxStyle (VisCheckBoxStyle style) { super(style); this.focusBorder = style.focusBorder; this.errorBorder = style.errorBorder; this.checkBackground = style.checkBackground; this.checkBackgroundOver = style.checkBackgroundOver; this.checkBackgroundDown = style.checkBackgroundDown; this.tick = style.tick; this.tickDisabled = style.tickDisabled; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisDialog.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.FocusListener; import com.badlogic.gdx.utils.ObjectMap; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.VisTextButton.VisTextButtonStyle; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.sequence; /** * Displays a dialog, which is a modal window containing a content table with a button table underneath it. Methods are provided * to add a label to the content table and buttons to the button table, but any widgets can be added. When a button is clicked, * {@link #result(Object)} is called and the dialog is removed from the stage. *

* Due to scope of changes made this widget is not compatible with standard {@link Dialog}. * @author Nathan Sweet * @author Kotcrab */ public class VisDialog extends VisWindow { Table contentTable, buttonTable; private Skin skin; @SuppressWarnings({"unchecked", "rawtypes"}) ObjectMap values = new ObjectMap(); boolean cancelHide; Actor previousKeyboardFocus, previousScrollFocus; FocusListener focusListener; protected InputListener ignoreTouchDown = new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { event.cancel(); return false; } }; public VisDialog (String title) { super(title); this.skin = VisUI.getSkin(); setSkin(skin); initialize(); } public VisDialog (String title, String windowStyleName) { super(title, VisUI.getSkin().get(windowStyleName, WindowStyle.class)); this.skin = VisUI.getSkin(); setSkin(skin); initialize(); } public VisDialog (String title, WindowStyle windowStyle) { super(title, windowStyle); this.skin = VisUI.getSkin(); setSkin(skin); initialize(); } private void initialize () { setModal(true); getTitleLabel().setAlignment(VisUI.getDefaultTitleAlign()); defaults().space(6); add(contentTable = new Table(skin)).expand().fill(); row(); add(buttonTable = new Table(skin)); contentTable.defaults().space(2).padLeft(3).padRight(3); buttonTable.defaults().space(6).padBottom(3); buttonTable.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (!values.containsKey(actor)) return; while (actor.getParent() != buttonTable) actor = actor.getParent(); result(values.get(actor)); if (!cancelHide) hide(); cancelHide = false; } }); focusListener = new FocusListener() { @Override public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (!focused) focusChanged(event); } @Override public void scrollFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (!focused) focusChanged(event); } private void focusChanged (FocusEvent event) { Stage stage = getStage(); if (isModal() && stage != null && stage.getRoot().getChildren().size > 0 && stage.getRoot().getChildren().peek() == VisDialog.this) { // Dialog is top most actor. Actor newFocusedActor = event.getRelatedActor(); if (newFocusedActor != null && !newFocusedActor.isDescendantOf(VisDialog.this)) event.cancel(); } } }; } @Override protected void setStage (Stage stage) { if (stage == null) addListener(focusListener); else removeListener(focusListener); super.setStage(stage); } public Table getContentTable () { return contentTable; } public Table getButtonsTable () { return buttonTable; } /** Adds a label to the content table. The dialog must have been constructed with a skin to use this method. */ public VisDialog text (String text) { if (skin == null) throw new IllegalStateException("This method may only be used if the dialog was constructed with a Skin."); return text(text, skin.get(LabelStyle.class)); } /** Adds a label to the content table. */ public VisDialog text (String text, LabelStyle labelStyle) { return text(new Label(text, labelStyle)); } /** Adds the given Label to the content table */ public VisDialog text (Label label) { contentTable.add(label); return this; } /** * Adds a text button to the button table. Null will be passed to {@link #result(Object)} if this button is clicked. The dialog * must have been constructed with a skin to use this method. */ public VisDialog button (String text) { return button(text, null); } /** * Adds a text button to the button table. The dialog must have been constructed with a skin to use this method. * @param object The object that will be passed to {@link #result(Object)} if this button is clicked. May be null. */ public VisDialog button (String text, Object object) { if (skin == null) throw new IllegalStateException("This method may only be used if the dialog was constructed with a Skin."); return button(text, object, skin.get(VisTextButtonStyle.class)); } /** * Adds a text button to the button table. * @param object The object that will be passed to {@link #result(Object)} if this button is clicked. May be null. */ public VisDialog button (String text, Object object, VisTextButtonStyle buttonStyle) { return button(new VisTextButton(text, buttonStyle), object); } /** Adds the given button to the button table. */ public VisDialog button (Button button) { return button(button, null); } /** * Adds the given button to the button table. * @param object The object that will be passed to {@link #result(Object)} if this button is clicked. May be null. */ public VisDialog button (Button button, Object object) { buttonTable.add(button); setObject(button, object); return this; } /** {@link #pack() Packs} the dialog and adds it to the stage with custom action which can be null for instant show */ public VisDialog show (Stage stage, Action action) { clearActions(); removeCaptureListener(ignoreTouchDown); previousKeyboardFocus = null; Actor actor = stage.getKeyboardFocus(); if (actor != null && !actor.isDescendantOf(this)) previousKeyboardFocus = actor; previousScrollFocus = null; actor = stage.getScrollFocus(); if (actor != null && !actor.isDescendantOf(this)) previousScrollFocus = actor; pack(); stage.addActor(this); stage.setKeyboardFocus(this); stage.setScrollFocus(this); if (action != null) addAction(action); return this; } /** {@link #pack() Packs} the dialog and adds it to the stage, centered with default fadeIn action */ public VisDialog show (Stage stage) { show(stage, sequence(Actions.alpha(0), Actions.fadeIn(0.4f, Interpolation.fade))); setPosition(Math.round((stage.getWidth() - getWidth()) / 2), Math.round((stage.getHeight() - getHeight()) / 2)); return this; } /** Hides the dialog with the given action and then removes it from the stage. */ public void hide (Action action) { Stage stage = getStage(); if (stage != null) { removeListener(focusListener); if (previousKeyboardFocus != null && previousKeyboardFocus.getStage() == null) previousKeyboardFocus = null; Actor actor = stage.getKeyboardFocus(); if (actor == null || actor.isDescendantOf(this)) stage.setKeyboardFocus(previousKeyboardFocus); if (previousScrollFocus != null && previousScrollFocus.getStage() == null) previousScrollFocus = null; actor = stage.getScrollFocus(); if (actor == null || actor.isDescendantOf(this)) stage.setScrollFocus(previousScrollFocus); } if (action != null) { addCaptureListener(ignoreTouchDown); addAction(sequence(action, Actions.removeListener(ignoreTouchDown, true), Actions.removeActor())); } else remove(); } /** * Hides the dialog. Called automatically when a button is clicked. The default implementation fades out the dialog over 400 * milliseconds and then removes it from the stage. */ public void hide () { hide(sequence(Actions.fadeOut(FADE_TIME, Interpolation.fade), Actions.removeListener(ignoreTouchDown, true), Actions.removeActor())); } public void setObject (Actor actor, Object object) { values.put(actor, object); } /** * If this key is pressed, {@link #result(Object)} is called with the specified object. * @see Keys */ public VisDialog key (final int keycode, final Object object) { addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode2) { if (keycode == keycode2) { result(object); if (!cancelHide) hide(); cancelHide = false; } return false; } }); return this; } /** * Called when a button is clicked. The dialog will be hidden after this method returns unless {@link #cancel()} is called. * @param object The object specified when the button was added. */ protected void result (Object object) { } public void cancel () { cancelHide = true; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisImage.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.NinePatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link Image}. Does not provide additional features. * @author Kotcrab * @see Image */ public class VisImage extends Image { public VisImage () { } public VisImage (NinePatch patch) { super(patch); } public VisImage (TextureRegion region) { super(region); } public VisImage (Texture texture) { super(texture); } public VisImage (String drawableName) { super(VisUI.getSkin(), drawableName); } public VisImage (Skin skin, String drawableName) { super(skin, drawableName); } public VisImage (Drawable drawable) { super(drawable); } public VisImage (Drawable drawable, Scaling scaling) { super(drawable, scaling); } public VisImage (Drawable drawable, Scaling scaling, int align) { super(drawable, scaling, align); } public void setDrawable (Texture texture) { setDrawable(new TextureRegionDrawable(new TextureRegion(texture))); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisImageButton.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.ImageButton; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.BorderOwner; /** * Due to scope of changes made this widget is not compatible with standard {@link ImageButton}. *

* When listening for button press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling button and will still report button presses. * @author Kotcrab * @see ImageButton */ public class VisImageButton extends Button implements Focusable, BorderOwner { private Image image; private VisImageButtonStyle style; private boolean drawBorder; private boolean focusBorderEnabled = true; private boolean generateDisabledImage = false; public VisImageButton (Drawable imageUp) { this(imageUp, null, null); } public VisImageButton (Drawable imageUp, String tooltipText) { this(imageUp, null, null); if (tooltipText != null) new Tooltip.Builder(tooltipText).target(this).build(); } public VisImageButton (Drawable imageUp, Drawable imageDown) { this(imageUp, imageDown, null); } public VisImageButton (Drawable imageUp, Drawable imageDown, Drawable imageChecked) { this(imageUp, imageDown, imageChecked, "default"); } public VisImageButton (Drawable imageUp, Drawable imageDown, Drawable imageChecked, String styleName) { super(new VisImageButtonStyle(VisUI.getSkin().get(styleName, VisImageButtonStyle.class))); style.imageUp = imageUp; style.imageDown = imageDown; style.imageChecked = imageChecked; init(); } public VisImageButton (String styleName) { super(new VisImageButtonStyle(VisUI.getSkin().get(styleName, VisImageButtonStyle.class))); init(); } public VisImageButton (VisImageButtonStyle style) { super(style); init(); } private void init () { image = new Image(); image.setScaling(Scaling.fit); add(image); setSize(getPrefWidth(), getPrefHeight()); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (isDisabled() == false) FocusManager.switchFocus(getStage(), VisImageButton.this); return false; } }); updateImage(); } @Override public VisImageButtonStyle getStyle () { return style; } @Override public void setStyle (ButtonStyle style) { if (!(style instanceof VisImageButtonStyle)) throw new IllegalArgumentException("style must be an ImageButtonStyle."); super.setStyle(style); this.style = (VisImageButtonStyle) style; if (image != null) updateImage(); } private void updateImage () { Drawable drawable = null; if (isDisabled() && style.imageDisabled != null) drawable = style.imageDisabled; else if (isPressed() && style.imageDown != null) drawable = style.imageDown; else if (isChecked() && style.imageChecked != null) drawable = (style.imageCheckedOver != null && isOver()) ? style.imageCheckedOver : style.imageChecked; else if (isOver() && style.imageOver != null) drawable = style.imageOver; else if (style.imageUp != null) drawable = style.imageUp; image.setDrawable(drawable); if (generateDisabledImage && style.imageDisabled == null) { if (isDisabled()) { image.setColor(Color.GRAY); } else { image.setColor(Color.WHITE); } } } @Override public void draw (Batch batch, float parentAlpha) { updateImage(); super.draw(batch, parentAlpha); if (focusBorderEnabled && drawBorder && style.focusBorder != null) style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight()); } public Image getImage () { return image; } public Cell getImageCell () { return getCell(image); } @Override public void setDisabled (boolean disabled) { super.setDisabled(disabled); if (disabled) FocusManager.resetFocus(getStage(), this); } @Override public void focusLost () { drawBorder = false; } @Override public void focusGained () { drawBorder = true; } @Override public boolean isFocusBorderEnabled () { return focusBorderEnabled; } @Override public void setFocusBorderEnabled (boolean focusBorderEnabled) { this.focusBorderEnabled = focusBorderEnabled; } public boolean isGenerateDisabledImage () { return generateDisabledImage; } /** * @param generate when set to true and button state is set to disabled then button image will be tinted with gray * color to better symbolize that button is disabled. This works best for white images. */ public void setGenerateDisabledImage (boolean generate) { generateDisabledImage = generate; } /** * The style for an image button, see {@link ImageButton}. * @author Nathan Sweet */ static public class VisImageButtonStyle extends ButtonStyle { /** Optional. */ public Drawable imageUp, imageDown, imageOver, imageChecked, imageCheckedOver, imageDisabled; public Drawable focusBorder; public VisImageButtonStyle () { } public VisImageButtonStyle (Drawable up, Drawable down, Drawable checked, Drawable imageUp, Drawable imageDown, Drawable imageChecked) { super(up, down, checked); this.imageUp = imageUp; this.imageDown = imageDown; this.imageChecked = imageChecked; } public VisImageButtonStyle (VisImageButtonStyle style) { super(style); this.imageUp = style.imageUp; this.imageDown = style.imageDown; this.imageOver = style.imageOver; this.imageChecked = style.imageChecked; this.imageCheckedOver = style.imageCheckedOver; this.imageDisabled = style.imageDisabled; this.focusBorder = style.focusBorder; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisImageTextButton.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Null; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.BorderOwner; import com.kotcrab.vis.ui.widget.VisTextButton.VisTextButtonStyle; /** * A button with a child {@link Image} and {@link Label}. *

* Due to scope of changes made this widget is not compatible with standard {@link ImageTextButton}. *

* When listening for button press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling button and will still report button presses. * @author Nathan Sweet * @author Kotcrab * @see ImageButton * @see TextButton * @see Button */ public class VisImageTextButton extends Button implements Focusable, BorderOwner { public enum Orientation { TEXT_RIGHT, TEXT_LEFT, TEXT_TOP, TEXT_BOTTOM } private Image image; private Label label; private boolean drawBorder; private boolean focusBorderEnabled = true; private boolean generateDisabledImage = false; private VisImageTextButtonStyle style; private Orientation orientation = Orientation.TEXT_RIGHT; public VisImageTextButton (String text, Drawable imageUp) { this(text, "default", imageUp, null); } public VisImageTextButton (String text, String styleName, Drawable imageUp) { this(text, styleName, imageUp, null); } public VisImageTextButton (String text, String styleName, Drawable imageUp, Drawable imageDown) { super(new VisImageTextButtonStyle(VisUI.getSkin().get(styleName, VisImageTextButtonStyle.class))); style.imageUp = imageUp; style.imageDown = imageDown; init(text); } public VisImageTextButton (String text, String styleName) { super(new VisImageTextButtonStyle(VisUI.getSkin().get(styleName, VisImageTextButtonStyle.class))); init(text); } public VisImageTextButton (String text, VisImageTextButtonStyle style) { super(style); init(text); } private void init (String text) { defaults().space(3); image = new Image(); image.setScaling(Scaling.fit); label = new Label(text, new LabelStyle(style.font, style.fontColor)); label.setAlignment(Align.center); addActorsBasedOnOrientation(); setStyle(style); setSize(getPrefWidth(), getPrefHeight()); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (isDisabled() == false) FocusManager.switchFocus(getStage(), VisImageTextButton.this); return false; } }); } private void addActorsBasedOnOrientation() { switch (orientation) { case TEXT_RIGHT: add(image); add(label); break; case TEXT_LEFT: add(label); add(image); break; case TEXT_TOP: add(label); row(); add(image); break; case TEXT_BOTTOM: add(image); row(); add(label); break; } } @Override public void setStyle (ButtonStyle style) { if (!(style instanceof VisImageTextButtonStyle)) throw new IllegalArgumentException("style must be a VisImageTextButtonStyle."); super.setStyle(style); this.style = (VisImageTextButtonStyle) style; if (image != null) updateImage(); if (label != null) { VisImageTextButtonStyle textButtonStyle = (VisImageTextButtonStyle) style; LabelStyle labelStyle = label.getStyle(); labelStyle.font = textButtonStyle.font; labelStyle.fontColor = textButtonStyle.fontColor; label.setStyle(labelStyle); } } @Override public VisImageTextButtonStyle getStyle () { return style; } private void updateImage () { Drawable drawable = null; if (isDisabled() && style.imageDisabled != null) drawable = style.imageDisabled; else if (isPressed() && style.imageDown != null) drawable = style.imageDown; else if (isChecked() && style.imageChecked != null) drawable = (style.imageCheckedOver != null && isOver()) ? style.imageCheckedOver : style.imageChecked; else if (isOver() && style.imageOver != null) drawable = style.imageOver; else if (style.imageUp != null) drawable = style.imageUp; image.setDrawable(drawable); if (generateDisabledImage && style.imageDisabled == null) { if (isDisabled()) { image.setColor(Color.GRAY); } else { image.setColor(Color.WHITE); } } } /** Returns the appropriate label font color from the style based on the current button state. */ protected @Null Color getFontColor () { if (isDisabled() && style.disabledFontColor != null) return style.disabledFontColor; if (isPressed()) { if (isChecked() && style.checkedDownFontColor != null) return style.checkedDownFontColor; if (style.downFontColor != null) return style.downFontColor; } if (isOver()) { if (isChecked()) { if (style.checkedOverFontColor != null) return style.checkedOverFontColor; } else { if (style.overFontColor != null) return style.overFontColor; } } boolean focused = hasKeyboardFocus(); if (isChecked()) { if (focused && style.checkedFocusedFontColor != null) return style.checkedFocusedFontColor; if (style.checkedFontColor != null) return style.checkedFontColor; if (isOver() && style.overFontColor != null) return style.overFontColor; } if (focused && style.focusedFontColor != null) return style.focusedFontColor; return style.fontColor; } @Override public void draw (Batch batch, float parentAlpha) { updateImage(); Color fontColor = getFontColor(); if (fontColor != null) label.getStyle().fontColor = fontColor; super.draw(batch, parentAlpha); if (focusBorderEnabled && drawBorder && style.focusBorder != null) style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight()); } public Orientation getOrientation() { return orientation; } public void setOrientation(Orientation orientation) { this.orientation = orientation; clearChildren(); addActorsBasedOnOrientation(); } public Image getImage () { return image; } public Cell getImageCell () { return getCell(image); } public Label getLabel () { return label; } public Cell getLabelCell () { return getCell(label); } public void setText (CharSequence text) { label.setText(text); } public CharSequence getText () { return label.getText(); } public String toString () { return super.toString() + ": " + label.getText(); } @Override public void setDisabled (boolean disabled) { super.setDisabled(disabled); if (disabled) FocusManager.resetFocus(getStage(), this); } @Override public void focusLost () { drawBorder = false; } @Override public void focusGained () { drawBorder = true; } @Override public boolean isFocusBorderEnabled () { return focusBorderEnabled; } @Override public void setFocusBorderEnabled (boolean focusBorderEnabled) { this.focusBorderEnabled = focusBorderEnabled; } public boolean isGenerateDisabledImage () { return generateDisabledImage; } /** * @param generate when set to true and button state is set to disabled then button image will be tinted with gray * color to better symbolize that button is disabled. This works best for white images. */ public void setGenerateDisabledImage (boolean generate) { generateDisabledImage = generate; } /** * The style for an image text button, see {@link ImageTextButton}. * @author Nathan Sweet */ static public class VisImageTextButtonStyle extends VisTextButtonStyle { /** Optional. */ public Drawable imageUp, imageDown, imageOver, imageChecked, imageCheckedOver, imageDisabled; public VisImageTextButtonStyle () { } public VisImageTextButtonStyle (Drawable up, Drawable down, Drawable checked, BitmapFont font) { super(up, down, checked, font); } public VisImageTextButtonStyle (VisImageTextButtonStyle style) { super(style); if (style.imageUp != null) this.imageUp = style.imageUp; if (style.imageDown != null) this.imageDown = style.imageDown; if (style.imageOver != null) this.imageOver = style.imageOver; if (style.imageChecked != null) this.imageChecked = style.imageChecked; if (style.imageCheckedOver != null) this.imageCheckedOver = style.imageCheckedOver; if (style.imageDisabled != null) this.imageDisabled = style.imageDisabled; } public VisImageTextButtonStyle (VisTextButtonStyle style) { super(style); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisLabel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link Label}. Does not provide additional features. * @author Kotcrab * @see Label */ public class VisLabel extends Label { public VisLabel () { super("", VisUI.getSkin()); } public VisLabel (CharSequence text, Color textColor) { super(text, VisUI.getSkin()); setColor(textColor); } public VisLabel (CharSequence text, int alignment) { this(text); setAlignment(alignment); } public VisLabel (CharSequence text) { super(text, VisUI.getSkin()); } public VisLabel (CharSequence text, LabelStyle style) { super(text, style); } public VisLabel (CharSequence text, String styleName) { super(text, VisUI.getSkin(), styleName); } public VisLabel (CharSequence text, String fontName, Color color) { super(text, VisUI.getSkin(), fontName, color); } public VisLabel (CharSequence text, String fontName, String colorName) { super(text, VisUI.getSkin(), fontName, colorName); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisList.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.List; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link List}. Does not provide additional features however for proper VisUI focus management List * should be always preferred. * @author Kotcrab * @see List */ public class VisList extends List { public VisList () { super(VisUI.getSkin()); init(); } public VisList (String styleName) { super(VisUI.getSkin(), styleName); init(); } public VisList (ListStyle style) { super(style); init(); } private void init () { addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { FocusManager.resetFocus(getStage()); return false; } }); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisProgressBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.ProgressBar; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link ProgressBar}. Does not provide additional features. * @author Kotcrab * @see ProgressBar */ public class VisProgressBar extends ProgressBar { public VisProgressBar (float min, float max, float stepSize, boolean vertical) { this(min, max, stepSize, vertical, VisUI.getSkin().get("default-" + (vertical ? "vertical" : "horizontal"), ProgressBarStyle.class)); } public VisProgressBar (float min, float max, float stepSize, boolean vertical, String styleName) { this(min, max, stepSize, vertical, VisUI.getSkin().get(styleName, ProgressBarStyle.class)); } public VisProgressBar (float min, float max, float stepSize, boolean vertical, ProgressBarStyle style) { super(min, max, stepSize, vertical, style); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisRadioButton.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.VisUI; /** * Similar to {@link VisCheckBox} however uses round (instead of square) button {@link Drawable}. Note that if you * want to achieve 'select only one option' behaviour you need to use {@link ButtonGroup}. *

* When listening for button press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling button and will still report button presses. * @author Kotcrab * @see VisCheckBox */ public class VisRadioButton extends VisCheckBox { public VisRadioButton (String text) { this(text, VisUI.getSkin().get("radio", VisCheckBoxStyle.class)); } public VisRadioButton (String text, VisCheckBoxStyle style) { super(text, style); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisScrollPane.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link ScrollPane}. Does not provide additional features. * @author Kotcrab * @see ScrollPane */ public class VisScrollPane extends ScrollPane { public VisScrollPane (Actor widget, ScrollPaneStyle style) { super(widget, style); } public VisScrollPane (Actor widget, String styleName) { super(widget, VisUI.getSkin(), styleName); } public VisScrollPane (Actor widget) { super(widget, VisUI.getSkin(), "list"); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisSelectBox.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.SelectBox; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link SelectBox}. Does not provide additional features however for proper VisUI focus management VisSelectBox * should be always preferred. * @author Kotcrab * @see SelectBox */ public class VisSelectBox extends SelectBox { public VisSelectBox (SelectBoxStyle style) { super(style); init(); } public VisSelectBox (String styleName) { super(VisUI.getSkin(), styleName); init(); } public VisSelectBox () { super(VisUI.getSkin()); init(); } private void init () { addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { FocusManager.resetFocus(getStage()); return false; } }); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisSlider.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.Slider; import com.kotcrab.vis.ui.VisUI; /** * Compatible with {@link Slider}. Does not provide additional features. * @author Kotcrab * @see Slider */ public class VisSlider extends Slider { public VisSlider (float min, float max, float stepSize, boolean vertical) { super(min, max, stepSize, vertical, VisUI.getSkin()); } public VisSlider (float min, float max, float stepSize, boolean vertical, String styleName) { super(min, max, stepSize, vertical, VisUI.getSkin(), styleName); } public VisSlider (float min, float max, float stepSize, boolean vertical, SliderStyle style) { super(min, max, stepSize, vertical, style); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisSplitPane.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.SplitPane; import com.badlogic.gdx.scenes.scene2d.ui.SplitPane.SplitPaneStyle; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; import com.badlogic.gdx.utils.GdxRuntimeException; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.internal.SplitPaneCursorManager; /** * Extends functionality of standard {@link SplitPane}. Style supports handle over {@link Drawable}. Due to scope of * changes made this widget is not compatible with {@link SplitPane}. * @author mzechner * @author Nathan Sweet * @author Kotcrab * @see SplitPane */ public class VisSplitPane extends WidgetGroup { VisSplitPaneStyle style; private Actor firstWidget, secondWidget; boolean vertical; float splitAmount = 0.5f, minAmount, maxAmount = 1; // private float oldSplitAmount; private Rectangle firstWidgetBounds = new Rectangle(); private Rectangle secondWidgetBounds = new Rectangle(); Rectangle handleBounds = new Rectangle(); private Rectangle firstScissors = new Rectangle(); private Rectangle secondScissors = new Rectangle(); Vector2 lastPoint = new Vector2(); Vector2 handlePosition = new Vector2(); private boolean mouseOnHandle; /** * @param firstWidget May be null. * @param secondWidget May be null. */ public VisSplitPane (Actor firstWidget, Actor secondWidget, boolean vertical) { this(firstWidget, secondWidget, vertical, "default-" + (vertical ? "vertical" : "horizontal")); } /** * @param firstWidget May be null. * @param secondWidget May be null. */ public VisSplitPane (Actor firstWidget, Actor secondWidget, boolean vertical, String styleName) { this(firstWidget, secondWidget, vertical, VisUI.getSkin().get(styleName, VisSplitPaneStyle.class)); } /** * @param firstWidget May be null. * @param secondWidget May be null. */ public VisSplitPane (Actor firstWidget, Actor secondWidget, boolean vertical, VisSplitPaneStyle style) { this.firstWidget = firstWidget; this.secondWidget = secondWidget; this.vertical = vertical; setStyle(style); setFirstWidget(firstWidget); setSecondWidget(secondWidget); setSize(getPrefWidth(), getPrefHeight()); initialize(); } private void initialize () { addListener(new SplitPaneCursorManager(this, vertical) { @Override protected boolean handleBoundsContains (float x, float y) { return handleBounds.contains(x, y); } @Override protected boolean contains (float x, float y) { return firstWidgetBounds.contains(x, y) || secondWidgetBounds.contains(x, y) || handleBounds.contains(x, y); } }); addListener(new InputListener() { int draggingPointer = -1; @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { //TODO potential bug with libgdx scene2d? //fixes issue when split bar could be still dragged even when touchable is set to childrenOnly, probably scene2d issue if (isTouchable() == false) return false; if (draggingPointer != -1) return false; if (pointer == 0 && button != 0) return false; if (handleBounds.contains(x, y)) { FocusManager.resetFocus(getStage()); draggingPointer = pointer; lastPoint.set(x, y); handlePosition.set(handleBounds.x, handleBounds.y); return true; } return false; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (pointer == draggingPointer) draggingPointer = -1; } @Override public boolean mouseMoved (InputEvent event, float x, float y) { mouseOnHandle = handleBounds.contains(x, y); return false; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { if (pointer != draggingPointer) return; Drawable handle = style.handle; if (!vertical) { float delta = x - lastPoint.x; float availWidth = getWidth() - handle.getMinWidth(); float dragX = handlePosition.x + delta; handlePosition.x = dragX; dragX = Math.max(0, dragX); dragX = Math.min(availWidth, dragX); splitAmount = dragX / availWidth; if (splitAmount < minAmount) splitAmount = minAmount; if (splitAmount > maxAmount) splitAmount = maxAmount; lastPoint.set(x, y); } else { float delta = y - lastPoint.y; float availHeight = getHeight() - handle.getMinHeight(); float dragY = handlePosition.y + delta; handlePosition.y = dragY; dragY = Math.max(0, dragY); dragY = Math.min(availHeight, dragY); splitAmount = 1 - (dragY / availHeight); if (splitAmount < minAmount) splitAmount = minAmount; if (splitAmount > maxAmount) splitAmount = maxAmount; lastPoint.set(x, y); } invalidate(); } }); } /** * Returns the split pane's style. Modifying the returned style may not have an effect until {@link #setStyle(VisSplitPaneStyle)} * is called. */ public VisSplitPaneStyle getStyle () { return style; } public void setStyle (VisSplitPaneStyle style) { this.style = style; invalidateHierarchy(); } @Override public void layout () { if (!vertical) calculateHorizBoundsAndPositions(); else calculateVertBoundsAndPositions(); Actor firstWidget = this.firstWidget; if (firstWidget != null) { Rectangle firstWidgetBounds = this.firstWidgetBounds; firstWidget.setBounds(firstWidgetBounds.x, firstWidgetBounds.y, firstWidgetBounds.width, firstWidgetBounds.height); if (firstWidget instanceof Layout) ((Layout) firstWidget).validate(); } Actor secondWidget = this.secondWidget; if (secondWidget != null) { Rectangle secondWidgetBounds = this.secondWidgetBounds; secondWidget.setBounds(secondWidgetBounds.x, secondWidgetBounds.y, secondWidgetBounds.width, secondWidgetBounds.height); if (secondWidget instanceof Layout) ((Layout) secondWidget).validate(); } } @Override public float getPrefWidth () { float width = 0; if (firstWidget != null) width = firstWidget instanceof Layout ? ((Layout) firstWidget).getPrefWidth() : firstWidget.getWidth(); if (secondWidget != null) width += secondWidget instanceof Layout ? ((Layout) secondWidget).getPrefWidth() : secondWidget.getWidth(); if (!vertical) width += style.handle.getMinWidth(); return width; } @Override public float getPrefHeight () { float height = 0; if (firstWidget != null) height = firstWidget instanceof Layout ? ((Layout) firstWidget).getPrefHeight() : firstWidget.getHeight(); if (secondWidget != null) height += secondWidget instanceof Layout ? ((Layout) secondWidget).getPrefHeight() : secondWidget.getHeight(); if (vertical) height += style.handle.getMinHeight(); return height; } @Override public float getMinWidth () { return 0; } @Override public float getMinHeight () { return 0; } /** @return first widgets bounds, changing returned rectangle values does not have any effect */ public Rectangle getFirstWidgetBounds () { return new Rectangle(firstWidgetBounds); } /** @return seconds widgets bounds, changing returned rectangle values does not have any effect */ public Rectangle getSecondWidgetBounds () { return new Rectangle(secondWidgetBounds); } public void setVertical (boolean vertical) { this.vertical = vertical; } private void calculateHorizBoundsAndPositions () { Drawable handle = style.handle; float height = getHeight(); float availWidth = getWidth() - handle.getMinWidth(); float leftAreaWidth = (int) (availWidth * splitAmount); float rightAreaWidth = availWidth - leftAreaWidth; float handleWidth = handle.getMinWidth(); firstWidgetBounds.set(0, 0, leftAreaWidth, height); secondWidgetBounds.set(leftAreaWidth + handleWidth, 0, rightAreaWidth, height); handleBounds.set(leftAreaWidth, 0, handleWidth, height); } private void calculateVertBoundsAndPositions () { Drawable handle = style.handle; float width = getWidth(); float height = getHeight(); float availHeight = height - handle.getMinHeight(); float topAreaHeight = (int) (availHeight * splitAmount); float bottomAreaHeight = availHeight - topAreaHeight; float handleHeight = handle.getMinHeight(); firstWidgetBounds.set(0, height - topAreaHeight, width, topAreaHeight); secondWidgetBounds.set(0, 0, width, bottomAreaHeight); handleBounds.set(0, bottomAreaHeight, width, handleHeight); } @Override public void draw (Batch batch, float parentAlpha) { validate(); Color color = getColor(); applyTransform(batch, computeTransform()); // Matrix4 transform = batch.getTransformMatrix(); if (firstWidget != null) { getStage().calculateScissors(firstWidgetBounds, firstScissors); if (ScissorStack.pushScissors(firstScissors)) { if (firstWidget.isVisible()) firstWidget.draw(batch, parentAlpha * color.a); batch.flush(); ScissorStack.popScissors(); } } if (secondWidget != null) { getStage().calculateScissors(secondWidgetBounds, secondScissors); if (ScissorStack.pushScissors(secondScissors)) { if (secondWidget.isVisible()) secondWidget.draw(batch, parentAlpha * color.a); batch.flush(); ScissorStack.popScissors(); } } Drawable handle = style.handle; if (mouseOnHandle && isTouchable() && style.handleOver != null) handle = style.handleOver; batch.setColor(color.r, color.g, color.b, parentAlpha * color.a); handle.draw(batch, handleBounds.x, handleBounds.y, handleBounds.width, handleBounds.height); resetTransform(batch); } @Override public Actor hit (float x, float y, boolean touchable) { if (touchable && getTouchable() == Touchable.disabled) return null; if (handleBounds.contains(x, y)) { return this; } else { return super.hit(x, y, touchable); } } /** @param split The split amount between the min and max amount. */ public void setSplitAmount (float split) { this.splitAmount = Math.max(Math.min(maxAmount, split), minAmount); invalidate(); } public float getSplit () { return splitAmount; } public void setMinSplitAmount (float minAmount) { if (minAmount < 0) throw new GdxRuntimeException("minAmount has to be >= 0"); if (minAmount >= maxAmount) throw new GdxRuntimeException("minAmount has to be < maxAmount"); this.minAmount = minAmount; } public void setMaxSplitAmount (float maxAmount) { if (maxAmount > 1) throw new GdxRuntimeException("maxAmount has to be >= 0"); if (maxAmount <= minAmount) throw new GdxRuntimeException("maxAmount has to be > minAmount"); this.maxAmount = maxAmount; } /** * @param firstWidget May be null * @param secondWidget May be null */ public void setWidgets (Actor firstWidget, Actor secondWidget) { setFirstWidget(firstWidget); setSecondWidget(secondWidget); } /** @param widget May be null. */ public void setFirstWidget (Actor widget) { if (firstWidget != null) super.removeActor(firstWidget); firstWidget = widget; if (widget != null) super.addActor(widget); invalidate(); } /** @param widget May be null. */ public void setSecondWidget (Actor widget) { if (secondWidget != null) super.removeActor(secondWidget); secondWidget = widget; if (widget != null) super.addActor(widget); invalidate(); } @Override public void addActor (Actor actor) { throw new UnsupportedOperationException("Use ScrollPane#setWidget."); } @Override public void addActorAt (int index, Actor actor) { throw new UnsupportedOperationException("Use ScrollPane#setWidget."); } @Override public void addActorBefore (Actor actorBefore, Actor actor) { throw new UnsupportedOperationException("Use ScrollPane#setWidget."); } @Override public boolean removeActor (Actor actor) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); if (actor == firstWidget) { setFirstWidget(null); return true; } if (actor == secondWidget) { setSecondWidget(null); return true; } return true; } @Override public boolean removeActor (Actor actor, boolean unfocus) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); if (actor == firstWidget) { super.removeActor(actor, unfocus); firstWidget = null; invalidate(); return true; } if (actor == secondWidget) { super.removeActor(actor, unfocus); secondWidget = null; invalidate(); return true; } return false; } public static class VisSplitPaneStyle extends SplitPaneStyle { /** Optional **/ public Drawable handleOver; public VisSplitPaneStyle () { } public VisSplitPaneStyle (VisSplitPaneStyle style) { super(style); this.handleOver = style.handleOver; } public VisSplitPaneStyle (Drawable handle, Drawable handleOver) { super(handle); this.handleOver = handleOver; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisTable.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.TableUtils; /** * Extends functionality of standard {@link Table}, supports setting default VisUI spacing and has utilities method for adding * separators. Compatible with {@link Table}. * @author Kotcrab * @see Table */ public class VisTable extends Table { public VisTable () { super(VisUI.getSkin()); } /** @param setVisDefaults if true default vis spacing defaults will be set */ public VisTable (boolean setVisDefaults) { super(VisUI.getSkin()); if (setVisDefaults) TableUtils.setSpacingDefaults(this); } /** * Adds vertical or horizontal {@link Separator} widget to table with padding top, bottom 2px with fill and expand properties. * If vertical == false then inserts new row after separator (not before!) */ public Cell addSeparator (boolean vertical) { Cell cell = add(new Separator(vertical ? "vertical" : "default")).padTop(2).padBottom(2); if (vertical) cell.fillY().expandY(); else { cell.fillX().expandX(); row(); } return cell; } /** * Adds horizontal {@link Separator} widget to table with padding top, bottom 2px with fillX and expandX properties and inserts new row * after separator (not before!) */ public Cell addSeparator () { return addSeparator(false); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisTextArea.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.GlyphLayout; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.ui.TextArea; import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pools; /** * A multiple-line text input field, entirely based on {@link TextField}. * @author Kotcrab (few modifications, orginal {@link TextArea} is missing authors) * @see TextArea */ public class VisTextArea extends VisTextField { /** Array storing lines breaks positions * */ IntArray linesBreak; /** Last text processed. This attribute is used to avoid unnecessary computations while calculating offsets * */ private String lastText; /** Current line for the cursor * */ int cursorLine; /** Index of the first line showed by the text area * */ int firstLineShowing; /** Number of lines showed by the text area * */ int linesShowing; /** Variable to maintain the x offset of the cursor when moving up and down. If it's set to -1, the offset is reset * */ float moveOffset; private float prefRows; /** * Allows to disable, enable disabling softwrapping. Note this isn't exposed property because TextArea can't handle it's by default. * You must have text area which can calculate its max width such as {@link HighlightTextArea} */ boolean softwrap = true; float cursorX; public VisTextArea () { super(); } public VisTextArea (String text, String styleName) { super(text, styleName); } public VisTextArea (String text, VisTextFieldStyle style) { super(text, style); } public VisTextArea (String text) { super(text); } @Override protected void initialize () { super.initialize(); writeEnters = true; linesBreak = new IntArray(); cursorLine = 0; firstLineShowing = 0; moveOffset = -1; linesShowing = 0; } @Override protected int letterUnderCursor (float x) { if (linesBreak.size > 0) { if (cursorLine * 2 >= linesBreak.size) { return text.length(); } else { float[] glyphPositions = this.glyphPositions.items; int start = linesBreak.items[cursorLine * 2]; x += glyphPositions[start]; int end = linesBreak.items[cursorLine * 2 + 1]; int i = start; for (; i < end; i++) if (glyphPositions[i] > x) break; if (i > 0 && glyphPositions[i] - x <= x - glyphPositions[i - 1]) { return Math.min(i, text.length()); } return Math.max(0, i - 1); } } else { return 0; } } /** Sets the preferred number of rows (lines) for this text area. Used to calculate preferred height */ public void setPrefRows (float prefRows) { this.prefRows = prefRows; } @Override public float getPrefHeight () { if (prefRows <= 0) { return super.getPrefHeight(); } else { float prefHeight = textHeight * prefRows; if (style.background != null) { prefHeight = Math.max(prefHeight + style.background.getBottomHeight() + style.background.getTopHeight(), style.background.getMinHeight()); } return prefHeight; } } /** Returns total number of lines that the text occupies * */ public int getLines () { return linesBreak.size / 2 + (newLineAtEnd() ? 1 : 0); } /** Returns if there's a new line at then end of the text * */ public boolean newLineAtEnd () { return text.length() != 0 && (text.charAt(text.length() - 1) == ENTER_ANDROID || text.charAt(text.length() - 1) == ENTER_DESKTOP); } /** Moves the cursor to the given number line * */ public void moveCursorLine (int line) { if (line < 0) { cursorLine = 0; cursor = 0; moveOffset = -1; } else if (line >= getLines()) { int newLine = getLines() - 1; cursor = text.length(); if (line > getLines() || newLine == cursorLine) { moveOffset = -1; } cursorLine = newLine; } else if (line != cursorLine) { if (moveOffset < 0) { moveOffset = linesBreak.size <= cursorLine * 2 ? 0 : glyphPositions.get(cursor) - glyphPositions.get(linesBreak.get(cursorLine * 2)); } cursorLine = line; cursor = cursorLine * 2 >= linesBreak.size ? text.length() : linesBreak.get(cursorLine * 2); while (cursor < text.length() && cursor <= linesBreak.get(cursorLine * 2 + 1) - 1 && glyphPositions.get(cursor) - glyphPositions.get(linesBreak.get(cursorLine * 2)) < moveOffset) { cursor++; } showCursor(); } } /** Updates the current line, checking the cursor position in the text * */ void updateCurrentLine () { int index = calculateCurrentLineIndex(cursor); int line = index / 2; // Special case when cursor moves to the beginning of the line from the end of another and a word // wider than the box if (index % 2 == 0 || index + 1 >= linesBreak.size || cursor != linesBreak.items[index] || linesBreak.items[index + 1] != linesBreak.items[index]) { if (line < linesBreak.size / 2 || text.length() == 0 || text.charAt(text.length() - 1) == ENTER_ANDROID || text.charAt(text.length() - 1) == ENTER_DESKTOP) { cursorLine = line; } } } /** Scroll the text area to show the line of the cursor * */ void showCursor () { updateCurrentLine(); if (cursorLine != firstLineShowing) { int step = cursorLine >= firstLineShowing ? 1 : -1; while (firstLineShowing > cursorLine || firstLineShowing + linesShowing - 1 < cursorLine) { firstLineShowing += step; } } } /** Calculates the text area line for the given cursor position * */ private int calculateCurrentLineIndex (int cursor) { int index = 0; while (index < linesBreak.size && cursor > linesBreak.items[index]) { index++; } return index; } // OVERRIDE from TextField @Override protected void sizeChanged () { lastText = null; // Cause calculateOffsets to recalculate the line breaks. // The number of lines showed must be updated whenever the height is updated BitmapFont font = style.font; Drawable background = style.background; float availableHeight = getHeight() - (background == null ? 0 : background.getBottomHeight() + background.getTopHeight()); linesShowing = (int) Math.floor(availableHeight / font.getLineHeight()); } @Override protected float getTextY (BitmapFont font, Drawable background) { float textY = getHeight(); if (background != null) { textY = (int) (textY - background.getTopHeight()); } return textY; } @Override protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) { int i = firstLineShowing * 2; float offsetY = 0; int minIndex = Math.min(cursor, selectionStart); int maxIndex = Math.max(cursor, selectionStart); while (i + 1 < linesBreak.size && i < (firstLineShowing + linesShowing) * 2) { int lineStart = linesBreak.get(i); int lineEnd = linesBreak.get(i + 1); if (!((minIndex < lineStart && minIndex < lineEnd && maxIndex < lineStart && maxIndex < lineEnd) || (minIndex > lineStart && minIndex > lineEnd && maxIndex > lineStart && maxIndex > lineEnd))) { int start = Math.max(linesBreak.get(i), minIndex); int end = Math.min(linesBreak.get(i + 1), maxIndex); float selectionX = glyphPositions.get(start) - glyphPositions.get(linesBreak.get(i)); float selectionWidth = glyphPositions.get(end) - glyphPositions.get(start); selection.draw(batch, x + selectionX + fontOffset, y - textHeight - font.getDescent() - offsetY, selectionWidth, font.getLineHeight()); } offsetY += font.getLineHeight(); i += 2; } } @Override protected void drawText (Batch batch, BitmapFont font, float x, float y) { float offsetY = 0; for (int i = firstLineShowing * 2; i < (firstLineShowing + linesShowing) * 2 && i < linesBreak.size; i += 2) { font.draw(batch, displayText, x, y + offsetY, linesBreak.items[i], linesBreak.items[i + 1], 0, Align.left, false); offsetY -= font.getLineHeight(); } } @Override protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) { float textOffset = cursor >= glyphPositions.size || cursorLine * 2 >= linesBreak.size ? 0 : glyphPositions.get(cursor) - glyphPositions.get(linesBreak.items[cursorLine * 2]); cursorX = textOffset + fontOffset + font.getData().cursorX; cursorPatch.draw(batch, x + cursorX, y - font.getDescent() / 2 - (cursorLine - firstLineShowing + 1) * font.getLineHeight(), cursorPatch.getMinWidth(), font.getLineHeight()); } @Override protected void calculateOffsets () { super.calculateOffsets(); if (!this.text.equals(lastText)) { this.lastText = text; BitmapFont font = style.font; float maxWidthLine = this.getWidth() - (style.background != null ? style.background.getLeftWidth() + style.background.getRightWidth() : 0); linesBreak.clear(); int lineStart = 0; int lastSpace = 0; char lastCharacter; Pool layoutPool = Pools.get(GlyphLayout.class); GlyphLayout layout = layoutPool.obtain(); for (int i = 0; i < text.length(); i++) { lastCharacter = text.charAt(i); if (lastCharacter == ENTER_DESKTOP || lastCharacter == ENTER_ANDROID) { linesBreak.add(lineStart); linesBreak.add(i); lineStart = i + 1; } else { lastSpace = (continueCursor(i, 0) ? lastSpace : i); layout.setText(font, text.subSequence(lineStart, i + 1)); if (layout.width > maxWidthLine && softwrap) { if (lineStart >= lastSpace) { lastSpace = i - 1; } linesBreak.add(lineStart); linesBreak.add(lastSpace + 1); lineStart = lastSpace + 1; lastSpace = lineStart; } } } layoutPool.free(layout); // Add last line if (lineStart < text.length()) { linesBreak.add(lineStart); linesBreak.add(text.length()); } showCursor(); } } @Override protected InputListener createInputListener () { return new TextAreaListener(); } @Override public void setSelection (int selectionStart, int selectionEnd) { super.setSelection(selectionStart, selectionEnd); updateCurrentLine(); } @Override protected void moveCursor (boolean forward, boolean jump) { int count = forward ? 1 : -1; int index = (cursorLine * 2) + count; if (index >= 0 && index + 1 < linesBreak.size && linesBreak.items[index] == cursor && linesBreak.items[index + 1] == cursor) { cursorLine += count; if (jump) { super.moveCursor(forward, jump); } showCursor(); } else { super.moveCursor(forward, jump); } updateCurrentLine(); } @Override protected boolean continueCursor (int index, int offset) { int pos = calculateCurrentLineIndex(index + offset); return super.continueCursor(index, offset) && (pos < 0 || pos >= linesBreak.size - 2 || (linesBreak.items[pos + 1] != index) || (linesBreak.items[pos + 1] == linesBreak.items[pos + 2])); } public int getCursorLine () { return cursorLine; } public int getFirstLineShowing () { return firstLineShowing; } public int getLinesShowing () { return linesShowing; } public float getCursorX () { return cursorX; } public float getCursorY () { BitmapFont font = style.font; return -(-font.getDescent() / 2 - (cursorLine - firstLineShowing + 1) * font.getLineHeight()); } /** Input listener for the text area **/ public class TextAreaListener extends TextFieldClickListener { @Override protected void setCursorPosition (float x, float y) { moveOffset = -1; Drawable background = style.background; BitmapFont font = style.font; float height = getHeight(); if (background != null) { height -= background.getTopHeight(); x -= background.getLeftWidth(); } x = Math.max(0, x); if (background != null) { y -= background.getTopHeight(); } cursorLine = (int) Math.floor((height - y) / font.getLineHeight()) + firstLineShowing; cursorLine = Math.max(0, Math.min(cursorLine, getLines() - 1)); super.setCursorPosition(x, y); updateCurrentLine(); } @Override public boolean keyDown (InputEvent event, int keycode) { boolean result = super.keyDown(event, keycode); Stage stage = getStage(); if (stage != null && stage.getKeyboardFocus() == VisTextArea.this) { boolean repeat = false; boolean shift = UIUtils.shift(); if (keycode == Input.Keys.DOWN) { if (shift) { if (!hasSelection) { selectionStart = cursor; hasSelection = true; } } else { clearSelection(); } moveCursorLine(cursorLine + 1); repeat = true; } else if (keycode == Input.Keys.UP) { if (shift) { if (!hasSelection) { selectionStart = cursor; hasSelection = true; } } else { clearSelection(); } moveCursorLine(cursorLine - 1); repeat = true; } else { moveOffset = -1; } if (repeat) { scheduleKeyRepeatTask(keycode); } showCursor(); return true; } return result; } @Override public boolean keyTyped (InputEvent event, char character) { boolean result = super.keyTyped(event, character); showCursor(); return result; } @Override protected void goHome (boolean jump) { if (jump) { cursor = 0; } else if (cursorLine * 2 < linesBreak.size) { cursor = linesBreak.get(cursorLine * 2); } } @Override protected void goEnd (boolean jump) { if (jump || cursorLine >= getLines()) { cursor = text.length(); } else if (cursorLine * 2 + 1 < linesBreak.size) { cursor = linesBreak.get(cursorLine * 2 + 1); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisTextButton.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.BorderOwner; /** * Extends functionality of standard {@link TextButton}, supports focus border. Compatible with standard {@link TextButton}. *

* When listening for button press {@link ChangeListener} should be always preferred (instead of {@link ClickListener}). * {@link ClickListener} does not support disabling button and will still report button presses. * @author Kotcrab * @see TextButton */ public class VisTextButton extends TextButton implements Focusable, BorderOwner { private VisTextButtonStyle style; private boolean drawBorder; private boolean focusBorderEnabled = true; public VisTextButton (String text, String styleName) { super(text, VisUI.getSkin().get(styleName, VisTextButtonStyle.class)); init(); } public VisTextButton (String text) { super(text, VisUI.getSkin().get(VisTextButtonStyle.class)); init(); } public VisTextButton (String text, ChangeListener listener) { super(text, VisUI.getSkin().get(VisTextButtonStyle.class)); init(); addListener(listener); } public VisTextButton (String text, String styleName, ChangeListener listener) { super(text, VisUI.getSkin().get(styleName, VisTextButtonStyle.class)); init(); addListener(listener); } public VisTextButton (String text, VisTextButtonStyle buttonStyle) { super(text, buttonStyle); init(); } private void init () { style = (VisTextButtonStyle) getStyle(); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (isDisabled() == false) FocusManager.switchFocus(getStage(), VisTextButton.this); return false; } }); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); if (focusBorderEnabled && drawBorder && style.focusBorder != null) { style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight()); } } static public class VisTextButtonStyle extends TextButtonStyle { public Drawable focusBorder; public VisTextButtonStyle () { super(); } public VisTextButtonStyle (Drawable up, Drawable down, Drawable checked, BitmapFont font) { super(up, down, checked, font); } public VisTextButtonStyle (VisTextButtonStyle style) { super(style); this.focusBorder = style.focusBorder; } } @Override public boolean isFocusBorderEnabled () { return focusBorderEnabled; } @Override public void setFocusBorderEnabled (boolean focusBorderEnabled) { this.focusBorderEnabled = focusBorderEnabled; } @Override public void focusLost () { drawBorder = false; } @Override public void focusGained () { drawBorder = true; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisTextField.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Cursor.SystemCursor; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData; import com.badlogic.gdx.graphics.g2d.GlyphLayout; import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.ui.TextField.DefaultOnscreenKeyboard; import com.badlogic.gdx.scenes.scene2d.ui.TextField.OnscreenKeyboard; import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle; import com.badlogic.gdx.scenes.scene2d.ui.Widget; import com.badlogic.gdx.scenes.scene2d.ui.Window; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Disableable; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; import com.badlogic.gdx.utils.*; import com.badlogic.gdx.utils.Timer.Task; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.BorderOwner; import com.kotcrab.vis.ui.util.CursorManager; import java.lang.StringBuilder; /** * Extends functionality of standard {@link TextField}. Style supports over, and focus border. Improved text input. * Due to scope of changes made this widget is not compatible with {@link TextField}. * @author mzechner * @author Nathan Sweet * @author Kotcrab * @see TextField */ public class VisTextField extends Widget implements Disableable, Focusable, BorderOwner { static private final char BACKSPACE = 8; static protected final char ENTER_DESKTOP = '\r'; static protected final char ENTER_ANDROID = '\n'; static private final char TAB = '\t'; static private final char DELETE = 127; static private final char BULLET = 8226; static private final Vector2 tmp1 = new Vector2(); static private final Vector2 tmp2 = new Vector2(); static private final Vector2 tmp3 = new Vector2(); static public float keyRepeatInitialTime = 0.4f; /** Repeat times for keys handled by {@link InputListener#keyDown(InputEvent, int)} such as navigation arrows */ static public float keyRepeatTime = 0.04f; protected String text; protected int cursor, selectionStart; protected boolean hasSelection; protected boolean writeEnters; protected final GlyphLayout layout = new GlyphLayout(); protected final FloatArray glyphPositions = new FloatArray(); private String messageText; protected CharSequence displayText; Clipboard clipboard; InputListener inputListener; TextFieldListener listener; TextFieldFilter filter; OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard(); boolean focusTraversal = true, onlyFontChars = true, disabled; boolean enterKeyFocusTraversal = false; private int textHAlign = Align.left; private float selectionX, selectionWidth; String undoText = ""; int undoCursorPos = 0; long lastChangeTime; boolean passwordMode; private StringBuilder passwordBuffer; private char passwordCharacter = BULLET; protected float fontOffset, textHeight, textOffset; float renderOffset; private int visibleTextStart, visibleTextEnd; private int maxLength = 0; private float blinkTime = 0.45f; boolean cursorOn = true; long lastBlink; KeyRepeatTask keyRepeatTask = new KeyRepeatTask(); boolean programmaticChangeEvents; // vis fields VisTextFieldStyle style; private ClickListener clickListener; private boolean drawBorder; private boolean focusBorderEnabled = true; private boolean inputValid = true; private boolean ignoreEqualsTextChange = true; private boolean readOnly = false; private float cursorPercentHeight = 0.8f; public VisTextField () { this("", VisUI.getSkin().get(VisTextFieldStyle.class)); } public VisTextField (String text) { this(text, VisUI.getSkin().get(VisTextFieldStyle.class)); } public VisTextField (String text, String styleName) { this(text, VisUI.getSkin().get(styleName, VisTextFieldStyle.class)); } public VisTextField (String text, VisTextFieldStyle style) { setStyle(style); clipboard = Gdx.app.getClipboard(); initialize(); setText(text); setSize(getPrefWidth(), getPrefHeight()); } protected void initialize () { addListener(inputListener = createInputListener()); addListener(clickListener = new ClickListener() { @Override public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) { super.enter(event, x, y, pointer, fromActor); if (pointer == -1 && isDisabled() == false) { Gdx.graphics.setSystemCursor(SystemCursor.Ibeam); } } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { super.exit(event, x, y, pointer, toActor); if (pointer == -1) { CursorManager.restoreDefaultCursor(); } } }); } protected InputListener createInputListener () { return new TextFieldClickListener(); } protected int letterUnderCursor (float x) { x -= textOffset + fontOffset - style.font.getData().cursorX - glyphPositions.get(visibleTextStart); int n = this.glyphPositions.size; float[] glyphPositions = this.glyphPositions.items; for (int i = 1; i < n; i++) { if (glyphPositions[i] > x) { if (glyphPositions[i] - x <= x - glyphPositions[i - 1]) return i; return i - 1; } } return n - 1; } protected boolean isWordCharacter (char c) { return Character.isLetterOrDigit(c); } protected int[] wordUnderCursor (int at) { String text = this.text; int start = Math.min(text.length(), at), right = text.length(), left = 0, index = start; for (; index < right; index++) { if (!isWordCharacter(text.charAt(index))) { right = index; break; } } for (index = start - 1; index > -1; index--) { if (!isWordCharacter(text.charAt(index))) { left = index + 1; break; } } return new int[]{left, right}; } int[] wordUnderCursor (float x) { return wordUnderCursor(letterUnderCursor(x)); } boolean withinMaxLength (int size) { return maxLength <= 0 || size < maxLength; } public int getMaxLength () { return this.maxLength; } public void setMaxLength (int maxLength) { this.maxLength = maxLength; } /** * When false, text set by {@link #setText(String)} may contain characters not in the font, a space will be displayed instead. * When true (the default), characters not in the font are stripped by setText. Characters not in the font are always stripped * when typed or pasted. */ public void setOnlyFontChars (boolean onlyFontChars) { this.onlyFontChars = onlyFontChars; } /** * Returns the text field's style. Modifying the returned style may not have an effect until * {@link #setStyle(VisTextFieldStyle)} is called. */ public VisTextFieldStyle getStyle () { return style; } public void setStyle (VisTextFieldStyle style) { if (style == null) throw new IllegalArgumentException("style cannot be null."); this.style = style; textHeight = style.font.getCapHeight() - style.font.getDescent() * 2; invalidateHierarchy(); } @Override public String toString () { return getText(); } protected void calculateOffsets () { float visibleWidth = getWidth(); if (style.background != null) visibleWidth -= style.background.getLeftWidth() + style.background.getRightWidth(); int glyphCount = glyphPositions.size; float[] glyphPositions = this.glyphPositions.items; // Check if the cursor has gone out the left or right side of the visible area and adjust renderOffset. float distance = glyphPositions[Math.max(0, cursor - 1)] + renderOffset; if (distance <= 0) renderOffset -= distance; else { int index = Math.min(glyphCount - 1, cursor + 1); float minX = glyphPositions[index] - visibleWidth; if (-renderOffset < minX) renderOffset = -minX; } // Prevent renderOffset from starting too close to the end, eg after text was deleted. float maxOffset = 0; float width = glyphPositions[glyphCount - 1]; for (int i = glyphCount - 2; i >= 0; i--) { float x = glyphPositions[i]; if (width - x > visibleWidth) break; maxOffset = x; } if (-renderOffset > maxOffset) renderOffset = -maxOffset; // calculate first visible char based on render offset visibleTextStart = 0; float startX = 0; for (int i = 0; i < glyphCount; i++) { if (glyphPositions[i] >= -renderOffset) { visibleTextStart = Math.max(0, i); startX = glyphPositions[i]; break; } } // calculate last visible char based on visible width and render offset int length = Math.min(displayText.length(), glyphPositions.length - 1); visibleTextEnd = Math.min(length, cursor + 1); for (; visibleTextEnd <= length; visibleTextEnd++) if (glyphPositions[visibleTextEnd] > startX + visibleWidth) break; visibleTextEnd = Math.max(0, visibleTextEnd - 1); if ((textHAlign & Align.left) == 0) { textOffset = visibleWidth - (glyphPositions[visibleTextEnd] - startX); if ((textHAlign & Align.center) != 0) textOffset = Math.round(textOffset * 0.5f); } else textOffset = startX + renderOffset; // calculate selection x position and width if (hasSelection) { int minIndex = Math.min(cursor, selectionStart); int maxIndex = Math.max(cursor, selectionStart); float minX = Math.max(glyphPositions[minIndex] - glyphPositions[visibleTextStart], -textOffset); float maxX = Math.min(glyphPositions[maxIndex] - glyphPositions[visibleTextStart], visibleWidth - textOffset); selectionX = minX; selectionWidth = maxX - minX - style.font.getData().cursorX; } } @Override public void draw (Batch batch, float parentAlpha) { Stage stage = getStage(); boolean focused = stage != null && stage.getKeyboardFocus() == this; if (!focused) keyRepeatTask.cancel(); final BitmapFont font = style.font; final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor : ((focused && style.focusedFontColor != null) ? style.focusedFontColor : style.fontColor); final Drawable selection = style.selection; final Drawable cursorPatch = style.cursor; Drawable background = (disabled && style.disabledBackground != null) ? style.disabledBackground : ((focused && style.focusedBackground != null) ? style.focusedBackground : style.background); // vis if (!disabled && style.backgroundOver != null && (clickListener.isOver() || focused)) { background = style.backgroundOver; } Color color = getColor(); float x = getX(); float y = getY(); float width = getWidth(); float height = getHeight(); batch.setColor(color.r, color.g, color.b, color.a * parentAlpha); float bgLeftWidth = 0, bgRightWidth = 0; if (background != null) { background.draw(batch, x, y, width, height); bgLeftWidth = background.getLeftWidth(); bgRightWidth = background.getRightWidth(); } float textY = getTextY(font, background); calculateOffsets(); if (focused && hasSelection && selection != null) { drawSelection(selection, batch, font, x + bgLeftWidth, y + textY); } float yOffset = font.isFlipped() ? -textHeight : 0; if (displayText.length() == 0) { if (!focused && messageText != null) { if (style.messageFontColor != null) { font.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b, style.messageFontColor.a * color.a * parentAlpha); } else font.setColor(0.7f, 0.7f, 0.7f, color.a * parentAlpha); BitmapFont messageFont = style.messageFont != null ? style.messageFont : font; messageFont.draw(batch, messageText, x + bgLeftWidth, y + textY + yOffset, 0, messageText.length(), width - bgLeftWidth - bgRightWidth, textHAlign, false, "..."); } } else { BitmapFontData data = font.getData(); boolean markupEnabled = data.markupEnabled; data.markupEnabled = false; font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * color.a * parentAlpha); drawText(batch, font, x + bgLeftWidth, y + textY + yOffset); data.markupEnabled = markupEnabled; } if (drawBorder && focused && !disabled) { blink(); if (cursorOn && cursorPatch != null) { drawCursor(cursorPatch, batch, font, x + bgLeftWidth, y + textY); } } // vis if (isDisabled() == false && inputValid == false && style.errorBorder != null) style.errorBorder.draw(batch, getX(), getY(), getWidth(), getHeight()); else if (focusBorderEnabled && drawBorder && style.focusBorder != null) style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight()); } protected float getTextY (BitmapFont font, Drawable background) { float height = getHeight(); float textY = textHeight / 2 + font.getDescent(); if (background != null) { float bottom = background.getBottomHeight(); textY = textY + (height - background.getTopHeight() - bottom) / 2 + bottom; } else { textY = textY + height / 2; } if (font.usesIntegerPositions()) textY = (int) textY; return textY; } /** Draws selection rectangle **/ protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) { selection.draw(batch, x + selectionX + textOffset + fontOffset, y - textHeight - font.getDescent(), selectionWidth, textHeight); } protected void drawText (Batch batch, BitmapFont font, float x, float y) { font.draw(batch, displayText, x + textOffset, y, visibleTextStart, visibleTextEnd, 0, Align.left, false); } protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) { float cursorHeight = textHeight * cursorPercentHeight; float cursorYPadding = (textHeight - cursorHeight) / 2; cursorPatch.draw(batch, x + textOffset + glyphPositions.get(cursor) - glyphPositions.get(visibleTextStart) + fontOffset + font.getData().cursorX, y - textHeight - font.getDescent() + cursorYPadding, cursorPatch.getMinWidth(), cursorHeight); } void updateDisplayText () { BitmapFont font = style.font; BitmapFontData data = font.getData(); String text = this.text; int textLength = text.length(); StringBuilder buffer = new StringBuilder(); for (int i = 0; i < textLength; i++) { char c = text.charAt(i); buffer.append(data.hasGlyph(c) ? c : ' '); } String newDisplayText = buffer.toString(); if (passwordMode && data.hasGlyph(passwordCharacter)) { if (passwordBuffer == null) passwordBuffer = new StringBuilder(newDisplayText.length()); if (passwordBuffer.length() > textLength) passwordBuffer.setLength(textLength); else { for (int i = passwordBuffer.length(); i < textLength; i++) passwordBuffer.append(passwordCharacter); } displayText = passwordBuffer; } else displayText = newDisplayText; boolean markupEnabled = data.markupEnabled; data.markupEnabled = false; layout.setText(font, displayText.toString().replace('\r', ' ').replace('\n', ' ')); data.markupEnabled = markupEnabled; glyphPositions.clear(); float x = 0; if (layout.runs.size > 0) { GlyphRun run = layout.runs.first(); fontOffset = run.xAdvances.first(); for (GlyphRun glyphRun : layout.runs) { FloatArray xAdvances = glyphRun.xAdvances; for (int i = 1, n = xAdvances.size; i < n; i++) { glyphPositions.add(x); x += xAdvances.get(i); } glyphPositions.add(x); } } else { fontOffset = 0; } glyphPositions.add(x); visibleTextStart = Math.min(visibleTextStart, glyphPositions.size); visibleTextEnd = MathUtils.clamp(visibleTextEnd, visibleTextStart, glyphPositions.size); if (selectionStart > newDisplayText.length()) selectionStart = textLength; } private void blink () { if (!Gdx.graphics.isContinuousRendering()) { cursorOn = true; return; } long time = TimeUtils.nanoTime(); if ((time - lastBlink) / 1000000000.0f > blinkTime) { cursorOn = !cursorOn; lastBlink = time; } } /** Copies the contents of this TextField to the {@link Clipboard} implementation set on this TextField. */ public void copy () { if (hasSelection && !passwordMode) { int beginIndex = Math.min(cursor, selectionStart); int endIndex = Math.max(cursor, selectionStart); clipboard.setContents(text.substring(Math.max(0, beginIndex), Math.min(text.length(), endIndex))); } } /** * Copies the selected contents of this TextField to the {@link Clipboard} implementation set on this TextField, then removes * it. */ public void cut () { cut(programmaticChangeEvents); } void cut (boolean fireChangeEvent) { if (hasSelection && !passwordMode) { copy(); cursor = delete(fireChangeEvent); updateDisplayText(); } } void paste (String content, boolean fireChangeEvent) { if (content == null) return; StringBuilder buffer = new StringBuilder(); int textLength = text.length(); if (hasSelection) textLength -= Math.abs(cursor - selectionStart); BitmapFontData data = style.font.getData(); for (int i = 0, n = content.length(); i < n; i++) { if (!withinMaxLength(textLength + buffer.length())) break; char c = content.charAt(i); if (!(writeEnters && (c == ENTER_ANDROID || c == ENTER_DESKTOP))) { if (c == '\r' || c == '\n') continue; if (onlyFontChars && !data.hasGlyph(c)) continue; if (filter != null && !filter.acceptChar(this, c)) continue; } buffer.append(c); } content = buffer.toString(); if (hasSelection) cursor = delete(fireChangeEvent); if (fireChangeEvent) changeText(text, insert(cursor, content, text)); else text = insert(cursor, content, text); updateDisplayText(); cursor += content.length(); } String insert (int position, CharSequence text, String to) { if (to.length() == 0) return text.toString(); return to.substring(0, position) + text + to.substring(position, to.length()); } int delete (boolean fireChangeEvent) { int from = selectionStart; int to = cursor; int minIndex = Math.min(from, to); int maxIndex = Math.max(from, to); String newText = (minIndex > 0 ? text.substring(0, minIndex) : "") + (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : ""); if (fireChangeEvent) changeText(text, newText); else text = newText; clearSelection(); return minIndex; } /** * Focuses the next TextField. If none is found, the keyboard is hidden. Does nothing if the text field is not in a stage. * @param up If true, the TextField with the same or next smallest y coordinate is found, else the next highest. */ public void next (boolean up) { Stage stage = getStage(); if (stage == null) return; getParent().localToStageCoordinates(tmp1.set(getX(), getY())); VisTextField textField = findNextTextField(stage.getActors(), null, tmp2, tmp1, up); if (textField == null) { // Try to wrap around. if (up) tmp1.set(Float.MIN_VALUE, Float.MIN_VALUE); else tmp1.set(Float.MAX_VALUE, Float.MAX_VALUE); textField = findNextTextField(getStage().getActors(), null, tmp2, tmp1, up); } if (textField != null) { textField.focusField(); textField.setCursorPosition(textField.getText().length()); } else keyboard.show(false); } private VisTextField findNextTextField (Array actors, VisTextField best, Vector2 bestCoords, Vector2 currentCoords, boolean up) { Window modalWindow = findModalWindow(this); for (int i = 0, n = actors.size; i < n; i++) { Actor actor = actors.get(i); if (actor == this) continue; if (actor instanceof VisTextField) { VisTextField textField = (VisTextField) actor; if (modalWindow != null) { Window nextFieldModalWindow = findModalWindow(textField); if (nextFieldModalWindow != modalWindow) continue; } if (textField.isDisabled() || !textField.focusTraversal || isActorVisibleInStage(textField) == false) continue; Vector2 actorCoords = actor.getParent().localToStageCoordinates(tmp3.set(actor.getX(), actor.getY())); if ((actorCoords.y < currentCoords.y || (actorCoords.y == currentCoords.y && actorCoords.x > currentCoords.x)) ^ up) { if (best == null || (actorCoords.y > bestCoords.y || (actorCoords.y == bestCoords.y && actorCoords.x < bestCoords.x)) ^ up) { best = (VisTextField) actor; bestCoords.set(actorCoords); } } } else if (actor instanceof Group) best = findNextTextField(((Group) actor).getChildren(), best, bestCoords, currentCoords, up); } return best; } /** * Checks if actor is visible in stage acknowledging parent visibility. * If any parent returns false from isVisible then this method return false. * True is returned when this actor and all its parent are visible. */ private boolean isActorVisibleInStage (Actor actor) { if (actor == null) return true; if (actor.isVisible() == false) return false; return isActorVisibleInStage(actor.getParent()); } private Window findModalWindow (Actor actor) { if (actor == null) return null; if (actor instanceof Window && ((Window) actor).isModal()) return (Window) actor; return findModalWindow(actor.getParent()); } public InputListener getDefaultInputListener () { return inputListener; } /** @param listener May be null. */ public void setTextFieldListener (TextFieldListener listener) { this.listener = listener; } /** @param filter May be null. */ public void setTextFieldFilter (TextFieldFilter filter) { this.filter = filter; } public TextFieldFilter getTextFieldFilter () { return filter; } /** If true (the default), tab/shift+tab will move to the next text field. */ public void setFocusTraversal (boolean focusTraversal) { this.focusTraversal = focusTraversal; } /** * If true, enter will move to the next text field with has focus traversal enabled. * False by default. Note that to enable or disable focus traversal completely you must * use {@link #setFocusTraversal(boolean)} */ public void setEnterKeyFocusTraversal (boolean enterKeyFocusTraversal) { this.enterKeyFocusTraversal = enterKeyFocusTraversal; } /** @return May be null. */ public String getMessageText () { return messageText; } /** * Sets the text that will be drawn in the text field if no text has been entered. * @param messageText may be null. */ public void setMessageText (String messageText) { this.messageText = messageText; } /** @param str If null, "" is used. */ public void appendText (String str) { if (str == null) str = ""; clearSelection(); cursor = text.length(); paste(str, programmaticChangeEvents); } /** @param str If null, "" is used. */ public void setText (String str) { if (str == null) str = ""; if (ignoreEqualsTextChange && str.equals(text)) return; clearSelection(); String oldText = text; text = ""; paste(str, false); if (programmaticChangeEvents) changeText(oldText, text); cursor = 0; } /** @return Never null, might be an empty string. */ public String getText () { return text; } /** * @param oldText May be null. * @return True if the text was changed. */ boolean changeText (String oldText, String newText) { if (ignoreEqualsTextChange && newText.equals(oldText)) return false; text = newText; beforeChangeEventFired(); ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); boolean cancelled = fire(changeEvent); text = cancelled ? oldText : newText; Pools.free(changeEvent); return !cancelled; } void beforeChangeEventFired () { } public boolean getProgrammaticChangeEvents () { return programmaticChangeEvents; } /** * If false, methods that change the text will not fire {@link ChangeEvent}, the event will be fired only when user changes * the text. */ public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) { this.programmaticChangeEvents = programmaticChangeEvents; } public int getSelectionStart () { return selectionStart; } public String getSelection () { return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : ""; } public boolean isTextSelected () { return hasSelection; } /** Sets the selected text. */ public void setSelection (int selectionStart, int selectionEnd) { if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0"); if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0"); selectionStart = Math.min(text.length(), selectionStart); selectionEnd = Math.min(text.length(), selectionEnd); if (selectionEnd == selectionStart) { clearSelection(); return; } if (selectionEnd < selectionStart) { int temp = selectionEnd; selectionEnd = selectionStart; selectionStart = temp; } hasSelection = true; this.selectionStart = selectionStart; cursor = selectionEnd; } public void selectAll () { setSelection(0, text.length()); } public void clearSelection () { hasSelection = false; } /** Clears VisTextField text. If programmatic change events are disabled then this will not fire change event. */ public void clearText () { setText(""); } /** Sets the cursor position and clears any selection. */ public void setCursorPosition (int cursorPosition) { if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0"); clearSelection(); cursor = Math.min(cursorPosition, text.length()); } public int getCursorPosition () { return cursor; } public void setCursorAtTextEnd () { setCursorPosition(0); calculateOffsets(); setCursorPosition(getText().length()); } /** @param cursorPercentHeight cursor size, value from 0..1 range */ public void setCursorPercentHeight (float cursorPercentHeight) { if (cursorPercentHeight < 0 || cursorPercentHeight > 1) throw new IllegalArgumentException("cursorPercentHeight must be >= 0 and <= 1"); this.cursorPercentHeight = cursorPercentHeight; } /** Default is an instance of {@link DefaultOnscreenKeyboard}. */ public OnscreenKeyboard getOnscreenKeyboard () { return keyboard; } public void setOnscreenKeyboard (OnscreenKeyboard keyboard) { this.keyboard = keyboard; } public void setClipboard (Clipboard clipboard) { this.clipboard = clipboard; } @Override public float getPrefWidth () { return 150; } @Override public float getPrefHeight () { float prefHeight = textHeight; if (style.background != null) { prefHeight = Math.max(prefHeight + style.background.getBottomHeight() + style.background.getTopHeight(), style.background.getMinHeight()); } return prefHeight; } /** * Sets text horizontal alignment (left, center or right). * @see Align */ public void setAlignment (int alignment) { this.textHAlign = alignment; } /** * If true, the text in this text field will be shown as bullet characters. * @see #setPasswordCharacter(char) */ public void setPasswordMode (boolean passwordMode) { this.passwordMode = passwordMode; updateDisplayText(); } public boolean isPasswordMode () { return passwordMode; } /** * Sets the password character for the text field. The character must be present in the {@link BitmapFont}. Default is 149 * (bullet). */ public void setPasswordCharacter (char passwordCharacter) { this.passwordCharacter = passwordCharacter; if (passwordMode) updateDisplayText(); } public void setBlinkTime (float blinkTime) { this.blinkTime = blinkTime; } public boolean isDisabled () { return disabled; } @Override public void setDisabled (boolean disabled) { this.disabled = disabled; if (disabled) { FocusManager.resetFocus(getStage(), this); keyRepeatTask.cancel(); } } public boolean isReadOnly () { return readOnly; } public void setReadOnly (boolean readOnly) { this.readOnly = readOnly; } protected void moveCursor (boolean forward, boolean jump) { int limit = forward ? text.length() : 0; int charOffset = forward ? 0 : -1; while ((forward ? ++cursor < limit : --cursor > limit) && jump) { if (!continueCursor(cursor, charOffset)) break; } } protected boolean continueCursor (int index, int offset) { char c = text.charAt(index + offset); return isWordCharacter(c); } /** Focuses this field, field must be added to stage before this method can be called */ public void focusField () { if (disabled) return; Stage stage = getStage(); FocusManager.switchFocus(stage, VisTextField.this); setCursorPosition(0); selectionStart = 0; //make sure textOffset was updated, prevent issue when there was long text selected and it was changed to short text //and field was focused. Without it textOffset would stay at max value and only one last letter will be visible in field calculateOffsets(); if (stage != null) stage.setKeyboardFocus(VisTextField.this); keyboard.show(true); hasSelection = true; } @Override public void focusLost () { drawBorder = false; } @Override public void focusGained () { drawBorder = true; } public boolean isEmpty () { return text.length() == 0; } public boolean isInputValid () { return inputValid; } public void setInputValid (boolean inputValid) { this.inputValid = inputValid; } @Override public boolean isFocusBorderEnabled () { return focusBorderEnabled; } @Override public void setFocusBorderEnabled (boolean focusBorderEnabled) { this.focusBorderEnabled = focusBorderEnabled; } /** @see #setIgnoreEqualsTextChange(boolean) */ public boolean isIgnoreEqualsTextChange () { return ignoreEqualsTextChange; } /** * Allows to control whether change event is sent when text field's text is changed to same same as was it before. * Eg. current text field is 'abc' and {@link #setText(String)} is called it with 'abc' again. * @param ignoreEqualsTextChange if true then setting text to the same as it was before will NOT fire change event. * Default is true however it is false default {@link VisValidatableTextField} to prevent form refreshment issues - * see issue VisEditor#165 */ public void setIgnoreEqualsTextChange (boolean ignoreEqualsTextChange) { this.ignoreEqualsTextChange = ignoreEqualsTextChange; } static public class VisTextFieldStyle extends TextFieldStyle { public Drawable focusBorder; public Drawable errorBorder; public Drawable backgroundOver; public VisTextFieldStyle () { } public VisTextFieldStyle (BitmapFont font, Color fontColor, Drawable cursor, Drawable selection, Drawable background) { super(font, fontColor, cursor, selection, background); } public VisTextFieldStyle (VisTextFieldStyle style) { super(style); this.focusBorder = style.focusBorder; this.errorBorder = style.errorBorder; this.backgroundOver = style.backgroundOver; } } /** * Interface for listening to typed characters. * @author mzechner */ static public interface TextFieldListener { public void keyTyped (VisTextField textField, char c); } /** * Interface for filtering characters entered into the text field. * @author mzechner */ static public interface TextFieldFilter { public boolean acceptChar (VisTextField textField, char c); static public class DigitsOnlyFilter implements TextFieldFilter { @Override public boolean acceptChar (VisTextField textField, char c) { return Character.isDigit(c); } } } class KeyRepeatTask extends Task { int keycode; @Override public void run () { inputListener.keyDown(null, keycode); } } /** Basic input listener for the text field */ public class TextFieldClickListener extends ClickListener { @Override public void clicked (InputEvent event, float x, float y) { int count = getTapCount() % 4; if (count == 0) clearSelection(); if (count == 2) { int[] array = wordUnderCursor(x); setSelection(array[0], array[1]); } if (count == 3) selectAll(); } @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (!super.touchDown(event, x, y, pointer, button)) return false; if (pointer == 0 && button != 0) return false; if (disabled) return true; Stage stage = getStage(); FocusManager.switchFocus(stage, VisTextField.this); setCursorPosition(x, y); selectionStart = cursor; if (stage != null) stage.setKeyboardFocus(VisTextField.this); if (readOnly == false) keyboard.show(true); hasSelection = true; return true; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { super.touchDragged(event, x, y, pointer); setCursorPosition(x, y); } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (selectionStart == cursor) hasSelection = false; super.touchUp(event, x, y, pointer, button); } protected void setCursorPosition (float x, float y) { lastBlink = 0; cursorOn = false; cursor = Math.min(letterUnderCursor(x), text.length()); } protected void goHome (boolean jump) { cursor = 0; } protected void goEnd (boolean jump) { cursor = text.length(); } @Override public boolean keyDown (InputEvent event, int keycode) { if (disabled) return false; lastBlink = 0; cursorOn = false; Stage stage = getStage(); if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false; if (drawBorder == false) return false; boolean repeat = false; boolean ctrl = UIUtils.ctrl(); boolean jump = ctrl && !passwordMode; if (ctrl) { if (keycode == Keys.V && readOnly == false) { paste(clipboard.getContents(), true); repeat = true; } if (keycode == Keys.C || keycode == Keys.INSERT) { copy(); return true; } if (keycode == Keys.X && readOnly == false) { cut(true); return true; } if (keycode == Keys.A) { selectAll(); return true; } if (keycode == Keys.Z && readOnly == false) { String oldText = text; int oldCursorPos = getCursorPosition(); setText(undoText); VisTextField.this.setCursorPosition(MathUtils.clamp(cursor, 0, undoText.length())); undoText = oldText; undoCursorPos = oldCursorPos; updateDisplayText(); return true; } } if (UIUtils.shift()) { if (keycode == Keys.INSERT && readOnly == false) paste(clipboard.getContents(), true); if (keycode == Keys.FORWARD_DEL && readOnly == false) cut(true); selection: { int temp = cursor; keys: { if (keycode == Keys.LEFT) { moveCursor(false, jump); repeat = true; break keys; } if (keycode == Keys.RIGHT) { moveCursor(true, jump); repeat = true; break keys; } if (keycode == Keys.HOME) { goHome(jump); break keys; } if (keycode == Keys.END) { goEnd(jump); break keys; } break selection; } if (!hasSelection) { selectionStart = temp; hasSelection = true; } } } else { // Cursor movement or other keys (kills selection). if (keycode == Keys.LEFT) { moveCursor(false, jump); clearSelection(); repeat = true; } if (keycode == Keys.RIGHT) { moveCursor(true, jump); clearSelection(); repeat = true; } if (keycode == Keys.HOME) { goHome(jump); clearSelection(); } if (keycode == Keys.END) { goEnd(jump); clearSelection(); } } cursor = MathUtils.clamp(cursor, 0, text.length()); if (repeat) { scheduleKeyRepeatTask(keycode); } return true; } protected void scheduleKeyRepeatTask (int keycode) { if (!keyRepeatTask.isScheduled() || keyRepeatTask.keycode != keycode) { keyRepeatTask.keycode = keycode; keyRepeatTask.cancel(); if (Gdx.input.isKeyPressed(keyRepeatTask.keycode)) { //issue #179 Timer.schedule(keyRepeatTask, keyRepeatInitialTime, keyRepeatTime); } } } @Override public boolean keyUp (InputEvent event, int keycode) { if (disabled) return false; keyRepeatTask.cancel(); return true; } @Override public boolean keyTyped (InputEvent event, char character) { if (disabled || readOnly) return false; // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true. switch (character) { case BACKSPACE: case TAB: case ENTER_ANDROID: case ENTER_DESKTOP: break; default: if (character < 32) return false; } Stage stage = getStage(); if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false; if (UIUtils.isMac && Gdx.input.isKeyPressed(Keys.SYM)) return true; if (focusTraversal && (character == TAB || (character == ENTER_ANDROID && enterKeyFocusTraversal))) { next(UIUtils.shift()); } else { boolean delete = character == DELETE; boolean backspace = character == BACKSPACE; boolean enter = character == ENTER_DESKTOP || character == ENTER_ANDROID; boolean add = enter ? writeEnters : (!onlyFontChars || style.font.getData().hasGlyph(character)); boolean remove = backspace || delete; if (add || remove) { String oldText = text; int oldCursor = cursor; if (hasSelection) cursor = delete(false); else { if (backspace && cursor > 0) { text = text.substring(0, cursor - 1) + text.substring(cursor--); renderOffset = 0; } if (delete && cursor < text.length()) { text = text.substring(0, cursor) + text.substring(cursor + 1); } } if (add && !remove) { // Character may be added to the text. if (!enter && filter != null && !filter.acceptChar(VisTextField.this, character)) return true; if (!withinMaxLength(text.length())) return true; String insertion = enter ? "\n" : String.valueOf(character); text = insert(cursor++, insertion, text); } if (changeText(oldText, text)) { long time = System.currentTimeMillis(); if (time - 750 > lastChangeTime) { undoText = oldText; undoCursorPos = getCursorPosition() - 1; } lastChangeTime = time; } else cursor = oldCursor; updateDisplayText(); } } if (listener != null) listener.keyTyped(VisTextField.this, character); return true; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisTree.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.Tree; import com.badlogic.gdx.scenes.scene2d.ui.Tree.Node; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.VisUI; /** * Does not provide additional features over standard {@link Tree}, however for proper VisUI focus border management VisTree * should be always preferred. Compatible with standard {@link Tree} * @author Kotcrab * @see Tree */ public class VisTree extends Tree { public VisTree (String styleName) { super(VisUI.getSkin(), styleName); init(); } public VisTree () { super(VisUI.getSkin()); init(); } public VisTree (TreeStyle style) { super(style); init(); } private void init () { addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { Focusable focusable = FocusManager.getFocusedWidget(); if (focusable instanceof Actor == false || isAscendantOf((Actor) focusable) == false) { FocusManager.resetFocus(getStage()); } return false; } }); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisValidatableTextField.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.utils.FocusListener; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.util.Validators; /** * Text field that input can be validated by custom input validators. * @author Kotcrab * @see InputValidator * @see Validators */ public class VisValidatableTextField extends VisTextField { private Array validators = new Array(); private boolean validationEnabled = true; private LastValidFocusListener restoreFocusListener; private boolean restoreLastValid = false; private String lastValid; public VisValidatableTextField () { super(); init(); } public VisValidatableTextField (String text) { super(text); init(); } public VisValidatableTextField (String text, String styleName) { super(text, styleName); init(); } public VisValidatableTextField (String text, VisTextFieldStyle style) { super(text, style); init(); } public VisValidatableTextField (InputValidator validator) { super(); addValidator(validator); init(); } public VisValidatableTextField (InputValidator... validators) { super(); for (InputValidator validator : validators) addValidator(validator); init(); } public VisValidatableTextField (boolean restoreLastValid, InputValidator validator) { super(); addValidator(validator); init(); setRestoreLastValid(restoreLastValid); } public VisValidatableTextField (boolean restoreLastValid, InputValidator... validators) { super(); for (InputValidator validator : validators) addValidator(validator); init(); setRestoreLastValid(restoreLastValid); } private void init () { setProgrammaticChangeEvents(true); setIgnoreEqualsTextChange(false); } @Override void beforeChangeEventFired () { validateInput(); } @Override public void setText (String str) { super.setText(str); validateInput(); } public void validateInput () { if (validationEnabled) { for (InputValidator validator : validators) { if (validator.validateInput(getText()) == false) { setInputValid(false); return; } } } // validation not enabled or validators does not returned false (input was valid) setInputValid(true); } public void addValidator (InputValidator validator) { validators.add(validator); validateInput(); } public Array getValidators () { return validators; } public boolean isValidationEnabled () { return validationEnabled; } /** * Enables or disables validation, after changing this setting validateInput() is called, if validationEnabled == false then * field is marked as valid otherwise standard validation is performed * @param validationEnabled enable or disable validation */ public void setValidationEnabled (boolean validationEnabled) { this.validationEnabled = validationEnabled; validateInput(); } public boolean isRestoreLastValid () { return restoreLastValid; } /** * If true this field will automatically restore last valid text if it loses keyboard focus during text edition. * This can't be called while field is selected, doing so will result in IllegalStateException. Default is false. */ public void setRestoreLastValid (boolean restoreLastValid) { if (hasSelection) throw new IllegalStateException("Last valid text restore can't be changed while filed has selection"); this.restoreLastValid = restoreLastValid; if (restoreLastValid) { if (restoreFocusListener == null) restoreFocusListener = new LastValidFocusListener(); addListener(restoreFocusListener); } else { removeListener(restoreFocusListener); } } public void restoreLastValidText () { if (restoreLastValid == false) throw new IllegalStateException("Restore last valid is not enabled, see #setRestoreLastValid(boolean)"); //use super.setText to skip input validation and do not fire programmatic change event VisValidatableTextField.super.setText(lastValid); setInputValid(true); } private class LastValidFocusListener extends FocusListener { @Override public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (focused && restoreLastValid) { lastValid = getText(); } if (focused == false && isInputValid() == false && restoreLastValid) { restoreLastValidText(); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/VisWindow.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.Window; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Align; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.VisUI; /** * Extends functionality of standard scene2d.ui {@link Window}. * @author Kotcrab * @see Window */ public class VisWindow extends Window { public static float FADE_TIME = 0.3f; private boolean centerOnAdd; private boolean keepWithinParent = false; private boolean fadeOutActionRunning; public VisWindow (String title) { this(title, true); getTitleLabel().setAlignment(VisUI.getDefaultTitleAlign()); } public VisWindow (String title, boolean showWindowBorder) { super(title, VisUI.getSkin(), showWindowBorder ? "default" : "noborder"); getTitleLabel().setAlignment(VisUI.getDefaultTitleAlign()); } public VisWindow (String title, String styleName) { super(title, VisUI.getSkin(), styleName); getTitleLabel().setAlignment(VisUI.getDefaultTitleAlign()); } public VisWindow (String title, WindowStyle style) { super(title, style); getTitleLabel().setAlignment(VisUI.getDefaultTitleAlign()); } @Override public void setPosition (float x, float y) { super.setPosition((int) x, (int) y); } /** * Centers this window, if it has parent it will be done instantly, if it does not have parent it will be centered when it will * be added to stage * @return true when window was centered, false when window will be centered when added to stage */ public boolean centerWindow () { Group parent = getParent(); if (parent == null) { centerOnAdd = true; return false; } else { moveToCenter(); return true; } } /** * @param centerOnAdd if true window position will be centered on screen after adding to stage * @see #centerWindow() */ public void setCenterOnAdd (boolean centerOnAdd) { this.centerOnAdd = centerOnAdd; } @Override protected void setStage (Stage stage) { super.setStage(stage); if (stage != null) { stage.setKeyboardFocus(this); //issue #10, newly created window does not acquire keyboard focus if (centerOnAdd) { centerOnAdd = false; moveToCenter(); } } } private void moveToCenter () { Stage parent = getStage(); if (parent != null) setPosition((parent.getWidth() - getWidth()) / 2, (parent.getHeight() - getHeight()) / 2); } /** * Fade outs this window, when fade out animation is completed, window is removed from Stage. Calling this for the * second time won't have any effect if previous animation is still running. */ public void fadeOut (float time) { if (fadeOutActionRunning) return; fadeOutActionRunning = true; final Touchable previousTouchable = getTouchable(); setTouchable(Touchable.disabled); Stage stage = getStage(); if (stage != null && stage.getKeyboardFocus() != null && stage.getKeyboardFocus().isDescendantOf(this)) { FocusManager.resetFocus(stage); } addAction(Actions.sequence(Actions.fadeOut(time, Interpolation.fade), new Action() { @Override public boolean act (float delta) { setTouchable(previousTouchable); remove(); getColor().a = 1f; fadeOutActionRunning = false; return true; } })); } /** @return this window for the purpose of chaining methods eg. stage.addActor(new MyWindow(stage).fadeIn(0.3f)); */ public VisWindow fadeIn (float time) { setColor(1, 1, 1, 0); addAction(Actions.fadeIn(time, Interpolation.fade)); return this; } /** Fade outs this window, when fade out animation is completed, window is removed from Stage */ public void fadeOut () { fadeOut(FADE_TIME); } /** @return this window for the purpose of chaining methods eg. stage.addActor(new MyWindow(stage).fadeIn()); */ public VisWindow fadeIn () { return fadeIn(FADE_TIME); } /** * Called by window when close button was pressed (added using {@link #addCloseButton()}) * or escape key was pressed (for close on escape {@link #closeOnEscape()} have to be called). * Default close behaviour is to fade out window, this can be changed by overriding this function. */ protected void close () { fadeOut(); } /** * Adds close button to window, next to window title. After pressing that button, {@link #close()} is called. If nothing * else was added to title table, and current title alignment is center then the title will be automatically centered. */ public void addCloseButton () { Label titleLabel = getTitleLabel(); Table titleTable = getTitleTable(); VisImageButton closeButton = new VisImageButton("close-window"); titleTable.add(closeButton).padRight(-getPadRight() + 0.7f); closeButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { close(); } }); closeButton.addListener(new ClickListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { event.cancel(); return true; } }); if (titleLabel.getLabelAlign() == Align.center && titleTable.getChildren().size == 2) titleTable.getCell(titleLabel).padLeft(closeButton.getWidth() * 2); } /** * Will make this window close when escape key or back key was pressed. After pressing escape or back, {@link #close()} is called. * Back key is Android and iOS only */ public void closeOnEscape () { addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ESCAPE) { close(); return true; } return false; } @Override public boolean keyUp (InputEvent event, int keycode) { if (keycode == Keys.BACK) { close(); return true; } return false; } }); } public boolean isKeepWithinParent () { return keepWithinParent; } public void setKeepWithinParent (boolean keepWithinParent) { this.keepWithinParent = keepWithinParent; } @Override public void draw (Batch batch, float parentAlpha) { if (keepWithinParent && getParent() != null) { float parentWidth = getParent().getWidth(); float parentHeight = getParent().getHeight(); if (getX() < 0) setX(0); if (getRight() > parentWidth) setX(parentWidth - getWidth()); if (getY() < 0) setY(0); if (getTop() > parentHeight) setY(parentHeight - getHeight()); } super.draw(batch, parentAlpha); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/BasicColorPicker.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Disposable; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.ColorUtils; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisTextField.TextFieldFilter; import com.kotcrab.vis.ui.widget.VisValidatableTextField; import com.kotcrab.vis.ui.widget.color.internal.AlphaImage; import com.kotcrab.vis.ui.widget.color.internal.Palette; import com.kotcrab.vis.ui.widget.color.internal.PickerCommons; import com.kotcrab.vis.ui.widget.color.internal.VerticalChannelBar; import static com.kotcrab.vis.ui.widget.color.internal.ColorPickerText.HEX; /** * Color Picker widget, allows user to select color. ColorPicker is relatively heavy widget and should be reused if possible. * Unlike other widgets, this one must be disposed when no longer needed! *

* Displays color pallet along with hue spectrum bar. Palette show all possible combination of saturation and value (SV * components of HSV color system) for given hue, spectrum bar shows all possible values of hue (H component). Displays * preview of current and new color and hex field that can be disabled. See {@link ExtendedColorPicker} if you need * more features. *

* Alpha channel can be only set from hex field and it disabled by default, use {@link #setAllowAlphaEdit(boolean)} to enable. * @author Kotcrab * @see ColorPicker * @see BasicColorPicker * @see ExtendedColorPicker * @since 0.9.3 */ public class BasicColorPicker extends VisTable implements Disposable { public static final int FIELD_WIDTH = 50; public static final int PALETTE_SIZE = 160; public static final int BAR_WIDTH = 130; public static final int BAR_HEIGHT = 12; private static final float VERTICAL_BAR_WIDTH = 15; private static final int HEX_FIELD_WIDTH = 95; private static final int HEX_COLOR_LENGTH = 6; private static final int HEX_COLOR_LENGTH_WITH_ALPHA = 8; protected ColorPickerWidgetStyle style; protected Sizes sizes; protected ColorPickerListener listener; Color oldColor; Color color; protected PickerCommons commons; protected Palette palette; protected VerticalChannelBar verticalBar; private VisTable mainTable; private VisTable colorPreviewsTable; private VisTable hexTable; private VisValidatableTextField hexField; private Image currentColorImg; private Image newColorImg; private boolean allowAlphaEdit = false; private boolean showHexFields = true; private boolean showColorPreviews = true; private boolean disposed = false; public BasicColorPicker () { this(null); } public BasicColorPicker (ColorPickerListener listener) { this("default", listener); } public BasicColorPicker (String styleName, ColorPickerListener listener) { this(VisUI.getSkin().get(styleName, ColorPickerWidgetStyle.class), listener, false); } public BasicColorPicker (ColorPickerWidgetStyle style, ColorPickerListener listener) { this(style, listener, false); } protected BasicColorPicker (ColorPickerWidgetStyle style, ColorPickerListener listener, boolean loadExtendedShaders) { this.listener = listener; this.style = style; this.sizes = VisUI.getSizes(); oldColor = new Color(Color.BLACK); color = new Color(Color.BLACK); commons = new PickerCommons(style, sizes, loadExtendedShaders); createColorWidgets(); createUI(); updateValuesFromCurrentColor(); updateUI(); } protected void createUI () { mainTable = new VisTable(true); colorPreviewsTable = createColorsPreviewTable(); hexTable = createHexTable(); rebuildMainTable(); add(mainTable).top(); } private void rebuildMainTable () { mainTable.clearChildren(); mainTable.add(palette).size(PALETTE_SIZE * sizes.scaleFactor); mainTable.add(verticalBar).size(VERTICAL_BAR_WIDTH * sizes.scaleFactor, PALETTE_SIZE * sizes.scaleFactor).top(); if (showColorPreviews) { mainTable.row(); mainTable.add(colorPreviewsTable).colspan(2).expandX().fillX(); } if (showHexFields) { mainTable.row(); mainTable.add(hexTable).colspan(2).expandX().left(); } } private VisTable createColorsPreviewTable () { VisTable table = new VisTable(false); table.add(currentColorImg = new AlphaImage(commons, 5 * sizes.scaleFactor)) .height(25 * sizes.scaleFactor).width(80 * sizes.scaleFactor).expandX().fillX(); table.add(new Image(style.iconArrowRight)).pad(0, 2, 0, 2); table.add(newColorImg = new AlphaImage(commons, 5 * sizes.scaleFactor)) .height(25 * sizes.scaleFactor).width(80 * sizes.scaleFactor).expandX().fillX(); currentColorImg.setColor(color); newColorImg.setColor(color); currentColorImg.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { restoreLastColor(); } }); return table; } private VisTable createHexTable () { VisTable table = new VisTable(true); table.add(new VisLabel(HEX.get())); table.add(hexField = new VisValidatableTextField("00000000")).width(HEX_FIELD_WIDTH * sizes.scaleFactor); table.row(); hexField.setMaxLength(HEX_COLOR_LENGTH); hexField.setProgrammaticChangeEvents(false); hexField.setTextFieldFilter(new TextFieldFilter() { @Override public boolean acceptChar (VisTextField textField, char c) { return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } }); hexField.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (hexField.getText().length() == (allowAlphaEdit ? HEX_COLOR_LENGTH_WITH_ALPHA : HEX_COLOR_LENGTH)) { setColor(Color.valueOf(hexField.getText()), false); } } }); return table; } public void focusHexField () { if (!isShowHexFields()) { return; } FocusManager.switchFocus(getStage(), hexField); getStage().setKeyboardFocus(hexField); } protected void createColorWidgets () { PickerChangeListener pickerListener = new PickerChangeListener(); palette = new Palette(commons, 100, pickerListener); verticalBar = new VerticalChannelBar(commons, 360, pickerListener); } protected void updateUI () { palette.setPickerHue(verticalBar.getValue()); newColorImg.setColor(color); hexField.setText(color.toString().toUpperCase()); hexField.setCursorPosition(hexField.getMaxLength()); if (listener != null) listener.changed(color); } /** Updates picker ui from current color */ protected void updateValuesFromCurrentColor () { int[] hsv = ColorUtils.RGBtoHSV(color); int ch = hsv[0]; int cs = hsv[1]; int cv = hsv[2]; verticalBar.setValue(ch); palette.setValue(cs, cv); } protected void updateValuesFromHSVFields () { color = ColorUtils.HSVtoRGB(verticalBar.getValue(), palette.getS(), palette.getV(), color.a); } public void restoreLastColor () { Color colorBeforeReset = new Color(color); setColor(oldColor); if (listener != null) listener.reset(colorBeforeReset, color); } /** Sets current selected color in picker. */ @Override public void setColor (Color newColor) { if (allowAlphaEdit == false) newColor.a = 1; //this method overrides setColor in Actor, not big deal we definitely don't need it setColor(newColor, true); } protected void setColor (Color newColor, boolean updateCurrentColor) { if (updateCurrentColor) { currentColorImg.setColor(new Color(newColor)); oldColor = new Color(newColor); } color = new Color(newColor); updateValuesFromCurrentColor(); updateUI(); } public ColorPickerListener getListener () { return listener; } public void setListener (ColorPickerListener listener) { this.listener = listener; } /** * @param allowAlphaEdit if false this picker will have disabled editing color alpha channel. If current picker color * has alpha it will be reset to 1. If true alpha editing will be re-enabled. For better UX this should not be called * while ColorPicker is visible. */ public void setAllowAlphaEdit (boolean allowAlphaEdit) { this.allowAlphaEdit = allowAlphaEdit; hexField.setMaxLength(allowAlphaEdit ? HEX_COLOR_LENGTH_WITH_ALPHA : HEX_COLOR_LENGTH); if (allowAlphaEdit == false) { setColor(new Color(color)); } } public boolean isAllowAlphaEdit () { return allowAlphaEdit; } public void setShowHexFields (boolean showHexFields) { this.showHexFields = showHexFields; hexTable.setVisible(showHexFields); rebuildMainTable(); } public boolean isShowHexFields () { return showHexFields; } public void setShowColorPreviews(boolean showColorPreviews) { this.showColorPreviews = showColorPreviews; colorPreviewsTable.setVisible(showColorPreviews); rebuildMainTable(); } public boolean isShowColorPreviews() { return showColorPreviews; } @Override public void draw (Batch batch, float parentAlpha) { boolean wasPedantic = ShaderProgram.pedantic; ShaderProgram.pedantic = false; super.draw(batch, parentAlpha); ShaderProgram.pedantic = wasPedantic; } public boolean isDisposed () { return disposed; } @Override public void dispose () { if (disposed) throw new IllegalStateException("ColorPicker can't be disposed twice!"); commons.dispose(); disposed = true; } /** Internal default picker listener used to get events from color widgets */ class PickerChangeListener extends ChangeListener { protected void updateLinkedWidget () { } @Override public void changed (ChangeEvent event, Actor actor) { updateLinkedWidget(); updateValuesFromHSVFields(); updateUI(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ColorPicker.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Disposable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.ButtonBar; import com.kotcrab.vis.ui.widget.ButtonBar.ButtonType; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.VisTextButton; import com.kotcrab.vis.ui.widget.VisWindow; import static com.kotcrab.vis.ui.widget.color.internal.ColorPickerText.*; /** * Color Picker dialog, allows user to select color. ColorPicker is relatively heavy dialog and should be reused whenever possible. * This dialog must be disposed when no longer needed! ColorPicker will be centered on screen after adding to Stage * use {@link #setCenterOnAdd(boolean)} to change this. * @author Kotcrab * @see ColorPicker * @see BasicColorPicker * @see ExtendedColorPicker * @since 0.6.0 */ public class ColorPicker extends VisWindow implements Disposable { private ExtendedColorPicker picker; private ColorPickerListener listener; private VisTextButton restoreButton; private VisTextButton cancelButton; private VisTextButton okButton; private boolean closeAfterPickingFinished = true; private boolean fadeOutDueToCanceled; public ColorPicker () { this((String) null); } public ColorPicker (String title) { this("default", title, null); } public ColorPicker (String title, ColorPickerListener listener) { this("default", title, listener); } public ColorPicker (ColorPickerListener listener) { this("default", null, listener); } public ColorPicker (String styleName, String title, ColorPickerListener listener) { super(title != null ? title : "", VisUI.getSkin().get(styleName, ColorPickerStyle.class)); this.listener = listener; ColorPickerStyle style = (ColorPickerStyle) getStyle(); if (title == null) getTitleLabel().setText(TITLE.get()); setModal(true); setMovable(true); addCloseButton(); closeOnEscape(); picker = new ExtendedColorPicker(style.pickerStyle, listener); add(picker); row(); add(createButtons()).pad(3).right().expandX().colspan(3); pack(); centerWindow(); createListeners(); } private VisTable createButtons () { ButtonBar buttonBar = new ButtonBar(); buttonBar.setIgnoreSpacing(true); buttonBar.setButton(ButtonType.LEFT, restoreButton = new VisTextButton(RESTORE.get())); buttonBar.setButton(ButtonType.OK, okButton = new VisTextButton(OK.get())); buttonBar.setButton(ButtonType.CANCEL, cancelButton = new VisTextButton(CANCEL.get())); return buttonBar.createTable(); } private void createListeners () { restoreButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { picker.restoreLastColor(); } }); okButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (listener != null) listener.finished(new Color(picker.color)); setColor(picker.color); if (closeAfterPickingFinished) fadeOut(); } }); cancelButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { fadeOutDueToCanceled = true; close(); } }); } @Override protected void setStage (Stage stage) { super.setStage(stage); if (stage == null && fadeOutDueToCanceled) { fadeOutDueToCanceled = false; setColor(picker.oldColor); } } /** * Controls whether to fade out color picker after users finished color picking and has pressed OK button. If * this is set to false picker won't close after pressing OK button. Default is true. * Note that by default picker is a modal window so might also want to call {@code colorPicker.setModal(false)} to * disable it. */ public void setCloseAfterPickingFinished (boolean closeAfterPickingFinished) { this.closeAfterPickingFinished = closeAfterPickingFinished; } @Override protected void close () { if (listener != null) listener.canceled(picker.oldColor); super.close(); } @Override public void dispose () { picker.dispose(); } /** @return internal dialog color picker */ public ExtendedColorPicker getPicker () { return picker; } // ColorPicker delegates public boolean isShowHexFields () { return picker.isShowHexFields(); } public void setShowHexFields (boolean showHexFields) { picker.setShowHexFields(showHexFields); } public boolean isDisposed () { return picker.isDisposed(); } public void setAllowAlphaEdit (boolean allowAlphaEdit) { picker.setAllowAlphaEdit(allowAlphaEdit); } public boolean isAllowAlphaEdit () { return picker.isAllowAlphaEdit(); } public void restoreLastColor () { picker.restoreLastColor(); } @Override public void setColor (Color newColor) { picker.setColor(newColor); } public void setListener (ColorPickerListener listener) { this.listener = listener; picker.setListener(listener); } public ColorPickerListener getListener () { return picker.getListener(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ColorPickerAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.graphics.Color; /** * Empty implementation of {@link ColorPickerListener}. * @author Kotcrab */ public class ColorPickerAdapter implements ColorPickerListener { @Override public void canceled (Color oldColor) { } @Override public void changed (Color newColor) { } @Override public void reset (Color previousColor, Color newColor) { } @Override public void finished (Color newColor) { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ColorPickerListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.graphics.Color; /** * Listener for {@link ColorPicker}. * @author Kotcrab */ public interface ColorPickerListener { /** * Called when color selection was canceled by user (either by clicking cancel or closing the window). Note that this * event can only occur when using {@link ColorPicker} dialog. */ void canceled (Color oldColor); /** * Called when currently selected color in picker has changed. This does not mean that user finished selecting color, if * you are only interested in that event use {@link #finished(Color)} or {@link #canceled(Color)}. */ void changed (Color newColor); /** * Called when selected color in picker were reset to previously select one. * @param previousColor color that was set before reset. * @param newColor new picker color. */ void reset (Color previousColor, Color newColor); /** * Called when user has finished selecting new color. Note that this * event can only occur when using {@link ColorPicker} dialog. */ void finished (Color newColor); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ColorPickerStyle.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.scenes.scene2d.ui.Window.WindowStyle; /** @author Kotcrab */ public class ColorPickerStyle extends WindowStyle { public ColorPickerWidgetStyle pickerStyle; public ColorPickerStyle () { } public ColorPickerStyle (ColorPickerStyle style) { super(style); if (style.pickerStyle != null) this.pickerStyle = new ColorPickerWidgetStyle(style.pickerStyle); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ColorPickerWidgetStyle.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; /** * Style used by {@link ExtendedColorPicker} and {@link BasicColorPicker}. * {@link ColorPicker} dialog is using {@link ColorPickerStyle}. * @author Kotcrab */ public class ColorPickerWidgetStyle { public Drawable barSelector; public Drawable cross; public Drawable verticalSelector; public Drawable horizontalSelector; public Drawable iconArrowRight; public ColorPickerWidgetStyle () { } public ColorPickerWidgetStyle (ColorPickerWidgetStyle other) { this.barSelector = other.barSelector; this.cross = other.cross; this.verticalSelector = other.verticalSelector; this.horizontalSelector = other.horizontalSelector; this.iconArrowRight = other.iconArrowRight; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/ExtendedColorPicker.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Disposable; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.util.ColorUtils; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.color.internal.ChannelBar; import com.kotcrab.vis.ui.widget.color.internal.ColorChannelWidget; import com.kotcrab.vis.ui.widget.color.internal.Palette; import com.kotcrab.vis.ui.widget.color.internal.VerticalChannelBar; /** * Color Picker widget, allows user to select color. ColorPicker is relatively heavy widget and should be reused if possible. * Unlike other widgets, this one must be disposed when no longer needed! *

* Extends {@link BasicColorPicker} functionality and additionally provides separate bars for H, S, V color components. * Additional 3 bars (R, G, B) for selecting colors using RGB systems and dedicated bar to alpha channel. Alpha edition * is enabled by default. *

* Used directly by {@link ColorPicker} dialog. * @author Kotcrab * @see BasicColorPicker * @see ColorPicker * @since 0.9.3 */ public class ExtendedColorPicker extends BasicColorPicker implements Disposable { private ColorChannelWidget hBar; private ColorChannelWidget sBar; private ColorChannelWidget vBar; private ColorChannelWidget rBar; private ColorChannelWidget gBar; private ColorChannelWidget bBar; private ColorChannelWidget aBar; public ExtendedColorPicker () { this(null); } public ExtendedColorPicker (ColorPickerListener listener) { this("default", listener); } public ExtendedColorPicker (String styleName, ColorPickerListener listener) { this(VisUI.getSkin().get(styleName, ColorPickerWidgetStyle.class), listener); } public ExtendedColorPicker (ColorPickerWidgetStyle style, ColorPickerListener listener) { super(style, listener, true); setAllowAlphaEdit(true); } @Override protected void createUI () { super.createUI(); VisTable extendedTable = new VisTable(true); //displayed next to mainTable extendedTable.add(hBar).row(); extendedTable.add(sBar).row(); extendedTable.add(vBar).row(); extendedTable.add(); extendedTable.row(); extendedTable.add(rBar).row(); extendedTable.add(gBar).row(); extendedTable.add(bBar).row(); extendedTable.add(); extendedTable.row(); extendedTable.add(aBar).row(); add(extendedTable).expand().left().top().pad(0, 9, 4, 4); } @Override protected void createColorWidgets () { palette = new Palette(commons, 100, new PickerChangeListener() { @Override public void updateLinkedWidget () { sBar.setValue(palette.getS()); vBar.setValue(palette.getV()); } }); verticalBar = new VerticalChannelBar(commons, 360, new PickerChangeListener() { @Override public void updateLinkedWidget () { hBar.setValue(verticalBar.getValue()); } }); HsvChannelBarListener svListener = new HsvChannelBarListener() { @Override protected void updateLinkedWidget () { palette.setValue(sBar.getValue(), vBar.getValue()); } }; hBar = new ColorChannelWidget(commons, "H", ChannelBar.MODE_H, 360, new HsvChannelBarListener() { @Override protected void updateLinkedWidget () { verticalBar.setValue(hBar.getValue()); } }); sBar = new ColorChannelWidget(commons, "S", ChannelBar.MODE_S, 100, svListener); vBar = new ColorChannelWidget(commons, "V", ChannelBar.MODE_V, 100, svListener); RgbChannelBarListener rgbListener = new RgbChannelBarListener(); rBar = new ColorChannelWidget(commons, "R", ChannelBar.MODE_R, 255, rgbListener); gBar = new ColorChannelWidget(commons, "G", ChannelBar.MODE_G, 255, rgbListener); bBar = new ColorChannelWidget(commons, "B", ChannelBar.MODE_B, 255, rgbListener); aBar = new ColorChannelWidget(commons, "A", ChannelBar.MODE_ALPHA, 255, new AlphaChannelBarListener()); } @Override public void setAllowAlphaEdit (boolean allowAlphaEdit) { aBar.setVisible(allowAlphaEdit); super.setAllowAlphaEdit(allowAlphaEdit); } @Override protected void updateValuesFromCurrentColor () { int[] hsv = ColorUtils.RGBtoHSV(color); int ch = hsv[0]; int cs = hsv[1]; int cv = hsv[2]; int cr = MathUtils.round(color.r * 255.0f); int cg = MathUtils.round(color.g * 255.0f); int cb = MathUtils.round(color.b * 255.0f); int ca = MathUtils.round(color.a * 255.0f); hBar.setValue(ch); sBar.setValue(cs); vBar.setValue(cv); rBar.setValue(cr); gBar.setValue(cg); bBar.setValue(cb); aBar.setValue(ca); verticalBar.setValue(hBar.getValue()); palette.setValue(sBar.getValue(), vBar.getValue()); } /** Updates picker from H, S and V bars */ @Override protected void updateValuesFromHSVFields () { int[] hsv = ColorUtils.RGBtoHSV(color); int h = hsv[0]; int s = hsv[1]; int v = hsv[2]; if (hBar.isInputValid()) h = hBar.getValue(); if (sBar.isInputValid()) s = sBar.getValue(); if (vBar.isInputValid()) v = vBar.getValue(); color = ColorUtils.HSVtoRGB(h, s, v, color.a); int cr = MathUtils.round(color.r * 255.0f); int cg = MathUtils.round(color.g * 255.0f); int cb = MathUtils.round(color.b * 255.0f); rBar.setValue(cr); gBar.setValue(cg); bBar.setValue(cb); } /** Updates picker from R, G and B bars */ private void updateValuesFromRGBFields () { int r = MathUtils.round(color.r * 255.0f); int g = MathUtils.round(color.g * 255.0f); int b = MathUtils.round(color.b * 255.0f); if (rBar.isInputValid()) r = rBar.getValue(); if (gBar.isInputValid()) g = gBar.getValue(); if (bBar.isInputValid()) b = bBar.getValue(); color.set(r / 255.0f, g / 255.0f, b / 255.0f, color.a); int[] hsv = ColorUtils.RGBtoHSV(color); int ch = hsv[0]; int cs = hsv[1]; int cv = hsv[2]; hBar.setValue(ch); sBar.setValue(cs); vBar.setValue(cv); verticalBar.setValue(hBar.getValue()); palette.setValue(sBar.getValue(), vBar.getValue()); } private class RgbChannelBarListener implements ChannelBar.ChannelBarListener { @Override public void updateFields () { updateValuesFromRGBFields(); updateUI(); } @Override public void setShaderUniforms (ShaderProgram shader) { shader.setUniformf("u_r", color.r); shader.setUniformf("u_g", color.g); shader.setUniformf("u_b", color.b); } } private class AlphaChannelBarListener extends RgbChannelBarListener { @Override public void updateFields () { if (aBar.isInputValid()) color.a = aBar.getValue() / 255.0f; updateUI(); } } private abstract class HsvChannelBarListener implements ChannelBar.ChannelBarListener { @Override public void updateFields () { updateLinkedWidget(); updateValuesFromHSVFields(); updateUI(); } @Override public void setShaderUniforms (ShaderProgram shader) { shader.setUniformf("u_h", hBar.getValue() / 360.0f); shader.setUniformf("u_s", sBar.getValue() / 100.0f); shader.setUniformf("u_v", vBar.getValue() / 100.0f); } protected abstract void updateLinkedWidget (); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/AlphaChannelBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; /** * Channel bar intended for alpha channel, renders alpha grid below channel texture. * @author Kotcrab */ public class AlphaChannelBar extends ChannelBar { private GridSubImage gridImage; public AlphaChannelBar (PickerCommons commons, int mode, int maxValue, ChangeListener changeListener) { super(commons, mode, maxValue, changeListener); gridImage = new GridSubImage(commons.gridShader, commons.whiteTexture, 6 * commons.sizes.scaleFactor); } @Override public void draw (Batch batch, float parentAlpha) { gridImage.draw(batch, this); super.draw(batch, parentAlpha); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/AlphaImage.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.g2d.Batch; import com.kotcrab.vis.ui.widget.VisImage; /** * Image that displays checkerboard as background, used by ColorPicker to display selected colors with alphas. Note that for perfect grid * this image should have size which is multiplication of gridSize. Eg. if gridSize is equal to 5, this image can have size 65x100. * (because both 65 and 100 are divisible by 5) * @author Kotcrab */ public class AlphaImage extends VisImage { private GridSubImage gridImage; public AlphaImage (PickerCommons commons, float gridSize) { super(commons.whiteTexture); gridImage = new GridSubImage(commons.gridShader, commons.whiteTexture, gridSize); } @Override public void draw (Batch batch, float parentAlpha) { //don't draw grid if widget alpha is different than 1 because //this creates weird affect when window is fading in/out, //both parent image and grid is visible if (getColor().a != 1) gridImage.draw(batch, this); super.draw(batch, parentAlpha); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/ChannelBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.utils.Pools; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.widget.color.BasicColorPicker; import com.kotcrab.vis.ui.widget.color.ColorPickerWidgetStyle; /** * Used to display channel color bars in color picker. * @author Kotcrab */ public class ChannelBar extends ShaderImage { public static final int MODE_ALPHA = 0; public static final int MODE_R = 1; public static final int MODE_G = 2; public static final int MODE_B = 3; public static final int MODE_H = 4; public static final int MODE_S = 5; public static final int MODE_V = 6; protected ColorPickerWidgetStyle style; private Sizes sizes; private int maxValue; private int value; private float selectorX; private int mode; private ChannelBarListener channelBarListener; public ChannelBar (PickerCommons commons, int mode, int maxValue, ChangeListener changeListener) { super(commons.getBarShader(mode), commons.whiteTexture); this.style = commons.style; this.sizes = commons.sizes; this.mode = mode; this.maxValue = maxValue; setTouchable(Touchable.enabled); setValue(value); addListener(changeListener); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { updateValueFromTouch(x); return true; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { updateValueFromTouch(x); } }); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); style.barSelector.draw(batch, getX() + selectorX - style.barSelector.getMinWidth() / 2, getY() - 1, style.barSelector.getMinWidth(), style.barSelector.getMinHeight()); } public void setValue (int newValue) { this.value = newValue; if (value < 0) value = 0; if (value > maxValue) value = maxValue; selectorX = ((float) value / maxValue) * BasicColorPicker.BAR_WIDTH * sizes.scaleFactor; } public int getValue () { return value; } private void updateValueFromTouch (float x) { int newValue = (int) (x / BasicColorPicker.BAR_WIDTH * maxValue / sizes.scaleFactor); setValue(newValue); ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); fire(changeEvent); Pools.free(changeEvent); } @Override protected void setShaderUniforms (ShaderProgram shader) { shader.setUniformi("u_mode", mode); channelBarListener.setShaderUniforms(shader); } public void setChannelBarListener (ChannelBarListener channelBarListener) { this.channelBarListener = channelBarListener; } public interface ChannelBarListener { void updateFields (); void setShaderUniforms (ShaderProgram shader); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/ColorChannelWidget.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.color.BasicColorPicker; import com.kotcrab.vis.ui.widget.color.ColorPickerWidgetStyle; import com.kotcrab.vis.ui.widget.color.internal.ColorInputField.ColorInputFieldListener; /** * Used to display one color channel (hue, saturation etc.) with label, ColorInputField and ChannelBar. * @author Kotcrab */ public class ColorChannelWidget extends VisTable { private PickerCommons commons; private ColorPickerWidgetStyle style; private Sizes sizes; private ChannelBar bar; private ChangeListener barListener; private ColorInputField inputField; private int mode; private int value; private int maxValue; public ColorChannelWidget (PickerCommons commons, String label, int mode, int maxValue, final ChannelBar.ChannelBarListener listener) { super(true); this.commons = commons; this.style = commons.style; this.sizes = commons.sizes; this.mode = mode; this.maxValue = maxValue; barListener = new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { value = bar.getValue(); listener.updateFields(); inputField.setValue(value); } }; add(new VisLabel(label)).width(10 * sizes.scaleFactor).center(); add(inputField = new ColorInputField(maxValue, new ColorInputFieldListener() { @Override public void changed (int newValue) { value = newValue; listener.updateFields(); bar.setValue(newValue); } })).width(BasicColorPicker.FIELD_WIDTH * sizes.scaleFactor); add(bar = createBarImage()).size(BasicColorPicker.BAR_WIDTH * sizes.scaleFactor, BasicColorPicker.BAR_HEIGHT * sizes.scaleFactor); bar.setChannelBarListener(listener); inputField.setValue(0); } public int getValue () { return value; } public void setValue (int value) { this.value = value; inputField.setValue(value); bar.setValue(value); } private ChannelBar createBarImage () { if (mode == ChannelBar.MODE_ALPHA) return new AlphaChannelBar(commons, mode, maxValue, barListener); else return new ChannelBar(commons, mode, maxValue, barListener); } public ChannelBar getBar () { return bar; } public boolean isInputValid () { return inputField.isInputValid(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/ColorInputField.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.FocusListener; import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Fields used to enter color numbers in color picker, verifies max allowed value * provides quick increment/decrement of current value by pressing [shift +] plus or minus on numpad * @author Kotcrab */ public class ColorInputField extends VisValidatableTextField { private int value; private int maxValue; public ColorInputField (final int maxValue, final ColorInputFieldListener listener) { super(new ColorFieldValidator(maxValue)); this.value = 0; this.maxValue = maxValue; setProgrammaticChangeEvents(false); setMaxLength(3); setTextFieldFilter(new NumberFilter()); addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (getText().length() > 0) value = Integer.valueOf(getText()); } }); addListener(new InputListener() { @Override public boolean keyTyped (InputEvent event, char character) { ColorInputField field = (ColorInputField) event.getListenerActor(); if (character == '+') field.changeValue(UIUtils.shift() ? 10 : 1); if (character == '-') field.changeValue(UIUtils.shift() ? -10 : -1); if (character != 0) listener.changed(getValue()); return true; } }); addListener(new FocusListener() { @Override public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (focused == false && isInputValid() == false) setValue(maxValue); //only possibility on invalid field is that entered value will be bigger than maxValue so we set field value to maxValue } }); } public void changeValue (int byValue) { this.value += byValue; if (value > maxValue) value = maxValue; if (value < 0) value = 0; updateUI(); } public int getValue () { return value; } public void setValue (int value) { this.value = value; updateUI(); } private void updateUI () { setText(String.valueOf(value)); setCursorPosition(getMaxLength()); } public interface ColorInputFieldListener { void changed (int newValue); } private static class NumberFilter implements TextFieldFilter { @Override public boolean acceptChar (VisTextField textField, char c) { return Character.isDigit(c); } } private static class ColorFieldValidator implements InputValidator { private int maxValue; public ColorFieldValidator (int maxValue) { this.maxValue = maxValue; } @Override public boolean validateInput (String input) { if (input.equals("")) return false; Integer number = Integer.parseInt(input); if (number > maxValue) return false; return true; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/ColorPickerText.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.utils.I18NBundle; import com.kotcrab.vis.ui.Locales; import com.kotcrab.vis.ui.i18n.BundleText; /** * Contains texts for chooser access via I18NBundle. * @author Kotcrab * @since 0.7.0 */ public enum ColorPickerText implements BundleText { TITLE("title"), RESTORE("restore"), CANCEL("cancel"), OK("ok"), HEX("hex"); private final String name; ColorPickerText (final String name) { this.name = name; } private static I18NBundle getBundle () { return Locales.getColorPickerBundle(); } @Override public final String getName () { return name; } @Override public final String get () { return getBundle().get(name); } @Override public final String format () { return getBundle().format(name); } @Override public final String format (final Object... arguments) { return getBundle().format(name, arguments); } @Override public final String toString () { return get(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/GridSubImage.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.scenes.scene2d.ui.Image; /** @author Kotcrab */ public class GridSubImage { private ShaderProgram gridShader; private Texture whiteTexture; private float gridSize; public GridSubImage (ShaderProgram gridShader, Texture whiteTexture, float gridSize) { this.gridShader = gridShader; this.whiteTexture = whiteTexture; this.gridSize = gridSize; } public void draw (Batch batch, Image parent) { ShaderProgram originalShader = batch.getShader(); batch.setShader(gridShader); gridShader.setUniformf("u_width", parent.getWidth()); gridShader.setUniformf("u_height", parent.getHeight()); gridShader.setUniformf("u_gridSize", gridSize); batch.draw(whiteTexture, parent.getX() + parent.getImageX(), parent.getY() + parent.getImageY(), parent.getImageWidth() * parent.getScaleX(), parent.getImageHeight() * parent.getScaleY()); batch.setShader(originalShader); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/Palette.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.utils.Pools; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.widget.color.BasicColorPicker; import com.kotcrab.vis.ui.widget.color.ColorPickerWidgetStyle; /** * Colors palette used to display colors using all possible values of saturation and value. * @author Kotcrab */ public class Palette extends ShaderImage { private ColorPickerWidgetStyle style; private Sizes sizes; private int xV, yS; private int maxValue; private float selectorX; private float selectorY; private float pickerHue; public Palette (PickerCommons commons, int maxValue, ChangeListener listener) { super(commons.paletteShader, commons.whiteTexture); this.style = commons.style; this.sizes = commons.sizes; this.maxValue = maxValue; setTouchable(Touchable.enabled); setValue(0, 0); addListener(listener); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { updateValueFromTouch(x, y); return true; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { updateValueFromTouch(x, y); } }); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); style.verticalSelector.draw(batch, getX(), getY() + selectorY - style.verticalSelector.getMinHeight() / 2 + 0.1f, getImageWidth(), style.verticalSelector.getMinHeight()); style.horizontalSelector.draw(batch, getX() + selectorX - style.horizontalSelector.getMinWidth() / 2 + 0.1f, getY(), style.horizontalSelector.getMinWidth(), getImageHeight()); style.cross.draw(batch, getX() + selectorX - style.cross.getMinWidth() / 2 + 0.1f, getY() + selectorY - style.cross.getMinHeight() / 2 + 0.1f, style.cross.getMinWidth(), style.cross.getMinHeight()); } @Override protected void setShaderUniforms (ShaderProgram shader) { shader.setUniformf("u_h", pickerHue); } public void setPickerHue (int pickerHue) { this.pickerHue = pickerHue / 360.0f; } public void setValue (int s, int v) { xV = v; yS = s; if (xV < 0) xV = 0; if (xV > maxValue) xV = maxValue; if (yS < 0) yS = 0; if (yS > maxValue) yS = maxValue; selectorX = ((float) xV / maxValue) * BasicColorPicker.PALETTE_SIZE * sizes.scaleFactor; selectorY = ((float) yS / maxValue) * BasicColorPicker.PALETTE_SIZE * sizes.scaleFactor; } private void updateValueFromTouch (float touchX, float touchY) { int newV = (int) (touchX / BasicColorPicker.PALETTE_SIZE * maxValue / sizes.scaleFactor); int newS = (int) (touchY / BasicColorPicker.PALETTE_SIZE * maxValue / sizes.scaleFactor); setValue(newS, newV); ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); fire(changeEvent); Pools.free(changeEvent); } public int getV () { return xV; } public int getS () { return yS; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/PickerCommons.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureWrap; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.utils.Disposable; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.widget.color.ColorPickerWidgetStyle; /** @author Kotcrab */ public class PickerCommons implements Disposable { final ColorPickerWidgetStyle style; final Sizes sizes; private boolean loadExtendedShaders; ShaderProgram paletteShader; ShaderProgram verticalChannelShader; ShaderProgram hsvShader; ShaderProgram rgbShader; ShaderProgram gridShader; Texture whiteTexture; public PickerCommons (ColorPickerWidgetStyle style, Sizes sizes, boolean loadExtendedShaders) { this.style = style; this.sizes = sizes; this.loadExtendedShaders = loadExtendedShaders; createPixmap(); loadShaders(); } private void createPixmap () { Pixmap whitePixmap = new Pixmap(2, 2, Format.RGB888); whitePixmap.setColor(Color.WHITE); whitePixmap.drawRectangle(0, 0, 2, 2); whiteTexture = new Texture(whitePixmap); whiteTexture.setWrap(TextureWrap.Repeat, TextureWrap.Repeat); whitePixmap.dispose(); } private void loadShaders () { paletteShader = loadShader("default.vert", "palette.frag"); verticalChannelShader = loadShader("default.vert", "verticalBar.frag"); gridShader = loadShader("default.vert", "checkerboard.frag"); if (loadExtendedShaders) { hsvShader = loadShader("default.vert", "hsv.frag"); rgbShader = loadShader("default.vert", "rgb.frag"); } } private ShaderProgram loadShader (String vertFile, String fragFile) { ShaderProgram program = new ShaderProgram( Gdx.files.classpath("com/kotcrab/vis/ui/widget/color/internal/" + vertFile), Gdx.files.classpath("com/kotcrab/vis/ui/widget/color/internal/" + fragFile)); if (program.isCompiled() == false) { throw new IllegalStateException("ColorPicker shader compilation failed. Shader: " + vertFile + ", " + fragFile + ": " + program.getLog()); } return program; } ShaderProgram getBarShader (int mode) { switch (mode) { case ChannelBar.MODE_ALPHA: case ChannelBar.MODE_R: case ChannelBar.MODE_G: case ChannelBar.MODE_B: return rgbShader; case ChannelBar.MODE_H: case ChannelBar.MODE_S: case ChannelBar.MODE_V: return hsvShader; default: throw new IllegalStateException("Unsupported mode: " + mode); } } @Override public void dispose () { whiteTexture.dispose(); paletteShader.dispose(); verticalChannelShader.dispose(); gridShader.dispose(); if (loadExtendedShaders) { hsvShader.dispose(); rgbShader.dispose(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/ShaderImage.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.kotcrab.vis.ui.widget.VisImage; /** * Allow to render standard {@link VisImage} with shader. Shaders uniforms can be set in {@link #setShaderUniforms(ShaderProgram)} * @author Kotcrab */ public class ShaderImage extends VisImage { private ShaderProgram shader; public ShaderImage (ShaderProgram shader, Texture texture) { super(texture); this.shader = shader; } @Override public void draw (Batch batch, float parentAlpha) { ShaderProgram originalShader = batch.getShader(); batch.setShader(shader); setShaderUniforms(shader); super.draw(batch, parentAlpha); batch.setShader(originalShader); } protected void setShaderUniforms (ShaderProgram shader) { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/VerticalChannelBar.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.color.internal; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.utils.Pools; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.widget.color.BasicColorPicker; import com.kotcrab.vis.ui.widget.color.ColorPickerWidgetStyle; /** * Vertical channel bar is used to display vertical hue bar * @author Kotcrab */ public class VerticalChannelBar extends ShaderImage { private ColorPickerWidgetStyle style; private Sizes sizes; private int maxValue; private float selectorY; private int value; public VerticalChannelBar (PickerCommons commons, int maxValue, ChangeListener listener) { super(commons.verticalChannelShader, commons.whiteTexture); this.style = commons.style; this.sizes = commons.sizes; this.maxValue = maxValue; setTouchable(Touchable.enabled); setValue(0); addListener(listener); addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { updateValueFromTouch(y); return true; } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { updateValueFromTouch(y); } }); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); style.verticalSelector.draw(batch, getX(), getY() + getImageY() + selectorY - 2.5f, getImageWidth(), style.verticalSelector.getMinHeight()); } public void setValue (int newValue) { value = newValue; if (value < 0) value = 0; if (value > maxValue) value = maxValue; selectorY = ((float) value / maxValue) * BasicColorPicker.PALETTE_SIZE * sizes.scaleFactor; } private void updateValueFromTouch (float y) { int newValue = (int) (y / BasicColorPicker.PALETTE_SIZE * maxValue / sizes.scaleFactor); setValue(newValue); ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class); fire(changeEvent); Pools.free(changeEvent); } public int getValue () { return value; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/color/internal/package-info.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Internal ColorPicker utilities. All files in this package are not considered as public VisUI API. Changes to those classes * won't be listed in CHANGES file. * @author Kotcrab */ package com.kotcrab.vis.ui.widget.color.internal; ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileChooser.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.*; import com.badlogic.gdx.utils.*; import com.kotcrab.vis.ui.FocusManager; import com.kotcrab.vis.ui.Focusable; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.layout.GridGroup; import com.kotcrab.vis.ui.util.OsUtils; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.util.dialog.Dialogs.OptionDialogType; import com.kotcrab.vis.ui.util.dialog.InputDialogAdapter; import com.kotcrab.vis.ui.util.dialog.OptionDialogAdapter; import com.kotcrab.vis.ui.util.value.ConstantIfVisibleValue; import com.kotcrab.vis.ui.util.value.PrefHeightIfVisibleValue; import com.kotcrab.vis.ui.util.value.PrefWidthIfVisibleValue; import com.kotcrab.vis.ui.widget.*; import com.kotcrab.vis.ui.widget.Tooltip; import com.kotcrab.vis.ui.widget.ButtonBar.ButtonType; import com.kotcrab.vis.ui.widget.file.internal.*; import com.kotcrab.vis.ui.widget.file.internal.DriveCheckerService.DriveCheckerListener; import com.kotcrab.vis.ui.widget.file.internal.DriveCheckerService.RootMode; import com.kotcrab.vis.ui.widget.file.internal.FileChooserWinService.RootNameListener; import com.kotcrab.vis.ui.widget.file.internal.FileHistoryManager.FileHistoryCallback; import com.kotcrab.vis.ui.widget.file.internal.FilePopupMenu.FilePopupMenuCallback; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.lang.StringBuilder; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static com.kotcrab.vis.ui.widget.file.internal.FileChooserText.*; /** * Widget allowing user to choose files. FileChooser is heavy widget and should be reused whenever possible, typically * one instance is enough for application. Chooser is platform dependent and can be only used on desktop. *

* FileChooser will be centered on screen after adding to Stage use {@link #setCenterOnAdd(boolean)} to change this. * @author Kotcrab * @since 0.1.0 */ public class FileChooser extends VisWindow implements FileHistoryCallback { private static final long FILE_WATCHER_CHECK_DELAY_MILLIS = 2000; private static final ShortcutsComparator SHORTCUTS_COMPARATOR = new ShortcutsComparator(); private static final Vector2 tmpVector = new Vector2(); private static boolean saveLastDirectory = false; public static boolean focusFileScrollPaneOnShow = true; public static boolean focusSelectedFileTextFieldOnShow = true; private Mode mode; private ViewMode viewMode = ViewMode.DETAILS; private SelectionMode selectionMode = SelectionMode.FILES; private AtomicReference sorting = new AtomicReference(FileSorting.NAME); private AtomicBoolean sortingOrderAscending = new AtomicBoolean(true); private FileChooserListener listener = new FileChooserAdapter(); private FileFilter fileFilter = new DefaultFileFilter(this); private FileDeleter fileDeleter = new DefaultFileDeleter(); private FileTypeFilter fileTypeFilter = null; private FileTypeFilter.Rule activeFileTypeRule = null; private FileIconProvider iconProvider; private DriveCheckerService driveCheckerService = DriveCheckerService.getInstance(); private Array driveCheckerListeners = new Array(); private FileChooserWinService chooserWinService = FileChooserWinService.getInstance(); private ExecutorService listDirExecutor = Executors.newSingleThreadExecutor(new ServiceThreadFactory("FileChooserListDirThread")); private Future listDirFuture; private ShowBusyBarTask showBusyBarTask = new ShowBusyBarTask(); private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private boolean showSelectionCheckboxes = false; public static final int DEFAULT_KEY = -1; private boolean multiSelectionEnabled = false; private int groupMultiSelectKey = DEFAULT_KEY; //shift by default private int multiSelectKey = DEFAULT_KEY; //ctrl (or command on mac) by default private PreferencesIO preferencesIO; private Array favorites; private Array recentDirectories; private FileHandle currentDirectory; private Array currentFiles = new Array(); private IdentityMap currentFilesMetadata = new IdentityMap(); private FileListAdapter fileListAdapter; private Array selectedItems = new Array(); private ShortcutItem selectedShortcut; private String defaultFileName; private boolean watchingFilesEnabled = true; private Thread fileWatcherThread; private boolean shortcutsListRebuildScheduled; private boolean filesListRebuildScheduled; private FileHistoryManager historyManager; // UI private FileChooserStyle style; private Sizes sizes; private VisSplitPane mainSplitPane; private VisTable shortcutsTable; private VerticalGroup shortcutsMainPanel; private VerticalGroup shortcutsRootsPanel; private VerticalGroup shortcutsFavoritesPanel; private ListView fileListView; private float maxDateLabelWidth; private BusyBar fileListBusyBar; private VisImageButton favoriteFolderButton; private VisImageButton viewModeButton; private Tooltip favoriteFolderButtonTooltip; private VisTextField currentPath; private VisTextField selectedFileTextField; private VisSelectBox fileTypeSelectBox; private VisTextButton confirmButton; private FilePopupMenu fileMenu; private FileSuggestionPopup fileNameSuggestionPopup; private DirsSuggestionPopup dirsSuggestionPopup; private VisLabel fileTypeLabel; private PopupMenu viewModePopupMenu; /** @param mode whether this chooser will be used to open or save files */ public FileChooser (Mode mode) { this((FileHandle) null, mode); } /** * @param directory starting chooser directory * @param mode whether this chooser will be used to open or save files */ public FileChooser (FileHandle directory, Mode mode) { super(""); this.mode = mode; getTitleLabel().setText(TITLE_CHOOSE_FILES.get()); style = VisUI.getSkin().get(FileChooserStyle.class); sizes = VisUI.getSizes(); init(directory); } /** * @param title chooser window title * @param mode whether this chooser will be used to open or save files */ public FileChooser (String title, Mode mode) { this("default", title, mode); } /** * @param styleName skin style name * @param title chooser window title * @param mode whether this chooser will be used to open or save files */ public FileChooser (String styleName, String title, Mode mode) { super(title); this.mode = mode; style = VisUI.getSkin().get(styleName, FileChooserStyle.class); sizes = VisUI.getSizes(); init(null); } /** * @param prefsName file name that will be used to store chooser preferences such as favorites or recent directories. * Should be your application package name with appended `.filechooser` e.g. com.seriouscompay.seriousprogram.filechooser. * This name should be unique and should not be reused with other preferences of your application to avoid key collisions. */ public static void setDefaultPrefsName (String prefsName) { PreferencesIO.setDefaultPrefsName(prefsName); } /** @deprecated replaced by {@link #setDefaultPrefsName(String)} */ @Deprecated public static void setFavoritesPrefsName (String name) { PreferencesIO.setDefaultPrefsName(name); } private void init (FileHandle directory) { setModal(true); setResizable(true); setMovable(true); addCloseButton(); closeOnEscape(); iconProvider = new DefaultFileIconProvider(this); preferencesIO = new PreferencesIO(); reloadPreferences(false); createToolbar(); viewModePopupMenu = new PopupMenu(style.popupMenuStyle); createViewModePopupMenu(); createCenterContentPanel(); createFileTextBox(); createBottomButtons(); createShortcutsMainPanel(); shortcutsRootsPanel = new VerticalGroup(); shortcutsFavoritesPanel = new VerticalGroup(); rebuildShortcutsFavoritesPanel(); fileMenu = new FilePopupMenu(this, new FilePopupMenuCallback() { @Override public void showNewDirDialog () { showNewDirectoryDialog(); } @Override public void showFileDelDialog (FileHandle file) { showFileDeleteDialog(file); } }); fileNameSuggestionPopup = new FileSuggestionPopup(this); fileNameSuggestionPopup.setListener(new PopupMenu.PopupMenuListener() { @Override public void activeItemChanged (MenuItem newItem, boolean changedByKeyboard) { if (changedByKeyboard == false || newItem == null) return; highlightFiles(currentDirectory.child(newItem.getText().toString())); updateSelectedFileFieldText(true); } }); rebuildShortcutsList(); if (directory == null) { FileHandle startingDir = null; if (saveLastDirectory) startingDir = preferencesIO.loadLastDirectory(); if (startingDir == null || startingDir.exists() == false) startingDir = getDefaultStartingDirectory(); setDirectory(startingDir, HistoryPolicy.IGNORE); } else { setDirectory(directory, HistoryPolicy.IGNORE); } setSize(500, 600); centerWindow(); createListeners(); setFileTypeFilter(null); setFavoriteFolderButtonVisible(false); } private void createToolbar () { VisTable toolbarTable = new VisTable(true); toolbarTable.defaults().minWidth(30).right(); add(toolbarTable).fillX().expandX().pad(3).padRight(2); historyManager = new FileHistoryManager(style, this); currentPath = new VisTextField(); final VisImageButton showRecentDirButton = new VisImageButton(style.expandDropdown); showRecentDirButton.setFocusBorderEnabled(false); dirsSuggestionPopup = new DirsSuggestionPopup(this, currentPath); dirsSuggestionPopup.setListener(new PopupMenu.PopupMenuListener() { @Override public void activeItemChanged (MenuItem newItem, boolean changedByKeyboard) { if (changedByKeyboard == false || newItem == null) return; setCurrentPathFieldText(newItem.getText().toString()); } }); currentPath.addListener(new InputListener() { @Override public boolean keyTyped (InputEvent event, char character) { if (event.getKeyCode() == Keys.ENTER) { dirsSuggestionPopup.remove(); return false; } float targetWidth = currentPath.getWidth() + showRecentDirButton.getWidth(); dirsSuggestionPopup.pathFieldKeyTyped(getChooserStage(), targetWidth); return false; } @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ENTER) { FileHandle file = Gdx.files.absolute(currentPath.getText()); if (file.exists()) { if (file.isDirectory() == false) file = file.parent(); setDirectory(file, HistoryPolicy.ADD); addRecentDirectory(file); } else { showDialog(POPUP_DIRECTORY_DOES_NOT_EXIST.get()); setCurrentPathFieldText(currentDirectory.path()); } event.stop(); } return false; } }); currentPath.addListener(new FocusListener() { @Override public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (focused == false) { setCurrentPathFieldText(currentDirectory.path()); } } }); showRecentDirButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { float targetWidth = currentPath.getWidth() + showRecentDirButton.getWidth(); dirsSuggestionPopup.showRecentDirectories(getChooserStage(), recentDirectories, targetWidth); } }); VisImageButton folderParentButton = new VisImageButton(style.iconFolderParent, PARENT_DIRECTORY.get()); favoriteFolderButton = new VisImageButton(style.iconStar); favoriteFolderButtonTooltip = new Tooltip.Builder(CONTEXT_MENU_ADD_TO_FAVORITES.get()).target(favoriteFolderButton).build(); viewModeButton = new VisImageButton(style.iconListSettings); new Tooltip.Builder(CHANGE_VIEW_MODE.get()).target(viewModeButton).build(); VisImageButton folderNewButton = new VisImageButton(style.iconFolderNew, NEW_DIRECTORY.get()); toolbarTable.add(historyManager.getButtonsTable()); toolbarTable.add(currentPath).spaceRight(0).expand().fill(); toolbarTable.add(showRecentDirButton).width(15 * sizes.scaleFactor).growY(); toolbarTable.add(folderParentButton); toolbarTable.add(favoriteFolderButton).width(PrefWidthIfVisibleValue.INSTANCE).spaceRight(new ConstantIfVisibleValue(sizes.spacingRight)); toolbarTable.add(viewModeButton).width(PrefWidthIfVisibleValue.INSTANCE).spaceRight(new ConstantIfVisibleValue(sizes.spacingRight)); toolbarTable.add(folderNewButton); folderParentButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { FileHandle parent = currentDirectory.parent(); // if current directory is drive root (eg. "C:/") navigating to parent // would navigate to "/" which would work but it is bad for UX if (OsUtils.isWindows() && currentDirectory.path().endsWith(":/")) return; setDirectory(parent, HistoryPolicy.ADD); } }); favoriteFolderButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (favorites.contains(currentDirectory, false)) { removeFavorite(currentDirectory); } else { addFavorite(currentDirectory); } } }); folderNewButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { showNewDirectoryDialog(); } }); addListener(historyManager.getDefaultClickListener()); } private void createViewModePopupMenu () { rebuildViewModePopupMenu(); viewModeButton.addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { //show menu on next frame, without it menu would be closed instantly it was opened //the other solution is to call event.stop but this could lead to some other PopupMenu not being closed //on touchDown event because event.stop stops event propagation Gdx.app.postRunnable(new Runnable() { @Override public void run () { viewModePopupMenu.showMenu(getChooserStage(), viewModeButton); } }); return true; } }); } private void rebuildViewModePopupMenu () { viewModePopupMenu.clear(); for (final ViewMode mode : ViewMode.values()) { if (mode.thumbnailMode && iconProvider.isThumbnailModesSupported() == false) continue; viewModePopupMenu.addItem(new MenuItem(mode.getBundleText(), new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { setViewMode(mode); } })); } } private void updateFavoriteFolderButton () { VisLabel label = (VisLabel) favoriteFolderButtonTooltip.getContent(); if (favorites.contains(currentDirectory, false)) { favoriteFolderButton.getStyle().imageUp = style.iconStar; label.setText(CONTEXT_MENU_REMOVE_FROM_FAVORITES.get()); } else { favoriteFolderButton.getStyle().imageUp = style.iconStarOutline; label.setText(CONTEXT_MENU_ADD_TO_FAVORITES.get()); } favoriteFolderButtonTooltip.pack(); } private void createCenterContentPanel () { fileListAdapter = new FileListAdapter(this, currentFiles); fileListView = new ListView(fileListAdapter); setupDefaultScrollPane(fileListView.getScrollPane()); VisTable fileScrollPaneTable = new VisTable(); fileListBusyBar = new BusyBar(); fileListBusyBar.setVisible(false); fileScrollPaneTable.add(fileListBusyBar).space(0).height(PrefHeightIfVisibleValue.INSTANCE).growX().row(); fileScrollPaneTable.add(fileListView.getMainTable()).pad(2).top().expand().fillX(); fileScrollPaneTable.setTouchable(Touchable.enabled); // shortcutsTable is contained in shortcutsScrollPane contained in shortcutsScrollPaneTable contained in mainSplitPane shortcutsTable = new VisTable(); final VisScrollPane shortcutsScrollPane = setupDefaultScrollPane(new VisScrollPane(shortcutsTable)); VisTable shortcutsScrollPaneTable = new VisTable(); shortcutsScrollPaneTable.add(shortcutsScrollPane).pad(2).top().expand().fillX(); mainSplitPane = new VisSplitPane(shortcutsScrollPaneTable, fileScrollPaneTable, false) { @Override public void invalidate () { super.invalidate(); invalidateChildHierarchy(shortcutsScrollPane); } }; mainSplitPane.setSplitAmount(0.3f); mainSplitPane.setMinSplitAmount(0.05f); mainSplitPane.setMaxSplitAmount(0.80f); row(); add(mainSplitPane).expand().fill(); row(); fileScrollPaneTable.addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (button == Buttons.RIGHT && fileMenu.isAddedToStage() == false) { fileMenu.build(); fileMenu.showMenu(getChooserStage(), event.getStageX(), event.getStageY()); } } }); } private void invalidateChildHierarchy (WidgetGroup layout) { if (layout != null) { layout.invalidate(); for (Actor actor : layout.getChildren()) { if (actor instanceof WidgetGroup) invalidateChildHierarchy((WidgetGroup) actor); else if (actor instanceof Layout) ((Layout) actor).invalidate(); } } } private void setCurrentPathFieldText (String text) { currentPath.setText(text); currentPath.setCursorAtTextEnd(); } private void createFileTextBox () { VisTable table = new VisTable(true); VisLabel nameLabel = new VisLabel(FILE_NAME.get()); selectedFileTextField = new VisTextField(); selectedFileTextField.setProgrammaticChangeEvents(false); fileTypeLabel = new VisLabel(FILE_TYPE.get()); fileTypeSelectBox = new VisSelectBox(); fileTypeSelectBox.getSelection().setProgrammaticChangeEvents(false); fileTypeSelectBox.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { activeFileTypeRule = fileTypeSelectBox.getSelected(); rebuildFileList(); } }); table.defaults().left(); table.add(nameLabel).spaceBottom(new ConstantIfVisibleValue(fileTypeSelectBox, 5f)); table.add(selectedFileTextField).expandX().fillX() .spaceBottom(new ConstantIfVisibleValue(fileTypeSelectBox, 5f)).row(); table.add(fileTypeLabel).height(PrefHeightIfVisibleValue.INSTANCE) .spaceBottom(new ConstantIfVisibleValue(sizes.spacingBottom)); table.add(fileTypeSelectBox).height(PrefHeightIfVisibleValue.INSTANCE) .spaceBottom(new ConstantIfVisibleValue(sizes.spacingBottom)).expand().fill(); selectedFileTextField.addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ENTER) { selectionFinished(); return true; } return false; } }); selectedFileTextField.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { deselectAll(false); fileNameSuggestionPopup.pathFieldKeyTyped(getChooserStage(), currentFiles, selectedFileTextField); FileHandle enteredFile = currentDirectory.child(selectedFileTextField.getText()); if (currentFiles.contains(enteredFile, false)) { highlightFiles(enteredFile); } } }); add(table).expandX().fillX().pad(3f).padRight(2f).padBottom(2f); row(); } private void updateFileTypeSelectBox () { if (fileTypeFilter == null || selectionMode == SelectionMode.DIRECTORIES) { fileTypeLabel.setVisible(false); fileTypeSelectBox.setVisible(false); fileTypeSelectBox.invalidateHierarchy(); return; } else { fileTypeLabel.setVisible(true); fileTypeSelectBox.setVisible(true); fileTypeSelectBox.invalidateHierarchy(); } Array rules = new Array(fileTypeFilter.getRules()); if (fileTypeFilter.isAllTypesAllowed()) { FileTypeFilter.Rule allTypesRule = new FileTypeFilter.Rule(ALL_FILES.get()); rules.add(allTypesRule); } fileTypeSelectBox.setItems(rules); fileTypeSelectBox.setSelected(activeFileTypeRule); } private void createBottomButtons () { VisTextButton cancelButton = new VisTextButton(CANCEL.get()); confirmButton = new VisTextButton(mode == Mode.OPEN ? OPEN.get() : SAVE.get()); VisTable buttonTable = new VisTable(true); buttonTable.defaults().minWidth(70).right(); add(buttonTable).padTop(3).padBottom(3).padRight(2).fillX().expandX(); ButtonBar buttonBar = new ButtonBar(); buttonBar.setIgnoreSpacing(true); buttonBar.setButton(ButtonType.CANCEL, cancelButton); buttonBar.setButton(ButtonType.OK, confirmButton); buttonTable.add(buttonBar.createTable()).expand().right(); cancelButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { fadeOut(); listener.canceled(); } }); confirmButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { selectionFinished(); } }); } private void createShortcutsMainPanel () { shortcutsMainPanel = new VerticalGroup(); String userHome = System.getProperty("user.home"); String userName = System.getProperty("user.name"); File userDesktop = new File(userHome + "/Desktop"); if (userDesktop.exists()) shortcutsMainPanel.addActor(new ShortcutItem(userDesktop, DESKTOP.get(), style.iconFolder)); shortcutsMainPanel.addActor(new ShortcutItem(new File(userHome), userName, style.iconFolder)); } private void createListeners () { addListener(new InputListener() { @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.A && UIUtils.ctrl() && getChooserStage().getKeyboardFocus() instanceof VisTextField == false) { selectAll(); return true; } return false; } @Override public boolean keyTyped (InputEvent event, char character) { if (getChooserStage().getKeyboardFocus() instanceof VisTextField) return false; if (Character.isLetterOrDigit(character) == false) return false; String name = String.valueOf(character); for (FileHandle file : currentFiles) { if (file.name().toLowerCase().startsWith(name)) { deselectAll(); highlightFiles(file); return true; } } return false; } }); } private void selectionFinished () { if (selectedItems.size == 1) { // only files allowed but directory is selected? // navigate to that directory! if (selectionMode == SelectionMode.FILES) { FileHandle selected = selectedItems.get(0).getFile(); if (selected.isDirectory()) { setDirectory(selected, HistoryPolicy.ADD); return; } } // only directories allowed but file is selected? // display dialog :( if (selectionMode == SelectionMode.DIRECTORIES) { FileHandle selected = selectedItems.get(0).getFile(); if (selected.isDirectory() == false) { showDialog(POPUP_ONLY_DIRECTORIES.get()); return; } } } if (selectedItems.size > 0 || mode == Mode.SAVE) { Array files = getFileListFromSelected(); notifyListenerAndCloseDialog(files); } else { if (selectionMode == SelectionMode.FILES) { showDialog(POPUP_CHOOSE_FILE.get()); } else { Array files = new Array(); if (selectedFileTextField.getText().length() != 0) { files.add(currentDirectory.child(selectedFileTextField.getText())); } else { // this part is executed when nothing is selected but selection mode is `directories` or `files and directories` // it is perfectly valid, nothing is selected so that means the current chooser directory have to be // selected and passed to listener files.add(currentDirectory); } notifyListenerAndCloseDialog(files); } } } @Override protected void close () { listener.canceled(); super.close(); } private void notifyListenerAndCloseDialog (Array files) { if (files == null) return; if (mode == Mode.OPEN) { for (FileHandle file : files) { if (file.exists() == false) { showDialog(POPUP_SELECTED_FILE_DOES_NOT_EXIST.get()); return; } } } if (files.size != 0) { listener.selected(files); if (saveLastDirectory) { preferencesIO.saveLastDirectory(currentDirectory); } } fadeOut(); } @Override public void fadeOut (float time) { super.fadeOut(time); fileMenu.remove(); dirsSuggestionPopup.remove(); fileNameSuggestionPopup.remove(); viewModePopupMenu.remove(); } protected VisScrollPane setupDefaultScrollPane (VisScrollPane scrollPane) { scrollPane.setOverscroll(false, false); scrollPane.setFlickScroll(false); scrollPane.setFadeScrollBars(false); scrollPane.setScrollingDisabled(true, false); return scrollPane; } private Array getFileListFromSelected () { Array list = new Array(); if (mode == Mode.OPEN) { for (FileItem item : selectedItems) list.add(item.getFile()); return list; } else if (selectedItems.size > 0) { for (FileItem item : selectedItems) list.add(item.getFile()); showOverwriteQuestion(list); return null; } else { String fileName = selectedFileTextField.getText(); FileHandle file = currentDirectory.child(fileName); if (FileUtils.isValidFileName(fileName) == false) { showDialog(POPUP_FILENAME_INVALID.get()); return null; } if (file.exists()) { list.add(file); showOverwriteQuestion(list); return null; } else { //if user typed no extension or extension is wrong and there is active file type rule //then the first extension rule will be appended/replaced automatically to entered file name if (activeFileTypeRule != null) { Array ruleExts = activeFileTypeRule.getExtensions(); if (ruleExts.size > 0 && ruleExts.contains(file.extension(), false) == false) { file = file.sibling(file.nameWithoutExtension() + "." + ruleExts.first()); } } list.add(file); if (file.exists()) { showOverwriteQuestion(list); return null; } else { return list; } } } } private void showDialog (String text) { Dialogs.showOKDialog(getChooserStage(), POPUP_TITLE.get(), text); } private void showOverwriteQuestion (final Array filesList) { String text = filesList.size == 1 ? POPUP_FILE_EXIST_OVERWRITE.get() : POPUP_MULTIPLE_FILE_EXIST_OVERWRITE.get(); Dialogs.showOptionDialog(getChooserStage(), POPUP_TITLE.get(), text, OptionDialogType.YES_NO, new OptionDialogAdapter() { @Override public void yes () { notifyListenerAndCloseDialog(filesList); } }); } private void rebuildShortcutsList (boolean rebuildRootCache) { shortcutsTable.clear(); shortcutsTable.add(shortcutsMainPanel).left().row(); shortcutsTable.addSeparator(); if (rebuildRootCache) rebuildFileRootsCache(); shortcutsTable.add(shortcutsRootsPanel).left().row(); if (shortcutsFavoritesPanel.getChildren().size > 0) shortcutsTable.addSeparator(); shortcutsTable.add(shortcutsFavoritesPanel).left().row(); } private void rebuildShortcutsList () { shortcutsListRebuildScheduled = false; rebuildShortcutsList(true); } private void rebuildFileRootsCache () { shortcutsRootsPanel.clear(); File[] roots = File.listRoots(); driveCheckerListeners.clear(); for (final File root : roots) { DriveCheckerListener listener = new DriveCheckerListener() { @Override public void rootMode (File root, RootMode mode) { if (driveCheckerListeners.removeValue(this, true) == false) return; String initialName = root.toString(); if (initialName.equals("/")) initialName = COMPUTER.get(); final ShortcutItem item = new ShortcutItem(root, initialName, style.iconDrive); if (OsUtils.isWindows()) chooserWinService.addListener(root, item); shortcutsRootsPanel.addActor(item); shortcutsRootsPanel.getChildren().sort(SHORTCUTS_COMPARATOR); } }; driveCheckerListeners.add(listener); driveCheckerService.addListener(root, mode == Mode.OPEN ? RootMode.READABLE : RootMode.WRITABLE, listener); } } private void rebuildShortcutsFavoritesPanel () { shortcutsFavoritesPanel.clear(); if (favorites.size > 0) { for (FileHandle f : favorites) shortcutsFavoritesPanel.addActor(new ShortcutItem(f.file(), f.name(), style.iconFolder)); } } private void rebuildFileList () { rebuildFileList(false); } private void rebuildFileList (final boolean stageChanged) { filesListRebuildScheduled = false; final FileHandle[] selectedFiles = new FileHandle[selectedItems.size]; for (int i = 0; i < selectedFiles.length; i++) { selectedFiles[i] = selectedItems.get(i).getFile(); } deselectAll(); setCurrentPathFieldText(currentDirectory.path()); if (showBusyBarTask.isScheduled() == false) { Timer.schedule(showBusyBarTask, 0.2f); //quiet period before busy bar is shown } if (listDirFuture != null) listDirFuture.cancel(true); listDirFuture = listDirExecutor.submit(new Runnable() { @Override public void run () { if (currentDirectory.exists() == false || currentDirectory.isDirectory() == false) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { setDirectory(getDefaultStartingDirectory(), HistoryPolicy.ADD); } }); return; } final Array files = FileUtils.sortFiles(listFilteredCurrentDirectory(), sorting.get().comparator, !sortingOrderAscending.get()); if (Thread.currentThread().isInterrupted()) return; final IdentityMap metadata = new IdentityMap(files.size); for (FileHandle file : files) { metadata.put(file, FileHandleMetadata.of(file)); } if (Thread.currentThread().isInterrupted()) return; Gdx.app.postRunnable(new Runnable() { @Override public void run () { buildFileList(files, metadata, selectedFiles, stageChanged); } }); } }); } private void buildFileList (Array files, IdentityMap metadata, FileHandle[] selectedFiles, boolean stageChanged) { currentFiles.clear(); currentFilesMetadata.clear(); showBusyBarTask.cancel(); fileListBusyBar.setVisible(false); if (files.size == 0) { fileListAdapter.itemsChanged(); return; } maxDateLabelWidth = 0; currentFiles.addAll(files); currentFilesMetadata = metadata; fileListAdapter.itemsChanged(); fileListView.getScrollPane().setScrollX(0); fileListView.getScrollPane().setScrollY(0); highlightFiles(selectedFiles); if (stageChanged && selectedFiles.length == 0 && defaultFileName != null) { selectedFileTextField.setText(defaultFileName); FileHandle enteredFile = currentDirectory.child(selectedFileTextField.getText()); if (currentFiles.contains(enteredFile, false)) { highlightFiles(enteredFile); } } } /** * Sets chooser selected files. All files that are invalid for current selection won't be selected. Files that doesn't * exist will be ignored. * @param files absolute {@link FileHandle}s of files to be selected */ public void setSelectedFiles (FileHandle... files) { deselectAll(false); for (FileHandle file : files) { FileItem item = fileListAdapter.getViews().get(file); if (item != null) { item.select(false); } } removeInvalidSelections(); updateSelectedFileFieldText(); } /** * Changes default file name that will be displayed in file name text field after chooser is added to stage. * This for example can be used to suggest default file name when chooser is in SAVE mode. *

* Note that when chooser is in OPEN mode and file with such name doesn't exist then pressing "Open" button * will still display message that selected file does not exist. *

* Default file name is only used after chooser is added to {@link Stage} and no other file was selected by * {@link #setSelectedFiles(FileHandle...)} or user. *

* This will automatically highlight matching file in file list (if file already exist). * @param text new default file name, may be null */ public void setDefaultFileName (String text) { defaultFileName = text; } /** Refresh chooser lists content */ public void refresh () { refresh(false); } private void refresh (boolean stageChanged) { rebuildShortcutsList(); rebuildFileList(stageChanged); } /** * Adds favorite to favorite list * @param favourite to be added */ public void addFavorite (FileHandle favourite) { favorites.add(favourite); preferencesIO.saveFavorites(favorites); rebuildShortcutsFavoritesPanel(); rebuildShortcutsList(false); updateFavoriteFolderButton(); } /** * Removes favorite from current favorite list * @param favorite to be removed (path to favorite) * @return true if favorite was removed, false otherwise */ public boolean removeFavorite (FileHandle favorite) { boolean removed = favorites.removeValue(favorite, false); preferencesIO.saveFavorites(favorites); rebuildShortcutsFavoritesPanel(); rebuildShortcutsList(false); updateFavoriteFolderButton(); return removed; } private void addRecentDirectory (FileHandle file) { if (recentDirectories.contains(file, false)) return; recentDirectories.insert(0, file); if (recentDirectories.size > AbstractSuggestionPopup.MAX_SUGGESTIONS) recentDirectories.pop(); preferencesIO.saveRecentDirectories(recentDirectories); } public void clearRecentDirectories () { recentDirectories.clear(); preferencesIO.saveRecentDirectories(recentDirectories); } @Override public void setVisible (boolean visible) { if (isVisible() == false && visible) deselectAll(); // reset selected item when dialog is changed from invisible to visible super.setVisible(visible); } private void deselectAll () { deselectAll(true); } private void deselectAll (boolean updateTextField) { for (FileItem item : selectedItems) item.deselect(false); selectedItems.clear(); if (updateTextField) updateSelectedFileFieldText(); } private void selectAll () { for (FileItem item : fileListAdapter.getOrderedViews()) item.select(false); removeInvalidSelections(); updateSelectedFileFieldText(); } /** * Sets chooser selected files. Compared to {@link #setSelectedFiles(FileHandle...)} does not remove invalid files * from selection. */ public void highlightFiles (FileHandle... files) { for (FileHandle file : files) { FileItem item = fileListAdapter.getViews().get(file); if (item != null) { item.select(false); } } if (files.length > 0) { FileItem item = fileListAdapter.getViews().get(files[0]); if (item != null) { if (item.getParent() instanceof Table) { //table at this point may need additional layout to calculate proper target scroll cords ((Table) item.getParent()).layout(); } item.localToParentCoordinates(tmpVector.setZero()); fileListView.getScrollPane().scrollTo(tmpVector.x, tmpVector.y, item.getWidth(), item.getHeight(), false, true); } } updateSelectedFileFieldText(); } private void updateSelectedFileFieldText () { updateSelectedFileFieldText(false); } private void updateSelectedFileFieldText (boolean ignoreKeyboardFocus) { if (ignoreKeyboardFocus == false && getChooserStage() != null) { if (getChooserStage().getKeyboardFocus() == selectedFileTextField) return; } if (selectedItems.size == 0) { selectedFileTextField.setText(""); } else if (selectedItems.size == 1) { selectedFileTextField.setText(selectedItems.get(0).getFile().name()); } else { StringBuilder builder = new StringBuilder(); for (FileItem item : selectedItems) { builder.append('"'); builder.append(item.file.name()); builder.append("\" "); } selectedFileTextField.setText(builder.toString()); } selectedFileTextField.setCursorAtTextEnd(); } private void removeInvalidSelections () { if (selectionMode == SelectionMode.FILES) { Iterator it = selectedItems.iterator(); while (it.hasNext()) { FileItem item = it.next(); if (item.file.isDirectory()) { item.deselect(false); it.remove(); } } } if (selectionMode == SelectionMode.DIRECTORIES) { Iterator it = selectedItems.iterator(); while (it.hasNext()) { FileItem item = it.next(); if (item.file.isDirectory() == false) { item.deselect(false); it.remove(); } } } } public Mode getMode () { return mode; } public void setMode (Mode mode) { this.mode = mode; confirmButton.setText(mode == Mode.OPEN ? OPEN.get() : SAVE.get()); refresh(); } public ViewMode getViewMode () { return viewMode; } public void setViewMode (ViewMode viewMode) { if (this.viewMode == viewMode) return; this.viewMode = viewMode; iconProvider.viewModeChanged(viewMode); rebuildFileList(); } public void setDirectory (String directory) { setDirectory(Gdx.files.absolute(directory), HistoryPolicy.CLEAR); } public void setDirectory (File directory) { setDirectory(Gdx.files.absolute(directory.getAbsolutePath()), HistoryPolicy.CLEAR); } public void setDirectory (FileHandle directory) { setDirectory(directory, HistoryPolicy.CLEAR); } /** * Changes file chooser active directory. * Warning: To avoid hanging listing directory is performed asynchronously. In case of passing invalid file handle * file chooser will fallback to default one. */ @Override public void setDirectory (FileHandle directory, HistoryPolicy historyPolicy) { if (directory.equals(currentDirectory)) return; if (historyPolicy == HistoryPolicy.ADD) historyManager.historyAdd(); currentDirectory = directory; iconProvider.directoryChanged(directory); rebuildFileList(); if (historyPolicy == HistoryPolicy.CLEAR) historyManager.historyClear(); updateFavoriteFolderButton(); } @Override public FileHandle getCurrentDirectory () { return currentDirectory; } private FileHandle getDefaultStartingDirectory () { return Gdx.files.absolute(System.getProperty("user.home")); } /** List currently set directory with all active filters */ private FileHandle[] listFilteredCurrentDirectory () { FileHandle[] files = currentDirectory.list(fileFilter); if (fileTypeFilter == null || activeFileTypeRule == null) return files; FileHandle[] filtered = new FileHandle[files.length]; int count = 0; for (FileHandle file : files) { if (file.isDirectory() == false && activeFileTypeRule.accept(file) == false) continue; filtered[count++] = file; } if (count == 0) return new FileHandle[0]; FileHandle[] newFiltered = new FileHandle[count]; System.arraycopy(filtered, 0, newFiltered, 0, count); return newFiltered; } public FileFilter getFileFilter () { return fileFilter; } public void setFileFilter (FileFilter fileFilter) { this.fileFilter = fileFilter; rebuildFileList(); } /** * Sets new {@link FileTypeFilter}. Note that if you modify {@link FileTypeFilter} you must call this method again with * modified instance to apply changes. Setting file type filter won't have any effect when selection mode is set to * directories. */ public void setFileTypeFilter (FileTypeFilter fileTypeFilter) { if (fileTypeFilter == null) { this.fileTypeFilter = null; this.activeFileTypeRule = null; } else { if (fileTypeFilter.getRules().size == 0) throw new IllegalArgumentException("FileTypeFilter doesn't have any rules added"); this.fileTypeFilter = new FileTypeFilter(fileTypeFilter); this.activeFileTypeRule = this.fileTypeFilter.getRules().first(); } updateFileTypeSelectBox(); rebuildFileList(); } public FileTypeFilter.Rule getActiveFileTypeFilterRule () { return activeFileTypeRule; } public SelectionMode getSelectionMode () { return selectionMode; } /** * Changes selection mode, also updates the title of this file chooser to match current selection mode (eg. Choose file, Choose * directory etc.) */ public void setSelectionMode (SelectionMode selectionMode) { if (selectionMode == null) selectionMode = SelectionMode.FILES; this.selectionMode = selectionMode; switch (selectionMode) { case FILES: getTitleLabel().setText(TITLE_CHOOSE_FILES.get()); break; case DIRECTORIES: getTitleLabel().setText(TITLE_CHOOSE_DIRECTORIES.get()); break; case FILES_AND_DIRECTORIES: getTitleLabel().setText(TITLE_CHOOSE_FILES_AND_DIRECTORIES.get()); break; } updateFileTypeSelectBox(); rebuildFileList(); } public FileSorting getSorting () { return sorting.get(); } public void setSorting (FileSorting sorting, boolean sortingOrderAscending) { this.sorting.set(sorting); this.sortingOrderAscending.set(sortingOrderAscending); rebuildFileList(); } public void setSorting (FileSorting sorting) { this.sorting.set(sorting); rebuildFileList(); } public boolean isSortingOrderAscending () { return sortingOrderAscending.get(); } public void setSortingOrderAscending (boolean sortingOrderAscending) { this.sortingOrderAscending.set(sortingOrderAscending); rebuildFileList(); } public void setFavoriteFolderButtonVisible (boolean favoriteFolderButtonVisible) { favoriteFolderButton.setVisible(favoriteFolderButtonVisible); } public boolean isFavoriteFolderButtonVisible () { return favoriteFolderButton.isVisible(); } public void setViewModeButtonVisible (boolean viewModeButtonVisible) { viewModeButton.setVisible(viewModeButtonVisible); } public boolean isViewModeButtonVisible () { return viewModeButton.isVisible(); } public boolean isMultiSelectionEnabled () { return multiSelectionEnabled; } public void setMultiSelectionEnabled (boolean multiSelectionEnabled) { this.multiSelectionEnabled = multiSelectionEnabled; } public void setListener (FileChooserListener newListener) { this.listener = newListener; if (listener == null) listener = new FileChooserAdapter(); } public boolean isShowSelectionCheckboxes () { return showSelectionCheckboxes; } public void setShowSelectionCheckboxes (boolean showSelectionCheckboxes) { this.showSelectionCheckboxes = showSelectionCheckboxes; rebuildFileList(); } public int getMultiSelectKey () { return multiSelectKey; } /** @param multiSelectKey from {@link Keys} or {@link FileChooser#DEFAULT_KEY} to restore default */ public void setMultiSelectKey (int multiSelectKey) { this.multiSelectKey = multiSelectKey; } public int getGroupMultiSelectKey () { return groupMultiSelectKey; } /** @param groupMultiSelectKey from {@link Keys} or {@link FileChooser#DEFAULT_KEY} to restore default */ public void setGroupMultiSelectKey (int groupMultiSelectKey) { this.groupMultiSelectKey = groupMultiSelectKey; } private boolean isMultiSelectKeyPressed () { if (multiSelectKey == DEFAULT_KEY) return UIUtils.ctrl(); else return Gdx.input.isKeyPressed(multiSelectKey); } private boolean isGroupMultiSelectKeyPressed () { if (groupMultiSelectKey == DEFAULT_KEY) return UIUtils.shift(); else return Gdx.input.isKeyPressed(groupMultiSelectKey); } public FileChooserStyle getChooserStyle () { return style; } public Sizes getSizes () { return sizes; } private Stage getChooserStage () { return getStage(); } /** * If false file chooser won't pool directories for changes, adding new files or connecting new drive won't refresh file list. * This must be called when file chooser is not added to Stage */ public void setWatchingFilesEnabled (boolean watchingFilesEnabled) { if (getChooserStage() != null) throw new IllegalStateException("Pooling setting cannot be changed when file chooser is added to Stage!"); this.watchingFilesEnabled = watchingFilesEnabled; } public void setPrefsName (String prefsName) { preferencesIO = new PreferencesIO(prefsName); reloadPreferences(true); } private void reloadPreferences (boolean rebuildUI) { favorites = preferencesIO.loadFavorites(); recentDirectories = preferencesIO.loadRecentDirectories(); if (rebuildUI) rebuildShortcutsFavoritesPanel(); } @Override public void draw (Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); if (shortcutsListRebuildScheduled) rebuildShortcutsList(); if (filesListRebuildScheduled) rebuildFileList(); } @Override protected void setStage (Stage stage) { super.setStage(stage); if (stage != null) { refresh(true); rebuildShortcutsFavoritesPanel(); //if by any chance multiple choosers changed favorites deselectAll(); if (focusFileScrollPaneOnShow) { stage.setScrollFocus(fileListView.getScrollPane()); } if (focusSelectedFileTextFieldOnShow) { FocusManager.switchFocus(stage, selectedFileTextField); stage.setKeyboardFocus(selectedFileTextField); } } if (watchingFilesEnabled) { if (stage != null) { startFileWatcher(); } else { stopFileWatcher(); } } } private void startFileWatcher () { if (fileWatcherThread != null) return; fileWatcherThread = new Thread(new Runnable() { File[] lastRoots; FileHandle lastCurrentDirectory; FileHandle[] lastCurrentFiles; @Override public void run () { lastRoots = File.listRoots(); lastCurrentDirectory = currentDirectory; lastCurrentFiles = currentDirectory.list(); while (fileWatcherThread != null) { File[] roots = File.listRoots(); if (roots.length != lastRoots.length || Arrays.equals(lastRoots, roots) == false) shortcutsListRebuildScheduled = true; lastRoots = roots; // if current directory changed during pools then our lastCurrentDirectoryFiles list is outdated and we shouldn't // schedule files list rebuild if (lastCurrentDirectory.equals(currentDirectory) == true) { FileHandle[] currentFiles = currentDirectory.list(); if (lastCurrentFiles.length != currentFiles.length || Arrays.equals(lastCurrentFiles, currentFiles) == false) filesListRebuildScheduled = true; lastCurrentFiles = currentFiles; } else lastCurrentFiles = currentDirectory.list(); // if list is outdated, refresh it lastCurrentDirectory = currentDirectory; try { Thread.sleep(FILE_WATCHER_CHECK_DELAY_MILLIS); } catch (InterruptedException ignored) { } } } }, "FileWatcherThread"); fileWatcherThread.setDaemon(true); fileWatcherThread.start(); } private void stopFileWatcher () { if (fileWatcherThread == null) return; fileWatcherThread.interrupt(); fileWatcherThread = null; } private void showNewDirectoryDialog () { Dialogs.showInputDialog(getChooserStage(), NEW_DIRECTORY_DIALOG_TITLE.get(), NEW_DIRECTORY_DIALOG_TEXT.get(), true, new InputDialogAdapter() { @Override public void finished (String input) { if (FileUtils.isValidFileName(input) == false) { Dialogs.showErrorDialog(getChooserStage(), NEW_DIRECTORY_DIALOG_ILLEGAL_CHARACTERS.get()); return; } for (FileHandle file : currentDirectory.list()) { if (file.name().equals(input)) { Dialogs.showErrorDialog(getChooserStage(), NEW_DIRECTORY_DIALOG_ALREADY_EXISTS.get()); return; } } FileHandle newDir = currentDirectory.child(input); newDir.mkdirs(); refresh(); highlightFiles(newDir); } }); } private void showFileDeleteDialog (final FileHandle fileToDelete) { Dialogs.showOptionDialog(getChooserStage(), POPUP_TITLE.get(), fileDeleter.hasTrash() ? CONTEXT_MENU_MOVE_TO_TRASH_WARNING.get() : CONTEXT_MENU_DELETE_WARNING.get(), OptionDialogType.YES_NO, new OptionDialogAdapter() { @Override public void yes () { try { boolean success = fileDeleter.delete(fileToDelete); if (success == false) { Dialogs.showErrorDialog(getChooserStage(), POPUP_DELETE_FILE_FAILED.get()); } } catch (IOException e) { Dialogs.showErrorDialog(getChooserStage(), POPUP_DELETE_FILE_FAILED.get(), e); e.printStackTrace(); } refresh(); } }); } /** * Sets {@link FileChooser.FileDeleter} that will be used for deleting files. You SHOULD NOT set your own file deleter. * You should use either {@link DefaultFileDeleter} or JNAFileDeleter from vis-ui-contrib project. JNAFileDeleter * supports moving file to system trash instead of deleting it permanently, however it requires JNA library in your * project classpath. */ public void setFileDeleter (FileDeleter fileDeleter) { if (fileDeleter == null) throw new IllegalStateException("fileDeleter can't be null"); this.fileDeleter = fileDeleter; fileMenu.fileDeleterChanged(fileDeleter.hasTrash()); } public void setIconProvider (FileIconProvider iconProvider) { this.iconProvider = iconProvider; rebuildViewModePopupMenu(); } public FileIconProvider getIconProvider () { return iconProvider; } public static boolean isSaveLastDirectory () { return saveLastDirectory; } /** * @param saveLastDirectory if true then chooser will store last directory user browsed in preferences file. Note that * this only applies to using chooser between separate app launches. When single instance of chooser is reused in single * app session then last directory is always remembered. Default is false. This must be called before creating FileChooser. */ public static void setSaveLastDirectory (boolean saveLastDirectory) { FileChooser.saveLastDirectory = saveLastDirectory; } public enum Mode { OPEN, SAVE } public enum SelectionMode { FILES, DIRECTORIES, FILES_AND_DIRECTORIES } public enum FileSorting { NAME(FileUtils.FILE_NAME_COMPARATOR), MODIFIED_DATE(FileUtils.FILE_MODIFIED_DATE_COMPARATOR), SIZE(FileUtils.FILE_SIZE_COMPARATOR); private final Comparator comparator; FileSorting (Comparator comparator) { this.comparator = comparator; } } public enum HistoryPolicy { ADD, CLEAR, IGNORE } public enum ViewMode { DETAILS(false, VIEW_MODE_DETAILS), BIG_ICONS(true, VIEW_MODE_BIG_ICONS), MEDIUM_ICONS(true, VIEW_MODE_MEDIUM_ICONS), SMALL_ICONS(true, VIEW_MODE_SMALL_ICONS), LIST(false, VIEW_MODE_LIST); private final FileChooserText bundleText; private final boolean thumbnailMode; ViewMode (boolean thumbnailMode, FileChooserText bundleText) { this.thumbnailMode = thumbnailMode; this.bundleText = bundleText; } public String getBundleText () { return bundleText.get(); } public void setupGridGroup (Sizes sizes, GridGroup group) { if (isGridMode() == false) return; float gridSize = getGridSize(sizes); if (gridSize < 0) { throw new IllegalStateException("FileChooser's ViewMode " + this.toString() + " has invalid size defined in Sizes. " + "Expected value greater than 0, got: " + gridSize + ". Check your skin Sizes definition."); } if (this == LIST) { group.setItemSize(gridSize, 22 * sizes.scaleFactor); return; } group.setItemSize(gridSize); } public boolean isGridMode () { return isThumbnailMode() || this == LIST; } public boolean isThumbnailMode () { return thumbnailMode; } public float getGridSize (Sizes sizes) { switch (this) { case DETAILS: return -1; case BIG_ICONS: return sizes.fileChooserViewModeBigIconsSize; case MEDIUM_ICONS: return sizes.fileChooserViewModeMediumIconsSize; case SMALL_ICONS: return sizes.fileChooserViewModeSmallIconsSize; case LIST: return sizes.fileChooserViewModeListWidthSize; default: return -1; } } } /** * Provides icons that will be used for file thumbnail on file list. If not set default is used that supports * directories and few basic file types. If you want to add your custom icon your should extend {@link DefaultFileIconProvider} */ public interface FileIconProvider { /** @return icon that will be used for this file or null if no icon should be displayed */ Drawable provideIcon (FileItem item); /** * @return true if this icon provider can supply proper icons for {@link ViewMode#BIG_ICONS}, {@link ViewMode#MEDIUM_ICONS} * and {@link ViewMode#SMALL_ICONS} view modes, false otherwise. If false thumbnail view modes won't be available for selection. */ boolean isThumbnailModesSupported (); void directoryChanged (FileHandle newDirectory); void viewModeChanged (ViewMode newViewMode); } public static class DefaultFileIconProvider implements FileIconProvider { protected FileChooser chooser; protected FileChooserStyle style; public DefaultFileIconProvider (FileChooser chooser) { this.chooser = chooser; this.style = chooser.style; } @Override public Drawable provideIcon (FileItem item) { if (item.isDirectory()) return getDirIcon(item); String ext = item.getFile().extension().toLowerCase(); if (ext.equals("jpg") || ext.equals("jpeg") || ext.equals("png") || ext.equals("bmp")) return getImageIcon(item); if (ext.equals("wav") || ext.equals("ogg") || ext.equals("mp3")) return getAudioIcon(item); if (ext.equals("pdf")) return getPdfIcon(item); if (ext.equals("txt")) return getTextIcon(item); return getDefaultIcon(item); } protected Drawable getDirIcon (FileItem item) { return style.iconFolder; } protected Drawable getImageIcon (FileItem item) { return style.iconFileImage; } protected Drawable getAudioIcon (FileItem item) { return style.iconFileAudio; } protected Drawable getPdfIcon (FileItem item) { return style.iconFilePdf; } protected Drawable getTextIcon (FileItem item) { return style.iconFileText; } protected Drawable getDefaultIcon (FileItem item) { return null; } @Override public boolean isThumbnailModesSupported () { return false; } @Override public void directoryChanged (FileHandle newDirectory) { } @Override public void viewModeChanged (ViewMode newViewMode) { } } public static class DefaultFileFilter implements FileFilter { private FileChooser chooser; private boolean ignoreChooserSelectionMode = false; public DefaultFileFilter (FileChooser chooser) { this.chooser = chooser; } @Override public boolean accept (File f) { if (f.isHidden()) return false; if (chooser.getMode() == Mode.OPEN ? f.canRead() == false : f.canWrite() == false) return false; if (ignoreChooserSelectionMode == false && f.isDirectory() == false && chooser.getSelectionMode() == SelectionMode.DIRECTORIES) { return false; } return true; } public boolean isIgnoreChooserSelectionMode () { return ignoreChooserSelectionMode; } public void setIgnoreChooserSelectionMode (boolean ignoreChooserSelectionMode) { this.ignoreChooserSelectionMode = ignoreChooserSelectionMode; } } public interface FileDeleter { boolean hasTrash (); boolean delete (FileHandle file) throws IOException; } public static final class DefaultFileDeleter implements FileDeleter { @Override public boolean hasTrash () { return false; } @Override public boolean delete (FileHandle file) { return file.deleteDirectory(); } } private class ShowBusyBarTask extends Timer.Task { @Override public void run () { fileListBusyBar.resetSegment(); fileListBusyBar.setVisible(true); currentFiles.clear(); currentFilesMetadata.clear(); fileListAdapter.itemsChanged(); } @Override public synchronized void cancel () { super.cancel(); fileListBusyBar.setVisible(false); } } /** Internal FileChooser API. */ public class FileItem extends Table implements Focusable { private FileHandle file; private FileHandleMetadata metadata; private VisCheckBox selectCheckBox; private VisImage iconImage; public FileItem (final FileHandle file, ViewMode viewMode) { this.file = file; this.metadata = currentFilesMetadata.get(file); if (metadata == null) metadata = FileHandleMetadata.of(file); //fallback, should not ever happen setTouchable(Touchable.enabled); VisLabel name = new VisLabel(metadata.name(), viewMode == ViewMode.SMALL_ICONS ? "small" : "default"); name.setEllipsis(true); Drawable icon = iconProvider.provideIcon(this); selectCheckBox = new VisCheckBox(""); selectCheckBox.setFocusBorderEnabled(false); selectCheckBox.setProgrammaticChangeEvents(false); boolean shouldShowItemShowCheckBox = showSelectionCheckboxes && ( (selectionMode == SelectionMode.FILES_AND_DIRECTORIES) || (selectionMode == SelectionMode.FILES && metadata.isDirectory() == false) || (selectionMode == SelectionMode.DIRECTORIES && metadata.isDirectory()) ); left(); if (viewMode.isThumbnailMode()) { if (shouldShowItemShowCheckBox) { IconStack stack = new IconStack(iconImage = new VisImage(icon, Scaling.none), selectCheckBox); add(stack).padTop(3).grow().row(); add(name).minWidth(1); } else { add(iconImage = new VisImage(icon, Scaling.none)).padTop(3).grow().row(); add(name).minWidth(1); } } else { if (shouldShowItemShowCheckBox) add(selectCheckBox).padLeft(3); add(iconImage = new VisImage(icon)).padTop(3).minWidth(22 * sizes.scaleFactor); add(name).minWidth(1).growX().padRight(10); VisLabel size = new VisLabel(isDirectory() ? "" : metadata.readableFileSize(), "small"); VisLabel dateLabel = new VisLabel(dateFormat.format(metadata.lastModified()), "small"); size.setAlignment(Align.right); if (viewMode == ViewMode.DETAILS) { maxDateLabelWidth = Math.max(dateLabel.getWidth(), maxDateLabelWidth); add(size).right().padRight(isDirectory() ? 0 : 10); add(dateLabel).padRight(6).width(new Value() { @Override public float get (Actor context) { return maxDateLabelWidth; } }); } } addListeners(); } /** * Updates file item icon, can be used for asynchronous icon loading. Note that icon provided must not return null * even if this item icon will be loaded later. */ public void setIcon (Drawable icon, Scaling scaling) { iconImage.setDrawable(icon); iconImage.setScaling(scaling); iconImage.invalidateHierarchy(); } private void addListeners () { addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { FocusManager.switchFocus(getChooserStage(), FileItem.this); getChooserStage().setKeyboardFocus(FileItem.this); return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (event.getButton() == Buttons.RIGHT) { fileMenu.build(favorites, file); fileMenu.showMenu(getChooserStage(), event.getStageX(), event.getStageY()); } } @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.FORWARD_DEL) { showFileDeleteDialog(file); return true; } return false; } }); addListener(new ClickListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { // very fast selecting and deselecting folder would navigate to that folder // return false will protect against that (tap count won't be increased) if (handleSelectClick(false) == false) return false; return super.touchDown(event, x, y, pointer, button); } @Override public void clicked (InputEvent event, float x, float y) { super.clicked(event, x, y); if (getTapCount() == 2 && selectedItems.contains(FileItem.this, true)) { if (file.isDirectory()) { setDirectory(file, HistoryPolicy.ADD); } else selectionFinished(); } } }); selectCheckBox.addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { event.stop(); return true; } }); selectCheckBox.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { event.stop(); handleSelectClick(true); } }); } private boolean handleSelectClick (boolean checkboxClicked) { if (selectedShortcut != null) selectedShortcut.deselect(); if (checkboxClicked) { if (multiSelectionEnabled == false && selectedItems.contains(FileItem.this, true) == false) deselectAll(); } else { if (multiSelectionEnabled == false || (isMultiSelectKeyPressed() == false && isGroupMultiSelectKeyPressed() == false)) deselectAll(); } boolean itemSelected = select(); if (selectedItems.size > 1 && multiSelectionEnabled && isGroupMultiSelectKeyPressed()) selectGroup(); if (selectedItems.size > 1) removeInvalidSelections(); updateSelectedFileFieldText(); return itemSelected; } private void selectGroup () { Array actors = fileListAdapter.getOrderedViews(); int thisSelectionIndex = getItemId(actors, FileItem.this); int lastSelectionIndex = getItemId(actors, selectedItems.get(selectedItems.size - 2)); int start; int end; if (thisSelectionIndex > lastSelectionIndex) { start = lastSelectionIndex; end = thisSelectionIndex; } else { start = thisSelectionIndex; end = lastSelectionIndex; } for (int i = start; i < end; i++) { FileItem item = actors.get(i); item.select(false); } } private int getItemId (Array actors, FileItem item) { for (int i = 0; i < actors.size; i++) { if (actors.get(i) == item) return i; } throw new IllegalStateException("Item not found in cells"); } /** Selects this items, if item is already in selectedList it will be deselected */ private boolean select () { return select(true); } private boolean select (boolean deselectIfAlreadySelected) { if (deselectIfAlreadySelected && selectedItems.contains(this, true)) { deselect(); return false; } setBackground(style.highlight); selectCheckBox.setChecked(true); if (selectedItems.contains(this, true) == false) selectedItems.add(this); return true; } private void deselect () { deselect(true); } private void deselect (boolean removeFromList) { setBackground((Drawable) null); selectCheckBox.setChecked(false); if (removeFromList) selectedItems.removeValue(this, true); } @Override public void focusLost () { } @Override public void focusGained () { } public FileHandle getFile () { return file; } public boolean isDirectory () { return metadata.isDirectory(); } } private class ShortcutItem extends Table implements RootNameListener, Focusable { public File file; private VisLabel name; /** Used only by shortcuts panel */ public ShortcutItem (final File file, String customName, Drawable icon) { this.file = file; name = new VisLabel(customName); name.setEllipsis(true); add(new Image(icon)).padTop(3); Cell labelCell = add(name).padRight(6); labelCell.width(new Value() { @Override public float get (Actor context) { return mainSplitPane.getFirstWidgetBounds().width - 30; } }); addListener(); } private void addListener () { addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { FocusManager.switchFocus(getChooserStage(), ShortcutItem.this); getChooserStage().setKeyboardFocus(ShortcutItem.this); return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (event.getButton() == Buttons.RIGHT) { fileMenu.buildForFavorite(favorites, file); fileMenu.showMenu(getChooserStage(), event.getStageX(), event.getStageY()); } } @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.FORWARD_DEL) { FileHandle gdxFile = Gdx.files.absolute(file.getAbsolutePath()); if (favorites.contains(gdxFile, false)) { removeFavorite(gdxFile); } return true; } return false; } }); addListener(new ClickListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { deselectAll(); updateSelectedFileFieldText(); select(); return super.touchDown(event, x, y, pointer, button); } @Override public void clicked (InputEvent event, float x, float y) { super.clicked(event, x, y); if (getTapCount() == 1) { File file = ShortcutItem.this.file; if (file.exists() == false) { showDialog(POPUP_DIRECTORY_DOES_NOT_EXIST.get()); refresh(); return; } if (file.isDirectory()) { setDirectory(Gdx.files.absolute(file.getAbsolutePath()), HistoryPolicy.ADD); getChooserStage().setScrollFocus(fileListView.getScrollPane()); } } } }); } public void setLabelText (String text) { name.setText(text); } public String getLabelText () { return name.getText().toString(); } private void select () { if (selectedShortcut != null) selectedShortcut.deselect(); selectedShortcut = ShortcutItem.this; setBackground(style.highlight); } private void deselect () { setBackground((Drawable) null); } @Override public void setRootName (String newName) { setLabelText(newName); } @Override public void focusGained () { } @Override public void focusLost () { } } private static class ShortcutsComparator implements Comparator { @Override public int compare (Actor o1, Actor o2) { ShortcutItem s1 = (ShortcutItem) o1; ShortcutItem s2 = (ShortcutItem) o2; return s1.getLabelText().compareTo(s2.getLabelText()); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileChooserAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; /** * Empty implementation of {@link FileChooserListener}. * @author Kotcrab */ public class FileChooserAdapter implements FileChooserListener { @Override public void canceled () { } @Override public void selected (Array files) { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileChooserListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; /** * Used to get events from {@link FileChooser}. * @author Kotcrab */ public interface FileChooserListener { /** Called when user finished selecting files. It is guaranteed that array will contain at least one file. */ void selected (Array files); /** Called when selection dialog was canceled by user. */ void canceled (); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileChooserStyle.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.kotcrab.vis.ui.widget.PopupMenu.PopupMenuStyle; /** @author Kotcrab */ public class FileChooserStyle { public PopupMenuStyle popupMenuStyle; public Drawable highlight; public Drawable iconArrowLeft; public Drawable iconArrowRight; public Drawable iconFolder; public Drawable iconFolderParent; public Drawable iconFolderStar; public Drawable iconFolderNew; public Drawable iconDrive; public Drawable iconTrash; public Drawable iconStar; public Drawable iconStarOutline; public Drawable iconRefresh; public Drawable iconListSettings; public Drawable iconFileText; public Drawable iconFileImage; public Drawable iconFilePdf; public Drawable iconFileAudio; public Drawable contextMenuSelectedItem; public Drawable expandDropdown; public FileChooserStyle () { } public FileChooserStyle (FileChooserStyle style) { this.popupMenuStyle = style.popupMenuStyle; this.highlight = style.highlight; this.iconArrowLeft = style.iconArrowLeft; this.iconArrowRight = style.iconArrowRight; this.iconFolder = style.iconFolder; this.iconFolderParent = style.iconFolderParent; this.iconFolderStar = style.iconFolderStar; this.iconFolderNew = style.iconFolderNew; this.iconDrive = style.iconDrive; this.iconTrash = style.iconTrash; this.iconStar = style.iconStar; this.iconStarOutline = style.iconStarOutline; this.iconRefresh = style.iconRefresh; this.iconListSettings = style.iconListSettings; this.iconFileText = style.iconFileText; this.iconFileImage = style.iconFileImage; this.iconFilePdf = style.iconFilePdf; this.iconFileAudio = style.iconFileAudio; this.contextMenuSelectedItem = style.contextMenuSelectedItem; this.expandDropdown = style.expandDropdown; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileTypeFilter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import java.io.FileFilter; /** * FileTypeFilter is used to limit {@link FileChooser} selection only to specified set of extensions. User can use file * chooser's select box to select that extension or all types (if it is allowed). *

* This class is not replacement for {@link FileChooser#setFileFilter(FileFilter)}. While the main file chooser * filter does general filtering (such as removing hidden or inaccessible files), FileTypeFilter is used to limit extensions * of files than user can select. *

* This filter works by adding rules. Each rule has a description (showed in file chooser's filter select box) and a * list of extensions that it accepts. During selection user can switch active rule via select box. Additionally each * FileTypeFilter can support 'all types allowed' where all files are accepted regardless of their extension. * @author Kotcrab * @since 1.1.0 */ public class FileTypeFilter { private boolean allTypesAllowed; private Array rules = new Array(); public FileTypeFilter (FileTypeFilter other) { this.allTypesAllowed = other.allTypesAllowed; this.rules = new Array(other.rules); } /** @param allTypesAllowed if true then user can choose "All types" in file chooser's filter select box. In that mode all files are shown */ public FileTypeFilter (boolean allTypesAllowed) { this.allTypesAllowed = allTypesAllowed; } /** * Adds new rule to {@link FileTypeFilter} * @param description rule description used in FileChooser's file type select box * @param extensions list of extensions without leading dot, eg. 'jpg', 'png' etc. */ public void addRule (String description, String... extensions) { rules.add(new Rule(description, extensions)); } public Array getRules () { return rules; } /** * Controls whether to allow 'all types allowed' mode, where all file types are shown. * @param allTypesAllowed if true then user can choose "All types" in file chooser's filter select box where all files are shown */ public void setAllTypesAllowed (boolean allTypesAllowed) { this.allTypesAllowed = allTypesAllowed; } public boolean isAllTypesAllowed () { return allTypesAllowed; } /** Defines single rule for {@link FileTypeFilter}. Rule instances are immutable. */ public static class Rule { private final String description; private final Array extensions = new Array(); private final boolean allowAll; public Rule (String description) { if (description == null) throw new IllegalArgumentException("description can't be null"); this.description = description; this.allowAll = true; } public Rule (String description, String... extensionList) { if (description == null) throw new IllegalArgumentException("description can't be null"); if (extensionList == null || extensionList.length == 0) throw new IllegalArgumentException("extensionList can't be null nor empty"); this.description = description; this.allowAll = false; for (String ext : extensionList) { if (ext.startsWith(".")) ext = ext.substring(1); extensions.add(ext.toLowerCase()); } } public boolean accept (FileHandle file) { if (allowAll) return true; String ext = file.extension().toLowerCase(); return extensions.contains(ext, false); } public String getDescription () { return description; } /** @return copy of extension list. */ public Array getExtensions () { return new Array(extensions); } @Override public String toString () { return description; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileUtils.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Sort; import com.kotcrab.vis.ui.util.OsUtils; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.text.DecimalFormat; import java.util.Comparator; /** * File related utils. Note that FileUtils are not available on GWT. * @author Kotcrab */ public class FileUtils { private static final String[] UNITS = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; /** Sorts file by names ignoring upper case */ public static final Comparator FILE_NAME_COMPARATOR = new Comparator() { @Override public int compare (FileHandle f1, FileHandle f2) { return f1.name().toLowerCase().compareTo(f2.name().toLowerCase()); } }; /** Sorts file by modified date then by name. */ public static final Comparator FILE_MODIFIED_DATE_COMPARATOR = new Comparator() { @Override public int compare (FileHandle f1, FileHandle f2) { long l1 = f1.lastModified(); long l2 = f2.lastModified(); return l1 > l2 ? 1 : (l1 == l2 ? FILE_NAME_COMPARATOR.compare(f1, f2) : -1); } }; /** Sorts file by their size then by name. */ public static final Comparator FILE_SIZE_COMPARATOR = new Comparator() { @Override public int compare (FileHandle f1, FileHandle f2) { long l1 = f1.length(); long l2 = f2.length(); return l1 > l2 ? -1 : (l1 == l2 ? FILE_NAME_COMPARATOR.compare(f1, f2) : 1); } }; /** * Converts byte file size to human readable, eg:
* 500 becomes 500 B
* 1024 becomes 1 KB
* 123456 becomes 120.6 KB
* 10000000000 becomes 9.3 GB
* Max supported unit is yottabyte (YB). * @param size file size in bytes. * @return human readable file size. */ public static String readableFileSize (long size) { if (size <= 0) return "0 B"; int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)).replace(",", ".") + " " + UNITS[digitGroups]; } /** * Sorts file list, using this rules: directories first, sorted by names ignoring uppercase, then files sorted by names * ignoring uppercase. * @param files list to sort * @return sorted file list */ public static Array sortFiles (FileHandle[] files) { return sortFiles(files, FILE_NAME_COMPARATOR); } /** * Sorts file list, using this rules: directories first, sorted using provided comparator, then files sorted using provided comparator. * @param files list to sort * @param comparator comparator used to sort files and directories list * @return sorted file list */ public static Array sortFiles (FileHandle[] files, Comparator comparator) { return sortFiles(files, comparator, false); } /** * Sorts file list, using this rules: directories first, sorted using provided comparator, then files sorted using provided comparator. * @param files list to sort * @param comparator comparator used to sort files list * @param descending if true then sorted list will be in reversed order * @return sorted file list */ public static Array sortFiles (FileHandle[] files, Comparator comparator, boolean descending) { Array directoriesList = new Array(); Array filesList = new Array(); for (FileHandle f : files) { if (f.isDirectory()) { directoriesList.add(f); } else { filesList.add(f); } } Sort sorter = new Sort(); sorter.sort(directoriesList, comparator); sorter.sort(filesList, comparator); if (descending) { directoriesList.reverse(); filesList.reverse(); } directoriesList.addAll(filesList); // combine lists return directoriesList; } /** * Checks whether given name is valid for current user OS. * @param name that will be checked * @return true if name is valid, false otherwise */ public static boolean isValidFileName (String name) { try { if (OsUtils.isWindows()) { if (name.contains(">") || name.contains("<")) return false; name = name.toLowerCase(); //Windows is case insensitive } return new File(name).getCanonicalFile().getName().equals(name); } catch (Exception e) { return false; } } /** Converts {@link File} to absolute {@link FileHandle}. */ public static FileHandle toFileHandle (File file) { return Gdx.files.absolute(file.getAbsolutePath()); } /** Shows given directory in system explorer window. */ @SuppressWarnings("unchecked") public static void showDirInExplorer (FileHandle dir) throws IOException { File dirToShow; if (dir.isDirectory()) { dirToShow = dir.file(); } else { dirToShow = dir.parent().file(); } try { // Using reflection to avoid importing AWT desktop which would trigger Android Lint errors // This is desktop only, rarely called, performance drop is negligible // Basically 'Desktop.getDesktop().open(dirToShow);' Class desktopClass = Class.forName("java.awt.Desktop"); Object desktop = desktopClass.getMethod("getDesktop").invoke(null); try { // browseFileDirectory was introduced in JDK 9 desktopClass.getMethod("browseFileDirectory", File.class).invoke(desktop, dirToShow); } catch (NoSuchMethodException | InvocationTargetException e) { // browseFileDirectory throws UnsupportedOperationException on some platforms, which is then // wrapped in InvocationTargetException because it's accessed via reflection. // throw again all other exceptions we didn't expect if (e instanceof InvocationTargetException && !(e.getCause() instanceof UnsupportedOperationException)) { throw e; } desktopClass.getMethod("open", File.class).invoke(desktop, dirToShow); } } catch (Exception e) { Gdx.app.log("VisUI", "Can't open file " + dirToShow.getPath(), e); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/SingleFileChooserListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.file.FileChooser.SelectionMode; /** * Implementation of {@link FileChooserListener} that can be used when user picks only one file. Provides convenient * {@link #selected(FileHandle)} method. If user picked more than one file (note that chooser must be in multiple select * mode for that to happen, see {@link FileChooser#setSelectionMode(SelectionMode)}), that method * will be called only for first selected file and remaining files will be ignored. * @author Kotcrab * @since 1.0.0 */ public abstract class SingleFileChooserListener implements FileChooserListener { @Override public final void selected (Array files) { selected(files.first()); } /** Called for first file in selection. See {@link SingleFileChooserListener}. */ protected abstract void selected (FileHandle file); @Override public void canceled () { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/StreamingFileChooserListener.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; /** * Implementation of {@link FileChooserListener} that streams chooser selection. Provides convenient * {@link #selected(FileHandle)} method that will be called for every selected file after user finished choosing files. * Before streaming starts {@link #begin()} is called, after streaming has finished {@link #end()} is called. * @author Kotcrab * @since 1.0.0 */ public abstract class StreamingFileChooserListener implements FileChooserListener { @Override public final void selected (Array files) { begin(); for (FileHandle file : files) { selected(file); } end(); } /** * Called after user finished selecting files. If user picked multiple files this will be called separately * for every selected file. */ public abstract void selected (FileHandle file); /** Called after user finished selecting files, before streaming started. */ public void begin () { } /** Called after file selection streaming has finished. */ public void end () { } @Override public void canceled () { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/AbstractSuggestionPopup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.kotcrab.vis.ui.widget.MenuItem; import com.kotcrab.vis.ui.widget.PopupMenu; import com.kotcrab.vis.ui.widget.file.FileChooser; /** @author Kotcrab */ public class AbstractSuggestionPopup extends PopupMenu { public static final int MAX_SUGGESTIONS = 10; final FileChooser chooser; public AbstractSuggestionPopup (FileChooser chooser) { super(chooser.getChooserStyle().popupMenuStyle); this.chooser = chooser; } protected MenuItem createMenuItem (String name) { MenuItem item = new MenuItem(name); item.getImageCell().size(0); item.getShortcutCell().space(0).pad(0); item.getSubMenuIconCell().size(0).space(0).pad(0); return item; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/DirsSuggestionPopup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.actions.Actions; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.MenuItem; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.file.FileChooser; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** @author Kotcrab */ public class DirsSuggestionPopup extends AbstractSuggestionPopup { private final VisTextField pathField; private ExecutorService listDirExecutor = Executors.newSingleThreadExecutor(new ServiceThreadFactory("FileChooserListDirThread")); private Future listDirFuture; public DirsSuggestionPopup (FileChooser chooser, VisTextField pathField) { super(chooser); this.pathField = pathField; } public void pathFieldKeyTyped (Stage stage, float width) { if (pathField.getText().length() == 0) { remove(); return; } createDirSuggestions(stage, width); } private void createDirSuggestions (final Stage stage, final float width) { final String pathFieldText = pathField.getText(); //quiet period before listing files takes too long and popup will be removed addAction(Actions.sequence(Actions.delay(0.2f, Actions.removeActor()))); if (listDirFuture != null) listDirFuture.cancel(true); listDirFuture = listDirExecutor.submit(new Runnable() { @Override public void run () { FileHandle enteredDir = Gdx.files.absolute(pathFieldText); final FileHandle listDir; final String partialPath; if (enteredDir.exists()) { listDir = enteredDir; partialPath = ""; } else { listDir = enteredDir.parent(); partialPath = enteredDir.name(); } final FileHandle[] files = listDir.list(chooser.getFileFilter()); if (Thread.currentThread().isInterrupted()) return; Gdx.app.postRunnable(new Runnable() { @Override public void run () { clearChildren(); clearActions(); int suggestions = 0; for (final FileHandle file : files) { if (file.exists() == false || file.isDirectory() == false) continue; if (file.name().startsWith(partialPath) == false || file.name().equals(partialPath)) continue; MenuItem item = createMenuItem(file.path()); item.getLabel().setEllipsis(true); item.getLabelCell().width(width - 20); addItem(item); item.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setDirectory(file, FileChooser.HistoryPolicy.ADD); } }); suggestions++; if (suggestions == MAX_SUGGESTIONS) { break; } } if (suggestions == 0) { remove(); return; } showMenu(stage, pathField); setWidth(width); layout(); } }); } }); } public void showRecentDirectories (Stage stage, Array recentDirectories, float width) { int suggestions = createRecentDirSuggestions(recentDirectories, width); if (suggestions == 0) { remove(); return; } showMenu(stage, pathField); setWidth(width); layout(); } private int createRecentDirSuggestions (Array files, float width) { clearChildren(); int suggestions = 0; for (final FileHandle file : files) { if (file.exists() == false) continue; MenuItem item = createMenuItem(file.path()); item.getLabel().setEllipsis(true); item.getLabelCell().width(width - 20); addItem(item); item.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setDirectory(file, FileChooser.HistoryPolicy.ADD); } }); suggestions++; if (suggestions == MAX_SUGGESTIONS) { break; } } return suggestions; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/DriveCheckerService.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.Array; import java.io.File; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Used to check whether file system root is readable or writeable. * @author Kotcrab */ public class DriveCheckerService { private static DriveCheckerService instance; private final ExecutorService pool; private Array readableRoots = new Array(); private Array writableRoots = new Array(); private Map readableListeners = new HashMap(); private Map writableListeners = new HashMap(); public static synchronized DriveCheckerService getInstance () { if (instance == null) instance = new DriveCheckerService(); return instance; } public DriveCheckerService () { pool = Executors.newFixedThreadPool(3, new ServiceThreadFactory("DriveStatusChecker")); File[] roots = File.listRoots(); for (File root : roots) { processRoot(root); } } private void processRoot (final File root) { pool.execute(new Runnable() { @Override public void run () { processResults(root, root.canRead(), root.canWrite()); } }); } private void processResults (final File root, final boolean readable, final boolean writable) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { if (readable) { readableRoots.add(root); ListenerSet set = readableListeners.get(root); if (set != null) { set.notifyListeners(root, RootMode.READABLE); } } if (writable) { writableRoots.add(root); ListenerSet set = writableListeners.get(root); if (set != null) { set.notifyListeners(root, RootMode.WRITABLE); } } } }); } public void addListener (File root, RootMode mode, DriveCheckerListener listener) { switch (mode) { case READABLE: addListener(root, mode, listener, readableRoots, readableListeners); break; case WRITABLE: addListener(root, mode, listener, writableRoots, writableListeners); break; } } private void addListener (File root, RootMode mode, DriveCheckerListener listener, Array cachedRoots, Map listeners) { if (cachedRoots.contains(root, false)) { listener.rootMode(root, mode); return; } ListenerSet set = listeners.get(root); if (set == null) { set = new ListenerSet(); listeners.put(root, set); } set.add(listener); processRoot(root); } public enum RootMode { READABLE, WRITABLE } public interface DriveCheckerListener { void rootMode (File root, RootMode mode); } public class ListenerSet { Array list = new Array(); public void add (DriveCheckerListener listener) { list.add(listener); } public void notifyListeners (File root, RootMode mode) { Iterator it = list.iterator(); while (it.hasNext()) { DriveCheckerListener listener = it.next(); listener.rootMode(root, mode); it.remove(); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileChooserText.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.utils.I18NBundle; import com.kotcrab.vis.ui.Locales; import com.kotcrab.vis.ui.i18n.BundleText; /** * Contains texts for chooser access via I18NBundle. * @author Kotcrab * @since 0.7.0 */ public enum FileChooserText implements BundleText { TITLE_CHOOSE_FILES("titleChooseFiles"), TITLE_CHOOSE_DIRECTORIES("titleChooseDirectories"), TITLE_CHOOSE_FILES_AND_DIRECTORIES("titleChooseFilesAndDirectories"), CANCEL("cancel"), FILE_NAME("fileName"), FILE_TYPE("fileType"), ALL_FILES("allFiles"), DESKTOP("desktop"), COMPUTER("computer"), OPEN("open"), SAVE("save"), BACK("back"), FORWARD("forward"), PARENT_DIRECTORY("parentDirectory"), NEW_DIRECTORY("newDirectory"), DIRECTORY_NO_LONGER_EXISTS("directoryNoLongerExists"), POPUP_TITLE("popupTitle"), POPUP_CHOOSE_FILE("popupChooseFile"), POPUP_SELECTED_FILE_DOES_NOT_EXIST("popupSelectedFileDoesNotExist"), POPUP_DIRECTORY_DOES_NOT_EXIST("popupDirectoryDoesNotExist"), POPUP_ONLY_DIRECTORIES("popupOnlyDirectories"), POPUP_FILENAME_INVALID("popupFilenameInvalid"), POPUP_FILE_EXIST_OVERWRITE("popupFileExistOverwrite"), POPUP_MULTIPLE_FILE_EXIST_OVERWRITE("popupMultipleFileExistOverwrite"), POPUP_DELETE_FILE_FAILED("popupDeleteFileFailed"), CONTEXT_MENU_DELETE("contextMenuDelete"), CONTEXT_MENU_MOVE_TO_TRASH("contextMenuMoveToTrash"), CONTEXT_MENU_SHOW_IN_EXPLORER("contextMenuShowInExplorer"), CONTEXT_MENU_REFRESH("contextMenuRefresh"), CONTEXT_MENU_ADD_TO_FAVORITES("contextMenuAddToFavorites"), CONTEXT_MENU_REMOVE_FROM_FAVORITES("contextMenuRemoveFromFavorites"), CONTEXT_MENU_DELETE_WARNING("contextMenuDeleteWarning"), CONTEXT_MENU_MOVE_TO_TRASH_WARNING("contextMenuMoveToTrashWarning"), CONTEXT_MENU_NEW_DIRECTORY("contextMenuNewDirectory"), CONTEXT_MENU_SORT_BY("contextMenuSortBy"), SORT_BY_NAME("sortByName"), SORT_BY_DATE("sortByDate"), SORT_BY_SIZE("sortBySize"), SORT_BY_ASCENDING("sortByAscending"), SORT_BY_DESCENDING("sortByDescending"), NEW_DIRECTORY_DIALOG_TITLE("newDirectoryDialogTitle"), NEW_DIRECTORY_DIALOG_TEXT("newDirectoryDialogText"), NEW_DIRECTORY_DIALOG_ILLEGAL_CHARACTERS("newDirectoryDialogIllegalCharacters"), NEW_DIRECTORY_DIALOG_ALREADY_EXISTS("newDirectoryDialogAlreadyExists"), CHANGE_VIEW_MODE("changeViewMode"), VIEW_MODE_LIST("viewModeList"), VIEW_MODE_DETAILS("viewModeDetails"), VIEW_MODE_BIG_ICONS("viewModeBigIcons"), VIEW_MODE_MEDIUM_ICONS("viewModeMediumIcons"), VIEW_MODE_SMALL_ICONS("viewModeSmallIcons"); private final String name; FileChooserText (final String name) { this.name = name; } private static I18NBundle getBundle () { return Locales.getFileChooserBundle(); } @Override public final String getName () { return name; } @Override public final String get () { return getBundle().get(name); } @Override public final String format () { return getBundle().format(name); } @Override public final String format (final Object... arguments) { return getBundle().format(name, arguments); } @Override public final String toString () { return get(); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileChooserWinService.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ObjectMap; import com.kotcrab.vis.ui.util.OsUtils; import java.io.File; import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Used to get system drive name. Only used on Windows. * @author Kotcrab */ public class FileChooserWinService { private static FileChooserWinService instance; private ObjectMap nameCache = new ObjectMap(); private Map listeners = new HashMap(); private final ExecutorService pool; private boolean shellFolderSupported = false; private Method getShellFolderMethod; private Method getShellFolderDisplayNameMethod; public static synchronized FileChooserWinService getInstance () { if (OsUtils.isWindows() == false) return null; if (instance == null) instance = new FileChooserWinService(); return instance; } @SuppressWarnings("unchecked") protected FileChooserWinService () { pool = Executors.newFixedThreadPool(3, new ServiceThreadFactory("SystemDisplayNameGetter")); try { Class shellFolderClass = Class.forName("sun.awt.shell.ShellFolder"); getShellFolderMethod = shellFolderClass.getMethod("getShellFolder", File.class); getShellFolderDisplayNameMethod = shellFolderClass.getMethod("getDisplayName"); shellFolderSupported = true; } catch (ClassNotFoundException ignored) { //ShellFolder not supported on current JVM, ignoring } catch (NoSuchMethodException ignored) { } File[] roots = File.listRoots(); for (File root : roots) { processRoot(root); } } private void processRoot (final File root) { pool.execute(new Runnable() { @Override public void run () { processResult(root, getSystemDisplayName(root)); } }); } private void processResult (final File root, final String name) { Gdx.app.postRunnable(new Runnable() { @Override public void run () { if (name != null) nameCache.put(root, name); else nameCache.put(root, root.toString()); ListenerSet set = listeners.get(root); if (set != null) set.notifyListeners(name); } }); } public void addListener (File root, RootNameListener listener) { String cachedName = nameCache.get(root); if (cachedName != null) { listener.setRootName(cachedName); return; } ListenerSet set = listeners.get(root); if (set == null) { set = new ListenerSet(); listeners.put(root, set); } set.add(listener); processRoot(root); } private String getSystemDisplayName (File f) { if (shellFolderSupported == false) return null; String name; try { //name = ShellFolder.getShellFolder(f).getDisplayName(); Object shellFolder = getShellFolderMethod.invoke(null, f); name = (String) getShellFolderDisplayNameMethod.invoke(shellFolder); } catch (InvocationTargetException e) { return null; } catch (IllegalAccessException e) { return null; } if (name == null || name.length() == 0) { name = f.getPath(); // the "/" directory } return name; } public interface RootNameListener { void setRootName (String newName); } private static class ListenerSet { Array> list = new Array>(); public void add (RootNameListener listener) { list.add(new WeakReference(listener)); } public void notifyListeners (String newName) { Iterator> it = list.iterator(); while (it.hasNext()) { RootNameListener listener = it.next().get(); if (listener == null) { it.remove(); continue; } listener.setRootName(newName); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileHandleMetadata.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.files.FileHandle; import com.kotcrab.vis.ui.widget.file.FileUtils; public class FileHandleMetadata { private final String name; private final boolean directory; private final long lastModified; private final long length; private final String readableFileSize; public static FileHandleMetadata of (FileHandle file) { return new FileHandleMetadata(file); } private FileHandleMetadata (FileHandle file) { this.name = file.name(); this.directory = file.isDirectory(); this.lastModified = file.lastModified(); this.length = file.length(); this.readableFileSize = FileUtils.readableFileSize(length); } public String name () { return name; } public boolean isDirectory () { return directory; } public long lastModified () { return lastModified; } public long length () { return length; } public String readableFileSize () { return readableFileSize; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileHistoryManager.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.widget.VisImageButton; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.file.FileChooser; import com.kotcrab.vis.ui.widget.file.FileChooser.HistoryPolicy; import com.kotcrab.vis.ui.widget.file.FileChooserStyle; import static com.kotcrab.vis.ui.widget.file.internal.FileChooserText.*; /** * Manages {@link FileChooser} history of directories that user navigated into. This is internal VisUI API however this class * is also reused by VisEditor. * @author Kotcrab */ public class FileHistoryManager { private final FileHistoryCallback callback; private Array history = new Array(); private Array historyForward = new Array(); private VisTable buttonsTable; private VisImageButton backButton; private VisImageButton forwardButton; public FileHistoryManager (FileChooserStyle style, FileHistoryCallback callback) { this.callback = callback; backButton = new VisImageButton(style.iconArrowLeft, BACK.get()); backButton.setGenerateDisabledImage(true); backButton.setDisabled(true); forwardButton = new VisImageButton(style.iconArrowRight, FORWARD.get()); forwardButton.setGenerateDisabledImage(true); forwardButton.setDisabled(true); buttonsTable = new VisTable(true); buttonsTable.add(backButton); buttonsTable.add(forwardButton); backButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { historyBack(); } }); forwardButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { historyForward(); } }); } public ClickListener getDefaultClickListener () { return new ClickListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (button == Buttons.BACK || button == Buttons.FORWARD) { return true; } return super.touchDown(event, x, y, pointer, button); } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { if (button == Buttons.BACK && hasHistoryBack()) { historyBack(); } else if (button == Buttons.FORWARD && hasHistoryForward()) { historyForward(); } else { super.touchUp(event, x, y, pointer, button); } } }; } public VisTable getButtonsTable () { return buttonsTable; } public void historyClear () { history.clear(); historyForward.clear(); forwardButton.setDisabled(true); backButton.setDisabled(true); } public void historyAdd () { history.add(callback.getCurrentDirectory()); historyForward.clear(); backButton.setDisabled(false); forwardButton.setDisabled(true); } public void historyBack () { FileHandle dir = history.pop(); historyForward.add(callback.getCurrentDirectory()); if (setDirectoryFromHistory(dir) == false) historyForward.pop(); if (!hasHistoryBack()) backButton.setDisabled(true); forwardButton.setDisabled(false); } public void historyForward () { FileHandle dir = historyForward.pop(); history.add(callback.getCurrentDirectory()); if (setDirectoryFromHistory(dir) == false) history.pop(); if (!hasHistoryForward()) forwardButton.setDisabled(true); backButton.setDisabled(false); } private boolean setDirectoryFromHistory (FileHandle dir) { if (dir.exists()) { callback.setDirectory(dir, HistoryPolicy.IGNORE); return true; } else { Dialogs.showErrorDialog(callback.getStage(), DIRECTORY_NO_LONGER_EXISTS.get()); return false; } } /** @return returns {@code true} if a forward-history is available */ private boolean hasHistoryForward () { return historyForward.size != 0; } /** @return returns {@code true} if a back-history is available */ private boolean hasHistoryBack () { return history.size != 0; } public interface FileHistoryCallback { FileHandle getCurrentDirectory (); void setDirectory (FileHandle directory, HistoryPolicy policy); Stage getStage (); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileListAdapter.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ObjectMap; import com.kotcrab.vis.ui.layout.GridGroup; import com.kotcrab.vis.ui.util.adapter.ArrayAdapter; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.file.FileChooser; /** @author Kotcrab */ public class FileListAdapter extends ArrayAdapter { private final FileChooser chooser; private final Array orderedViews = new Array(); private GridGroup gridGroup; public FileListAdapter (FileChooser chooser, Array files) { super(files); this.chooser = chooser; gridGroup = new GridGroup(128f, 2f); } @Override protected FileChooser.FileItem createView (FileHandle item) { return chooser.new FileItem(item, chooser.getViewMode()); } @Override public void fillTable (VisTable itemsTable) { getViews().clear(); //clear cache orderedViews.clear(); gridGroup.clear(); if (getItemsSorter() != null) sort(getItemsSorter()); FileChooser.ViewMode viewMode = chooser.getViewMode(); if (viewMode.isGridMode()) { viewMode.setupGridGroup(chooser.getSizes(), gridGroup); for (final FileHandle item : iterable()) { final FileChooser.FileItem view = getView(item); orderedViews.add(view); prepareViewBeforeAddingToTable(item, view); gridGroup.addActor(view); } itemsTable.add(gridGroup).growX().minWidth(0); } else { for (final FileHandle item : iterable()) { final FileChooser.FileItem view = getView(item); orderedViews.add(view); prepareViewBeforeAddingToTable(item, view); itemsTable.add(view).growX(); itemsTable.row(); } } } @Override public ObjectMap getViews () { return super.getViews(); } public Array getOrderedViews () { return orderedViews; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FilePopupMenu.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.MenuItem; import com.kotcrab.vis.ui.widget.PopupMenu; import com.kotcrab.vis.ui.widget.file.FileChooser; import com.kotcrab.vis.ui.widget.file.FileChooserStyle; import com.kotcrab.vis.ui.widget.file.FileUtils; import java.io.File; import java.io.IOException; import static com.kotcrab.vis.ui.widget.file.internal.FileChooserText.*; /** @author Kotcrab */ public class FilePopupMenu extends PopupMenu { private final FileChooserStyle style; private SortingPopupMenu sortingPopupMenu; private FileHandle file; private MenuItem delete; private MenuItem newDirectory; private MenuItem showInExplorer; private MenuItem refresh; private MenuItem addToFavorites; private MenuItem removeFromFavorites; private MenuItem sortBy; public FilePopupMenu (final FileChooser chooser, final FilePopupMenuCallback callback) { super(chooser.getChooserStyle().popupMenuStyle); this.style = chooser.getChooserStyle(); sortingPopupMenu = new SortingPopupMenu(chooser); delete = new MenuItem(CONTEXT_MENU_DELETE.get(), style.iconTrash); newDirectory = new MenuItem(CONTEXT_MENU_NEW_DIRECTORY.get(), style.iconFolderNew); showInExplorer = new MenuItem(CONTEXT_MENU_SHOW_IN_EXPLORER.get()); refresh = new MenuItem(CONTEXT_MENU_REFRESH.get(), style.iconRefresh); addToFavorites = new MenuItem(CONTEXT_MENU_ADD_TO_FAVORITES.get(), style.iconFolderStar); removeFromFavorites = new MenuItem(CONTEXT_MENU_REMOVE_FROM_FAVORITES.get(), style.iconFolderStar); sortBy = new MenuItem(CONTEXT_MENU_SORT_BY.get()); sortBy.setSubMenu(sortingPopupMenu); delete.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { callback.showFileDelDialog(file); } }); newDirectory.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { callback.showNewDirDialog(); } }); showInExplorer.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { try { FileUtils.showDirInExplorer(file); } catch (IOException e) { e.printStackTrace(); } } }); refresh.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.refresh(); } }); addToFavorites.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { chooser.addFavorite(file); } }); removeFromFavorites.addListener(new ClickListener() { @Override public void clicked (InputEvent event, float x, float y) { chooser.removeFavorite(file); } }); } public void build () { sortingPopupMenu.build(); clearChildren(); addItem(newDirectory); addItem(sortBy); addItem(refresh); } public void build (Array favorites, FileHandle file) { sortingPopupMenu.build(); this.file = file; clearChildren(); addItem(newDirectory); addItem(sortBy); addItem(refresh); addSeparator(); if (file.type() == FileType.Absolute || file.type() == FileType.External) addItem(delete); if (file.type() == FileType.Absolute) { addItem(showInExplorer); if (file.isDirectory()) { if (favorites.contains(file, false)) addItem(removeFromFavorites); else addItem(addToFavorites); } } } public void buildForFavorite (Array favorites, File file) { this.file = Gdx.files.absolute(file.getAbsolutePath()); clearChildren(); addItem(showInExplorer); if (favorites.contains(this.file, false)) addItem(removeFromFavorites); } public boolean isAddedToStage () { return getStage() != null; } public void fileDeleterChanged (boolean trashAvailable) { delete.setText(trashAvailable ? CONTEXT_MENU_MOVE_TO_TRASH.get() : CONTEXT_MENU_DELETE.get()); } public interface FilePopupMenuCallback { void showNewDirDialog (); void showFileDelDialog (FileHandle file); } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/FileSuggestionPopup.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.widget.MenuItem; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.file.FileChooser; import com.kotcrab.vis.ui.widget.file.FileTypeFilter; /** @author Kotcrab */ public class FileSuggestionPopup extends AbstractSuggestionPopup { public FileSuggestionPopup (FileChooser chooser) { super(chooser); } public void pathFieldKeyTyped (Stage stage, Array files, VisTextField pathField) { if (pathField.getText().length() == 0) { remove(); return; } int suggestions = createSuggestions(files, pathField); if (suggestions == 0) { remove(); return; } showMenu(stage, pathField); } private int createSuggestions (Array files, final VisTextField fileNameField) { clearChildren(); int suggestions = 0; for (final FileHandle file : files) { if (file.name().startsWith(fileNameField.getText()) && file.name().equals(fileNameField.getText()) == false) { MenuItem item = createMenuItem(getTrimmedName(file.name())); item.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.highlightFiles(file); } }); addItem(item); suggestions++; } if (suggestions == MAX_SUGGESTIONS) { break; } } if (chooser.getMode() == FileChooser.Mode.SAVE && suggestions == 0 //don't show matches when there are files that actually exist and matched previous search && chooser.getActiveFileTypeFilterRule() != null && fileNameField.getText().matches(".*\\.")) { FileTypeFilter.Rule rule = chooser.getActiveFileTypeFilterRule(); for (String extension : rule.getExtensions()) { MenuItem item = createMenuItem(fileNameField.getText() + extension); final String arbitraryPath = fileNameField.getText() + extension; item.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { fileNameField.setText(arbitraryPath); fileNameField.setCursorAtTextEnd(); } }); addItem(item); suggestions++; } } return suggestions; } private String getTrimmedName (String name) { if (name.length() > 40) { return name.substring(0, 40) + "..."; } else { return name; } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/IconStack.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup; import com.badlogic.gdx.scenes.scene2d.utils.Layout; import com.badlogic.gdx.utils.SnapshotArray; import com.kotcrab.vis.ui.widget.VisCheckBox; import com.kotcrab.vis.ui.widget.VisImage; /** @author Kotcrab */ public class IconStack extends WidgetGroup { private float prefWidth, prefHeight, minWidth, minHeight, maxWidth, maxHeight; private boolean sizeInvalid = true; private VisImage icon; private VisCheckBox checkBox; public IconStack (VisImage icon, VisCheckBox checkBox) { this.icon = icon; this.checkBox = checkBox; setTransform(false); setTouchable(Touchable.childrenOnly); addActor(icon); addActor(checkBox); } @Override public void invalidate () { super.invalidate(); sizeInvalid = true; } private void computeSize () { sizeInvalid = false; prefWidth = 0; prefHeight = 0; minWidth = 0; minHeight = 0; maxWidth = 0; maxHeight = 0; SnapshotArray children = getChildren(); for (int i = 0, n = children.size; i < n; i++) { Actor child = children.get(i); float childMaxWidth, childMaxHeight; if (child instanceof Layout) { Layout layout = (Layout) child; prefWidth = Math.max(prefWidth, layout.getPrefWidth()); prefHeight = Math.max(prefHeight, layout.getPrefHeight()); minWidth = Math.max(minWidth, layout.getMinWidth()); minHeight = Math.max(minHeight, layout.getMinHeight()); childMaxWidth = layout.getMaxWidth(); childMaxHeight = layout.getMaxHeight(); } else { prefWidth = Math.max(prefWidth, child.getWidth()); prefHeight = Math.max(prefHeight, child.getHeight()); minWidth = Math.max(minWidth, child.getWidth()); minHeight = Math.max(minHeight, child.getHeight()); childMaxWidth = 0; childMaxHeight = 0; } if (childMaxWidth > 0) maxWidth = maxWidth == 0 ? childMaxWidth : Math.min(maxWidth, childMaxWidth); if (childMaxHeight > 0) maxHeight = maxHeight == 0 ? childMaxHeight : Math.min(maxHeight, childMaxHeight); } } public void add (Actor actor) { addActor(actor); } @Override public void layout () { if (sizeInvalid) computeSize(); float width = getWidth(), height = getHeight(); icon.setBounds(0, 0, width, height); icon.validate(); float checkHeight = checkBox.getStyle().checkBackground.getMinHeight(); checkBox.setBounds(3, height - checkHeight - 3, checkBox.getPrefWidth(), checkBox.getPrefHeight()); checkBox.validate(); } @Override public float getPrefWidth () { if (sizeInvalid) computeSize(); return prefWidth; } @Override public float getPrefHeight () { if (sizeInvalid) computeSize(); return prefHeight; } @Override public float getMinWidth () { if (sizeInvalid) computeSize(); return minWidth; } @Override public float getMinHeight () { if (sizeInvalid) computeSize(); return minHeight; } @Override public float getMaxWidth () { if (sizeInvalid) computeSize(); return maxWidth; } @Override public float getMaxHeight () { if (sizeInvalid) computeSize(); return maxHeight; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/PreferencesIO.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Preferences; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; /** @author Kotcrab */ public class PreferencesIO { private static final String VIS_DEFAULT_PREFS_NAME = "com.kotcrab.vis.ui.widget.file.filechooser_favorites"; private static String defaultPrefsName = VIS_DEFAULT_PREFS_NAME; private String favoritesKeyName = "favorites"; private String recentDirKeyName = "recentDirectories"; private String lastDirKeyName = "lastDirectory"; private Preferences prefs; private Json json = new Json(); public PreferencesIO () { this(defaultPrefsName); } public PreferencesIO (String prefsName) { prefs = Gdx.app.getPreferences(prefsName); checkIfUsingDefaultName(); } public void checkIfUsingDefaultName () { if (defaultPrefsName.equals(VIS_DEFAULT_PREFS_NAME)) { Gdx.app.log("VisUI", "Warning, using default preferences file name for file chooser! (see FileChooser.setDefaultPrefsName(String))"); } } public static void setDefaultPrefsName (String prefsName) { if (prefsName == null) throw new IllegalStateException("prefsName can't be null"); PreferencesIO.defaultPrefsName = prefsName; } public Array loadFavorites () { String data = prefs.getString(favoritesKeyName, null); if (data == null) return new Array(); else return json.fromJson(FileArrayData.class, data).toFileHandleArray(); } public void saveFavorites (Array favorites) { prefs.putString(favoritesKeyName, json.toJson(new FileArrayData(favorites))); prefs.flush(); } public Array loadRecentDirectories () { String data = prefs.getString(recentDirKeyName, null); if (data == null) return new Array(); else return json.fromJson(FileArrayData.class, data).toFileHandleArray(); } public void saveRecentDirectories (Array recentDirs) { prefs.putString(recentDirKeyName, json.toJson(new FileArrayData(recentDirs))); prefs.flush(); } public FileHandle loadLastDirectory () { String data = prefs.getString(lastDirKeyName, null); if (data == null) return null; return json.fromJson(FileHandleData.class, data).toFileHandle(); } public void saveLastDirectory (FileHandle file) { prefs.putString(lastDirKeyName, json.toJson(new FileHandleData(file))); prefs.flush(); } private static class FileArrayData { public Array data; public FileArrayData () { } public FileArrayData (Array favourites) { data = new Array(); for (FileHandle file : favourites) data.add(new FileHandleData(file)); } public Array toFileHandleArray () { Array files = new Array(); for (FileHandleData fileData : data) { files.add(fileData.toFileHandle()); } return files; } } private static class FileHandleData { public FileType type; public String path; public FileHandleData () { } public FileHandleData (FileHandle file) { type = file.type(); path = file.path(); } public FileHandle toFileHandle () { switch (type) { case Absolute: return Gdx.files.absolute(path); case Classpath: return Gdx.files.classpath(path); case External: return Gdx.files.external(path); case Internal: return Gdx.files.internal(path); case Local: return Gdx.files.local(path); default: throw new IllegalStateException("Unknown file type!"); } } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/ServiceThreadFactory.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicLong; /** @author Kotcrab */ public class ServiceThreadFactory implements ThreadFactory { private final AtomicLong count = new AtomicLong(0); private final String threadPrefix; public ServiceThreadFactory (String threadPrefix) { super(); this.threadPrefix = threadPrefix + "-"; } @Override public Thread newThread (Runnable runnable) { Thread thread = Executors.defaultThreadFactory().newThread(runnable); thread.setName(threadPrefix + count.getAndIncrement()); thread.setDaemon(true); return thread; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/SortingPopupMenu.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.file.internal; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.widget.MenuItem; import com.kotcrab.vis.ui.widget.PopupMenu; import com.kotcrab.vis.ui.widget.file.FileChooser; import static com.kotcrab.vis.ui.widget.file.internal.FileChooserText.*; /** @author Kotcrab */ public class SortingPopupMenu extends PopupMenu { private final FileChooser chooser; private final Drawable selectedMenuItem; private MenuItem sortByName; private MenuItem sortByDate; private MenuItem sortBySize; private MenuItem sortByAscending; private MenuItem sortByDescending; private Image sortByNameImage; private Image sortByDateImage; private Image sortBySizeImage; private Image sortByAscendingImage; private Image sortByDescendingImage; public SortingPopupMenu (final FileChooser chooser) { selectedMenuItem = chooser.getChooserStyle().contextMenuSelectedItem; this.chooser = chooser; addItem(sortByName = new MenuItem(SORT_BY_NAME.get(), selectedMenuItem, new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setSorting(FileChooser.FileSorting.NAME, true); } })); addItem(sortByDate = new MenuItem(SORT_BY_DATE.get(), selectedMenuItem, new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setSorting(FileChooser.FileSorting.MODIFIED_DATE, false); } })); addItem(sortBySize = new MenuItem(SORT_BY_SIZE.get(), selectedMenuItem, new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setSorting(FileChooser.FileSorting.SIZE, true); } })); addSeparator(); addItem(sortByAscending = new MenuItem(SORT_BY_ASCENDING.get(), selectedMenuItem, new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setSortingOrderAscending(true); } })); addItem(sortByDescending = new MenuItem(SORT_BY_DESCENDING.get(), selectedMenuItem, new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { chooser.setSortingOrderAscending(false); } })); sortByNameImage = sortByName.getImage(); sortByDateImage = sortByDate.getImage(); sortBySizeImage = sortBySize.getImage(); sortByAscendingImage = sortByAscending.getImage(); sortByDescendingImage = sortByDescending.getImage(); sortByNameImage.setScaling(Scaling.none); sortByDateImage.setScaling(Scaling.none); sortBySizeImage.setScaling(Scaling.none); sortByAscendingImage.setScaling(Scaling.none); sortByDescendingImage.setScaling(Scaling.none); } public void build () { sortByNameImage.setDrawable(null); sortByDateImage.setDrawable(null); sortBySizeImage.setDrawable(null); sortByAscendingImage.setDrawable(null); sortByDescendingImage.setDrawable(null); switch (chooser.getSorting()) { case NAME: sortByNameImage.setDrawable(selectedMenuItem); break; case MODIFIED_DATE: sortByDateImage.setDrawable(selectedMenuItem); break; case SIZE: sortBySizeImage.setDrawable(selectedMenuItem); break; } if (chooser.isSortingOrderAscending()) { sortByAscendingImage.setDrawable(selectedMenuItem); } else { sortByDescendingImage.setDrawable(selectedMenuItem); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/file/internal/package-info.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Internal FileChooser utilities. All files in this package are not considered as public VisUI API. Changes to those classes * won't be listed in CHANGES file. * @author Kotcrab */ package com.kotcrab.vis.ui.widget.file.internal; ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/internal/SplitPaneCursorManager.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.internal; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Cursor; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.kotcrab.vis.ui.util.CursorManager; /** * Manages setting custom cursor for split panes. * This is VisUI internal class * @author Kotcrab * @since 1.4.0 */ public abstract class SplitPaneCursorManager extends ClickListener { private Actor owner; private boolean vertical; private Cursor.SystemCursor currentCursor; public SplitPaneCursorManager (Actor owner, boolean vertical) { this.owner = owner; this.vertical = vertical; } @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { return handleBoundsContains(x, y); } @Override public void touchDragged (InputEvent event, float x, float y, int pointer) { super.touchDragged(event, x, y, pointer); //handles setting cursor when mouse returned to widget after exiting it while dragged if (contains(x, y)) { setCustomCursor(); } else { clearCustomCursor(); } } @Override public boolean mouseMoved (InputEvent event, float x, float y) { super.mouseMoved(event, x, y); if (handleBoundsContains(x, y)) { setCustomCursor(); } else { clearCustomCursor(); } return false; } @Override public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) { super.exit(event, x, y, pointer, toActor); if (pointer == -1 && (toActor == null || toActor.isDescendantOf(owner) == false)) { clearCustomCursor(); } } private void setCustomCursor () { Cursor.SystemCursor targetCursor; if (vertical) { targetCursor = Cursor.SystemCursor.VerticalResize; } else { targetCursor = Cursor.SystemCursor.HorizontalResize; } if (currentCursor != targetCursor) { Gdx.graphics.setSystemCursor(targetCursor); currentCursor = targetCursor; } } private void clearCustomCursor () { if (currentCursor != null) { CursorManager.restoreDefaultCursor(); currentCursor = null; } } protected abstract boolean handleBoundsContains (float x, float y); protected abstract boolean contains (float x, float y); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/AbstractSpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; /** * Basic implementation of {@link SpinnerModel} simplifying event handling for custom models. * @author Kotcrab * @see SpinnerModel * @see IntSpinnerModel * @see FloatSpinnerModel * @see SimpleFloatSpinnerModel * @see ArraySpinnerModel * @since 1.0.2 */ public abstract class AbstractSpinnerModel implements SpinnerModel { protected Spinner spinner; private boolean allowRebind; private boolean wrap; public AbstractSpinnerModel (boolean allowRebind) { this.allowRebind = allowRebind; } @Override public void bind (Spinner spinner) { if (this.spinner != null && allowRebind == false) throw new IllegalStateException("this spinner model can't be reused"); this.spinner = spinner; } /** * Step model up by one. Event and spinner update will be handled by {@link AbstractSpinnerModel}. * @return true if value was changed, false otherwise. */ protected abstract boolean incrementModel (); /** * Step model down by one. Event and spinner update will be handled by {@link AbstractSpinnerModel}. * @return true if value was changed, false otherwise. */ protected abstract boolean decrementModel (); @Override public final boolean increment () { return increment(spinner.isProgrammaticChangeEvents()); } @Override public final boolean increment (boolean fireEvent) { boolean valueChanged = incrementModel(); if (valueChanged) spinner.notifyValueChanged(fireEvent); return valueChanged; } @Override public final boolean decrement () { return decrement(spinner.isProgrammaticChangeEvents()); } @Override public final boolean decrement (boolean fireEvent) { boolean valueChanged = decrementModel(); if (valueChanged) spinner.notifyValueChanged(fireEvent); return valueChanged; } @Override public boolean isWrap () { return wrap; } @Override public void setWrap (boolean wrap) { this.wrap = wrap; } /** @return true if this model can be reused with different spinner, false otherwise */ public boolean isAllowRebind () { return allowRebind; } protected void setAllowRebind (boolean allowRebind) { this.allowRebind = allowRebind; } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/ArraySpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * {@link Spinner} model allowing to browse through items from object {@link Array}. *

* Note that this (by default) uses item's toString() method to get string representation of objects used to validate * that user has entered valid value which due to {@link VisValidatableTextField} nature has to be done for every * entered letter. Item's toString() should cache it's result internally to optimize this check. To customize how string * representation is obtained override {@link #itemToString(Object)}. * @author Kotcrab * @since 1.0.2 */ public class ArraySpinnerModel extends AbstractSpinnerModel { private Array items = new Array(); private T current; private int currentIndex; /** * Creates empty instance with no items set. Note that spinner with empty array model will be always treated as in * invalid state. */ public ArraySpinnerModel () { super(false); } /** * Creates new instance of {@link ArraySpinnerModel} using provided items. * @param items array containing items for the model. It is copied to new array in order to prevent accidental * modification. Array may be empty however in such case spinner will be always in invalid input state. */ public ArraySpinnerModel (Array items) { super(false); this.items.addAll(items); } @Override public void bind (Spinner spinner) { super.bind(spinner); updateCurrentItem(0); spinner.getTextField().addValidator(new InputValidator() { @Override public boolean validateInput (String input) { return getItemIndexForText(input) != -1; } }); spinner.notifyValueChanged(true); } /** * Creates string representation displayed in {@link Spinner} for given object. By default toString() is used. * @param item that string representation should be created. It is necessary to check if item is null! * @return string representation of item */ protected String itemToString (T item) { if (item == null) return ""; return item.toString(); } private int getItemIndexForText (String text) { for (int i = 0; i < items.size; i++) { T item = items.get(i); if (itemToString(item).equals(text)) return i; } return -1; } @Override public void textChanged () { String text = spinner.getTextField().getText(); int index = getItemIndexForText(text); if (index == -1) return; updateCurrentItem(index); } @Override public boolean incrementModel () { if (currentIndex + 1 >= items.size) { if (isWrap()) { updateCurrentItem(0); return true; } return false; } updateCurrentItem(currentIndex + 1); return true; } @Override public boolean decrementModel () { if (currentIndex - 1 < 0) { if (isWrap()) { updateCurrentItem(items.size - 1); return true; } return false; } updateCurrentItem(currentIndex - 1); return true; } @Override public String getText () { return itemToString(current); } /** Notifies model that items has changed and view must be refreshed. This will trigger a change event. */ public void invalidateDataSet () { updateCurrentItem(MathUtils.clamp(currentIndex, 0, items.size - 1)); spinner.notifyValueChanged(true); } /** @return array containing model items. If you modify returned array you must call {@link #invalidateDataSet()}. */ public Array getItems () { return items; } /** Changes items of this model. Current index is not preserved. This will trigger a change event. */ public void setItems (Array newItems) { items.clear(); items.addAll(newItems); currentIndex = 0; invalidateDataSet(); } /** @return current item index or -1 if items array is empty */ public int getCurrentIndex () { return currentIndex; } /** @return current item or null if items array is empty */ public T getCurrent () { return current; } /** Sets current item. If array is empty then current value will be set to null. */ public void setCurrent (int newIndex) { setCurrent(newIndex, spinner.isProgrammaticChangeEvents()); } /** Sets current item. If array is empty then current value will be set to null. */ public void setCurrent (int newIndex, boolean fireEvent) { updateCurrentItem(newIndex); spinner.notifyValueChanged(fireEvent); } /** @param item if does not exist in items array, model item will be set to first item. */ public void setCurrent (T item) { setCurrent(item, spinner.isProgrammaticChangeEvents()); } /** @param item if does not exist in items array, model item will be set to first item. */ public void setCurrent (T item, boolean fireEvent) { int index = items.indexOf(item, true); if (index == -1) { setCurrent(0, fireEvent); } else { setCurrent(index, fireEvent); } } private void updateCurrentItem (int newIndex) { if (items.size == 0) { current = null; currentIndex = -1; } else { currentIndex = newIndex; current = items.get(newIndex); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/FloatSpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; import com.kotcrab.vis.ui.util.*; import com.kotcrab.vis.ui.widget.VisValidatableTextField; import java.math.BigDecimal; /** * Spinner model allowing to select float values. Uses {@link BigDecimal} to support high precession and support large numbers. * Consider using {@link SimpleFloatSpinnerModel} when such high precision is not needed as it will be faster and simpler to use. * @author Kotcrab * @see FloatSpinnerModel * @see IntSpinnerModel * @since 1.0.2 */ public class FloatSpinnerModel extends AbstractSpinnerModel { private InputValidator boundsValidator = new BoundsValidator(); private NumberDigitsTextFieldFilter textFieldFilter; private BigDecimal max; private BigDecimal min; private BigDecimal step; private BigDecimal current; private int scale = 0; public FloatSpinnerModel (String initialValue, String min, String max) { this(initialValue, min, max, "1", 1); } public FloatSpinnerModel (String initialValue, String min, String max, String step) { this(initialValue, min, max, step, 1); } public FloatSpinnerModel (String initialValue, String min, String max, String step, int scale) { this(new BigDecimal(initialValue), new BigDecimal(min), new BigDecimal(max), new BigDecimal(step), scale); } public FloatSpinnerModel (BigDecimal initialValue, BigDecimal min, BigDecimal max, BigDecimal step, int scale) { super(false); this.current = initialValue; this.max = max; this.min = min; this.step = step; this.scale = scale; if (this.min.compareTo(this.max) > 0) throw new IllegalArgumentException("min can't be > max"); if (this.step.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("step must be > 0"); if (scale < 0) throw new IllegalArgumentException("scale must be >= 0"); } @Override public void bind (Spinner spinner) { super.bind(spinner); setScale(scale, false); spinner.notifyValueChanged(true); } @Override public void textChanged () { String text = spinner.getTextField().getText(); if (text.equals("")) { current = min.setScale(scale, BigDecimal.ROUND_HALF_UP); } else if (checkInputBounds(text)) { current = new BigDecimal(text); } } @Override public boolean incrementModel () { if (current.add(step).compareTo(max) > 0) { if (current.compareTo(max) == 0) { if (isWrap()) { current = min.setScale(scale, BigDecimal.ROUND_HALF_UP); return true; } return false; } current = max.setScale(scale, BigDecimal.ROUND_HALF_UP); } else { current = current.add(step); } return true; } @Override public boolean decrementModel () { if (current.subtract(step).compareTo(min) < 0) { if (current.compareTo(min) == 0) { if (isWrap()) { current = max.setScale(scale, BigDecimal.ROUND_HALF_UP); return true; } return false; } current = min.setScale(scale, BigDecimal.ROUND_HALF_UP); } else { current = current.subtract(step); } return true; } @Override public String getText () { return current.toPlainString(); } public int getScale () { return scale; } /** * Sets scale of this selector. Scale defines how many digits after decimal point can be entered. By default * this is set to 0, meaning that only integers are allowed. Setting scale to 1 would allow 0.0, scale = 2 would * allow 0.00 and etc. */ public void setScale (final int scale) { setScale(scale, true); } private void setScale (final int scale, boolean notifySpinner) { if (scale < 0) throw new IllegalStateException("Scale can't be < 0"); this.scale = scale; current = current.setScale(scale, BigDecimal.ROUND_HALF_UP); VisValidatableTextField valueText = spinner.getTextField(); valueText.getValidators().clear(); valueText.addValidator(boundsValidator); //Both need the bounds check if (scale == 0) { valueText.addValidator(Validators.INTEGERS); valueText.setTextFieldFilter(textFieldFilter = new IntDigitsOnlyFilter(true)); } else { valueText.addValidator(Validators.FLOATS); valueText.addValidator(new InputValidator() { @Override public boolean validateInput (String input) { int dotIndex = input.indexOf('.'); if (dotIndex == -1) return true; return input.length() - input.indexOf('.') - 1 <= scale; } }); valueText.setTextFieldFilter(textFieldFilter = new FloatDigitsOnlyFilter(true)); } textFieldFilter.setUseFieldCursorPosition(true); if (min.compareTo(BigDecimal.ZERO) >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } if (notifySpinner) { spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public void setValue (BigDecimal newValue) { setValue(newValue, spinner.isProgrammaticChangeEvents()); } public void setValue (BigDecimal newValue, boolean fireEvent) { if (newValue.compareTo(max) > 0) { current = max.setScale(scale, BigDecimal.ROUND_HALF_UP); } else if (newValue.compareTo(min) < 0) { current = min.setScale(scale, BigDecimal.ROUND_HALF_UP); } else { current = newValue.setScale(scale, BigDecimal.ROUND_HALF_UP); } spinner.notifyValueChanged(fireEvent); } public BigDecimal getValue () { return current; } public BigDecimal getMin () { return min; } /** Sets min value. If current is lesser than min, the current value is set to min value */ public void setMin (BigDecimal min) { if (min.compareTo(max) > 0) throw new IllegalArgumentException("min can't be > max"); this.min = min; if (min.compareTo(BigDecimal.ZERO) >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } if (current.compareTo(min) < 0) { current = min.setScale(scale, BigDecimal.ROUND_HALF_UP); spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public BigDecimal getMax () { return max; } /** Sets max value. If current is greater than max, the current value is set to max value. */ public void setMax (BigDecimal max) { if (min.compareTo(max) > 0) throw new IllegalArgumentException("min can't be > max"); this.max = max; if (current.compareTo(max) > 0) { current = max.setScale(scale, BigDecimal.ROUND_HALF_UP); spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public BigDecimal getStep () { return step; } public void setStep (BigDecimal step) { if (step.compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("step must be > 0"); this.step = step; } private boolean checkInputBounds (String input) { try { BigDecimal x = new BigDecimal(input); return x.compareTo(min) >= 0 && x.compareTo(max) <= 0; } catch (NumberFormatException e) { return false; } } private class BoundsValidator implements InputValidator { @Override public boolean validateInput (String input) { return checkInputBounds(input); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/IntSpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; import com.kotcrab.vis.ui.util.InputValidator; import com.kotcrab.vis.ui.util.IntDigitsOnlyFilter; import com.kotcrab.vis.ui.util.Validators; import com.kotcrab.vis.ui.widget.VisValidatableTextField; /** * Spinner models allowing to select int values. * @author Kotcrab * @see SimpleFloatSpinnerModel * @see FloatSpinnerModel * @since 1.0.2 */ public class IntSpinnerModel extends AbstractSpinnerModel { private BoundsValidator boundsValidator = new BoundsValidator(); private IntDigitsOnlyFilter textFieldFilter; private int max; private int min; private int step; private int current; public IntSpinnerModel (int initialValue, int min, int max) { this(initialValue, min, max, 1); } public IntSpinnerModel (int initialValue, int min, int max, int step) { super(false); if (min > max) throw new IllegalArgumentException("min can't be > max"); if (step <= 0) throw new IllegalArgumentException("step must be > 0"); this.current = initialValue; this.max = max; this.min = min; this.step = step; } @Override public void bind (Spinner spinner) { super.bind(spinner); VisValidatableTextField valueText = spinner.getTextField(); valueText.getValidators().clear(); valueText.addValidator(boundsValidator); valueText.addValidator(Validators.INTEGERS); valueText.setTextFieldFilter(textFieldFilter = new IntDigitsOnlyFilter(true)); textFieldFilter.setUseFieldCursorPosition(true); if (min >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } spinner.notifyValueChanged(true); } @Override public void textChanged () { String text = spinner.getTextField().getText(); if (text.equals("")) { current = min; } else if (checkInputBounds(text)) { current = Integer.parseInt(text); } } @Override public boolean incrementModel () { if (current + step > max) { if (current == max) { if (isWrap()) { current = min; return true; } return false; } current = max; } else { current += step; } return true; } @Override public boolean decrementModel () { if (current - step < min) { if (current == min) { if (isWrap()) { current = max; return true; } return false; } current = min; } else { current -= step; } return true; } @Override public String getText () { return String.valueOf(current); } public void setValue (int newValue) { setValue(newValue, spinner.isProgrammaticChangeEvents()); } public void setValue (int newValue, boolean fireEvent) { if (newValue > max) { current = max; } else if (newValue < min) { current = min; } else { current = newValue; } spinner.notifyValueChanged(fireEvent); } public int getValue () { return current; } public int getMin () { return min; } /** Sets min value. If current is lesser than min, the current value is set to min value. */ public void setMin (int min) { if (min > max) throw new IllegalArgumentException("min can't be > max"); this.min = min; if (min >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } if (current < min) { current = min; spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public int getMax () { return max; } /** Sets max value. If current is greater than max, the current value is set to max value. */ public void setMax (int max) { if (min > max) throw new IllegalArgumentException("min can't be > max"); this.max = max; if (current > max) { current = max; spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public int getStep () { return step; } public void setStep (int step) { if (step <= 0) throw new IllegalArgumentException("step must be > 0"); this.step = step; } private boolean checkInputBounds (String input) { try { float x = Integer.parseInt(input); return x >= min && x <= max; } catch (NumberFormatException e) { return false; } } private class BoundsValidator implements InputValidator { @Override public boolean validateInput (String input) { return checkInputBounds(input); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/SimpleFloatSpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; import com.kotcrab.vis.ui.util.*; import com.kotcrab.vis.ui.widget.VisValidatableTextField; import java.math.BigDecimal; /** * Spinner models allowing to select float values. Uses float to store values, good for small numbers * with low precession. If high precession is required or very big numbers are used then {@link FloatSpinnerModel} should be used. * If only ints are needed then {@link IntSpinnerModel} should be used. * @author Kotcrab * @see FloatSpinnerModel * @see IntSpinnerModel * @since 1.0.2 */ public class SimpleFloatSpinnerModel extends AbstractSpinnerModel { private InputValidator boundsValidator = new BoundsValidator(); private NumberDigitsTextFieldFilter textFieldFilter; private float max; private float min; private float step; private float current; private int precision = 0; public SimpleFloatSpinnerModel (float initialValue, float min, float max) { this(initialValue, min, max, 1, 1); } public SimpleFloatSpinnerModel (float initialValue, float min, float max, float step) { this(initialValue, min, max, step, 1); } public SimpleFloatSpinnerModel (float initialValue, float min, float max, float step, int precision) { super(false); if (min > max) throw new IllegalArgumentException("min can't be > max"); if (step <= 0) throw new IllegalArgumentException("step must be > 0"); if (precision < 0) throw new IllegalArgumentException("precision must be >= 0"); this.current = initialValue; this.max = max; this.min = min; this.step = step; this.precision = precision; } @Override public void bind (Spinner spinner) { super.bind(spinner); setPrecision(precision, false); spinner.notifyValueChanged(true); } @Override public void textChanged () { String text = spinner.getTextField().getText(); if (text.equals("")) { current = min; } else if (checkInputBounds(text)) { current = Float.parseFloat(text); } } @Override public boolean incrementModel () { if (current + step > max) { if (current == max) { if (isWrap()) { current = min; return true; } return false; } current = max; } else { current += step; } return true; } @Override public boolean decrementModel () { if (current - step < min) { if (current == min) { if (isWrap()) { current = max; return true; } return false; } current = min; } else { this.current -= step; } return true; } @Override public String getText () { if (precision >= 1) { //dealing with float rounding errors BigDecimal bd = new BigDecimal(String.valueOf(current)); bd = bd.setScale(precision, BigDecimal.ROUND_HALF_UP); return String.valueOf(bd.floatValue()); } else { return String.valueOf((int) current); } } public int getPrecision () { return precision; } /** * Sets precision of this selector. Precision defines how many digits after decimal point can be entered. By default * this is set to 0, meaning that only integers are allowed. Setting precision to 1 would allow 0.0, precision = 2 would * allow 0.00 and etc. */ public void setPrecision (final int precision) { setPrecision(precision, true); } private void setPrecision (final int precision, boolean notifySpinner) { if (precision < 0) throw new IllegalStateException("Precision can't be < 0"); this.precision = precision; VisValidatableTextField valueText = spinner.getTextField(); valueText.getValidators().clear(); valueText.addValidator(boundsValidator); //Both need the bounds check if (precision == 0) { valueText.addValidator(Validators.INTEGERS); valueText.setTextFieldFilter(textFieldFilter = new IntDigitsOnlyFilter(true)); } else { valueText.addValidator(Validators.FLOATS); valueText.addValidator(new InputValidator() { @Override public boolean validateInput (String input) { int dotIndex = input.indexOf('.'); if (dotIndex == -1) return true; return input.length() - input.indexOf('.') - 1 <= precision; } }); valueText.setTextFieldFilter(textFieldFilter = new FloatDigitsOnlyFilter(true)); } textFieldFilter.setUseFieldCursorPosition(true); if (min >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } if (notifySpinner) { spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public void setValue (float newValue) { setValue(newValue, spinner.isProgrammaticChangeEvents()); } public void setValue (float newValue, boolean fireEvent) { if (newValue > max) { current = max; } else if (newValue < min) { current = min; } else { current = newValue; } spinner.notifyValueChanged(fireEvent); } public float getValue () { return current; } public float getMin () { return min; } /** Sets min value, if current is lesser than min, the current value is set to min value */ public void setMin (float min) { if (min > max) throw new IllegalArgumentException("min can't be > max"); this.min = min; if (min >= 0) { textFieldFilter.setAcceptNegativeValues(false); } else { textFieldFilter.setAcceptNegativeValues(true); } if (current < min) { current = min; spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public float getMax () { return max; } /** Sets max value. If current is greater than max, the current value is set to max value. */ public void setMax (float max) { if (min > max) throw new IllegalArgumentException("min can't be > max"); this.max = max; if (current > max) { current = max; spinner.notifyValueChanged(spinner.isProgrammaticChangeEvents()); } } public float getStep () { return step; } public void setStep (float step) { if (step <= 0) throw new IllegalArgumentException("step must be > 0"); this.step = step; } private boolean checkInputBounds (String input) { try { float x = Float.parseFloat(input); return x >= min && x <= max; } catch (NumberFormatException e) { return false; } } private class BoundsValidator implements InputValidator { @Override public boolean validateInput (String input) { return checkInputBounds(input); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/Spinner.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.Disableable; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.FocusListener; import com.badlogic.gdx.utils.Pools; import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.Timer.Task; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.widget.*; /** * Spinner can be used to select number or object using up and down buttons or by entering value into text field. * Supports custom models that allows selecting either int, floats or even custom objects. *

* Fires {@link ChangeListener.ChangeEvent} when value has changed however unlike some other widgets canceling the event * won't undo value change. * @author Kotcrab * @see SimpleFloatSpinnerModel * @see FloatSpinnerModel * @see IntSpinnerModel * @see ArraySpinnerModel * @since 1.0.2 */ public class Spinner extends VisTable implements Disableable { private final Sizes sizes; private SpinnerModel model; //task is shared between two buttons private ButtonRepeatTask buttonRepeatTask = new ButtonRepeatTask(); private VisImageButton upButton; private VisImageButton downButton; private Cell textFieldCell; private Cell labelCell; private TextFieldEventPolicy textFieldEventPolicy = TextFieldEventPolicy.ON_FOCUS_LOST; private boolean programmaticChangeEvents = true; private boolean disabled; public Spinner (String name, SpinnerModel model) { this("default", name, model); } public Spinner (String styleName, String name, SpinnerModel model) { this(VisUI.getSkin().get(styleName, SpinnerStyle.class), VisUI.getSizes(), name, model); } public Spinner (SpinnerStyle style, Sizes sizes, String name, SpinnerModel model) { this.sizes = sizes; this.model = model; VisTable buttonsTable = new VisTable(); VisValidatableTextField textField = createTextField(); upButton = new VisImageButton(style.up); downButton = new VisImageButton(style.down); buttonsTable.add(upButton).height(sizes.spinnerButtonHeight).row(); buttonsTable.add(downButton).height(sizes.spinnerButtonHeight); labelCell = add(new VisLabel("")); setSelectorName(name); textFieldCell = add(textField).height(sizes.spinnerButtonHeight * 2).growX(); add(buttonsTable); addButtonsListeners(upButton, downButton); model.bind(this); } private VisValidatableTextField createTextField () { VisValidatableTextField textField = new VisValidatableTextField() { @Override public float getPrefWidth () { return sizes.spinnerFieldSize; } }; textField.setRestoreLastValid(true); textField.setProgrammaticChangeEvents(false); addTextFieldListeners(textField); return textField; } public void setModel (SpinnerModel model) { this.model = model; textFieldCell.setActor(createTextField()); model.bind(this); } private void addButtonsListeners (VisImageButton upButton, VisImageButton downButton) { upButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { event.stop(); getStage().setScrollFocus(getTextField()); increment(true); } }); downButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { event.stop(); getStage().setScrollFocus(getTextField()); decrement(true); } }); upButton.addListener(new ButtonInputListener(true)); downButton.addListener(new ButtonInputListener(false)); } private void addTextFieldListeners (final VisTextField textField) { textField.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { event.stop(); model.textChanged(); if (textField.isInputValid() && textFieldEventPolicy == TextFieldEventPolicy.ON_KEY_TYPED) { notifyValueChanged(true); } } }); textField.addListener(new FocusListener() { @Override public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused) { if (focused == false) { getStage().setScrollFocus(null); if (textFieldEventPolicy == TextFieldEventPolicy.ON_FOCUS_LOST) { notifyValueChanged(true); } } } }); textField.addListener(new InputListener() { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { getStage().setScrollFocus(getTextField()); return true; } @Override public boolean scrolled (InputEvent event, float x, float y, float amountX, float amountY) { if (disabled) { return false; } if (amountY >= 1) { decrement(true); } else if (amountY <= -1) { increment(true); } return true; } @Override public boolean keyDown (InputEvent event, int keycode) { if (keycode == Keys.ENTER) { notifyValueChanged(true); return true; } return false; } }); } @Override public void setDisabled (boolean disabled) { this.disabled = disabled; upButton.setDisabled(disabled); downButton.setDisabled(disabled); getTextField().setDisabled(disabled); } @Override public boolean isDisabled () { return disabled; } public void setSelectorName (String name) { labelCell.getActor().setText(name); if (name == null || name.length() == 0) { labelCell.padRight(0); } else { labelCell.padRight(6); } } public String getSelectorName () { return labelCell.getActor().getText().toString(); } public void increment () { model.increment(programmaticChangeEvents); } private void increment (boolean fireEvent) { model.increment(fireEvent); } public void decrement () { model.decrement(programmaticChangeEvents); } private void decrement (boolean fireEvent) { model.decrement(fireEvent); } /** If false, methods changing spinner value form code won't trigger change event, it will be fired only when user has changed value. */ public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) { this.programmaticChangeEvents = programmaticChangeEvents; } public boolean isProgrammaticChangeEvents () { return programmaticChangeEvents; } public void setTextFieldEventPolicy (TextFieldEventPolicy textFieldEventPolicy) { this.textFieldEventPolicy = textFieldEventPolicy; } public TextFieldEventPolicy getTextFieldEventPolicy () { return textFieldEventPolicy; } public int getMaxLength () { return getTextField().getMaxLength(); } public void setMaxLength (int maxLength) { getTextField().setMaxLength(maxLength); } public SpinnerModel getModel () { return model; } /** * Called by {@link SpinnerModel}. Notifies when underlying model value has changed and spinner text field must updated. * Typically there is no need to call this method manually. * @param fireEvent if true then {@link ChangeListener.ChangeEvent} will be fired */ public void notifyValueChanged (boolean fireEvent) { VisValidatableTextField textField = getTextField(); int cursor = textField.getCursorPosition(); textField.setCursorPosition(0); textField.setText(model.getText()); textField.setCursorPosition(cursor); if (fireEvent) { ChangeListener.ChangeEvent changeEvent = Pools.obtain(ChangeListener.ChangeEvent.class); fire(changeEvent); Pools.free(changeEvent); } } public VisValidatableTextField getTextField () { return textFieldCell.getActor(); } public static class SpinnerStyle { public Drawable up; public Drawable down; public SpinnerStyle () { } public SpinnerStyle (SpinnerStyle style) { this.up = style.up; this.down = style.down; } public SpinnerStyle (Drawable up, Drawable down) { this.up = up; this.down = down; } } private class ButtonRepeatTask extends Task { boolean advance; @Override public void run () { if (advance) { increment(true); } else { decrement(true); } } } /** * Allows to configure how {@link Spinner} will fire {@link ChangeListener.ChangeEvent} after user interaction with * Spinner text field. * @since 1.1.6 */ public enum TextFieldEventPolicy { /** * Spinner change event will be only fired after user has pressed enter in text field. This mode is the default * one prior to VisUI 1.1.6 */ ON_ENTER_ONLY, /** * Spinner change event will be always fired after text field has lost focus and entered value is valid. Note * that event will be fired even if user has not changed actual value of spinner. Event won't be fired * if current model determined that entered value is invalid. This mode is the default one. */ ON_FOCUS_LOST, /** * Spinner change event will be fired right after user has typed something in the text field and model has * determined that entered value is valid. Event won't be fired if entered value is invalid. */ ON_KEY_TYPED } private class ButtonInputListener extends InputListener { private float buttonRepeatInitialTime = 0.4f; private float buttonRepeatTime = 0.08f; private boolean advance; public ButtonInputListener (boolean advance) { this.advance = advance; } @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { if (buttonRepeatTask.isScheduled() == false) { buttonRepeatTask.advance = advance; buttonRepeatTask.cancel(); Timer.schedule(buttonRepeatTask, buttonRepeatInitialTime, buttonRepeatTime); } return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { buttonRepeatTask.cancel(); } } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/spinner/SpinnerModel.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.spinner; /** * Classes implementing this interface represent model that can be used with {@link Spinner}. Model defines what is scrolled * in spinner (eg. int numbers, floats or some arbitrary strings), set-ups input validation and updates it's value if user * changed text in spinner value text field. *

* Classes wanting to implement this interface should inherit from {@link AbstractSpinnerModel} to simplify event handling. * @author Kotcrab * @see AbstractSpinnerModel * @see IntSpinnerModel * @see FloatSpinnerModel * @see SimpleFloatSpinnerModel * @see ArraySpinnerModel * @since 1.0.2 */ public interface SpinnerModel { /** * Called when model is assigned to {@link Spinner}. When this is called Spinner has been initialised so it's safe to * do operation on it such as adding custom validators to text field. *

* If this model can't be reused then in this function it should verify that it is not being bound for the second time. *

* After model has finished it's setup it should call {@link Spinner#notifyValueChanged(boolean)} with true to perform * first update and set initial spinner value. * @param spinner that this model was assigned to */ void bind (Spinner spinner); /** * Called when spinner text has changed. Usually this is the moment when model has to update it's current value variable. * If input is invalid when this it called then it should simply be ignored. If field loses focus while it is in * invalid state then last valid value will be automatically restored. This should NOT call {@link Spinner#notifyValueChanged(boolean)}. */ void textChanged (); /** * Steps model up by one. Depending of the implementation this could move model to next item or increment it's value by * arbitrary amount. Implementation class MUST call {@link Spinner#notifyValueChanged(boolean)} with fireEvent param set to * {@link Spinner#isProgrammaticChangeEvents()} *

* @return true when value was changed, false otherwise */ boolean increment (); /** * Steps model up by one. Depending of the implementation this could move model to next item or increment it's value by * arbitrary amount. Implementation class MUST call {@link Spinner#notifyValueChanged(boolean)} using fireEvent param as argument. *

* @return true when value was changed, false otherwise */ boolean increment (boolean fireEvent); /** * Steps model down by one. Depending of the implementation this could move model to previous item or decrement it's value by * arbitrary amount. Implementation class MUST call {@link Spinner#notifyValueChanged(boolean)} with fireEvent param set to * {@link Spinner#isProgrammaticChangeEvents()} *

* @return true when value was changed, false otherwise */ boolean decrement (); /** * Steps model down by one. Depending of the implementation this could move model to previous item or decrement it's value by * arbitrary amount. Implementation class MUST call {@link Spinner#notifyValueChanged(boolean)} using fireEvent param as argument. *

* @return true when value was changed, false otherwise */ boolean decrement (boolean fireEvent); /** * Allows to enable model wrapping: if last element of model is reached and {@link #decrement()} was called then it * will be looped to first element. Same applies for last element and {@link #increment()} * @param wrap whether to wrap this model or not */ void setWrap (boolean wrap); /** @return true if model wrapping is enabled, false otherwise. See {@link #setWrap(boolean)} */ boolean isWrap (); /** @return text representation of current model value */ String getText (); } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/tabbedpane/Tab.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.tabbedpane; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.utils.Disposable; /** * Base class for tabs used in TabbedPane. Tab can be savable, meaning that it can be saved and will display warning * dialog 'do you want to save changes' before closing. Tab can be also closeable by user meaning that user can close * this tab manually from tabbed pane (using 'X' button or by pressing mouse wheel on tab). * @author Kotcrab */ public abstract class Tab implements Disposable { private boolean activeTab; private TabbedPane pane; private boolean closeableByUser = true; private boolean savable = false; private boolean dirty = false; public Tab () { } /** @param savable if true tab can be saved and marked as dirty. */ public Tab (boolean savable) { this.savable = savable; } /** * @param savable if true tab can be saved and marked as dirty. * @param closeableByUser if true tab can be closed by user from tabbed pane. */ public Tab (boolean savable, boolean closeableByUser) { this.savable = savable; this.closeableByUser = closeableByUser; } /** @return tab title used by tabbed pane. */ public abstract String getTabTitle (); /** * @return table that contains this tab view, will be passed to tabbed pane listener. Should * return same table every time this is called. */ public abstract Table getContentTable (); /** Called by pane when this tab becomes shown. Class overriding this should call super.onShow(). */ public void onShow () { activeTab = true; } /** Called by pane when this tab becomes hidden. Class overriding this should call super.onHide(). */ public void onHide () { activeTab = false; } /** @return true is this tab is currently active. */ public boolean isActiveTab () { return activeTab; } /** @return pane that this tab belongs to, or null. */ public TabbedPane getPane () { return pane; } /** Should be called by TabbedPane only, when tab is added to pane. */ public void setPane (TabbedPane pane) { this.pane = pane; } public boolean isSavable () { return savable; } public boolean isCloseableByUser () { return closeableByUser; } public boolean isDirty () { return dirty; } public void setDirty (boolean dirty) { checkSavable(); boolean update = (dirty != this.dirty); if (update) { this.dirty = dirty; if (pane != null) getPane().updateTabTitle(this); } } /** Marks this tab as dirty */ public void dirty () { setDirty(true); } /** * Called when this tab should save its own state. After saving setDirty(false) must be called manually to remove dirty state. * @return true when save succeeded, false otherwise. */ public boolean save () { checkSavable(); return false; } private void checkSavable () { if (isSavable() == false) throw new IllegalStateException("Tab " + getTabTitle() + " is not savable!"); } /** Removes this tab from pane (if any). */ public void removeFromTabPane () { if (pane != null) pane.remove(this); } /** Called when tab is being removed from scene. */ @Override public void dispose () { } } ================================================ FILE: ui/src/main/java/com/kotcrab/vis/ui/widget/tabbedpane/TabbedPane.java ================================================ /* * Copyright 2014-2017 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.kotcrab.vis.ui.widget.tabbedpane; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Buttons; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup; import com.badlogic.gdx.scenes.scene2d.ui.Cell; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.UIUtils; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.I18NBundle; import com.badlogic.gdx.utils.IdentityMap; import com.badlogic.gdx.utils.ObjectMap.Entry; import com.badlogic.gdx.utils.Scaling; import com.kotcrab.vis.ui.Locales; import com.kotcrab.vis.ui.Sizes; import com.kotcrab.vis.ui.VisUI; import com.kotcrab.vis.ui.i18n.BundleText; import com.kotcrab.vis.ui.layout.DragPane; import com.kotcrab.vis.ui.layout.HorizontalFlowGroup; import com.kotcrab.vis.ui.layout.VerticalFlowGroup; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.util.dialog.Dialogs.OptionDialogType; import com.kotcrab.vis.ui.util.dialog.OptionDialogAdapter; import com.kotcrab.vis.ui.widget.Draggable; import com.kotcrab.vis.ui.widget.VisImageButton; import com.kotcrab.vis.ui.widget.VisImageButton.VisImageButtonStyle; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.VisTextButton; import com.kotcrab.vis.ui.widget.VisTextButton.VisTextButtonStyle; /** * A tabbed pane, allows to have multiple tabs open and switch between them. TabbedPane does not handle displaying tab content, * you have to do that manually using tabbed pane listener to get tab content table (see {@link Tab#getContentTable()} and * {@link TabbedPaneListener}). All tabs must extend {@link Tab} class. *

* Since 0.9.3, tabbed pane uses an internal {@link DragPane} to make the tabs draggable. You can completely turn off this * functionality by setting {@link TabbedPaneStyle#draggable} to false. To turn off the drag listener at runtime, use * {@link #getTabsPane()} method to get a reference of {@link DragPane}, and invoke {@link DragPane#setDraggable(Draggable)} with * null argument - this will clear draggable listener from all tabs' buttons; naturally, setting this value to non-null * {@link Draggable} listener will also add it to all buttons. * @author Kotcrab * @author MJ * @since 0.7.0 */ public class TabbedPane { private static final Vector2 tmpVector = new Vector2(); private static final Rectangle tmpRect = new Rectangle(); private TabbedPaneStyle style; private Sizes sizes; private VisImageButtonStyle sharedCloseActiveButtonStyle; private DragPane tabsPane; private TabbedPaneTable mainTable; private Array tabs; private IdentityMap tabsButtonMap; private ButtonGroup