diff --git a/.github/FIRST_TIMER_GUIDE.md b/.github/FIRST_TIMER_GUIDE.md new file mode 100644 index 00000000..c6f54275 --- /dev/null +++ b/.github/FIRST_TIMER_GUIDE.md @@ -0,0 +1,11 @@ +### Welcome, and thanks for taking this issue + +- Setup: install dependencies and confirm local build/test commands run successfully. +- Run locally: use the project run/dev script from `package.json`. +- Lint and test before pushing. +- Branch naming: use a focused branch like `fix/issue--short-summary` or `feat/issue--short-summary`. +- Commit messages: keep them concise and imperative (example: `fix(ui): handle empty state`). +- Open a draft PR early if your approach or scope may need maintainer feedback. +- Link your PR to the issue using `fixes #` (or `closes` / `resolves`). +- While active, post `/working` on the issue to signal progress. +- If blocked or no longer available, post `/unassign` so others can continue. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..98e7fe4a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,68 @@ +name: 🐛 Bug Report +description: Report a bug or issue +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: Thanks for reporting a bug! Please fill out the details below. + + - type: textarea + id: description + attributes: + label: Description + description: Describe the bug clearly + placeholder: What went wrong? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How do we reproduce the issue? + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See the error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What should happen? + placeholder: describe expected behavior + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Browser, OS, Node version, or other relevant info + placeholder: | + - OS: macOS 14.1 + - Browser: Chrome 120 + - Node: 20.10 + validations: + required: false + + - type: textarea + id: logs + attributes: + label: Error Logs or Screenshots + description: Paste any error messages or attach screenshots + placeholder: Errors, stack traces, or screenshots + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched existing issues + required: true + - label: I have provided a clear description + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..aaa1e881 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/orgs/open-elements/discussions + about: Questions about usage or architecture diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..84207323 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,53 @@ +name: ✨ Feature Request +description: Suggest an idea or enhancement +labels: ["enhancement", "triage"] +body: + - type: markdown + attributes: + value: We'd love to hear your ideas! Please describe the feature you'd like to see. + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: Describe the problem or use case + placeholder: "Is your request related to a problem? Ex. I'm frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like + placeholder: How should this feature work? + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Other options you've explored + placeholder: Any other approaches? + validations: + required: false + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other relevant information + placeholder: Screenshots, examples, links, etc. + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched existing issues and discussions + required: true + - label: This request aligns with project goals + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..a2d5addf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +## Description +Brief description of changes made in this PR. + +## Type of Change +- [ ] 🐛 Bug fix +- [ ] ✨ New feature +- [ ] 📝 Documentation update +- [ ] 🎨 Style/formatting +- [ ] ♻️ Refactor +- [ ] 🔧 Configuration +- [ ] ⚡ Performance +- [ ] 🧪 Tests +- [ ] 🔐 Security + +## Related Issue(s) +Closes #ISSUE_NUMBER + +## Changes Made +- Change 1 +- Change 2 +- Change 3 + +## How to Test +1. Step 1 +2. Step 2 +3. Verify the expected behavior + +## Checklist +- [ ] Code follows project style guidelines +- [ ] Self-review conducted +- [ ] Comments added for complex logic +- [ ] Documentation updated (if needed) +- [ ] No new warnings generated +- [ ] Tests added/updated (if applicable) +- [ ] All tests passing locally diff --git a/.github/reviewers.json b/.github/reviewers.json new file mode 100644 index 00000000..d3a598ab --- /dev/null +++ b/.github/reviewers.json @@ -0,0 +1,35 @@ +{ + "default_reviewers": [], + "path_reviewers": [ + { + "glob": "/**", + "reviewers": [ + "danielmarv", + "hendrikebbers", + "Jexsie", + "sebtiem", + "Ndacyayisenga-droid" + ] + }, + { + "glob": ".github/**", + "reviewers": [ + "danielmarv", + "hendrikebbers", + "Jexsie", + "sebtiem", + "Ndacyayisenga-droid" + ] + }, + { + "glob": "docs/**", + "reviewers": [ + "danielmarv", + "hendrikebbers", + "Jexsie", + "sebtiem", + "Ndacyayisenga-droid" + ] + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3aa40d43 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + with: + version: 10 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run lint check + run: pnpm lint + + - name: Build Next.js application + run: pnpm build + env: + NEXT_TELEMETRY_DISABLED: 1 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..07aee083 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,64 @@ +name: E2E Tests + +on: + push: + branches: [main, next-js-migration] + pull_request: + branches: [main, next-js-migration] + +jobs: + test: + timeout-minutes: 15 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + + - name: Run E2E tests + run: pnpm test:e2e + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload test traces + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-traces + path: test-results/ + retention-days: 7 diff --git a/.github/workflows/first-timer-guidance.yml b/.github/workflows/first-timer-guidance.yml new file mode 100644 index 00000000..e50d7728 --- /dev/null +++ b/.github/workflows/first-timer-guidance.yml @@ -0,0 +1,134 @@ +name: First Timer Issue Guidance + +on: + issues: + types: [assigned] + +permissions: + issues: write + pull-requests: read + contents: read + +jobs: + post-guidance: + runs-on: ubuntu-latest + steps: + - name: Post guidance for first-time or first-issue assignees + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = context.payload.issue; + const assignee = context.payload.assignee?.login; + if (!assignee || issue.pull_request) return; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issue.number, + per_page: 100 + }); + if (comments.some(c => (c.body || '').includes('[bot-first-timer-guide]'))) return; + + const issueLabels = (issue.labels || []).map(l => (l.name || '').toLowerCase()); + const isFirstIssueLabel = issueLabels.includes('good first issue') || issueLabels.includes('first timers'); + + const prSearch = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:pr author:${assignee}`, + per_page: 1 + }); + const issueSearch = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:issue author:${assignee}`, + per_page: 1 + }); + const assignedIssueSearch = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:issue assignee:${assignee}`, + per_page: 2 + }); + + let hasCommit = false; + try { + const commits = await github.rest.repos.listCommits({ + owner, + repo, + author: assignee, + per_page: 1 + }); + hasCommit = (commits.data || []).length > 0; + } catch (e) { + hasCommit = false; + } + + const noPriorPrs = (prSearch.data.total_count || 0) === 0; + const noPriorIssues = (issueSearch.data.total_count || 0) === 0; + const noPriorActivity = noPriorPrs && noPriorIssues && !hasCommit; + const firstAssignedIssue = (assignedIssueSearch.data.total_count || 0) <= 1; + const qualifies = noPriorActivity || (isFirstIssueLabel && firstAssignedIssue); + if (!qualifies) return; + + async function readRepoFile(path) { + try { + const res = await github.rest.repos.getContent({ owner, repo, path }); + if (!('content' in res.data)) return null; + return Buffer.from(res.data.content, 'base64').toString('utf8'); + } catch (e) { + return null; + } + } + + function detectScripts(packageJsonContent) { + if (!packageJsonContent) return {}; + try { + const pkg = JSON.parse(packageJsonContent); + return pkg.scripts || {}; + } catch { + return {}; + } + } + + function pickScript(scripts, choices) { + for (const name of choices) { + if (scripts[name]) return `npm run ${name}`; + } + return null; + } + + function buildDefaultGuide(issueNumber, scripts) { + const runCmd = pickScript(scripts, ['dev', 'start', 'serve']) || 'npm run dev'; + const lintCmd = pickScript(scripts, ['lint']) || 'npm run lint'; + const testCmd = pickScript(scripts, ['test', 'test:unit']) || 'npm test'; + return [ + '### First-time contributor guide', + '', + '- Setup: clone the repo, install dependencies with `npm install`.', + `- Run locally: \`${runCmd}\``, + `- Lint: \`${lintCmd}\``, + `- Test: \`${testCmd}\``, + '- Branch naming: use a focused branch such as `fix/issue-' + issueNumber + '-short-summary`.', + '- Commit messages: short imperative style, e.g. `fix(auth): handle token refresh`.', + '- Open a draft PR early so maintainers can provide guidance before final polish.', + '- Link your PR with `fixes #' + issueNumber + '` (or `closes`/`resolves`).', + '- Comment `/working` periodically while actively working on this issue.', + '- If blocked or unavailable, comment `/unassign` so others can pick it up.', + '', + '[bot-first-timer-guide]' + ].join('\n'); + } + + const [guideFromRepo, packageJsonContent] = await Promise.all([ + readRepoFile('.github/FIRST_TIMER_GUIDE.md'), + readRepoFile('package.json') + ]); + const scripts = detectScripts(packageJsonContent); + const body = guideFromRepo + ? `${guideFromRepo.trim()}\n\n[bot-first-timer-guide]` + : buildDefaultGuide(issue.number, scripts); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body + }); + diff --git a/.github/workflows/issue-automation.yml b/.github/workflows/issue-automation.yml new file mode 100644 index 00000000..b9e35d27 --- /dev/null +++ b/.github/workflows/issue-automation.yml @@ -0,0 +1,46 @@ +name: Issue Automation + +on: + issues: + types: [opened] + +jobs: + auto-label-and-assign: + runs-on: ubuntu-latest + steps: + - name: Auto-label bug reports + if: contains(github.event.issue.title, 'bug') || contains(github.event.issue.body, 'bug') + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['bug'] + }) + + - name: Auto-label feature requests + if: contains(github.event.issue.title, 'feature') || contains(github.event.issue.title, 'enhancement') + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['enhancement'] + }) + + - name: Auto-assign to maintainer + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addAssignees({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + assignees: ['${{ vars.DEFAULT_MAINTAINER || github.repository_owner }}'] + }).catch(err => { + console.log('Could not assign maintainer: ' + err.message); + }) diff --git a/.github/workflows/issue-commands.yml b/.github/workflows/issue-commands.yml new file mode 100644 index 00000000..ca42f511 --- /dev/null +++ b/.github/workflows/issue-commands.yml @@ -0,0 +1,265 @@ +name: Issue Command Automation + +on: + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: read + +jobs: + handle-issue-commands: + runs-on: ubuntu-latest + steps: + - name: Parse and handle issue commands + uses: actions/github-script@v7 + with: + script: | + if (context.payload.issue.pull_request) return; + + const comment = context.payload.comment.body.trim(); + const actor = context.payload.sender.login; + const issue_number = context.payload.issue.number; + const repo_owner = context.repo.owner; + const repo_name = context.repo.repo; + const issue_labels = context.payload.issue.labels.map(l => l.name); + + // Config variables + const FRONTEND_TEAM = (process.env.FRONTEND_TEAM || '').split(',').map(u => u.trim()).filter(Boolean); + const BACKEND_TEAM = (process.env.BACKEND_TEAM || '').split(',').map(u => u.trim()).filter(Boolean); + const ASSIGN_ALLOWLIST = (process.env.ASSIGN_ALLOWLIST || '').split(',').map(u => u.trim()).filter(Boolean); + const MAINTAINER_ALLOWLIST = (process.env.MAINTAINER_ALLOWLIST || '').split(',').map(u => u.trim()).filter(Boolean); + const WORKING_LABEL = (process.env.WORKING_LABEL || 'in progress').trim(); + + // Command parsing + const assignMatch = comment.match(/^\/assign(?:\s+@?(\w[\w-]+))?$/); + const unassignMatch = comment.match(/^\/unassign(?:\s+@?(\w[\w-]+))?$/); + const workingMatch = comment.match(/^\/working$/); + if (!assignMatch && !unassignMatch && !workingMatch) return; + + const assignees = context.payload.issue.assignees.map(a => a.login); + const toSet = list => new Set(list.map(v => v.toLowerCase())); + const assigneeSet = toSet(assignees); + const maintainerSet = toSet(MAINTAINER_ALLOWLIST); + const actorIsMaintainer = maintainerSet.has(actor.toLowerCase()); + + async function upsertWorkingMarker(timestampIso) { + const marker = `[bot-working:${timestampIso}]`; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: repo_owner, + repo: repo_name, + issue_number, + per_page: 100 + }); + const existing = comments.find(c => + c.user?.login === 'github-actions[bot]' && + /\[bot-working:[^\]]+\]/.test(c.body || '') + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: repo_owner, + repo: repo_name, + comment_id: existing.id, + body: marker + }); + } else { + await github.rest.issues.createComment({ + owner: repo_owner, + repo: repo_name, + issue_number, + body: marker + }); + } + } + + async function addReaction(content) { + await github.rest.reactions.createForIssueComment({ + comment_id: context.payload.comment.id, + owner: repo_owner, + repo: repo_name, + content + }); + } + + async function commentIssue(body) { + await github.rest.issues.createComment({ + issue_number, + owner: repo_owner, + repo: repo_name, + body + }); + } + + if (workingMatch) { + const actorIsAssigned = assigneeSet.has(actor.toLowerCase()); + if (!actorIsAssigned && !actorIsMaintainer) { + await addReaction('confused'); + await commentIssue('Only a current assignee (or maintainer allowlist member) can use `/working`.'); + return; + } + + if (!issue_labels.some(l => l.toLowerCase() === WORKING_LABEL.toLowerCase())) { + await github.rest.issues.addLabels({ + issue_number, + owner: repo_owner, + repo: repo_name, + labels: [WORKING_LABEL] + }); + } + + const nowIso = new Date().toISOString(); + await upsertWorkingMarker(nowIso); + await addReaction('rocket'); + await commentIssue(`Marked as actively worked on by @${actor}.`); + return; + } + + // Assignment rules + const allowedLabels = ['ready', 'help wanted', 'good first issue', 'available']; + const restrictedLabels = ['security', 'private']; + const frontendLabel = 'frontend'; + const backendLabel = 'backend'; + + // Helper: check org membership + async function isOrgMember(username) { + try { + const res = await github.rest.orgs.checkMembershipForUser({ + org: repo_owner, + username + }); + return res.status === 204; + } catch (e) { + return false; + } + } + + // Helper: check collaborator + async function isCollaborator(username) { + try { + const res = await github.rest.repos.checkCollaborator({ + owner: repo_owner, + repo: repo_name, + username + }); + return res.status === 204; + } catch (e) { + return false; + } + } + + // Determine target user + let targetUser = actor; + if (assignMatch && assignMatch[1]) targetUser = assignMatch[1]; + if (unassignMatch && unassignMatch[1]) targetUser = unassignMatch[1]; + + // Assignment restrictions + if (assignMatch) { + // Blocked labels + const blockedLabels = ['blocked', 'do-not-assign', 'needs-triage']; + if (issue_labels.some(l => blockedLabels.includes(l))) { + await commentIssue('Cannot assign: blocked label present.'); + return; + } + + // Only allow self-assignment if allowed label present + if (targetUser === actor && !issue_labels.some(l => allowedLabels.includes(l))) { + await commentIssue('Cannot self-assign until label help wanted, ready, available, or good first issue is applied.'); + return; + } + // Restricted labels + if (issue_labels.some(l => restrictedLabels.includes(l))) { + if (!ASSIGN_ALLOWLIST.includes(targetUser)) { + const isMember = await isOrgMember(targetUser); + const isCollab = await isCollaborator(targetUser); + if (!isMember && !isCollab) { + await commentIssue('Assignment restricted: only org members, collaborators, or allowlisted users may be assigned.'); + return; + } + } + } + // Frontend label + if (issue_labels.includes(frontendLabel) && !FRONTEND_TEAM.includes(targetUser)) { + await commentIssue('Assignment restricted: only frontend team members may be assigned.'); + return; + } + // Backend label + if (issue_labels.includes(backendLabel) && !BACKEND_TEAM.includes(targetUser)) { + await commentIssue('Assignment restricted: only backend team members may be assigned.'); + return; + } + // Already assigned? + if (assignees.includes(targetUser)) { + await commentIssue(`Already assigned to @${targetUser}.`); + return; + } + // Assign + try { + await github.rest.issues.addAssignees({ + issue_number, + owner: repo_owner, + repo: repo_name, + assignees: [targetUser] + }); + await addReaction('rocket'); + await commentIssue(`Assigned to @${targetUser}.`); + } catch (e) { + await commentIssue(`Assignment failed: ${e.message}`); + } + return; + } + + // Unassign + if (unassignMatch) { + if (targetUser !== actor && !actorIsMaintainer) { + await addReaction('confused'); + await commentIssue('You may only unassign yourself unless you are in `MAINTAINER_ALLOWLIST`.'); + return; + } + + if (!assignees.includes(targetUser)) { + await commentIssue(`@${targetUser} is not assigned.`); + return; + } + try { + await github.rest.issues.removeAssignees({ + issue_number, + owner: repo_owner, + repo: repo_name, + assignees: [targetUser] + }); + const refreshed = await github.rest.issues.get({ + owner: repo_owner, + repo: repo_name, + issue_number + }); + const remaining = refreshed.data.assignees?.length || 0; + if (remaining === 0 && issue_labels.some(l => l.toLowerCase() === WORKING_LABEL.toLowerCase())) { + try { + await github.rest.issues.removeLabel({ + owner: repo_owner, + repo: repo_name, + issue_number, + name: WORKING_LABEL + }); + } catch (err) { + if (err.status !== 404) throw err; + } + } + + await addReaction('eyes'); + if (remaining === 0) { + await commentIssue(`Unassigned @${targetUser}. This issue is now available for others to assign.`); + } else { + await commentIssue(`Unassigned @${targetUser}.`); + } + } catch (e) { + await commentIssue(`Unassignment failed: ${e.message}`); + } + } + env: + FRONTEND_TEAM: ${{ vars.FRONTEND_TEAM }} + BACKEND_TEAM: ${{ vars.BACKEND_TEAM }} + ASSIGN_ALLOWLIST: ${{ vars.ASSIGN_ALLOWLIST }} + MAINTAINER_ALLOWLIST: ${{ vars.MAINTAINER_ALLOWLIST }} + WORKING_LABEL: ${{ vars.WORKING_LABEL }} diff --git a/.github/workflows/issue-reminders.yml b/.github/workflows/issue-reminders.yml new file mode 100644 index 00000000..7a028b32 --- /dev/null +++ b/.github/workflows/issue-reminders.yml @@ -0,0 +1,133 @@ +name: Issue Assignee Reminders + +on: + schedule: + - cron: '17 9 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: read + +jobs: + remind-stale-assignees: + runs-on: ubuntu-latest + steps: + - name: Remind on stale assigned issues + uses: actions/github-script@v7 + env: + REMIND_AFTER_DAYS: ${{ vars.REMIND_AFTER_DAYS }} + WORKING_GRACE_DAYS: ${{ vars.WORKING_GRACE_DAYS }} + WORKING_LABEL: ${{ vars.WORKING_LABEL }} + SKIP_REMINDER_LABELS: ${{ vars.SKIP_REMINDER_LABELS }} + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const remindAfterDays = Number(process.env.REMIND_AFTER_DAYS || 7); + const workingGraceDays = Number(process.env.WORKING_GRACE_DAYS || 7); + const workingLabel = (process.env.WORKING_LABEL || 'in progress').trim(); + const skipLabels = (process.env.SKIP_REMINDER_LABELS || 'blocked,needs-triage,do-not-assign') + .split(',') + .map(v => v.trim().toLowerCase()) + .filter(Boolean); + const nowMs = Date.now(); + const dayMs = 24 * 60 * 60 * 1000; + + function issueAgeDays(issue) { + return (nowMs - Date.parse(issue.created_at)) / dayMs; + } + + function hasSkipLabel(issue) { + const labels = (issue.labels || []).map(l => (l.name || '').toLowerCase()); + return labels.some(l => skipLabels.includes(l)); + } + + function hasOpenLinkedPr(issueNumber, openPrs) { + const keywordRef = new RegExp(`\\b(?:fixe[sd]?|close[sd]?|resolve[sd]?)\\s+#${issueNumber}\\b`, 'i'); + const genericRef = new RegExp(`(^|\\W)#${issueNumber}(\\W|$)`, 'i'); + for (const pr of openPrs) { + const text = `${pr.title || ''}\n${pr.body || ''}`; + if (keywordRef.test(text) || genericRef.test(text)) return true; + } + return false; + } + + async function listIssueComments(issueNumber) { + return github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: issueNumber, + per_page: 100 + }); + } + + function extractLatestMarkerTimestamp(comments, markerRegex) { + let latest = null; + for (const c of comments) { + const body = c.body || ''; + const m = body.match(markerRegex); + if (!m) continue; + const ts = Date.parse(m[1]); + if (Number.isNaN(ts)) continue; + if (latest === null || ts > latest) latest = ts; + } + return latest; + } + + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner, + repo, + state: 'open', + per_page: 100 + }); + const openPrs = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: 'open', + per_page: 100 + }); + + for (const issue of issues) { + if (issue.pull_request) continue; + if (!issue.assignees || issue.assignees.length === 0) continue; + if (issueAgeDays(issue) < remindAfterDays) continue; + if (hasSkipLabel(issue)) continue; + if (hasOpenLinkedPr(issue.number, openPrs)) continue; + + const comments = await listIssueComments(issue.number); + + const latestWorkingTs = extractLatestMarkerTimestamp( + comments, + /\[bot-working:([^\]]+)\]/ + ); + if (latestWorkingTs !== null && ((nowMs - latestWorkingTs) / dayMs) <= workingGraceDays) { + continue; + } + + const latestReminderTs = extractLatestMarkerTimestamp( + comments, + /\[bot-reminder:([^\]]+)\]/ + ); + if (latestReminderTs !== null && ((nowMs - latestReminderTs) / dayMs) < 7) { + continue; + } + + const assigneeMentions = issue.assignees.map(a => `@${a.login}`).join(' '); + const marker = `[bot-reminder:${new Date().toISOString()}]`; + const body = `${marker} + + ${assigneeMentions} friendly reminder: this issue has been assigned for ${Math.floor(issueAgeDays(issue))} day(s) without recent progress signals. + + - Comment \`/working\` if you are still working on this issue. + - Comment \`/unassign\` if you are no longer working on it so others can take it. + + Current working label: \`${workingLabel}\`.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body + }); + } diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml new file mode 100644 index 00000000..9b97aff9 --- /dev/null +++ b/.github/workflows/pr-automation.yml @@ -0,0 +1,205 @@ +name: PR Automation + +on: + pull_request: + types: [opened, reopened, ready_for_review, synchronize, review_request_removed] + +permissions: + pull-requests: write + issues: write + contents: read + +jobs: + auto-label-and-assign: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + id: files + uses: actions/github-script@v7 + with: + script: | + const response = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const files = response.data || []; + const labels = new Set(); + + files.forEach(fileObj => { + const file = fileObj.filename || ''; + if (file.match(/\.(ts|tsx|jsx|js)$/)) labels.add('typescript'); + if (file.match(/\.(css|scss|postcss)$/)) labels.add('styling'); + if (file.match(/test|spec/)) labels.add('tests'); + if (file.match(/package\.json|tsconfig|eslint|tailwind/)) labels.add('configuration'); + if (file.match(/README|\.md$/)) labels.add('documentation'); + if (file.match(/next\.config|public/)) labels.add('build'); + }); + + core.setOutput('labels', Array.from(labels).join(',')); + + - name: Enforce reviewers config edit policy + uses: actions/github-script@v7 + env: + MAINTAINER_ALLOWLIST: ${{ vars.MAINTAINER_ALLOWLIST }} + with: + script: | + const allowlist = (process.env.MAINTAINER_ALLOWLIST || '') + .split(',') + .map(v => v.trim().toLowerCase()) + .filter(Boolean); + const actor = context.actor.toLowerCase(); + const owner = context.repo.owner.toLowerCase(); + const isMaintainer = allowlist.includes(actor) || actor === owner; + + if (isMaintainer) return; + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100 + }); + + const changed = files.map(f => f.filename); + if (changed.includes('.github/reviewers.json')) { + core.setFailed('Only maintainers may modify .github/reviewers.json'); + } + + - name: Auto-label PR + if: steps.files.outputs.labels != '' + uses: actions/github-script@v7 + with: + script: | + const labels = '${{ steps.files.outputs.labels }}'.split(',').filter(l => l); + if (labels.length > 0) { + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: labels + }); + } + + - name: Auto-assign PR to author + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addAssignees({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + assignees: [context.payload.pull_request.user.login] + }).catch(err => { + console.log('Could not assign author: ' + err.message); + }) + + - name: Auto-request reviewers from JSON config + uses: actions/github-script@v7 + env: + MAINTAINER_ALLOWLIST: ${{ vars.MAINTAINER_ALLOWLIST }} + with: + script: | + const fs = require('fs'); + + function toRegex(glob) { + const escaped = glob + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*/g, '::DOUBLE_STAR::') + .replace(/\*/g, '[^/]*') + .replace(/::DOUBLE_STAR::/g, '.*'); + return new RegExp(`^${escaped}$`); + } + + const configPath = '.github/reviewers.json'; + if (!fs.existsSync(configPath)) { + core.info('No .github/reviewers.json found. Skipping reviewer requests.'); + return; + } + + let config; + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + } catch (e) { + core.setFailed(`Invalid JSON in ${configPath}: ${e.message}`); + return; + } + + const defaultReviewers = Array.isArray(config.default_reviewers) ? config.default_reviewers : []; + const pathRules = Array.isArray(config.path_reviewers) ? config.path_reviewers : []; + const allowlist = (process.env.MAINTAINER_ALLOWLIST || '') + .split(',') + .map(v => v.trim().toLowerCase()) + .filter(Boolean); + const actor = context.actor.toLowerCase(); + const ownerLower = context.repo.owner.toLowerCase(); + const actorIsMaintainer = allowlist.includes(actor) || actor === ownerLower; + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100 + }); + const changed = files.map(f => f.filename); + + const collected = new Set(defaultReviewers.filter(Boolean)); + for (const rule of pathRules) { + if (!rule || !rule.glob || !Array.isArray(rule.reviewers)) continue; + const re = toRegex(String(rule.glob)); + if (changed.some(f => re.test(f))) { + for (const reviewer of rule.reviewers) { + if (reviewer) collected.add(reviewer); + } + } + } + + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + const author = pr.data.user?.login; + const alreadyRequested = new Set((pr.data.requested_reviewers || []).map(r => r.login)); + const reviewers = Array.from(collected) + .map(r => String(r).trim()) + .filter(Boolean) + .filter(r => r !== author) + .filter(r => !alreadyRequested.has(r)) + .slice(0, 15); + + const action = context.payload.action; + const shouldEnforce = ['opened', 'reopened', 'ready_for_review', 'review_request_removed'].includes(action); + if (!shouldEnforce) { + core.info(`Skipping reviewer enforcement on action: ${action}`); + return; + } + + if (action === 'review_request_removed' && actorIsMaintainer) { + core.info('Maintainer removed reviewer; skipping re-request.'); + return; + } + + if (reviewers.length === 0) { + core.info('No new reviewers to request.'); + return; + } + + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + reviewers + }); + core.info(`Requested reviewers: ${reviewers.join(', ')}`); + } catch (e) { + core.warning(`Could not request reviewers: ${e.message}`); + } diff --git a/.github/workflows/status-labels.yml b/.github/workflows/status-labels.yml new file mode 100644 index 00000000..0e1bb332 --- /dev/null +++ b/.github/workflows/status-labels.yml @@ -0,0 +1,236 @@ +name: Status Labels + +on: + issues: + types: [opened, reopened, assigned, unassigned, labeled, unlabeled, closed] + pull_request: + types: [opened, reopened, synchronize, ready_for_review, converted_to_draft, review_requested, review_request_removed, closed] + pull_request_review: + types: [submitted, dismissed] + +permissions: + issues: write + pull-requests: read + +jobs: + issue-status-labels: + if: github.event_name == 'issues' + runs-on: ubuntu-latest + steps: + - name: Sync issue status labels + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue = context.payload.issue; + const issueNumber = issue.number; + const state = issue.state; + const assignees = issue.assignees || []; + const labels = (issue.labels || []).map(l => l.name.toLowerCase()); + const workingLabel = 'in progress'; + const blockedLabels = ['blocked', 'needs-triage', 'do-not-assign']; + + const STATUS_TRIAGE = 'status: triage'; + const STATUS_IN_PROGRESS = 'status: in-progress'; + const STATUS_BLOCKED = 'status: blocked'; + const STATUS_CLOSED = 'status: closed'; + const statusLabels = [STATUS_TRIAGE, STATUS_IN_PROGRESS, STATUS_BLOCKED, STATUS_CLOSED]; + + async function safeWrite(fn, op) { + try { + return await fn(); + } catch (e) { + if (e.status === 403) { + core.warning(`Skipping ${op}: token cannot write in this context (403).`); + return null; + } + throw e; + } + } + + async function ensureLabel(name) { + const existing = await github.rest.issues.getLabel({ owner, repo, name }).catch(e => { + if (e.status === 404) return null; + throw e; + }); + if (existing) return; + await safeWrite( + () => github.rest.issues.createLabel({ + owner, + repo, + name, + color: 'BFDADC', + description: `Issue status label: ${name}` + }), + `create label ${name}` + ); + } + + for (const label of statusLabels) await ensureLabel(label); + + const shouldBe = new Set(); + const isBlocked = labels.some(l => blockedLabels.includes(l)); + const hasAssignee = assignees.length > 0; + const hasWorking = labels.includes(workingLabel); + + if (state === 'closed') { + shouldBe.add(STATUS_CLOSED); + } else if (isBlocked) { + shouldBe.add(STATUS_BLOCKED); + } else if (hasAssignee || hasWorking) { + shouldBe.add(STATUS_IN_PROGRESS); + } else { + shouldBe.add(STATUS_TRIAGE); + } + + const remove = statusLabels.filter(l => !shouldBe.has(l) && labels.includes(l.toLowerCase())); + if (remove.length > 0) { + for (const name of remove) { + try { + await safeWrite( + () => github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name }), + `remove label ${name} from issue #${issueNumber}` + ); + } catch (e) { + if (e.status !== 404) throw e; + } + } + } + + const add = [...shouldBe].filter(l => !labels.includes(l.toLowerCase())); + if (add.length > 0) { + await safeWrite( + () => github.rest.issues.addLabels({ + owner, + repo, + issue_number: issueNumber, + labels: add + }), + `add labels to issue #${issueNumber}` + ); + } + + pr-status-labels: + if: github.event_name == 'pull_request' || github.event_name == 'pull_request_review' + runs-on: ubuntu-latest + steps: + - name: Sync PR status labels + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pullNumber = context.payload.pull_request.number; + const prPayload = context.payload.pull_request; + const fromFork = !!prPayload.head?.repo?.fork || prPayload.head?.repo?.full_name !== `${owner}/${repo}`; + + const pr = await github.rest.pulls.get({ + owner, + repo, + pull_number: pullNumber + }); + + const STATUS_DRAFT = 'status: draft'; + const STATUS_REVIEW = 'status: in-review'; + const STATUS_APPROVED = 'status: approved'; + const STATUS_CHANGES = 'status: changes-requested'; + const STATUS_MERGED = 'status: merged'; + const STATUS_CLOSED = 'status: closed'; + const statusLabels = [STATUS_DRAFT, STATUS_REVIEW, STATUS_APPROVED, STATUS_CHANGES, STATUS_MERGED, STATUS_CLOSED]; + + async function safeWrite(fn, op) { + try { + return await fn(); + } catch (e) { + if (e.status === 403) { + core.warning(`Skipping ${op}: token cannot write in this context (403).`); + return null; + } + throw e; + } + } + + if (fromFork) { + core.warning('Skipping PR status label writes for fork-origin PR context.'); + return; + } + + async function ensureLabel(name) { + const existing = await github.rest.issues.getLabel({ owner, repo, name }).catch(e => { + if (e.status === 404) return null; + throw e; + }); + if (existing) return; + await safeWrite( + () => github.rest.issues.createLabel({ + owner, + repo, + name, + color: 'D4C5F9', + description: `PR status label: ${name}` + }), + `create label ${name}` + ); + } + + for (const label of statusLabels) await ensureLabel(label); + + const currentLabels = (pr.data.labels || []).map(l => l.name.toLowerCase()); + const shouldBe = new Set(); + + if (pr.data.state === 'closed') { + if (pr.data.merged_at) shouldBe.add(STATUS_MERGED); + else shouldBe.add(STATUS_CLOSED); + } else if (pr.data.draft) { + shouldBe.add(STATUS_DRAFT); + } else { + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, + repo, + pull_number: pullNumber, + per_page: 100 + }); + + const latestByUser = new Map(); + for (const r of reviews) { + const login = r.user?.login; + if (!login) continue; + latestByUser.set(login, r.state); + } + + const states = [...latestByUser.values()]; + if (states.includes('CHANGES_REQUESTED')) shouldBe.add(STATUS_CHANGES); + else if (states.includes('APPROVED')) shouldBe.add(STATUS_APPROVED); + else shouldBe.add(STATUS_REVIEW); + } + + const remove = statusLabels.filter(l => !shouldBe.has(l) && currentLabels.includes(l.toLowerCase())); + for (const name of remove) { + try { + await safeWrite( + () => github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pullNumber, + name + }), + `remove label ${name} from PR #${pullNumber}` + ); + } catch (e) { + if (e.status !== 404) throw e; + } + } + + const add = [...shouldBe].filter(l => !currentLabels.includes(l.toLowerCase())); + if (add.length > 0) { + await safeWrite( + () => github.rest.issues.addLabels({ + owner, + repo, + issue_number: pullNumber, + labels: add + }), + `add labels to PR #${pullNumber}` + ); + } diff --git a/.gitignore b/.gitignore index 4c7e2317..ac4462cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ node_modules/ -public/ .DS_Store .idea -.DS_Store + +# Next.js +.next/ +dist/ + + +# pnpm +.pnpm-store/ +test-results/ diff --git a/.htmlhintrc b/.htmlhintrc deleted file mode 100644 index 79743920..00000000 --- a/.htmlhintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "attr-lowercase": true, - "alt-require": true -} diff --git a/.hugo_build.lock b/.hugo_build.lock deleted file mode 100644 index e69de29b..00000000 diff --git a/.husky/pre-commit b/.husky/pre-commit index 45baa02e..05fde6f9 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,12 @@ #!/bin/sh -. "$(dirname "$0")/_/husky.sh" set -e # die on error -npm run lint:html \ No newline at end of file +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) + +if command -v pnpm >/dev/null 2>&1; then + cd "$REPO_ROOT" + pnpm run lint +else + echo "pnpm not found. Skipping lint." >&2 +fi \ No newline at end of file diff --git a/ADDING_PAGES.md b/ADDING_PAGES.md new file mode 100644 index 00000000..69215f6f --- /dev/null +++ b/ADDING_PAGES.md @@ -0,0 +1,429 @@ +# Adding New Pages to the Open Elements Website + +For first-time contributors, start with the docs hub first: + +- [Repository Docs Hub](docs/README.md) +- [Content Folder Guide](docs/02-content-folder.md) + +This guide explains how to add new pages to the Open Elements website, using the [dlt-lecture](https://open-elements.com/dlt-lecture/) page as a reference example. + +## Overview + +The Open Elements website uses a hybrid architecture: +- **Next.js** for page rendering and routing (`src/app/[locale]/`) +- **Markdown files** for content (`content/`) +- **i18n support** for English (EN) and German (DE) versions + +## Step-by-Step Guide + +### 1. Create Content Markdown Files + +Create a new folder under `content/` with your page name: + +``` +content/ + your-page-name/ + index.md # English version + index.de.md # German version +``` + +#### English Content (`index.md`) + +```markdown +--- +title: "Your Page Title" +description: "Brief description for SEO and meta tags" +layout: "article" +url: "/your-page-name" +keywords: ["keyword1", "keyword2", "keyword3"] +--- + +Your page content here in Markdown format... + +## Section Heading + +Content for this section... + +### Subsection + +More content... +``` + +#### German Content (`index.de.md`) + +```markdown +--- +title: "Ihr Seitentitel" +description: "Kurze Beschreibung für SEO und Meta-Tags" +layout: "article" +url: "/de/your-page-name" +keywords: ["Schlüsselwort1", "Schlüsselwort2"] +--- + +Ihr Seiteninhalt hier im Markdown-Format... + +## Abschnittsüberschrift + +Inhalt für diesen Abschnitt... +``` + +### 2. Frontmatter Fields Explained + +| Field | Required | Description | Example Values | +|-------|----------|-------------|----------------| +| `title` | Yes | Page title (appears in browser tab and meta tags) | "DLT & Digital Trust Lecture" | +| `description` | Yes | Page description for SEO and social sharing | "Since 2023 Hendrik Ebbers has been offering..." | +| `layout` | Yes | Layout template to use | `"article"`, `"single"`, `"contact"`, `"about-us"` | +| `url` | Yes | URL path for the page (EN: `/page-name`, DE: `/de/page-name`) | `/dlt-lecture` or `/de/dlt-lecture` | +| `keywords` | No | SEO keywords | `["Java", "Open Source", "Support"]` | +| `aliases` | No | Alternative URLs that redirect to this page | `['/old-url', '/another-old-url']` | +| `newsletterPopup` | No | Whether to show newsletter popup | `true` or `false` | + +### 3. Available Layout Types + +Choose the appropriate layout for your page: + +- **`article`** - Standard article/content layout (most common) + - Used by: dlt-lecture, impressum, newsletter-archive + - Best for: Text-heavy content pages, documentation + +- **`single`** - Simple single-column layout + - Used by: support-care-landingpage, support-care-temurin + - Best for: Landing pages, promotional content + +- **`contact`** - Contact form layout + - Used by: contact page + - Best for: Contact forms + +- **`about-us`** - Special layout for about pages + - Used by: about page + - Best for: Team/company information + +- **`about-hendrik`** - Custom layout for founder page + - Used by: about-hendrik page + +- **`newsletter`** - Newsletter subscription layout + - Used by: newsletter page + +- **`index`** - Homepage layout + - Used by: _index.md (homepage only) + +### 4. Create Next.js Page Component + +Create a new folder under `src/app/[locale]/` matching your content folder name: + +``` +src/app/[locale]/ + your-page-name/ + page.tsx +``` + +#### Minimal Page Component Template + +```tsx +import { notFound } from 'next/navigation' +import type { Metadata } from 'next' + +interface YourPageProps { + params: Promise<{ + locale: string + }> +} + +export async function generateMetadata({ params }: YourPageProps): Promise { + const { locale } = await params + + const title = locale === 'de' + ? 'Ihr Seitentitel - Open Elements' + : 'Your Page Title - Open Elements' + + const description = locale === 'de' + ? 'Beschreibung auf Deutsch' + : 'Description in English' + + return { + title, + description, + openGraph: { + type: 'website', + title, + description, + siteName: 'Open Elements', + locale: locale === 'de' ? 'de_DE' : 'en_US', + }, + } +} + +export default async function YourPage({ params }: YourPageProps) { + const { locale } = await params + + return ( +
+ {/* Hero Section */} +
+ {/* Background image or styling */} +
+ + {/* Main Content */} +
+

+ {locale === 'de' ? 'Ihr Seitentitel' : 'Your Page Title'} +

+ +
+ {/* Page content */} +
+
+
+ ) +} +``` + +#### Notes on Page Components: +- Use the `locale` parameter to render different content for EN/DE +- Use `notFound()` if a locale isn't supported: `if (locale !== 'de') { notFound() }` +- Import and use shared components from `src/components/` +- Follow existing pages for styling patterns (Tailwind CSS) + +### 5. Adding Images + +#### Image Storage Locations + +Store images in appropriate subdirectories under `public/`: + +``` +public/ + images/ # General site images (logos, team photos, etc.) + illustrations/ # Illustrations and graphics + posts/ # Blog post images + your-page-name/ # Page-specific images (create new folder) +``` + +#### Recommended Structure for Page-Specific Images + +For a page like `dlt-lecture`, create: + +``` +public/ + dlt-lecture/ + hero-image.jpg + diagram-1.png + photo-classroom.jpg +``` + +#### Referencing Images in Markdown + +##### Hugo Shortcodes (in content markdown): + +```markdown +{{< centered-image src="/illustrations/my-image.svg" alt="Description" width="80%" >}} + +{{< centered-image src="/your-page-name/specific-image.png" showCaption="true" alt="Image caption" width="60%" >}} +``` + +**Note:** Image paths are relative to the `public/` folder. Do NOT include `public/` in the path. + +##### Next.js Image Component (in page.tsx): + +```tsx +import Image from 'next/image' + +Company logo + +{/* For full-width background images */} +
+ Hero background +
+``` + +#### Image Best Practices + +1. **Naming**: Use lowercase, hyphenated names: `team-photo.jpg`, `process-diagram.svg` +2. **Formats**: + - Use `.svg` for logos and simple graphics + - Use `.webp` or `.jpg` for photos + - Use `.png` for images requiring transparency +3. **Optimization**: Compress images before adding them (use tools like TinyPNG) +4. **Alt Text**: Always provide descriptive alt text for accessibility +5. **Dimensions**: Specify width/height to prevent layout shift + +### 6. Linking Between Pages + +#### In Markdown Files: + +```markdown +[Link to another page](/about) +[Link to German page](/de/contact) +[External link](https://example.com) +``` + +#### In Next.js Components: + +```tsx +import Link from 'next/link' + + + About Us + + + + Contact + +``` + +### 7. Using Hugo Shortcodes in Markdown + +The content markdown files support Hugo shortcodes: + +```markdown +{{< centered-image src="/path/to/image.png" alt="Description" width="80%" >}} + +{{< quote id="person-name">}} +``` + +See existing content files for more shortcode examples. + +### 8. Testing Your New Page + +1. **Start the development server:** + ```bash + pnpm run dev + ``` + +2. **Navigate to your page:** + - English: `http://localhost:3000/your-page-name` + - German: `http://localhost:3000/de/your-page-name` + +3. **Test both language versions** + +4. **Check responsive design** on mobile and desktop + +5. **Verify images load correctly** + +6. **Test navigation** to and from your page + +### 9. Checklist Before Publishing + +- [ ] Both `index.md` and `index.de.md` created with correct frontmatter +- [ ] Next.js `page.tsx` component created with locale support +- [ ] All images added to `public/` with appropriate naming +- [ ] Image paths are correct (relative to `public/` folder) +- [ ] All links work correctly +- [ ] SEO metadata (title, description, keywords) filled out +- [ ] Both EN and DE versions display correctly +- [ ] Page is responsive on mobile and desktop +- [ ] Accessibility: alt text on all images +- [ ] No console errors when viewing the page + +## Common Patterns and Examples + +### Example 1: Simple Article Page + +**Content structure:** +``` +content/my-article/ + index.md + index.de.md + +public/my-article/ + hero.jpg + diagram.svg +``` + +**Markdown frontmatter:** +```yaml +--- +title: "My Article Title" +description: "Article description" +layout: "article" +url: "/my-article" +--- +``` + +### Example 2: Multi-language Landing Page + +``` +content/support-program/ + index.md # English version + index.de.md # German version + +src/app/[locale]/support-program/ + page.tsx # Handles both locales + +public/support-program/ + logo.svg + screenshot.png +``` + +### Example 3: Page with Multiple Sections + +```markdown +--- +title: "Complex Page" +description: "A page with multiple sections" +layout: "single" +url: "/complex-page" +--- + +## Section 1 + +Content for section 1... + +{{< centered-image src="/complex-page/section1-image.jpg" alt="Section 1" width="100%" >}} + +## Section 2 + +Content for section 2... + +### Subsection 2.1 + +More detailed content... +``` + +## Troubleshooting + +### Page not found (404) +- Check that the URL in frontmatter matches the folder structure +- Verify the Next.js component is in the correct location +- Ensure the locale routing is set up correctly + +### Images not displaying +- Verify the image path is relative to `public/` without including "public" in the path +- Check that the image file exists in the correct location +- Verify file name capitalization matches exactly + +### Content not updating +- Restart the development server: `pnpm run dev` +- Clear Next.js cache: `rm -rf .next` then restart +- Check for typos in frontmatter YAML + +### Layout not working as expected +- Verify the layout value matches one of the available layouts +- Check if the layout requires specific frontmatter fields +- Look at similar pages for reference + +## Further Resources + +- **Next.js Documentation**: https://nextjs.org/docs +- **Tailwind CSS**: https://tailwindcss.com/docs +- **Markdown Guide**: https://www.markdownguide.org/ +- **Project README**: See [README.md](README.md) for development setup + +## Need Help? + +If you encounter issues not covered in this guide: +1. Check existing pages in `content/` and `src/app/[locale]/` for reference +2. Review the project [README.md](README.md) +3. Ask the development team for guidance diff --git a/README.md b/README.md index 83373343..5e34b478 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,97 @@ # Open Elements Website -This repo contains the website of Open Elements. -The website is still work in progress. -In future the website will be available at https://www.open-elements.de and https://www.open-elements.com. +This repository contains the Open Elements website built with Next.js and Tailwind CSS, with legacy Hugo content kept for migration and historical content. -Netlify status of English page: +## Architecture (2026) -[![Netlify status of English page](https://api.netlify.com/api/v1/badges/0a7875a4-d4ba-4358-8616-87200dcbe7c5/deploy-status)](https://app.netlify.com/sites/open-elements-en/deploys) +The project is a Next.js application using App Router, Tailwind CSS, and `next-intl` for i18n. Legacy Hugo content and templates are kept in the repo for migration and historical content. -Netlify status of German page: +### Runtime layers -[![Netlify status of German page](https://api.netlify.com/api/v1/badges/935f5408-eef5-4889-9cb6-ee55a0990a0f/deploy-status)](https://app.netlify.com/sites/open-elements-de/deploys) +- **Next.js App (primary)** + - App Router pages and layouts in `src/app`. + - UI components in `src/components`. + - Shared utilities in `src/lib`, data in `src/data`, types in `src/types`. + - Styling via Tailwind CSS and `src/app/globals.css`. +- **Internationalization** + - `next-intl` routing and helpers in `src/i18n`. + - Translation messages in `locales`. -## Building the website +- **Markdown content** + - Markdown content in `content/`. + - Built static artifacts live in `public` (do not edit manually). -Since the page is based on Hugo and React we use `npm-run-all` to execute several dev executions in parallel. -Therefore you need to install `npm-run-all` as a dev dependency: - -``` -npm install --save-dev npm-run-all -``` +- **Web components** + - Custom elements live in `react-src` and are bundled via `react-src/build.mjs` into `public/js`. +- **E2E tests** + - Playwright specs in `tests/e2e`. -The project is based on [Hugo](https://gohugo.io/) and you need to [install Hugo](https://gohugo.io/installation/) to build the website. -Once Hugo is installed you can host the website on localhost by executing to following command from the root folder of the repository: +## Development -``` -hugo serve -``` +### Requirements -While the process is running the English (default) version of the website can be reached at http://localhost:1313/ and the German can be reached at http://localhost:1314/. +- Node.js 22 +- pnpm 10 -## Adding Tailwind CSS +### Install dependencies -### 1-Install Tailwind CSS +``` +pnpm install +``` -Install tailwindcss via npm, and create your tailwind.config.js file in the root folder. +### Run locally ``` -npm install -D tailwindcss -npx tailwindcss init +pnpm run dev ``` -### 2-Configure your template paths +The app is available at http://localhost:3000. -Add the paths to all of your template files in your tailwind.config.js file. +### Build & start ``` -content: [ - "content/**/*.md", "layouts/**/*.html" -], +pnpm run build +pnpm run start ``` -### 3-Add the Tailwind directives to your CSS -Create 'input.css' file in the root folder and add the @tailwind directives for each of Tailwind’s layers to your input CSS file. +### Lint ``` -@tailwind base; -@tailwind components; -@tailwind utilities; +pnpm run lint ``` -### 4-Code snippet for Package.json - -Add the following code in 'Package.json' +### E2E tests ``` - "scripts": { - "dev:css": "npx tailwindcss -i input.css -o assets/css/style.css -w", - "dev:hugo": "hugo server", - "dev": "run-p dev:*", - "build:css": "NODE_ENV=production npx tailwindcss -i input.css -o assets/css/style.css -m", - "build:hugo": "hugo", - "build": "run-s build:*" - }, +pnpm run test:e2e ``` -### 5-Dev environment -For development run the following command in terminal. -``` -npm run dev -``` +## Repo structure -### 6-Production -For production ready css, run the following command in terminal. ``` -npm run build +src/app Next.js App Router pages & layouts +src/components UI components +src/i18n next-intl routing/messages helpers +locales Translation JSON files +content Markdown content +public Static assets and generated artifacts +react-src Web components source (bundled to public/js) +tests/e2e Playwright specs ``` + +## Web components build + +Custom elements in `react-src` are bundled with esbuild via `react-src/build.mjs` into `public/js`. This output is treated as generated code. + +## Deployment + +Netlify builds run `pnpm install` and `pnpm run build` (see `netlify.toml`). + +## Documentation + +- **[Docs Hub](docs/README.md)** - First stop for contributors (repository architecture, content workflows, quality checks) +- **[Adding Blog Posts](docs/04-adding-blog-post.md)** - Post-specific workflow for `content/posts/`, front matter, and `/posts/...` linking conventions +- **[Adding New Pages Guide](ADDING_PAGES.md)** - Legacy page-creation guide with EN/DE examples and image/layout notes + diff --git a/assets/css/style.css b/assets/css/style.css deleted file mode 100644 index a402aba9..00000000 --- a/assets/css/style.css +++ /dev/null @@ -1,3 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800;900&display=swap");*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } - -/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Montserrat,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}*{scrollbar-color:auto;scrollbar-width:auto}[x-cloak]{display:none!important}html{font-family:Montserrat,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.nav-link{font-size:1.125rem;line-height:1.75rem;font-weight:500;--tw-text-opacity:1;color:rgb(248 248 248/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.nav-link:focus{outline:2px solid transparent;outline-offset:2px}.nav-link:hover{--tw-text-opacity:1;color:rgb(93 185 245/var(--tw-text-opacity,1))}.nav-icon{font-size:1.5rem;line-height:2rem;line-height:1;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.nav-icon:hover{--tw-text-opacity:1;color:rgb(92 186 158/var(--tw-text-opacity,1))}.h1{font-size:32px;font-weight:700;line-height:1.375}@media (min-width:640px){.h1{font-size:2.25rem;line-height:2.5rem;line-height:1.375}}@media (min-width:1024px){.h1{font-size:52px;line-height:1.375}}.h2{font-size:28px;font-weight:700;line-height:2.5rem}@media (min-width:640px){.h2{font-size:44px}}.h3{font-size:32px;font-weight:700;line-height:2.5rem}.h4,.h4-card{font-size:1.5rem;font-weight:700;line-height:2.5rem}@media (min-width:640px){.h4-card{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1280px){.h4-card{font-size:1.5rem;line-height:2rem}}.badge-purple{flex-shrink:0;border-radius:9999px;border-width:1px;--tw-border-opacity:1;border-color:rgb(148 146 253/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(212 212 254/var(--tw-bg-opacity,1));padding:.25rem .625rem;text-align:center;font-weight:600;--tw-text-opacity:1;color:rgb(148 146 253/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.badge-purple:hover{--tw-bg-opacity:1;background-color:rgb(117 115 255/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1));--tw-shadow:2px 4px 22px rgba(117,115,255,.64);--tw-shadow-colored:2px 4px 22px var(--tw-shadow-color)}.badge-purple:active,.badge-purple:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.badge-purple:active{--tw-bg-opacity:1;background-color:rgb(148 146 253/var(--tw-bg-opacity,1));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.link-rose{font-weight:500;--tw-text-opacity:1;color:rgb(230 50 119/var(--tw-text-opacity,1));transition-property:all;transition-duration:.1s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.link-rose:focus{outline:2px solid transparent;outline-offset:2px}.link-rose:hover{text-decoration-line:underline}.link-rose:active{font-weight:500}.link-purple{font-weight:500;--tw-text-opacity:1;color:rgb(148 146 253/var(--tw-text-opacity,1));transition-property:all;transition-duration:.1s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.link-purple:focus{outline:2px solid transparent;outline-offset:2px}.link-purple:hover{text-decoration-line:underline}.link-purple:active{font-weight:500}.link-green{font-weight:500;--tw-text-opacity:1;color:rgb(92 186 158/var(--tw-text-opacity,1));transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.link-green:focus{outline:2px solid transparent;outline-offset:2px}.link-green:hover{text-decoration-line:underline}.link-green:active{font-weight:500}.footer-link{font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.group:hover .footer-link{--tw-text-opacity:1;color:rgb(93 185 245/var(--tw-text-opacity,1))}.footer-link-icon{flex-shrink:0;font-size:1.25rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.group:hover .footer-link-icon{--tw-text-opacity:1;color:rgb(93 185 245/var(--tw-text-opacity,1))}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:1.5rem;padding-left:1.5rem}@media (min-width:640px){.container{max-width:640px;padding-right:2rem;padding-left:2rem}}@media (min-width:768px){.container{max-width:768px;padding-right:3rem;padding-left:3rem}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px;padding-right:4rem;padding-left:4rem}}.aspect-h-9{--tw-aspect-h:9}.aspect-w-16{position:relative;padding-bottom:calc(var(--tw-aspect-h)/var(--tw-aspect-w)*100%);--tw-aspect-w:16}.aspect-w-16>*{position:absolute;height:100%;width:100%;top:0;right:0;bottom:0;left:0}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{left:0;right:0}.inset-x-1\/2{left:50%;right:50%}.-bottom-12{bottom:-3rem}.-bottom-16{bottom:-4rem}.-bottom-2{bottom:-.5rem}.-bottom-2\.5{bottom:-.625rem}.-bottom-24{bottom:-6rem}.-bottom-3{bottom:-.75rem}.-bottom-4{bottom:-1rem}.-bottom-48{bottom:-12rem}.-bottom-5{bottom:-1.25rem}.-bottom-6{bottom:-1.5rem}.-bottom-64{bottom:-16rem}.-bottom-8{bottom:-2rem}.-bottom-\[30\%\]{bottom:-30%}.-left-1\/3{left:-33.333333%}.-left-14{left:-3.5rem}.-left-2{left:-.5rem}.-left-24{left:-6rem}.-left-40{left:-10rem}.-left-6{left:-1.5rem}.-left-\[10\%\]{left:-10%}.-right-11{right:-2.75rem}.-right-12{right:-3rem}.-right-16{right:-4rem}.-right-2{right:-.5rem}.-right-4{right:-1rem}.-right-40{right:-10rem}.-right-5{right:-1.25rem}.-right-6{right:-1.5rem}.-right-8{right:-2rem}.-right-\[15\%\]{right:-15%}.-right-\[18\%\]{right:-18%}.-top-10{top:-2.5rem}.-top-12{top:-3rem}.-top-16{top:-4rem}.-top-20{top:-5rem}.-top-24{top:-6rem}.-top-3{top:-.75rem}.-top-full{top:-100%}.bottom-0{bottom:0}.bottom-1\/2{bottom:50%}.bottom-5{bottom:1.25rem}.bottom-\[-390\%\]{bottom:-390%}.left-0{left:0}.left-1\.5{left:.375rem}.left-1\/2{left:50%}.left-12{left:3rem}.left-2\.5{left:.625rem}.left-4{left:1rem}.left-52{left:13rem}.right-0{right:0}.right-12{right:3rem}.right-2\.5{right:.625rem}.right-5{right:1.25rem}.right-\[11\%\]{right:11%}.right-\[20\%\]{right:20%}.right-\[7\%\]{right:7%}.right-\[8\%\]{right:8%}.top-0{top:0}.top-1\/2{top:50%}.top-12{top:3rem}.top-16{top:4rem}.top-20{top:5rem}.top-24{top:6rem}.top-28{top:7rem}.top-36{top:9rem}.top-4{top:1rem}.top-48{top:12rem}.top-52{top:13rem}.top-6{top:1.5rem}.top-9{top:2.25rem}.top-\[103\%\]{top:103%}.top-\[110\%\]{top:110%}.top-\[110px\]{top:110px}.top-\[17\%\]{top:17%}.top-\[20\%\]{top:20%}.top-\[32\%\]{top:32%}.top-\[39\%\]{top:39%}.top-\[440px\]{top:440px}.top-\[500px\]{top:500px}.top-\[55\%\]{top:55%}.top-\[65\%\]{top:65%}.isolate{isolation:isolate}.-z-0,.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.order-1{order:1}.order-2{order:2}.order-3{order:3}.col-span-2{grid-column:span 2/span 2}.float-left{float:left}.m-0{margin:0}.-mx-4{margin-left:-1rem;margin-right:-1rem}.-mx-8{margin-left:-2rem;margin-right:-2rem}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-7{margin-top:1.75rem;margin-bottom:1.75rem}.-ml-0\.5{margin-left:-.125rem}.-ml-1{margin-left:-.25rem}.-ml-10{margin-left:-2.5rem}.-ml-12{margin-left:-3rem}.-ml-2\.5{margin-left:-.625rem}.-ml-20{margin-left:-5rem}.-ml-3{margin-left:-.75rem}.-ml-4{margin-left:-1rem}.-ml-5{margin-left:-1.25rem}.-ml-6{margin-left:-1.5rem}.-ml-8{margin-left:-2rem}.-mr-1{margin-right:-.25rem}.-mr-16{margin-right:-4rem}.-mr-2{margin-right:-.5rem}.-mr-20{margin-right:-5rem}.-mr-28{margin-right:-7rem}.-mr-5{margin-right:-1.25rem}.-mr-6{margin-right:-1.5rem}.-mt-1{margin-top:-.25rem}.-mt-16{margin-top:-4rem}.-mt-36{margin-top:-9rem}.-mt-4{margin-top:-1rem}.-mt-8{margin-top:-2rem}.-mt-\[10px\]{margin-top:-10px}.-mt-\[14\%\]{margin-top:-14%}.-mt-\[30px\]{margin-top:-30px}.-mt-px{margin-top:-1px}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-28{margin-bottom:7rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.ml-0{margin-left:0}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-12{margin-left:3rem}.ml-2{margin-left:.5rem}.ml-\[18px\]{margin-left:18px}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-14{margin-top:3.5rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.flow-root{display:flow-root}.grid{display:grid}.contents{display:contents}.hidden{display:none}.size-20{width:5rem;height:5rem}.size-4{width:1rem;height:1rem}.size-40{width:10rem;height:10rem}.size-44{width:11rem;height:11rem}.size-48{width:12rem;height:12rem}.size-5{width:1.25rem;height:1.25rem}.size-56{width:14rem;height:14rem}.size-6{width:1.5rem;height:1.5rem}.size-72{width:18rem;height:18rem}.size-8{width:2rem;height:2rem}.size-full{width:100%;height:100%}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-28{height:7rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-72{height:18rem}.h-8{height:2rem}.h-80{height:20rem}.h-\[179\.5px\]{height:179.5px}.h-\[195px\]{height:195px}.h-\[244px\]{height:244px}.h-\[274px\]{height:274px}.h-\[595px\]{height:595px}.h-\[calc\(100vh-70px\)\]{height:calc(100vh - 70px)}.h-full{height:100%}.h-screen{height:100vh}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-44{width:11rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[195px\]{width:195px}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.min-w-0{min-width:0}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-\[1080px\]{max-width:1080px}.max-w-\[450px\]{max-width:450px}.max-w-\[700px\]{max-width:700px}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-separate{border-collapse:separate}.border-spacing-3{--tw-border-spacing-x:0.75rem;--tw-border-spacing-y:0.75rem}.border-spacing-3,.border-spacing-x-8{border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-x-8{--tw-border-spacing-x:2rem}.border-spacing-y-2{--tw-border-spacing-y:0.5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.-rotate-45{--tw-rotate:-45deg}.-rotate-45,.rotate-45{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate:45deg}.rotate-\[-126deg\]{--tw-rotate:-126deg}.rotate-\[-126deg\],.skew-x-\[16deg\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.skew-x-\[16deg\]{--tw-skew-x:16deg}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-100,.scale-95{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.resize{resize:both}.scroll-mt-10{scroll-margin-top:2.5rem}.scroll-mt-24{scroll-margin-top:6rem}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-10{gap:2.5rem}.gap-11{gap:2.75rem}.gap-12{gap:3rem}.gap-16{gap:4rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-20{gap:5rem}.gap-24{gap:6rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-7{gap:1.75rem}.gap-8{gap:2rem}.gap-9{gap:2.25rem}.gap-x-5{-moz-column-gap:1.25rem;column-gap:1.25rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-10{row-gap:2.5rem}.gap-y-5{row-gap:1.25rem}.gap-y-6{row-gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-20>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.place-self-end{place-self:end}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.scroll-smooth{scroll-behavior:smooth}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[24px\]{border-radius:24px}.rounded-\[28px\]{border-radius:28px}.rounded-\[30px\]{border-radius:30px}.rounded-\[32px\]{border-radius:32px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-b-\[32px\]{border-bottom-right-radius:32px;border-bottom-left-radius:32px}.rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}.rounded-l-3xl{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem}.rounded-t-\[32px\]{border-top-left-radius:32px;border-top-right-radius:32px}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-bl-\[32px\]{border-bottom-left-radius:32px}.rounded-br-\[32px\]{border-bottom-right-radius:32px}.rounded-tl-\[32px\]{border-top-left-radius:32px}.rounded-tr-\[32px\]{border-top-right-radius:32px}.border{border-width:1px}.border-2{border-width:2px}.border-\[3px\]{border-width:3px}.border-l-0{border-left-width:0}.border-t-0{border-top-width:0}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-green{--tw-border-opacity:1;border-color:rgb(92 186 158/var(--tw-border-opacity,1))}.border-green\/40{border-color:rgba(92,186,158,.4)}.border-green\/45{border-color:rgba(92,186,158,.45)}.border-purple{--tw-border-opacity:1;border-color:rgb(148 146 253/var(--tw-border-opacity,1))}.border-purple-700{--tw-border-opacity:1;border-color:rgb(117 115 255/var(--tw-border-opacity,1))}.border-rose{--tw-border-opacity:1;border-color:rgb(230 50 119/var(--tw-border-opacity,1))}.border-sky{--tw-border-opacity:1;border-color:rgb(93 185 245/var(--tw-border-opacity,1))}.border-sky\/45{border-color:rgba(93,185,245,.45)}.border-transparent{border-color:transparent}.border-yellow-200\/45{border-color:rgba(205,193,59,.45)}.bg-\[\#DFF1FD\]{--tw-bg-opacity:1;background-color:rgb(223 241 253/var(--tw-bg-opacity,1))}.bg-\[\#E7F9F3\]{--tw-bg-opacity:1;background-color:rgb(231 249 243/var(--tw-bg-opacity,1))}.bg-blue{--tw-bg-opacity:1;background-color:rgb(2 1 68/var(--tw-bg-opacity,1))}.bg-blue\/5{background-color:rgba(2,1,68,.05)}.bg-blue\/\[0\.02\]{background-color:rgba(2,1,68,.02)}.bg-blue\/\[0\.03\]{background-color:rgba(2,1,68,.03)}.bg-gray{--tw-bg-opacity:1;background-color:rgb(248 248 248/var(--tw-bg-opacity,1))}.bg-green{--tw-bg-opacity:1;background-color:rgb(92 186 158/var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(222 241 236/var(--tw-bg-opacity,1))}.bg-green-50{background-color:#5cba9e1f}.bg-green\/20{background-color:rgba(92,186,158,.2)}.bg-purple{--tw-bg-opacity:1;background-color:rgb(148 146 253/var(--tw-bg-opacity,1))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(212 212 254/var(--tw-bg-opacity,1))}.bg-purple-200{--tw-bg-opacity:1;background-color:rgb(241 241 255/var(--tw-bg-opacity,1))}.bg-purple-700{--tw-bg-opacity:1;background-color:rgb(117 115 255/var(--tw-bg-opacity,1))}.bg-purple\/10{background-color:rgba(148,146,253,.1)}.bg-rose{--tw-bg-opacity:1;background-color:rgb(230 50 119/var(--tw-bg-opacity,1))}.bg-rose-100{--tw-bg-opacity:1;background-color:rgb(250 214 228/var(--tw-bg-opacity,1))}.bg-sky{--tw-bg-opacity:1;background-color:rgb(93 185 245/var(--tw-bg-opacity,1))}.bg-sky-100{--tw-bg-opacity:1;background-color:rgb(223 241 253/var(--tw-bg-opacity,1))}.bg-slate{--tw-bg-opacity:1;background-color:rgb(235 235 238/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/40{background-color:hsla(0,0%,100%,.4)}.bg-yellow-300{--tw-bg-opacity:1;background-color:rgb(230 215 61/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(252 249 219/var(--tw-bg-opacity,1))}.stroke-2{stroke-width:2}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-center{-o-object-position:center;object-position:center}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-0{padding-left:0;padding-right:0}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-20{padding-left:5rem;padding-right:5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-36{padding-top:9rem;padding-bottom:9rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-\[3px\]{padding-top:3px;padding-bottom:3px}.pb-10{padding-bottom:2.5rem}.pb-12{padding-bottom:3rem}.pb-14{padding-bottom:3.5rem}.pb-16{padding-bottom:4rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-28{padding-bottom:7rem}.pb-3{padding-bottom:.75rem}.pb-36{padding-bottom:9rem}.pb-4{padding-bottom:1rem}.pb-40{padding-bottom:10rem}.pb-5{padding-bottom:1.25rem}.pb-9{padding-bottom:2.25rem}.pl-1{padding-left:.25rem}.pl-16{padding-left:4rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pr-20{padding-right:5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pr-5{padding-right:1.25rem}.pr-6{padding-right:1.5rem}.pt-0{padding-top:0}.pt-10{padding-top:2.5rem}.pt-16{padding-top:4rem}.pt-20{padding-top:5rem}.pt-24{padding-top:6rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-60{padding-top:15rem}.pt-7{padding-top:1.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[14px\]{font-size:14px}.text-\[22px\]{font-size:22px}.text-\[48px\]{font-size:48px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-sm\/6{font-size:.875rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-10{line-height:2.5rem}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.leading-\[22px\]{line-height:22px}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-blue{--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.text-blue\/30{color:rgba(2,1,68,.3)}.text-blue\/60{color:rgba(2,1,68,.6)}.text-green{--tw-text-opacity:1;color:rgb(92 186 158/var(--tw-text-opacity,1))}.text-lightgray{--tw-text-opacity:1;color:rgb(211 211 211/var(--tw-text-opacity,1))}.text-purple{--tw-text-opacity:1;color:rgb(148 146 253/var(--tw-text-opacity,1))}.text-purple-100{--tw-text-opacity:1;color:rgb(212 212 254/var(--tw-text-opacity,1))}.text-purple-700{--tw-text-opacity:1;color:rgb(117 115 255/var(--tw-text-opacity,1))}.text-rose{--tw-text-opacity:1;color:rgb(230 50 119/var(--tw-text-opacity,1))}.text-sky{--tw-text-opacity:1;color:rgb(93 185 245/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-200{--tw-text-opacity:1;color:rgb(205 193 59/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-3{--tw-shadow:2px 4px 22px rgba(117,115,255,.64);--tw-shadow-colored:2px 4px 22px var(--tw-shadow-color)}.shadow-3,.shadow-4{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-4{--tw-shadow:0px 0px 24px rgba(190,227,216,.6);--tw-shadow-colored:0px 0px 24px var(--tw-shadow-color)}.shadow-5{--tw-shadow:0px 4px 20px 0px rgba(117,115,255,.22);--tw-shadow-colored:0px 4px 20px 0px var(--tw-shadow-color)}.shadow-5,.shadow-9{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-9{--tw-shadow:2px 4px 12px 0px rgba(92,186,158,.7);--tw-shadow-colored:2px 4px 12px 0px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-green{--tw-shadow-color:#5cba9e;--tw-shadow:var(--tw-shadow-colored)}.shadow-green\/20{--tw-shadow-color:rgba(92,186,158,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-sky\/20{--tw-shadow-color:rgba(93,185,245,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-yellow-200\/20{--tw-shadow-color:rgba(205,193,59,.2);--tw-shadow:var(--tw-shadow-colored)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow-card{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-card{--tw-drop-shadow:drop-shadow(0px 0px 24px rgba(190,227,216,.6))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(40px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-scroll{will-change:scroll-position}.scrollbar{--scrollbar-track:initial;--scrollbar-thumb:initial;--scrollbar-corner:initial;--scrollbar-track-hover:var(--scrollbar-track);--scrollbar-thumb-hover:var(--scrollbar-thumb);--scrollbar-corner-hover:var(--scrollbar-corner);--scrollbar-track-active:var(--scrollbar-track-hover);--scrollbar-thumb-active:var(--scrollbar-thumb-hover);--scrollbar-corner-active:var(--scrollbar-corner-hover);scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);overflow:overlay}.scrollbar.overflow-x-hidden{overflow-x:hidden}.scrollbar.overflow-y-hidden{overflow-y:hidden}.scrollbar::-webkit-scrollbar-track{background-color:var(--scrollbar-track)}.scrollbar::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb)}.scrollbar::-webkit-scrollbar-corner{background-color:var(--scrollbar-corner)}.scrollbar::-webkit-scrollbar-track:hover{background-color:var(--scrollbar-track-hover)}.scrollbar::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover)}.scrollbar::-webkit-scrollbar-corner:hover{background-color:var(--scrollbar-corner-hover)}.scrollbar::-webkit-scrollbar-track:active{background-color:var(--scrollbar-track-active)}.scrollbar::-webkit-scrollbar-thumb:active{background-color:var(--scrollbar-thumb-active)}.scrollbar::-webkit-scrollbar-corner:active{background-color:var(--scrollbar-corner-active)}.scrollbar{scrollbar-width:auto}.scrollbar::-webkit-scrollbar{width:16px;height:16px}.scrollbar-thin{--scrollbar-track:initial;--scrollbar-thumb:initial;--scrollbar-corner:initial;--scrollbar-track-hover:var(--scrollbar-track);--scrollbar-thumb-hover:var(--scrollbar-thumb);--scrollbar-corner-hover:var(--scrollbar-corner);--scrollbar-track-active:var(--scrollbar-track-hover);--scrollbar-thumb-active:var(--scrollbar-thumb-hover);--scrollbar-corner-active:var(--scrollbar-corner-hover);scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track);overflow:overlay}.scrollbar-thin.overflow-x-hidden{overflow-x:hidden}.scrollbar-thin.overflow-y-hidden{overflow-y:hidden}.scrollbar-thin::-webkit-scrollbar-track{background-color:var(--scrollbar-track)}.scrollbar-thin::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb)}.scrollbar-thin::-webkit-scrollbar-corner{background-color:var(--scrollbar-corner)}.scrollbar-thin::-webkit-scrollbar-track:hover{background-color:var(--scrollbar-track-hover)}.scrollbar-thin::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover)}.scrollbar-thin::-webkit-scrollbar-corner:hover{background-color:var(--scrollbar-corner-hover)}.scrollbar-thin::-webkit-scrollbar-track:active{background-color:var(--scrollbar-track-active)}.scrollbar-thin::-webkit-scrollbar-thumb:active{background-color:var(--scrollbar-thumb-active)}.scrollbar-thin::-webkit-scrollbar-corner:active{background-color:var(--scrollbar-corner-active)}.scrollbar-thin{scrollbar-width:thin}.scrollbar-thin::-webkit-scrollbar{width:8px;height:8px}.scrollbar-none{scrollbar-width:none}.scrollbar-none::-webkit-scrollbar{display:none}.scrollbar-thumb-blue{--scrollbar-thumb:#020144!important}.no-list-style-type{list-style-type:none;margin:.5em;padding-inline-start:0}p{padding-top:.8em}@media (min-width:640px){.sm\:h3{font-size:32px;font-weight:700;line-height:2.5rem}.sm\:container{width:100%;margin-right:auto;margin-left:auto;padding-right:1.5rem;padding-left:1.5rem}@media (min-width:640px){.sm\:container{max-width:640px;padding-right:2rem;padding-left:2rem}}@media (min-width:768px){.sm\:container{max-width:768px;padding-right:3rem;padding-left:3rem}}@media (min-width:1024px){.sm\:container{max-width:1024px}}@media (min-width:1280px){.sm\:container{max-width:1280px;padding-right:4rem;padding-left:4rem}}}@media (min-width:1024px){.lg\:container{width:100%;margin-right:auto;margin-left:auto;padding-right:1.5rem;padding-left:1.5rem}@media (min-width:640px){.lg\:container{max-width:640px;padding-right:2rem;padding-left:2rem}}@media (min-width:768px){.lg\:container{max-width:768px;padding-right:3rem;padding-left:3rem}}@media (min-width:1024px){.lg\:container{max-width:1024px}}@media (min-width:1280px){.lg\:container{max-width:1280px;padding-right:4rem;padding-left:4rem}}}@media (min-width:1280px){.xl\:container{width:100%;margin-right:auto;margin-left:auto;padding-right:1.5rem;padding-left:1.5rem}@media (min-width:640px){.xl\:container{max-width:640px;padding-right:2rem;padding-left:2rem}}@media (min-width:768px){.xl\:container{max-width:768px;padding-right:3rem;padding-left:3rem}}@media (min-width:1024px){.xl\:container{max-width:1024px}}@media (min-width:1280px){.xl\:container{max-width:1280px;padding-right:4rem;padding-left:4rem}}}.placeholder\:text-center::-moz-placeholder{text-align:center}.placeholder\:text-center::placeholder{text-align:center}.placeholder\:font-medium::-moz-placeholder{font-weight:500}.placeholder\:font-medium::placeholder{font-weight:500}.placeholder\:text-blue::-moz-placeholder{--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.placeholder\:text-blue::placeholder{--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.placeholder\:text-opacity-60::-moz-placeholder{--tw-text-opacity:0.6}.placeholder\:text-opacity-60::placeholder{--tw-text-opacity:0.6}.before\:block:before{content:var(--tw-content);display:block}.first\:pt-3:first-child{padding-top:.75rem}.last\:pb-3:last-child{padding-bottom:.75rem}.even\:mt-5:nth-child(2n),.odd\:mt-5:nth-child(odd){margin-top:1.25rem}.focus\:border-green:focus{--tw-border-opacity:1;border-color:rgb(92 186 158/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:text-blue{--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.group:hover .group-hover\:text-purple-700{--tw-text-opacity:1;color:rgb(117 115 255/var(--tw-text-opacity,1))}.aria-disabled\:bg-purple-100[aria-disabled=true]{--tw-bg-opacity:1;background-color:rgb(212 212 254/var(--tw-bg-opacity,1))}.prose-headings\:font-bold :is(:where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *))){font-weight:700}.prose-h4\:mb-6 :is(:where(h4):not(:where([class~=not-prose],[class~=not-prose] *))){margin-bottom:1.5rem}.prose-h4\:text-2xl :is(:where(h4):not(:where([class~=not-prose],[class~=not-prose] *))){font-size:1.5rem;line-height:2rem}.prose-h5\:mt-4 :is(:where(h5):not(:where([class~=not-prose],[class~=not-prose] *))){margin-top:1rem}.prose-h5\:text-lg :is(:where(h5):not(:where([class~=not-prose],[class~=not-prose] *))){font-size:1.125rem;line-height:1.75rem}.prose-p\:mt-3 :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){margin-top:.75rem}.prose-p\:text-base :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){font-size:1rem;line-height:1.5rem}.prose-p\:leading-7 :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){line-height:1.75rem}.prose-a\:block :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){display:block}.prose-a\:font-bold :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){font-weight:700}.prose-a\:text-purple-700 :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){--tw-text-opacity:1;color:rgb(117 115 255/var(--tw-text-opacity,1))}.prose-a\:no-underline :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){text-decoration-line:none}.prose-blockquote\:rounded-3xl :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){border-radius:1.5rem}.prose-blockquote\:border-l-0 :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){border-left-width:0}.prose-blockquote\:bg-green-100 :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){--tw-bg-opacity:1;background-color:rgb(222 241 236/var(--tw-bg-opacity,1))}.prose-blockquote\:px-8 :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){padding-left:2rem;padding-right:2rem}.prose-blockquote\:py-3 :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){padding-top:.75rem;padding-bottom:.75rem}.prose-blockquote\:not-italic :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){font-style:normal}.prose-code\:bg-yellow :is(:where(code):not(:where([class~=not-prose],[class~=not-prose] *))){--tw-bg-opacity:1;background-color:rgb(241 227 75/var(--tw-bg-opacity,1))}.prose-pre\:my-0 :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){margin-top:0;margin-bottom:0}.prose-pre\:rounded-none :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){border-radius:0}.prose-pre\:bg-\[\#010027\]\/80 :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){background-color:rgba(1,0,39,.8)}.prose-ul\:-ml-2\.5 :is(:where(ul):not(:where([class~=not-prose],[class~=not-prose] *))){margin-left:-.625rem}.prose-li\:marker\:text-purple-700 * :is(:where(li):not(:where([class~=not-prose],[class~=not-prose] *)))::marker{color:#7573ff}.prose-li\:marker\:text-purple-700 :is(:where(li):not(:where([class~=not-prose],[class~=not-prose] *)))::marker{color:#7573ff}.hover\:-translate-y-6:hover{--tw-translate-y:-1.5rem}.hover\:-translate-y-6:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-purple-700:hover{--tw-border-opacity:1;border-color:rgb(117 115 255/var(--tw-border-opacity,1))}.hover\:border-sky-200:hover{--tw-border-opacity:1;border-color:rgb(84 167 221/var(--tw-border-opacity,1))}.hover\:bg-green:hover{--tw-bg-opacity:1;background-color:rgb(92 186 158/var(--tw-bg-opacity,1))}.hover\:bg-green-200:hover{--tw-bg-opacity:1;background-color:rgb(190 227 216/var(--tw-bg-opacity,1))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(83 167 142/var(--tw-bg-opacity,1))}.hover\:bg-purple-100\/70:hover{background-color:rgba(212,212,254,.7)}.hover\:bg-purple-700:hover{--tw-bg-opacity:1;background-color:rgb(117 115 255/var(--tw-bg-opacity,1))}.hover\:bg-sky-200:hover{--tw-bg-opacity:1;background-color:rgb(84 167 221/var(--tw-bg-opacity,1))}.hover\:bg-white\/20:hover{background-color:hsla(0,0%,100%,.2)}.hover\:bg-yellow-400:hover{--tw-bg-opacity:1;background-color:rgb(207 194 55/var(--tw-bg-opacity,1))}.hover\:text-blue:hover{--tw-text-opacity:1;color:rgb(2 1 68/var(--tw-text-opacity,1))}.hover\:text-green:hover{--tw-text-opacity:1;color:rgb(92 186 158/var(--tw-text-opacity,1))}.hover\:text-purple-700:hover{--tw-text-opacity:1;color:rgb(117 115 255/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:shadow-3:hover{--tw-shadow:2px 4px 22px rgba(117,115,255,.64);--tw-shadow-colored:2px 4px 22px var(--tw-shadow-color)}.hover\:shadow-3:hover,.hover\:shadow-5:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-5:hover{--tw-shadow:0px 4px 20px 0px rgba(117,115,255,.22);--tw-shadow-colored:0px 4px 20px 0px var(--tw-shadow-color)}.hover\:shadow-6:hover{--tw-shadow:2px 4px 22px 0px rgba(205,193,59,.6);--tw-shadow-colored:2px 4px 22px 0px var(--tw-shadow-color)}.hover\:shadow-6:hover,.hover\:shadow-7:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-7:hover{--tw-shadow:2px 4px 22px 0px rgba(92,186,158,.6);--tw-shadow-colored:2px 4px 22px 0px var(--tw-shadow-color)}.hover\:shadow-8:hover{--tw-shadow:2px 4px 22px 0px rgba(93,185,245,.6);--tw-shadow-colored:2px 4px 22px 0px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.active\:bg-green:active{--tw-bg-opacity:1;background-color:rgb(92 186 158/var(--tw-bg-opacity,1))}.active\:bg-purple:active{--tw-bg-opacity:1;background-color:rgb(148 146 253/var(--tw-bg-opacity,1))}.active\:bg-rose:active{--tw-bg-opacity:1;background-color:rgb(230 50 119/var(--tw-bg-opacity,1))}.active\:text-rose-100:active{--tw-text-opacity:1;color:rgb(250 214 228/var(--tw-text-opacity,1))}.active\:text-white:active{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.active\:shadow-none:active{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.sm\:absolute{position:absolute}.sm\:inset-y-1\/2{top:50%;bottom:50%}.sm\:-bottom-32{bottom:-8rem}.sm\:-bottom-36{bottom:-9rem}.sm\:-bottom-8{bottom:-2rem}.sm\:-bottom-\[70\%\]{bottom:-70%}.sm\:-left-2\.5{left:-.625rem}.sm\:-left-24{left:-6rem}.sm\:-right-24{right:-6rem}.sm\:-right-8{right:-2rem}.sm\:-top-20{top:-5rem}.sm\:-top-24{top:-6rem}.sm\:bottom-2{bottom:.5rem}.sm\:bottom-auto{bottom:auto}.sm\:left-16{left:4rem}.sm\:right-1\/3{right:33.333333%}.sm\:right-28{right:7rem}.sm\:right-\[12\%\]{right:12%}.sm\:right-\[15\%\]{right:15%}.sm\:right-\[9\%\]{right:9%}.sm\:top-0{top:0}.sm\:top-28{top:7rem}.sm\:top-32{top:8rem}.sm\:top-36{top:9rem}.sm\:top-8{top:2rem}.sm\:top-\[110\%\]{top:110%}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:-ml-16{margin-left:-4rem}.sm\:-mr-16{margin-right:-4rem}.sm\:-mr-24{margin-right:-6rem}.sm\:-mr-32{margin-right:-8rem}.sm\:-mt-2\.5{margin-top:-.625rem}.sm\:-mt-\[4\%\]{margin-top:-4%}.sm\:-mt-\[5\%\]{margin-top:-5%}.sm\:mb-0{margin-bottom:0}.sm\:mb-12{margin-bottom:3rem}.sm\:mb-14{margin-bottom:3.5rem}.sm\:mb-5{margin-bottom:1.25rem}.sm\:mb-6{margin-bottom:1.5rem}.sm\:ml-0{margin-left:0}.sm\:ml-2{margin-left:.5rem}.sm\:ml-8{margin-left:2rem}.sm\:ml-auto{margin-left:auto}.sm\:mr-auto{margin-right:auto}.sm\:mt-10{margin-top:2.5rem}.sm\:mt-12{margin-top:3rem}.sm\:mt-16{margin-top:4rem}.sm\:mt-2{margin-top:.5rem}.sm\:mt-3{margin-top:.75rem}.sm\:mt-4{margin-top:1rem}.sm\:mt-5{margin-top:1.25rem}.sm\:mt-7{margin-top:1.75rem}.sm\:mt-8{margin-top:2rem}.sm\:line-clamp-none{overflow:visible;display:block;-webkit-box-orient:horizontal;-webkit-line-clamp:none}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:h-14{height:3.5rem}.sm\:h-16{height:4rem}.sm\:h-20{height:5rem}.sm\:h-24{height:6rem}.sm\:h-40{height:10rem}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-\[310px\]{height:310px}.sm\:h-auto{height:auto}.sm\:w-12{width:3rem}.sm\:w-14{width:3.5rem}.sm\:w-16{width:4rem}.sm\:w-20{width:5rem}.sm\:w-24{width:6rem}.sm\:w-28{width:7rem}.sm\:w-32{width:8rem}.sm\:w-40{width:10rem}.sm\:w-48{width:12rem}.sm\:w-52{width:13rem}.sm\:w-6{width:1.5rem}.sm\:w-64{width:16rem}.sm\:w-7{width:1.75rem}.sm\:w-72{width:18rem}.sm\:w-8{width:2rem}.sm\:w-96{width:24rem}.sm\:w-\[310px\]{width:310px}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-full{max-width:100%}.sm\:max-w-md{max-width:28rem}.sm\:max-w-xl{max-width:36rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:flex-col{flex-direction:column}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:0}.sm\:gap-12{gap:3rem}.sm\:gap-2{gap:.5rem}.sm\:gap-24{gap:6rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-6{gap:1.5rem}.sm\:gap-y-4{row-gap:1rem}.sm\:space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.sm\:space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.sm\:rounded-3xl{border-radius:1.5rem}.sm\:border-l{border-left-width:1px}.sm\:border-r-0{border-right-width:0}.sm\:p-10{padding:2.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-12{padding-left:3rem;padding-right:3rem}.sm\:px-16{padding-left:4rem;padding-right:4rem}.sm\:px-24{padding-left:6rem;padding-right:6rem}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:px-8{padding-left:2rem;padding-right:2rem}.sm\:py-16{padding-top:4rem;padding-bottom:4rem}.sm\:py-24{padding-top:6rem;padding-bottom:6rem}.sm\:py-3{padding-top:.75rem;padding-bottom:.75rem}.sm\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.sm\:py-7{padding-top:1.75rem;padding-bottom:1.75rem}.sm\:py-8{padding-top:2rem;padding-bottom:2rem}.sm\:pb-0{padding-bottom:0}.sm\:pb-12{padding-bottom:3rem}.sm\:pb-16{padding-bottom:4rem}.sm\:pb-32{padding-bottom:8rem}.sm\:pb-36{padding-bottom:9rem}.sm\:pb-44{padding-bottom:11rem}.sm\:pl-0{padding-left:0}.sm\:pl-16{padding-left:4rem}.sm\:pl-5{padding-left:1.25rem}.sm\:pr-20{padding-right:5rem}.sm\:pr-36{padding-right:9rem}.sm\:pt-12{padding-top:3rem}.sm\:pt-24{padding-top:6rem}.sm\:pt-28{padding-top:7rem}.sm\:pt-36{padding-top:9rem}.sm\:pt-5{padding-top:1.25rem}.sm\:pt-64{padding-top:16rem}.sm\:text-left{text-align:left}.sm\:text-center{text-align:center}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-7xl{font-size:4.5rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:leading-8{line-height:2rem}.sm\:placeholder\:text-left::-moz-placeholder{text-align:left}.sm\:placeholder\:text-left::placeholder{text-align:left}}@media (min-width:768px){.md\:-left-40{left:-10rem}.md\:-right-40{right:-10rem}.md\:-top-28{top:-7rem}.md\:right-\[10\%\]{right:10%}.md\:right-\[20\%\]{right:20%}.md\:top-40{top:10rem}.md\:float-none{float:none}.md\:-ml-16{margin-left:-4rem}.md\:mt-12{margin-top:3rem}.md\:mt-16{margin-top:4rem}.md\:flex{display:flex}.md\:h-auto{height:auto}.md\:w-16{width:4rem}.md\:w-24{width:6rem}.md\:w-28{width:7rem}.md\:w-36{width:9rem}.md\:w-48{width:12rem}.md\:max-w-2xl{max-width:42rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:px-12{padding-left:3rem;padding-right:3rem}.md\:py-28{padding-top:7rem;padding-bottom:7rem}.md\:pb-40{padding-bottom:10rem}.md\:pt-28{padding-top:7rem}}@media (min-width:1024px){.lg\:absolute{position:absolute}.lg\:-bottom-10{bottom:-2.5rem}.lg\:-bottom-48{bottom:-12rem}.lg\:-left-12{left:-3rem}.lg\:-left-24{left:-6rem}.lg\:-left-44{left:-11rem}.lg\:-left-\[11px\]{left:-11px}.lg\:-right-20{right:-5rem}.lg\:-right-32{right:-8rem}.lg\:-top-1\/2{top:-50%}.lg\:-top-12{top:-3rem}.lg\:bottom-auto{bottom:auto}.lg\:left-1\/2{left:50%}.lg\:left-32{left:8rem}.lg\:left-auto{left:auto}.lg\:right-80{right:20rem}.lg\:right-\[10\.5\%\]{right:10.5%}.lg\:right-\[15\%\]{right:15%}.lg\:right-\[31\%\]{right:31%}.lg\:right-auto{right:auto}.lg\:top-0{top:0}.lg\:top-12{top:3rem}.lg\:top-14{top:3.5rem}.lg\:top-16{top:4rem}.lg\:top-24{top:6rem}.lg\:top-36{top:9rem}.lg\:top-7{top:1.75rem}.lg\:top-\[25\%\]{top:25%}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-3{order:3}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:mx-0{margin-left:0;margin-right:0}.lg\:-mr-5{margin-right:-1.25rem}.lg\:-mt-16{margin-top:-4rem}.lg\:mb-16{margin-bottom:4rem}.lg\:ml-0{margin-left:0}.lg\:ml-\[19\%\]{margin-left:19%}.lg\:ml-\[25\%\]{margin-left:25%}.lg\:mr-\[5\%\]{margin-right:5%}.lg\:mt-0{margin-top:0}.lg\:mt-12{margin-top:3rem}.lg\:mt-20{margin-top:5rem}.lg\:mt-40{margin-top:10rem}.lg\:mt-5{margin-top:1.25rem}.lg\:mt-6{margin-top:1.5rem}.lg\:mt-8{margin-top:2rem}.lg\:mt-\[-12\%\]{margin-top:-12%}.lg\:block{display:block}.lg\:inline-block{display:inline-block}.lg\:inline{display:inline}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:h-40{height:10rem}.lg\:h-\[265px\]{height:265px}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.333333%}.lg\:w-2\/3{width:66.666667%}.lg\:w-24{width:6rem}.lg\:w-32{width:8rem}.lg\:w-36{width:9rem}.lg\:w-40{width:10rem}.lg\:w-48{width:12rem}.lg\:w-56{width:14rem}.lg\:w-64{width:16rem}.lg\:w-72{width:18rem}.lg\:w-80{width:20rem}.lg\:w-\[265px\]{width:265px}.lg\:w-\[380px\]{width:380px}.lg\:w-\[450px\]{width:450px}.lg\:w-full{width:100%}.lg\:max-w-5xl{max-width:64rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-full{max-width:100%}.lg\:max-w-md{max-width:28rem}.lg\:max-w-xl{max-width:36rem}.lg\:rotate-\[143deg\]{--tw-rotate:143deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:flex-col{flex-direction:column}.lg\:items-start{align-items:flex-start}.lg\:items-center{align-items:center}.lg\:justify-start{justify-content:flex-start}.lg\:justify-end{justify-content:flex-end}.lg\:gap-16{gap:4rem}.lg\:gap-20{gap:5rem}.lg\:gap-32{gap:8rem}.lg\:gap-5{gap:1.25rem}.lg\:gap-6{gap:1.5rem}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-20{padding-top:5rem;padding-bottom:5rem}.lg\:py-36{padding-top:9rem;padding-bottom:9rem}.lg\:pb-48{padding-bottom:12rem}.lg\:pb-52{padding-bottom:13rem}.lg\:pb-56{padding-bottom:14rem}.lg\:pr-11{padding-right:2.75rem}.lg\:pt-24{padding-top:6rem}.lg\:pt-36{padding-top:9rem}.lg\:pt-72{padding-top:18rem}.lg\:text-left{text-align:left}.lg\:text-center{text-align:center}.lg\:text-5xl{font-size:3rem;line-height:1}.lg\:text-\[100px\]{font-size:100px}.lg\:text-\[28px\]{font-size:28px}.lg\:text-base{font-size:1rem;line-height:1.5rem}}@media (min-width:1280px){.xl\:absolute{position:absolute}.xl\:-bottom-56{bottom:-14rem}.xl\:-bottom-64{bottom:-16rem}.xl\:-bottom-\[80\%\]{bottom:-80%}.xl\:-left-12{left:-3rem}.xl\:-left-8{left:-2rem}.xl\:-right-\[25\%\]{right:-25%}.xl\:left-9{left:2.25rem}.xl\:right-\[10\%\]{right:10%}.xl\:right-auto{right:auto}.xl\:top-0{top:0}.xl\:top-56{top:14rem}.xl\:top-\[120\%\]{top:120%}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:float-left{float:left}.xl\:-mt-32{margin-top:-8rem}.xl\:mr-6{margin-right:1.5rem}.xl\:mt-0{margin-top:0}.xl\:mt-16{margin-top:4rem}.xl\:mt-5{margin-top:1.25rem}.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:h-\[310px\]{height:310px}.xl\:w-32{width:8rem}.xl\:w-44{width:11rem}.xl\:w-56{width:14rem}.xl\:w-60{width:15rem}.xl\:w-64{width:16rem}.xl\:w-\[310px\]{width:310px}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-6xl{max-width:72rem}.xl\:max-w-7xl{max-width:80rem}.xl\:max-w-\[800px\]{max-width:800px}.xl\:max-w-md{max-width:28rem}.xl\:max-w-prose{max-width:65ch}.xl\:max-w-sm{max-width:24rem}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:justify-center{justify-content:center}.xl\:gap-12{gap:3rem}.xl\:gap-16{gap:4rem}.xl\:gap-28{gap:7rem}.xl\:gap-4{gap:1rem}.xl\:gap-8{gap:2rem}.xl\:place-self-end{place-self:end}.xl\:rounded-\[40px\]{border-radius:40px}.xl\:p-5{padding:1.25rem}.xl\:px-6{padding-left:1.5rem;padding-right:1.5rem}.xl\:py-28{padding-top:7rem;padding-bottom:7rem}.xl\:py-56{padding-top:14rem;padding-bottom:14rem}.xl\:pb-28{padding-bottom:7rem}.xl\:pb-36{padding-bottom:9rem}.xl\:pb-72{padding-bottom:18rem}.xl\:text-\[32px\]{font-size:32px}.xl\:text-lg{font-size:1.125rem;line-height:1.75rem}.xl\:leading-10{line-height:2.5rem}.\32xl\:-bottom-72{bottom:-18rem}.\32xl\:-right-24{right:-6rem}.\32xl\:bottom-14{bottom:3.5rem}.\32xl\:left-32{left:8rem}.\32xl\:right-20{right:5rem}.\32xl\:right-\[9\%\]{right:9%}.\32xl\:-ml-32{margin-left:-8rem}.\32xl\:-mt-2{margin-top:-.5rem}.\32xl\:block{display:block}.\32xl\:hidden{display:none}.\32xl\:w-1\/3{width:33.333333%}.\32xl\:w-52{width:13rem}.\32xl\:w-96{width:24rem}.\32xl\:w-\[325px\]{width:325px}.\32xl\:max-w-3xl{max-width:48rem}.\32xl\:max-w-md{max-width:28rem}.\32xl\:shrink-0{flex-shrink:0}.\32xl\:flex-row{flex-direction:row}.\32xl\:gap-20{gap:5rem}.\32xl\:gap-28{gap:7rem}.\32xl\:gap-40{gap:10rem}.\32xl\:pb-56{padding-bottom:14rem}.\32xl\:text-lg{font-size:1.125rem;line-height:1.75rem}} \ No newline at end of file diff --git a/config.toml b/config.toml deleted file mode 100644 index e36c23b2..00000000 --- a/config.toml +++ /dev/null @@ -1,30 +0,0 @@ -defaultContentLanguage = "en" -title = "Open Elements" -baseURL = "https://open-elements.com" - -enableRobotsTXT = true -log = true -logFile = "hugo-build.log" -enableGitInfo = true - -[languages.en] -weight = 1 -languageCode = "en-us" -languageName = "EN" -[languages.en.params] -description = "Open Source made right - Open Elements is a modern company with a clear focus on Open Source and Java" - -[languages.de] -weight = 2 -languageCode = "de" -languageName = "DE" -[languages.de.params] -description = "Open Source, aber richtig - Open Elements ist ein modernes Unternehmen mit einem Fokus auf Open Source und Java" - -[markup.goldmark.renderer] -unsafe = true - -[permalinks.page] -posts = '/posts/:year/:month/:day/:slug/' -[permalinks.section] -posts = '/posts' diff --git a/content/posts/2011-08-04-preview-multitouch-gestures-in-swing.md b/content/posts/2011-08-04-preview-multitouch-gestures-in-swing.md index d5367882..322755d8 100644 --- a/content/posts/2011-08-04-preview-multitouch-gestures-in-swing.md +++ b/content/posts/2011-08-04-preview-multitouch-gestures-in-swing.md @@ -1,31 +1,27 @@ ---- -outdated: true -showInBlog: false -title: 'Preview: Multitouch gestures in swing' -date: "2011-08-04" -author: hendrik -categories: [General] -excerpt: 'Apple added a listener based API for multitouch gestures to their eawt package. With this wrapper API you can easily integrate it in any app.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In my last [post]({{< ref "/posts/2011-07-28-fun-with-gestures" >}}) I described Apples gestures API. Up to date I´m developing a wrapper API. With this API you can add multitouch-listeners to any swing component. On any OS unlike Mac OS a `GesturesNotSupportedException` is thrown if you try to register a listener. So you can use the API in every application. If the Applications runs on a Mac it supports gestures. - -Here is how: - -{{< highlight java >}} -try { - GestureUtilities.add(panel, gestureRotationListener); -} catch (GesturesNotSupportedException e) { - System.out.println("Gestures-API not Supported!"); -} -{{< / highlight >}} - -Or you can just check if the Apple API is supported: - -{{< highlight java >}} -if(!GestureUtilities.isSupported()) { - System.out.println("Gestures-API not Supported!"); -} -{{< / highlight >}} - -I will add javadoc to the source and update the gestures demo next week. You can check out the source @ [https://code.google.com/p/gestures-wrapper/](https://code.google.com/p/gestures-wrapper/). +--- +outdated: true +showInBlog: false +title: 'Preview: Multitouch gestures in swing' +date: "2011-08-04" +author: hendrik +categories: [General] +excerpt: 'Apple added a listener based API for multitouch gestures to their eawt package. With this wrapper API you can easily integrate it in any app.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In my last [post](/posts/2011-07-28-fun-with-gestures) I described Apples gestures API. Up to date I´m developing a wrapper API. With this API you can add multitouch-listeners to any swing component. On any OS unlike Mac OS a `GesturesNotSupportedException` is thrown if you try to register a listener. So you can use the API in every application. If the Applications runs on a Mac it supports gestures. + +Here is how: + +```javatry { + GestureUtilities.add(panel, gestureRotationListener); +} catch (GesturesNotSupportedException e) { + System.out.println("Gestures-API not Supported!"); +} ``` + +Or you can just check if the Apple API is supported: + +```javaif(!GestureUtilities.isSupported()) { + System.out.println("Gestures-API not Supported!"); +} ``` + +I will add javadoc to the source and update the gestures demo next week. You can check out the source @ [https://code.google.com/p/gestures-wrapper/](https://code.google.com/p/gestures-wrapper/). diff --git a/content/posts/2011-09-01-gesture-wrapper-0-1-released.md b/content/posts/2011-09-01-gesture-wrapper-0-1-released.md index 353e088a..5896c163 100644 --- a/content/posts/2011-09-01-gesture-wrapper-0-1-released.md +++ b/content/posts/2011-09-01-gesture-wrapper-0-1-released.md @@ -12,4 +12,4 @@ The Gesture Wrapper API is a wrapper around the Apple multitouch gestures API. I You can download the jar and documentation [here]({{ site.baseurl }}{% link pages/projects/gesture-wrapper.md %}). -I posted a short [tutorial]({{< ref "/posts/2011-08-04-preview-multitouch-gestures-in-swing" >}}). +I posted a short [tutorial](/posts/2011-08-04-preview-multitouch-gestures-in-swing). diff --git a/content/posts/2011-09-12-garagetunes-demo.md b/content/posts/2011-09-12-garagetunes-demo.md index 358f7f85..fa2984d4 100644 --- a/content/posts/2011-09-12-garagetunes-demo.md +++ b/content/posts/2011-09-12-garagetunes-demo.md @@ -10,4 +10,4 @@ preview_image: "/posts/preview-images/software-development-green.svg" --- I created a new demo for my JGrid talk this week in Münster, Germany. I will release the code, a webstart link & my presentation later this week. -{{< youtube 3aCDywUeTw4 >}} + diff --git a/content/posts/2011-09-14-jgrid-tutorial-1.md b/content/posts/2011-09-14-jgrid-tutorial-1.md index c94c8521..12a5a974 100644 --- a/content/posts/2011-09-14-jgrid-tutorial-1.md +++ b/content/posts/2011-09-14-jgrid-tutorial-1.md @@ -1,41 +1,35 @@ ---- -outdated: true -showInBlog: false -title: 'JGrid Tutorial #1' -date: "2011-09-14" -author: hendrik -categories: [Swing] -excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the first out of five tutorials.' -slug: jgrid-tutorial-1 -preview_image: "/posts/preview-images/software-development-green.svg" ---- -At the moment all JGrid demonstrations are very complex and use a lot of Java2D code, web services an so on. So many people asked me to create some simple demos. For this reason I started some bottom-up tutorials for the JGrid. - -Here is the first one: -You can integrate a JGrid in every swing application. Just add it to a container: - -{{< highlight java >}} -JGrid grid = new JGrid(); -getContentPane().add(new JScrollPane(grid)); -{{< / highlight >}} - -Normally you want to visualize some data in the grid. All data must wrapped in a ListModel: - -{{< highlight java >}} -DefaultListModel model = new DefaultListModel(); -for(int i=0; i < 100; i++) { - model.addElement(new Integer(i)); -} -{{< / highlight >}} - -In a final step you must set the model for the grid: - -{{< highlight java >}} -grid.setModel(model); -{{< / highlight >}} - -With this few lines of code you can add a JGrid to your code. Because the default renderer of the grid uses a label and renders the `toString()`-result of the data to the grid you will see all Integers in a grid: - -![Tutorial1](/posts/guigarage-legacy/Tutorial1.png) - -You can download the [source file for the tutorial](/assets/downloads/jgrid/tutorial1.java). To run the program you need the jgrid.jar in your classpath. +--- +outdated: true +showInBlog: false +title: 'JGrid Tutorial #1' +date: "2011-09-14" +author: hendrik +categories: [Swing] +excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the first out of five tutorials.' +slug: jgrid-tutorial-1 +preview_image: "/posts/preview-images/software-development-green.svg" +--- +At the moment all JGrid demonstrations are very complex and use a lot of Java2D code, web services an so on. So many people asked me to create some simple demos. For this reason I started some bottom-up tutorials for the JGrid. + +Here is the first one: +You can integrate a JGrid in every swing application. Just add it to a container: + +```javaJGrid grid = new JGrid(); +getContentPane().add(new JScrollPane(grid)); ``` + +Normally you want to visualize some data in the grid. All data must wrapped in a ListModel: + +```javaDefaultListModel model = new DefaultListModel(); +for(int i=0; i < 100; i++) { + model.addElement(new Integer(i)); +} ``` + +In a final step you must set the model for the grid: + +```javagrid.setModel(model); ``` + +With this few lines of code you can add a JGrid to your code. Because the default renderer of the grid uses a label and renders the `toString()`-result of the data to the grid you will see all Integers in a grid: + +![Tutorial1](/posts/guigarage-legacy/Tutorial1.png) + +You can download the [source file for the tutorial](/assets/downloads/jgrid/tutorial1.java). To run the program you need the jgrid.jar in your classpath. diff --git a/content/posts/2011-09-16-jgrid-tutorial-2.md b/content/posts/2011-09-16-jgrid-tutorial-2.md index f52526eb..a2c5983e 100644 --- a/content/posts/2011-09-16-jgrid-tutorial-2.md +++ b/content/posts/2011-09-16-jgrid-tutorial-2.md @@ -1,32 +1,30 @@ ---- -outdated: true -showInBlog: false -title: 'JGrid Tutorial #2' -date: "2011-09-16" -author: hendrik -categories: [Swing] -excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the second out of five tutorials.' -slug: jgrid-tutorial-2 -preview_image: "/posts/preview-images/software-development-green.svg" ---- -After we created a simple JGrid (see [tutorial #1]({{< ref "/posts/2011-09-14-jgrid-tutorial-1" >}}) we want to modify the look now. The JGrid has a lot of getter/setter to change the visualization of the grid. Read the JavaDoc for a complete overview of all properties. - -Here is an example of changing colors and dimensions: - -{{< highlight java >}} -grid.setFont(grid.getFont().deriveFont(40.0f)); -grid.setFixedCellDimension(56); -grid.setHorizonztalMargin(4); -grid.setVerticalMargin(4); -grid.setHorizontalAlignment(SwingConstants.LEFT); -grid.setBackground(Color.WHITE); -grid.setSelectionBorderColor(Color.BLUE); -grid.setSelectionBackground(Color.CYAN); -grid.setCellBackground(Color.LIGHT_GRAY); -{{< / highlight >}} - -After setting all properties the grid looks like this: - -![Tutorial2](/posts/guigarage-legacy/Tutorial2.png) - -You can download the sources for the tutorial [here](/assets/downloads/jgrid/tutorial2.java). +--- +outdated: true +showInBlog: false +title: 'JGrid Tutorial #2' +date: "2011-09-16" +author: hendrik +categories: [Swing] +excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the second out of five tutorials.' +slug: jgrid-tutorial-2 +preview_image: "/posts/preview-images/software-development-green.svg" +--- +After we created a simple JGrid (see [tutorial #1](/posts/2011-09-14-jgrid-tutorial-1) we want to modify the look now. The JGrid has a lot of getter/setter to change the visualization of the grid. Read the JavaDoc for a complete overview of all properties. + +Here is an example of changing colors and dimensions: + +```javagrid.setFont(grid.getFont().deriveFont(40.0f)); +grid.setFixedCellDimension(56); +grid.setHorizonztalMargin(4); +grid.setVerticalMargin(4); +grid.setHorizontalAlignment(SwingConstants.LEFT); +grid.setBackground(Color.WHITE); +grid.setSelectionBorderColor(Color.BLUE); +grid.setSelectionBackground(Color.CYAN); +grid.setCellBackground(Color.LIGHT_GRAY); ``` + +After setting all properties the grid looks like this: + +![Tutorial2](/posts/guigarage-legacy/Tutorial2.png) + +You can download the sources for the tutorial [here](/assets/downloads/jgrid/tutorial2.java). diff --git a/content/posts/2011-09-17-jgrid-tutorial-3.md b/content/posts/2011-09-17-jgrid-tutorial-3.md index 09b9c412..49eeefa7 100644 --- a/content/posts/2011-09-17-jgrid-tutorial-3.md +++ b/content/posts/2011-09-17-jgrid-tutorial-3.md @@ -1,58 +1,52 @@ ---- -outdated: true -showInBlog: false -title: 'JGrid Tutorial #3' -date: "2011-09-17" -author: hendrik -categories: [Swing] -excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the third out of five tutorials.' -slug: jgrid-tutorial-3 -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In this tutorial I will show you how to visualize more complex data with renderers. First we have to create a data model. For this tutorial we will work with the `java.awt.Color` class and create a ListModel with some colors in it: - -{{< highlight java >}} -DefaultListModel model = new DefaultListModel(); -Random random = new Random(); -for(int i=0; i <= 100; i++) { -model.addElement(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); -} -grid.setModel(model); -{{< / highlight >}} - -After assigning this model to the JGrid the result will look like this: - -![Tutorial3](/posts/guigarage-legacy/Tutorial3-1.png) - -The JGrid uses a default renderer to visualize data. This renderer based on a JLabel and displays the toString() results from the given data. Therefore you see these "java.awt.Color..." strings in the grid cells. - -To visualize the colors inside the grid we need a new renderer. All renderers for the JGrid must implement the interface GridCellRenderer. Here is the code for a simple renderer for colors: - -{{< highlight java >}} -public class GridColorCellRenderer extends JPanel implements GridCellRenderer { - - private static final long serialVersionUID = 1L; - - @Override - public Component getGridCellRendererComponent(JGrid grid, Object value, int index, boolean isSelected, boolean cellHasFocus) { - if(value != null && value instanceof Color) { - this.setBackground((Color) value); - } - return this; - } -} -{{< / highlight >}} - -Now we have to assign the renderer to the JGrid. Here is a different to the default JList or JTable behavior. The renderer for JGrid are stored in a special handler. You can refer to this handler by `grid.getCellRendererManager()` / `grid.setCellRendererManager()`. By using this handlers you can manage the same renderers for different grids (On a later JGrid release I will add SPI support to the handlers). - -To add the custom renderer to your grid you have to add it to the handler: - -{{< highlight java >}} -grid.getCellRendererManager().setDefaultRenderer(new GridColorCellRenderer()); -{{< / highlight >}} - -Now our application shows the right colors inside the grid cells: - -![Tutorial3](/posts/guigarage-legacy/Tutorial3-2.png) - -You can download the source file for this tutorial [here](/assets/downloads/jgrid/tutorial3.java). +--- +outdated: true +showInBlog: false +title: 'JGrid Tutorial #3' +date: "2011-09-17" +author: hendrik +categories: [Swing] +excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the third out of five tutorials.' +slug: jgrid-tutorial-3 +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In this tutorial I will show you how to visualize more complex data with renderers. First we have to create a data model. For this tutorial we will work with the `java.awt.Color` class and create a ListModel with some colors in it: + +```javaDefaultListModel model = new DefaultListModel(); +Random random = new Random(); +for(int i=0; i <= 100; i++) { +model.addElement(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); +} +grid.setModel(model); ``` + +After assigning this model to the JGrid the result will look like this: + +![Tutorial3](/posts/guigarage-legacy/Tutorial3-1.png) + +The JGrid uses a default renderer to visualize data. This renderer based on a JLabel and displays the toString() results from the given data. Therefore you see these "java.awt.Color..." strings in the grid cells. + +To visualize the colors inside the grid we need a new renderer. All renderers for the JGrid must implement the interface GridCellRenderer. Here is the code for a simple renderer for colors: + +```javapublic class GridColorCellRenderer extends JPanel implements GridCellRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getGridCellRendererComponent(JGrid grid, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if(value != null && value instanceof Color) { + this.setBackground((Color) value); + } + return this; + } +} ``` + +Now we have to assign the renderer to the JGrid. Here is a different to the default JList or JTable behavior. The renderer for JGrid are stored in a special handler. You can refer to this handler by `grid.getCellRendererManager()` / `grid.setCellRendererManager()`. By using this handlers you can manage the same renderers for different grids (On a later JGrid release I will add SPI support to the handlers). + +To add the custom renderer to your grid you have to add it to the handler: + +```javagrid.getCellRendererManager().setDefaultRenderer(new GridColorCellRenderer()); ``` + +Now our application shows the right colors inside the grid cells: + +![Tutorial3](/posts/guigarage-legacy/Tutorial3-2.png) + +You can download the source file for this tutorial [here](/assets/downloads/jgrid/tutorial3.java). diff --git a/content/posts/2011-09-18-jgrid-tutorial-4.md b/content/posts/2011-09-18-jgrid-tutorial-4.md index 9b4979fc..cd2f6980 100644 --- a/content/posts/2011-09-18-jgrid-tutorial-4.md +++ b/content/posts/2011-09-18-jgrid-tutorial-4.md @@ -1,37 +1,35 @@ ---- -outdated: true -showInBlog: false -title: 'JGrid Tutorial #4' -date: "2011-09-18" -author: hendrik -categories: [Swing] -excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the fourth out of five tutorials.' -slug: jgrid-tutorial-4 -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In this tutorial we want to add zoom functionality to the JGrid. You can set the dimension of the grid cells be the property `fixedCellDimension`. Here is a example for two different dimensions: - -![tutorial4-1](/posts/guigarage-legacy/tutorial4-1.png) - -![tutorial4-2](/posts/guigarage-legacy/tutorial4-2.png) - -To add a zoom functionality to the grid you can set the dimension by using a JSlider. Here is the code: - -{{< highlight java >}} -final JSlider slider = new JSlider(32, 256); -slider.setValue(grid.getFixedCellDimension()); - -slider.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent arg0) { - grid.setFixedCellDimension(slider.getValue()); - } -}); -{{< / highlight >}} - -Now you can edit the dimension dynamically. Here is the result: - -{{< youtube Zyqf-P2ftFs >}} - - -You can download the source file [here](/assets/downloads/jgrid/tutorial4.java). +--- +outdated: true +showInBlog: false +title: 'JGrid Tutorial #4' +date: "2011-09-18" +author: hendrik +categories: [Swing] +excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the fourth out of five tutorials.' +slug: jgrid-tutorial-4 +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In this tutorial we want to add zoom functionality to the JGrid. You can set the dimension of the grid cells be the property `fixedCellDimension`. Here is a example for two different dimensions: + +![tutorial4-1](/posts/guigarage-legacy/tutorial4-1.png) + +![tutorial4-2](/posts/guigarage-legacy/tutorial4-2.png) + +To add a zoom functionality to the grid you can set the dimension by using a JSlider. Here is the code: + +```javafinal JSlider slider = new JSlider(32, 256); +slider.setValue(grid.getFixedCellDimension()); + +slider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent arg0) { + grid.setFixedCellDimension(slider.getValue()); + } +}); ``` + +Now you can edit the dimension dynamically. Here is the result: + + + + +You can download the source file [here](/assets/downloads/jgrid/tutorial4.java). diff --git a/content/posts/2011-09-25-jgrid-tutorial-5.md b/content/posts/2011-09-25-jgrid-tutorial-5.md index a4f992ca..f37e9926 100644 --- a/content/posts/2011-09-25-jgrid-tutorial-5.md +++ b/content/posts/2011-09-25-jgrid-tutorial-5.md @@ -1,78 +1,72 @@ ---- -outdated: true -showInBlog: false -title: 'JGrid Tutorial #5' -date: "2011-09-25" -author: hendrik -categories: [Swing] -excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the fifth out of five tutorials.' -slug: jgrid-tutorial-5 -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In this tutorial we want to take a deeper look at cell rendering. In the last tutorials we already implemented GridCellRenderer and set them as default renderer to the JGrid. This is exactly the same behavior as renderer in a JList. But if you have different data types in a grid only one renderer won´t fulfill the requirements. For this purpose you can add different GridCellRenderer to the JGrid. Like in a JTable you can add renderers for different data classes to the JGrid. - -Let´s say we have colors and percentages in our model: - -{{< highlight java >}} -DefaultListModel model = new DefaultListModel(); -Random random = new Random(); - -for(int i=0; i if(random.nextBoolean()) { - model.addElement(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); -} else { - model.addElement(new Float(random.nextFloat())); -} - -grid.setModel(model); -{{< / highlight >}} - -To visualize the color values we can use the renderer from the previous tutorial. For the new percentage values we need a different renderer: - -{{< highlight java >}} -public class GridPercentCellRenderer extends JLabel implements GridCellRenderer { - - private static final long serialVersionUID = 1L; - - private float f = 0.0f; - - public GridPercentCellRenderer() { - setHorizontalAlignment(SwingConstants.CENTER); - setBackground(Color.white); - setForeground(Color.black); - } - - @Override - public Component getGridCellRendererComponent(JGrid grid, Object value, int index, boolean isSelected, boolean cellHasFocus) { - if(value != null && value instanceof Float) { - this.f = ((Float) value).floatValue(); - setText(NumberFormat.getPercentInstance().format(f)); - } - return this; - } - - @Override - protected void paintComponent(Graphics g) { - g.setColor(getBackground()); - g.fillRect(0, 0, getWidth(), getHeight()); - g.setColor(Color.LIGHT_GRAY); - int height = (int)((float)getHeight() * f); - g.fillRect(0, getHeight() - height, getWidth(), height); - super.paintComponent(g); - } -} -{{< / highlight >}} - -Until now all tutorials used the `setDefaultRenderer(...)` methode to set a special design to the grid. Just now we have a problem using this practice: we need renderers for different data types. In the JGrid this is as simple as in the JTable. - -Here we go: - -{{< highlight java >}} -grid.getCellRendererManager().addRendererMapping(Color.class, new GridColorCellRenderer()); -grid.getCellRendererManager().addRendererMapping(Float.class, new GridPercentCellRenderer()); -{{< / highlight >}} - -Here you can see the effect: - -![Tutorial-5](/posts/guigarage-legacy/Tutorial-5.png) - -You can download the source file [here](/assets/downloads/jgrid/tutorial5.java). +--- +outdated: true +showInBlog: false +title: 'JGrid Tutorial #5' +date: "2011-09-25" +author: hendrik +categories: [Swing] +excerpt: 'I created a series of tutorials to get familiar with JGrid. This is the fifth out of five tutorials.' +slug: jgrid-tutorial-5 +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In this tutorial we want to take a deeper look at cell rendering. In the last tutorials we already implemented GridCellRenderer and set them as default renderer to the JGrid. This is exactly the same behavior as renderer in a JList. But if you have different data types in a grid only one renderer won´t fulfill the requirements. For this purpose you can add different GridCellRenderer to the JGrid. Like in a JTable you can add renderers for different data classes to the JGrid. + +Let´s say we have colors and percentages in our model: + +```javaDefaultListModel model = new DefaultListModel(); +Random random = new Random(); + +for(int i=0; i if(random.nextBoolean()) { + model.addElement(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256))); +} else { + model.addElement(new Float(random.nextFloat())); +} + +grid.setModel(model); ``` + +To visualize the color values we can use the renderer from the previous tutorial. For the new percentage values we need a different renderer: + +```javapublic class GridPercentCellRenderer extends JLabel implements GridCellRenderer { + + private static final long serialVersionUID = 1L; + + private float f = 0.0f; + + public GridPercentCellRenderer() { + setHorizontalAlignment(SwingConstants.CENTER); + setBackground(Color.white); + setForeground(Color.black); + } + + @Override + public Component getGridCellRendererComponent(JGrid grid, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if(value != null && value instanceof Float) { + this.f = ((Float) value).floatValue(); + setText(NumberFormat.getPercentInstance().format(f)); + } + return this; + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(getBackground()); + g.fillRect(0, 0, getWidth(), getHeight()); + g.setColor(Color.LIGHT_GRAY); + int height = (int)((float)getHeight() * f); + g.fillRect(0, getHeight() - height, getWidth(), height); + super.paintComponent(g); + } +} ``` + +Until now all tutorials used the `setDefaultRenderer(...)` methode to set a special design to the grid. Just now we have a problem using this practice: we need renderers for different data types. In the JGrid this is as simple as in the JTable. + +Here we go: + +```javagrid.getCellRendererManager().addRendererMapping(Color.class, new GridColorCellRenderer()); +grid.getCellRendererManager().addRendererMapping(Float.class, new GridPercentCellRenderer()); ``` + +Here you can see the effect: + +![Tutorial-5](/posts/guigarage-legacy/Tutorial-5.png) + +You can download the source file [here](/assets/downloads/jgrid/tutorial5.java). diff --git a/content/posts/2012-10-13-building-javafx-applications-with-maven.md b/content/posts/2012-10-13-building-javafx-applications-with-maven.md index 6ca01899..0a950e6e 100644 --- a/content/posts/2012-10-13-building-javafx-applications-with-maven.md +++ b/content/posts/2012-10-13-building-javafx-applications-with-maven.md @@ -1,27 +1,25 @@ ---- -outdated: true -showInBlog: false -title: 'Building JavaFX Applications with Maven' -date: "2012-10-13" -author: hendrik -categories: [JavaFX] -excerpt: 'With the preview of JavaFX 2 that is part of Java 6 and 7 you can already build custom JavaFX applications by using Maven' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -There are already a lot of posts out there that describe a workflow for integrating JavaFX in Maven. Here is one example by Adam Bien: [http://www.adam-bien.com/roller/abien/entry/how_to_compile_java_fx](http://www.adam-bien.com/roller/abien/entry/how_to_compile_java_fx) - -For all this solutions you have to specify your system-specific path to the JavaFX installation. You can set the property inside your Maven pom. This is ok for a single user environment but counterproductive when developing in a team. Another way is to set the property on OS Level. But then every developer have to do this on every workstation. - -Since Java 7 update 6 JavaFX 2.2 is bundled  in the JRE. You can find JavaFX at `JAVAHOME/lib/jfxrt.jar`. Since Maven 3 you can access the `JAVAHOME` path with the `${java.home}` property. With this informations you are able to create a system independent pom-file: - -{{< highlight xml >}} - - com.oracle - javafx - 2.2 - ${java.home}/lib/jfxrt.jar - system - -{{< / highlight >}} - -The solution is working since __Java 7u6__. If you have different JDKs / JREs installed on your system you have to start Maven with the right JDK. The pom also works in Eclipse with m2eclipse-plugin. Here you must ensure that Eclipse is started with Java 7u6+. +--- +outdated: true +showInBlog: false +title: 'Building JavaFX Applications with Maven' +date: "2012-10-13" +author: hendrik +categories: [JavaFX] +excerpt: 'With the preview of JavaFX 2 that is part of Java 6 and 7 you can already build custom JavaFX applications by using Maven' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +There are already a lot of posts out there that describe a workflow for integrating JavaFX in Maven. Here is one example by Adam Bien: [http://www.adam-bien.com/roller/abien/entry/how_to_compile_java_fx](http://www.adam-bien.com/roller/abien/entry/how_to_compile_java_fx) + +For all this solutions you have to specify your system-specific path to the JavaFX installation. You can set the property inside your Maven pom. This is ok for a single user environment but counterproductive when developing in a team. Another way is to set the property on OS Level. But then every developer have to do this on every workstation. + +Since Java 7 update 6 JavaFX 2.2 is bundled  in the JRE. You can find JavaFX at `JAVAHOME/lib/jfxrt.jar`. Since Maven 3 you can access the `JAVAHOME` path with the `${java.home}` property. With this informations you are able to create a system independent pom-file: + +```xml + com.oracle + javafx + 2.2 + ${java.home}/lib/jfxrt.jar + system + ``` + +The solution is working since __Java 7u6__. If you have different JDKs / JREs installed on your system you have to start Maven with the right JDK. The pom also works in Eclipse with m2eclipse-plugin. Here you must ensure that Eclipse is started with Java 7u6+. diff --git a/content/posts/2012-11-01-introducing-vagrant-binding.md b/content/posts/2012-11-01-introducing-vagrant-binding.md index 0c1732b2..82dae11f 100644 --- a/content/posts/2012-11-01-introducing-vagrant-binding.md +++ b/content/posts/2012-11-01-introducing-vagrant-binding.md @@ -1,212 +1,192 @@ ---- -outdated: true -showInBlog: false -title: 'Introducing Vagrant-Binding' -date: "2012-11-01" -author: hendrik -categories: [General, Vagrant-Binding] -excerpt: ' With the Vagrant-Binding you can put the automation in Java on the next level. The library is a wrapper around Vagrant that let you easily manage virtual machines from Java code at runtime.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Today I want to introduce the [Vagrant-Binding Java API](https://github.com/guigarage/vagrant-binding). The API is build on top of the OpenSource Tool [Vagrant](http://vagrantup.com). Vagrant is a ruby based tool for virtual machine automation. With Vagrant you can set up complex virtual systems in only a few minutes. Every vm that is created with Vagrant based on a config-file that describes the configuration of the virtual machine and all installed software on it. You can configure a Ubuntu 64 bit vm with a running MySql server for example. With the Java API "Vagrant-Binding" you can put the automation on the next level. - -Vagrant-Binding is OpenSource and available at [github](https://github.com/guigarage/vagrant-binding). It offers some nice builder APIs so that you can programmatically set up your vm. In addition to this Vagrant-Bindings gives you the opportunity to sync the lifecycle of your virtual machines with your JUnit Unit Tests. All you need is a [VirtualBox](https://www.virtualbox.org) installation on your system. - -## Getting started - -The simplest way to start building your virtual machines in Java is the Builder-API. With a few Builder-Classes you can set up a complete environment of different virtual machines at runtime. To run all the example you need a VirtualBox installation on your system. I suggest you to use version 4.1.16 because this is the version I am currently using. I Don't know if every version is working fine with Vagrant. The actual "Vagrant-Binding" snapshot bundles Vagrant version 1.0.5. So you don't need a additional Vagrant installation on your system. - -Here is a short example that creates an Ubuntu 32bit vm: - -{{< highlight java >}} -VagrantVmConfig vmConfig = VagrantVmConfigBuilder -.create() -.withLucid32Box() -.withName("myVM") -.build(); - -VagrantEnvironmentConfig environmentConfig = VagrantEnvironmentConfigBuilder -.create() -.withVagrantVmConfig(vmConfig) -.build(); - -Vagrant vagrant = new Vagrant(true); - -VagrantEnvironment environment = vagrant -.createEnvironment(new File("my/locale/path"), environmentConfig); - -environment.up(); -{{< / highlight >}} - -You can configure your vm by using a static ip and some port forwarding for example: - -{{< highlight java >}} -VagrantPortForwarding portForwarding = new VagrantPortForwarding("custom", 7777, 1399); - -VagrantVmConfig vmConfig = VagrantVmConfigBuilder -.create() -.withLucid32Box() -.withName("myVM"). -.withHostOnlyIp("192.168.50.4") -.withVagrantPortForwarding(portForwarding).build(); -{{< / highlight >}} - -The code creates a VmConfig with some special features. This VmConfig is put into a VagrantEnvironment. One Environment can capsulate as many virtual machines as you want. You can start the whole environment by calling `environment.up()`. This creates and starts up every virtual machine that is defined in the environment within a fe minutes. If you want you can access every machine and start or stop it manually: - -{{< highlight java >}} -for(VagrantVm vm : environment.getAllVms()) { - vm.destroy(); -} -{{< / highlight >}} - -Each vm has a lifecycle. You can change the state of the lifecycle easily: - -{{< highlight java >}} -vm.start(); -vm.suspend(); -vm.resume(); -vm.halt(); -vm.destroy(); -{{< / highlight >}} - -To configure the software, which will be installed on the virtual machine you need a Puppet configuration script. [Puppet](http://puppetlabs.com) is a tool that automates the installation and administration of software. Each virtual machine that is created by Vagrant runs Puppet by default. So you only need a configuration script. Here is a simple example that edits the welcome message of a virtual machine: - -{{< highlight java >}} -group { "puppet": -ensure => "present", -} - -File { owner => 0, group => 0, mode => 0644 } - -file { '/etc/motd': -content => "Welcome to your Vagrant-built virtual machine! -Managed by Puppet.\n" -} -{{< / highlight >}} - -You can easily use your puppet scripts with Vagrant-Binding: - -{{< highlight java >}} -PuppetProvisionerConfig puppetConfig = PuppetProvisionerConfigBuilder -.create() -.withManifestPath("path/to/puppetscript") -.withManifestFile("config.pp") -.build(); - -VagrantVmConfig vmConfig = VagrantVmConfigBuilder -.create() -.withLucid32Box() -.withPuppetProvisionerConfig(puppetConfig) -.build(); -{{< / highlight >}} - -After starting your virtual machine you can use SHH to connect on the machine. "Vagrant-Binding" provides a class for file upload and process execution over ssh. You can start your jobs on the virtual machine by simple using a code like this: - -{{< highlight java >}} -VagrantSSHConnection connection = vm.createConnection(); -connection.execute("touch /path/to/any/file", true); -{{< / highlight >}} - -## Using Vagrant-Binding for real sandbox testing - -Vagrant-Binding offers a special [@Rule for JUnit](http://www.junit.org/node/580). By using this Rule you can capsule each of your tests with a fully vm lifecycle. Let us assume we have the following Unit Test: - -{{< highlight java >}} -@Test -public void testJdbc() { - System.out.println("Test starts"); - try { - Class.forName("com.mysql.jdbc.Driver"); - Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.50.4/testapp?user=dbuser&password=dbuser"); - - Statement statement = connection.createStatement(); - String table = "CREATE TABLE mytable (data_entry VARCHAR(254))"; - statement.executeUpdate(table); - statement.close(); - - for(int i=0; i < 100; i++) { - statement = connection.createStatement(); - statement.executeUpdate("INSERT INTO mytable VALUES(\"" + UUID.randomUUID().toString() + "\")"); - statement.close(); - } - - statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM mytable"); - resultSet.next(); - Assert.assertEquals(100, resultSet.getInt(1)); - statement.close(); - connection.close(); - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(); - } -} -{{< / highlight >}} - -The test creates the table "mytable", adds 100 rows into it and checks the rowcount. To run this test successfully you need a machine with the hard ip "192.168.50.4" and a MySQL Server. You need a database "testapp" on the server too. On this database there must not be a table called "mytable". So you can only run this Unit Test one time because it doesn't drop the table "mytable" at the end. You can expand the test and drop the table at the end but what will happen in case of an error? Every time the test starts automatically you do not know the state of the server, database and table. Another problem is that you can't run the test parallel. - -With Vagrant-Binding you can create your database server as a vm on the fly. All you need to do is adding a `VagrantTestRule` to your test class: - -{{< highlight java >}} -public VagrantTestRule testRule = new VagrantTestRule(createConfig()); -{{< / highlight >}} - -The Rule needs a configuration that is easily created: - -{{< highlight java >}} -public static VagrantConfiguration createConfig() { - PuppetProvisionerConfig puppetConfig = PuppetProvisionerConfigBuilder - .create() - .withManifestPath("/path/to/manifests") - .withManifestFile("dbserver.pp") - .withDebug(true) - .build(); - - VagrantVmConfig vmConfig = VagrantVmConfigBuilder - .create() - .withName("mysqlvm") - .withHostOnlyIp("192.168.50.4") - .withBoxName("lucid32") - .withPuppetProvisionerConfig(puppetConfig) - .build(); - - VagrantEnvironmentConfig environmentConfig = VagrantEnvironmentConfigBuilder - .create() - .withVagrantVmConfig(vmConfig) - .build(); - - VagrantFileTemplateConfiguration fileTemplateConfiguration = VagrantFileTemplateConfigurationBuilder - .create() - .withLocalFile(new File("/path/to/my.cnf")) - .withPathInVagrantFolder("files/my.cnf") - .build(); - - VagrantConfiguration configuration = VagrantConfigurationBuilder - .create() - .withVagrantEnvironmentConfig(environmentConfig) - .withVagrantFileTemplateConfiguration(fileTemplateConfiguration) - .build(); - - return configuration; -} -{{< / highlight >}} - -The "VagrantTestRule" syncs every single test with the livecycle of the Vagrant environment. Each vm that is defined in the environments starts before the UnitTests runs and stops after the test: - -![unitTest](/posts/guigarage-legacy/unitTest.png) - -You can access the environment and execute commands on the virtual machines by SSH for example while the test is running by calling "testRule.getEnvironment()". - -I'm using a Puppet script that I found on [github](https://github.com/moolsan/vagrant-puppet-demo) to configure the database server vm with Puppet. - -I will host a project with some demos and tutorials within the next days on github. You need to download and install the Vagrant-Binding project as maven dependency because it is not available at Maven Central Repository at the moment. - -## Current state of the project and some limitations - -The API is in a very early state. I haven't even tried everything until today and there are only a few Unit Tests. There is currently no existing javadoc and only some simple examples. So please do not use this API in production - at the moment :) - -There is a [jruby bug](https://github.com/jarib/childprocess/issues/26) on windows 64 bit so that Vagrant-Binding is actually not working there. - -## Further reading - -Vagrant has a very good [quickstart](http://vagrantup.com/v1/docs/getting-started/index.html) and [documentation](http://vagrantup.com/v1/docs/index.html). All of the features I implemented until today are strongly adopted from Vagrant. There are are lot of [good](http://www.javacodegeeks.com/2012/08/introduction-to-puppet-for-vagrant-users.html) [Puppet](https://github.com/moolsan/vagrant-puppet-demo) [examples](http://www.tomcatexpert.com/blog/2010/04/29/deploying-tomcat-applications-puppet) [out](http://www.javacodegeeks.com/2012/06/serving-files-with-puppet-standalone-in.html) [there](http://blog.codecentric.de/en/2012/02/automated-virtual-test-environments-with-vagrant-and-puppet/). I read ["Pulling Strings With Puppet"](http://www.amazon.de/Pulling-Strings-With-Puppet-Configuration/dp/1590599780/ref=sr_1_7?ie=UTF8&qid=1351633216&sr=8-7) that helped me a lot. +--- +outdated: true +showInBlog: false +title: 'Introducing Vagrant-Binding' +date: "2012-11-01" +author: hendrik +categories: [General, Vagrant-Binding] +excerpt: ' With the Vagrant-Binding you can put the automation in Java on the next level. The library is a wrapper around Vagrant that let you easily manage virtual machines from Java code at runtime.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Today I want to introduce the [Vagrant-Binding Java API](https://github.com/guigarage/vagrant-binding). The API is build on top of the OpenSource Tool [Vagrant](http://vagrantup.com). Vagrant is a ruby based tool for virtual machine automation. With Vagrant you can set up complex virtual systems in only a few minutes. Every vm that is created with Vagrant based on a config-file that describes the configuration of the virtual machine and all installed software on it. You can configure a Ubuntu 64 bit vm with a running MySql server for example. With the Java API "Vagrant-Binding" you can put the automation on the next level. + +Vagrant-Binding is OpenSource and available at [github](https://github.com/guigarage/vagrant-binding). It offers some nice builder APIs so that you can programmatically set up your vm. In addition to this Vagrant-Bindings gives you the opportunity to sync the lifecycle of your virtual machines with your JUnit Unit Tests. All you need is a [VirtualBox](https://www.virtualbox.org) installation on your system. + +## Getting started + +The simplest way to start building your virtual machines in Java is the Builder-API. With a few Builder-Classes you can set up a complete environment of different virtual machines at runtime. To run all the example you need a VirtualBox installation on your system. I suggest you to use version 4.1.16 because this is the version I am currently using. I Don't know if every version is working fine with Vagrant. The actual "Vagrant-Binding" snapshot bundles Vagrant version 1.0.5. So you don't need a additional Vagrant installation on your system. + +Here is a short example that creates an Ubuntu 32bit vm: + +```javaVagrantVmConfig vmConfig = VagrantVmConfigBuilder +.create() +.withLucid32Box() +.withName("myVM") +.build(); + +VagrantEnvironmentConfig environmentConfig = VagrantEnvironmentConfigBuilder +.create() +.withVagrantVmConfig(vmConfig) +.build(); + +Vagrant vagrant = new Vagrant(true); + +VagrantEnvironment environment = vagrant +.createEnvironment(new File("my/locale/path"), environmentConfig); + +environment.up(); ``` + +You can configure your vm by using a static ip and some port forwarding for example: + +```javaVagrantPortForwarding portForwarding = new VagrantPortForwarding("custom", 7777, 1399); + +VagrantVmConfig vmConfig = VagrantVmConfigBuilder +.create() +.withLucid32Box() +.withName("myVM"). +.withHostOnlyIp("192.168.50.4") +.withVagrantPortForwarding(portForwarding).build(); ``` + +The code creates a VmConfig with some special features. This VmConfig is put into a VagrantEnvironment. One Environment can capsulate as many virtual machines as you want. You can start the whole environment by calling `environment.up()`. This creates and starts up every virtual machine that is defined in the environment within a fe minutes. If you want you can access every machine and start or stop it manually: + +```javafor(VagrantVm vm : environment.getAllVms()) { + vm.destroy(); +} ``` + +Each vm has a lifecycle. You can change the state of the lifecycle easily: + +```javavm.start(); +vm.suspend(); +vm.resume(); +vm.halt(); +vm.destroy(); ``` + +To configure the software, which will be installed on the virtual machine you need a Puppet configuration script. [Puppet](http://puppetlabs.com) is a tool that automates the installation and administration of software. Each virtual machine that is created by Vagrant runs Puppet by default. So you only need a configuration script. Here is a simple example that edits the welcome message of a virtual machine: + +```javagroup { "puppet": +ensure => "present", +} + +File { owner => 0, group => 0, mode => 0644 } + +file { '/etc/motd': +content => "Welcome to your Vagrant-built virtual machine! +Managed by Puppet.\n" +} ``` + +You can easily use your puppet scripts with Vagrant-Binding: + +```javaPuppetProvisionerConfig puppetConfig = PuppetProvisionerConfigBuilder +.create() +.withManifestPath("path/to/puppetscript") +.withManifestFile("config.pp") +.build(); + +VagrantVmConfig vmConfig = VagrantVmConfigBuilder +.create() +.withLucid32Box() +.withPuppetProvisionerConfig(puppetConfig) +.build(); ``` + +After starting your virtual machine you can use SHH to connect on the machine. "Vagrant-Binding" provides a class for file upload and process execution over ssh. You can start your jobs on the virtual machine by simple using a code like this: + +```javaVagrantSSHConnection connection = vm.createConnection(); +connection.execute("touch /path/to/any/file", true); ``` + +## Using Vagrant-Binding for real sandbox testing + +Vagrant-Binding offers a special [@Rule for JUnit](http://www.junit.org/node/580). By using this Rule you can capsule each of your tests with a fully vm lifecycle. Let us assume we have the following Unit Test: + +```java@Test +public void testJdbc() { + System.out.println("Test starts"); + try { + Class.forName("com.mysql.jdbc.Driver"); + Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.50.4/testapp?user=dbuser&password=dbuser"); + + Statement statement = connection.createStatement(); + String table = "CREATE TABLE mytable (data_entry VARCHAR(254))"; + statement.executeUpdate(table); + statement.close(); + + for(int i=0; i < 100; i++) { + statement = connection.createStatement(); + statement.executeUpdate("INSERT INTO mytable VALUES(\"" + UUID.randomUUID().toString() + "\")"); + statement.close(); + } + + statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM mytable"); + resultSet.next(); + Assert.assertEquals(100, resultSet.getInt(1)); + statement.close(); + connection.close(); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail(); + } +} ``` + +The test creates the table "mytable", adds 100 rows into it and checks the rowcount. To run this test successfully you need a machine with the hard ip "192.168.50.4" and a MySQL Server. You need a database "testapp" on the server too. On this database there must not be a table called "mytable". So you can only run this Unit Test one time because it doesn't drop the table "mytable" at the end. You can expand the test and drop the table at the end but what will happen in case of an error? Every time the test starts automatically you do not know the state of the server, database and table. Another problem is that you can't run the test parallel. + +With Vagrant-Binding you can create your database server as a vm on the fly. All you need to do is adding a `VagrantTestRule` to your test class: + +```javapublic VagrantTestRule testRule = new VagrantTestRule(createConfig()); ``` + +The Rule needs a configuration that is easily created: + +```javapublic static VagrantConfiguration createConfig() { + PuppetProvisionerConfig puppetConfig = PuppetProvisionerConfigBuilder + .create() + .withManifestPath("/path/to/manifests") + .withManifestFile("dbserver.pp") + .withDebug(true) + .build(); + + VagrantVmConfig vmConfig = VagrantVmConfigBuilder + .create() + .withName("mysqlvm") + .withHostOnlyIp("192.168.50.4") + .withBoxName("lucid32") + .withPuppetProvisionerConfig(puppetConfig) + .build(); + + VagrantEnvironmentConfig environmentConfig = VagrantEnvironmentConfigBuilder + .create() + .withVagrantVmConfig(vmConfig) + .build(); + + VagrantFileTemplateConfiguration fileTemplateConfiguration = VagrantFileTemplateConfigurationBuilder + .create() + .withLocalFile(new File("/path/to/my.cnf")) + .withPathInVagrantFolder("files/my.cnf") + .build(); + + VagrantConfiguration configuration = VagrantConfigurationBuilder + .create() + .withVagrantEnvironmentConfig(environmentConfig) + .withVagrantFileTemplateConfiguration(fileTemplateConfiguration) + .build(); + + return configuration; +} ``` + +The "VagrantTestRule" syncs every single test with the livecycle of the Vagrant environment. Each vm that is defined in the environments starts before the UnitTests runs and stops after the test: + +![unitTest](/posts/guigarage-legacy/unitTest.png) + +You can access the environment and execute commands on the virtual machines by SSH for example while the test is running by calling "testRule.getEnvironment()". + +I'm using a Puppet script that I found on [github](https://github.com/moolsan/vagrant-puppet-demo) to configure the database server vm with Puppet. + +I will host a project with some demos and tutorials within the next days on github. You need to download and install the Vagrant-Binding project as maven dependency because it is not available at Maven Central Repository at the moment. + +## Current state of the project and some limitations + +The API is in a very early state. I haven't even tried everything until today and there are only a few Unit Tests. There is currently no existing javadoc and only some simple examples. So please do not use this API in production - at the moment :) + +There is a [jruby bug](https://github.com/jarib/childprocess/issues/26) on windows 64 bit so that Vagrant-Binding is actually not working there. + +## Further reading + +Vagrant has a very good [quickstart](http://vagrantup.com/v1/docs/getting-started/index.html) and [documentation](http://vagrantup.com/v1/docs/index.html). All of the features I implemented until today are strongly adopted from Vagrant. There are are lot of [good](http://www.javacodegeeks.com/2012/08/introduction-to-puppet-for-vagrant-users.html) [Puppet](https://github.com/moolsan/vagrant-puppet-demo) [examples](http://www.tomcatexpert.com/blog/2010/04/29/deploying-tomcat-applications-puppet) [out](http://www.javacodegeeks.com/2012/06/serving-files-with-puppet-standalone-in.html) [there](http://blog.codecentric.de/en/2012/02/automated-virtual-test-environments-with-vagrant-and-puppet/). I read ["Pulling Strings With Puppet"](http://www.amazon.de/Pulling-Strings-With-Puppet-Configuration/dp/1590599780/ref=sr_1_7?ie=UTF8&qid=1351633216&sr=8-7) that helped me a lot. diff --git a/content/posts/2012-11-13-jgridfx-first-demo.md b/content/posts/2012-11-13-jgridfx-first-demo.md index 8727afbd..a75f5cc4 100644 --- a/content/posts/2012-11-13-jgridfx-first-demo.md +++ b/content/posts/2012-11-13-jgridfx-first-demo.md @@ -8,8 +8,8 @@ categories: [JavaFX] excerpt: 'This is the first demo of GridFX - the successor of JGrid.' preview_image: "/posts/preview-images/software-development-green.svg" --- -Greetings from[Devoxx](http://www.devoxx.com). I'm currently porting the [JGrid]({{< ref "/posts/2011-07-15-jgrid" >}}) project to JavaFX. Here is a first preview: +Greetings from[Devoxx](http://www.devoxx.com). I'm currently porting the [JGrid](/posts/2011-07-15-jgrid) project to JavaFX. Here is a first preview: -{{< vimeo 53374280 >}} + I will release the first sources later this week. diff --git a/content/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices.md b/content/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices.md index fce4cd09..2f2e296c 100644 --- a/content/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices.md +++ b/content/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices.md @@ -10,5 +10,5 @@ preview_image: "/posts/preview-images/software-development-green.svg" --- I started a second demo for the [GridFX]({{ site.baseurl }}{% link pages/projects/gridfx.md %}) component. The demo let you search for movie trailers and watch them. Therefore I used the [iTunes REST API](http://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) and a JavaFX [MediaView](http://docs.oracle.com/javafx/2/media/simpleplayer.htm) in combination with the GridFX. Here is a short movie: -{{< vimeo 53462905 >}} + diff --git a/content/posts/2012-11-17-custom-ui-controls-with-javafx-part-1.md b/content/posts/2012-11-17-custom-ui-controls-with-javafx-part-1.md index 4a116046..8181659d 100644 --- a/content/posts/2012-11-17-custom-ui-controls-with-javafx-part-1.md +++ b/content/posts/2012-11-17-custom-ui-controls-with-javafx-part-1.md @@ -1,147 +1,131 @@ ---- -outdated: true -showInBlog: false -title: 'Custom UI Controls with JavaFX - Part 1' -date: "2012-11-17" -author: hendrik -categories: [JavaFX] -excerpt: 'One thing I often done is Swing was customization of components and the creation of new components types. With the last release of JavaFX you can easily create custom controls with this new UI toolkit, too. This post gives a first overview about the JavaFX APIs to create custom controls.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -One thing I often done is Swing was customization of components and the creation of new components types. One example for this is the [JGrid]({{< ref "/posts/2011-07-15-jgrid" >}}). Since JavaFX was out I wanted to [port the JGrid to it]({{< ref "/posts/2012-11-14-gridfx-is-hosted-at-github" >}}). After some experiments and bad prototyps I think I found the right way to do it. The talks from [Gerrit Grunwald](http://harmoniccode.blogspot.de) and [Jonathan Giles](http://jonathangiles.net/blog/) at [JavaOne](http://www.oracle.com/javaone/index.html) helped me really a lot to do so. The records of this talks is online ([link](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=2425&tclass=popup), [link](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=4726&tclass=popup)) so I would advise everybody who is interest in this topic to spend some time and watch them. - -## Getting started - -Every UI component in JavaFX is composed by a __control__, a __skin__ and a __behavior__. In an ideal case there is a css part to. - -![custom-components1](/posts/guigarage-legacy/custom-components1.png) - -Best way to start is by creating a new control class that extends `javafx.scene.control.Control`. This class is basically comparable to `JComponent`. It should hold the properties of the component and acts as the main class for it because instances of this class will later created in your application code and added to the UI tree. - -{{< highlight java >}} -MyCustomControl myControl = new MyCustomControl(); -panel.getChildren().add(myControl); -{{< / highlight >}} - -When programming swing components the right way you put everything that depends on the visualization or user interaction into a UI class (see `LabelUI` for example). JavaFX goes one step further and provides the skin class for all visualization and layout related code and the behavior class for all user interaction. - -![custom-components2](/posts/guigarage-legacy/custom-components2.png) - -To do so in JavaFX you need to know how the classes depends on each other. Here is a short chart that shows the relations between them: - -![custom-componens3](/posts/guigarage-legacy/custom-componens3.png) - -## Creating the Behavior - -If your component only visualizes data and has no interaction it 's quite simple to create a behavior. Therefore you only need to extend the `com.sun.javafx.scene.control.behavior.BehaviorBase`. - -{{< highlight java >}} -public class MyCustomControlBehavior extends BehaviorBase { - - public MyCustomControlBehavior(MyCustomControl control) { - super(control); - } -} -{{< / highlight >}} - -Some of you may be confused when seeing the package of BehaviorBase. At the moment this is a __private API__ and normally you should not use this classes in your code but the guys at Oracle know about this problem and will provide the __BehaviorBase as a public API with JavaFX 8__. So best practice is to use the private class now and refactor the import once Java 8 is out. - -## Creating the Skin - -After the behavior class is created we can take a look at the skin. Your skin class will mostly extend `com.sun.javafx.scene.control.skin.BaseSkin` and create a new behavior for your control. Your code normally should look like this: - -{{< highlight java >}} -public class MyCustomControlSkin extends SkinBase{ - - public MyCustomControlSkin(MyCustomControl control) { - super(control, new MyCustomControlBehavior(control)); - } -} -{{< / highlight >}} - -As you can see the BaseSkin is a private API as well. It will also changed to public with Java 8. - -## Creating the Control - -The last class we will need is the control. First we create a simple empty class: - -{{< highlight java >}} -public class MyCustomControl extends Control { - - public MyCustomControl() { - } -} -{{< / highlight >}} - -At this point we have a leak in the dependencies of our three classes. The skin knows about the behavior and control. Here everything looks right. However in application code you will simply create a new control and use it as I showed earlier. The problem is that the control class do not know anything about the skin or behavior. This was one of the biggest pitfalls I was confronted with while learning JavaFX. - -## Putting it together - -What first looks as a great problem is part of the potency JavaFX provides. With JavaFX it should be very easy to create different visualisation (skins) for controls. For this part you can customize the look of components by css. Because the skin is the main part of this look it has to defined by css, too. So instead of creating a skin object for the control by your own you only define the skin class that should be used for your control. The instanciation and everything else is automatically done by the JavaFX APIs. To do so you have to bind your control to a css class. - -Firts off all you have to create a new css file in your project. I think best practice is to use the same package as the controls has and but a css file under src/main/resource: - -![custom-components4](/posts/guigarage-legacy/custom-components4.png) - -Inside the css you have to specify a new selector for your component and add the skin as a property to it. This will for example look like this: - -{{< highlight css >}} -.custom-control { - -fx-skin: "com.guigarage.customcontrol.MyCustomControlSkin"; -} -{{< / highlight >}} - -Once you have created the css you have to define it in your control. Therefore you have to configure the path to the css file and the selector of your component: - -{{< highlight java >}} -public class MyCustomControl extends Control { - - public MyCustomControl() { - getStyleClass().add("custom-control"); - } - - @Override - protected String getUserAgentStylesheet() { - return MyCustomControl.class.getResource("customcontrol.css").toExternalForm(); - } -} -{{< / highlight >}} - -After all this stuff is done correctly JavaFX will create a skin instance for your control. You do not need to take care about this instantiation or the dependency mechanism. At this point I want to thank [Jonathan Giles](http://jonathangiles.net/blog/) who taked some time to code the css integration for gridfx together with me and explained me all the mechanisms and benefits. - -## Access the Skin and Behavior - -Normally there is no need to access the skin or the behavior from within the controller. But if you have the need to do you can access them this way: - -![custom-controls5](/posts/guigarage-legacy/custom-controls5.png) - -Because controler.getSkin() receives a javafx.scene.control.Skin and not a SkinBase you have to cast it if you need the Behavior: - -{{< highlight java >}} -((SkinBase)getSkin()).getBehavior(); -{{< / highlight >}} - -## Workaround for css haters - -For some of you this mechanism seems to be a little to oversized. Maybe you only need a specific control once in your application and you do not plan to skin it with css and doing all this stuff. For this use case there is a nice workaround in the JavaFX API. You can ignore all the css stuff and set the skin class to your control in code: - -{{< highlight java >}} -public class MyCustomControl extends Control { - public MyCustomControl() { - setSkinClassName(MyCustomControlSkin.class.getName()); - } -} -{{< / highlight >}} - -The benefit of this workflow is that refactoring of packages or classnames don't break your code and you don't need a extra css file. On the other side there is a great handicap. You can't use css defined skins in any extension of this control. __I think that every public API like gridfx should use the css way__. In some internal use cases the hard coded way could be faster. - -## Conclusion - -Now we created a control, a skin and a behavior that are working fine and can be added to your UI tree. But like in swing when simply extending the JComponent you don't see anything on screen. So the next step is to style and layout your component. I will handle this topic in my next post. - -If you want to look at some code of existing custom components check out [jgridfx](https://github.com/guigarage/gridfx) or [JFXtras](https://github.com/JFXtras/jfxtras-labs). At jgridfx the following files match with this article: - -* `com.guigarage.fx.grid.GridView` (control) -* `com.guigarage.fx.grid.skin.GridViewSkin` (skin) -* `com.guigarage.fx.grid.behavior.GridViewBehavior` (behavior) -* `/src/main/resources/com/guigarage/fx/grid/gridview.css` (css) +--- +outdated: true +showInBlog: false +title: 'Custom UI Controls with JavaFX - Part 1' +date: "2012-11-17" +author: hendrik +categories: [JavaFX] +excerpt: 'One thing I often done is Swing was customization of components and the creation of new components types. With the last release of JavaFX you can easily create custom controls with this new UI toolkit, too. This post gives a first overview about the JavaFX APIs to create custom controls.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +One thing I often done is Swing was customization of components and the creation of new components types. One example for this is the [JGrid](/posts/2011-07-15-jgrid). Since JavaFX was out I wanted to [port the JGrid to it](/posts/2012-11-14-gridfx-is-hosted-at-github). After some experiments and bad prototyps I think I found the right way to do it. The talks from [Gerrit Grunwald](http://harmoniccode.blogspot.de) and [Jonathan Giles](http://jonathangiles.net/blog/) at [JavaOne](http://www.oracle.com/javaone/index.html) helped me really a lot to do so. The records of this talks is online ([link](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=2425&tclass=popup), [link](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=4726&tclass=popup)) so I would advise everybody who is interest in this topic to spend some time and watch them. + +## Getting started + +Every UI component in JavaFX is composed by a __control__, a __skin__ and a __behavior__. In an ideal case there is a css part to. + +![custom-components1](/posts/guigarage-legacy/custom-components1.png) + +Best way to start is by creating a new control class that extends `javafx.scene.control.Control`. This class is basically comparable to `JComponent`. It should hold the properties of the component and acts as the main class for it because instances of this class will later created in your application code and added to the UI tree. + +```javaMyCustomControl myControl = new MyCustomControl(); +panel.getChildren().add(myControl); ``` + +When programming swing components the right way you put everything that depends on the visualization or user interaction into a UI class (see `LabelUI` for example). JavaFX goes one step further and provides the skin class for all visualization and layout related code and the behavior class for all user interaction. + +![custom-components2](/posts/guigarage-legacy/custom-components2.png) + +To do so in JavaFX you need to know how the classes depends on each other. Here is a short chart that shows the relations between them: + +![custom-componens3](/posts/guigarage-legacy/custom-componens3.png) + +## Creating the Behavior + +If your component only visualizes data and has no interaction it 's quite simple to create a behavior. Therefore you only need to extend the `com.sun.javafx.scene.control.behavior.BehaviorBase`. + +```javapublic class MyCustomControlBehavior extends BehaviorBase { + + public MyCustomControlBehavior(MyCustomControl control) { + super(control); + } +} ``` + +Some of you may be confused when seeing the package of BehaviorBase. At the moment this is a __private API__ and normally you should not use this classes in your code but the guys at Oracle know about this problem and will provide the __BehaviorBase as a public API with JavaFX 8__. So best practice is to use the private class now and refactor the import once Java 8 is out. + +## Creating the Skin + +After the behavior class is created we can take a look at the skin. Your skin class will mostly extend `com.sun.javafx.scene.control.skin.BaseSkin` and create a new behavior for your control. Your code normally should look like this: + +```javapublic class MyCustomControlSkin extends SkinBase{ + + public MyCustomControlSkin(MyCustomControl control) { + super(control, new MyCustomControlBehavior(control)); + } +} ``` + +As you can see the BaseSkin is a private API as well. It will also changed to public with Java 8. + +## Creating the Control + +The last class we will need is the control. First we create a simple empty class: + +```javapublic class MyCustomControl extends Control { + + public MyCustomControl() { + } +} ``` + +At this point we have a leak in the dependencies of our three classes. The skin knows about the behavior and control. Here everything looks right. However in application code you will simply create a new control and use it as I showed earlier. The problem is that the control class do not know anything about the skin or behavior. This was one of the biggest pitfalls I was confronted with while learning JavaFX. + +## Putting it together + +What first looks as a great problem is part of the potency JavaFX provides. With JavaFX it should be very easy to create different visualisation (skins) for controls. For this part you can customize the look of components by css. Because the skin is the main part of this look it has to defined by css, too. So instead of creating a skin object for the control by your own you only define the skin class that should be used for your control. The instanciation and everything else is automatically done by the JavaFX APIs. To do so you have to bind your control to a css class. + +Firts off all you have to create a new css file in your project. I think best practice is to use the same package as the controls has and but a css file under src/main/resource: + +![custom-components4](/posts/guigarage-legacy/custom-components4.png) + +Inside the css you have to specify a new selector for your component and add the skin as a property to it. This will for example look like this: + +```css.custom-control { + -fx-skin: "com.guigarage.customcontrol.MyCustomControlSkin"; +} ``` + +Once you have created the css you have to define it in your control. Therefore you have to configure the path to the css file and the selector of your component: + +```javapublic class MyCustomControl extends Control { + + public MyCustomControl() { + getStyleClass().add("custom-control"); + } + + @Override + protected String getUserAgentStylesheet() { + return MyCustomControl.class.getResource("customcontrol.css").toExternalForm(); + } +} ``` + +After all this stuff is done correctly JavaFX will create a skin instance for your control. You do not need to take care about this instantiation or the dependency mechanism. At this point I want to thank [Jonathan Giles](http://jonathangiles.net/blog/) who taked some time to code the css integration for gridfx together with me and explained me all the mechanisms and benefits. + +## Access the Skin and Behavior + +Normally there is no need to access the skin or the behavior from within the controller. But if you have the need to do you can access them this way: + +![custom-controls5](/posts/guigarage-legacy/custom-controls5.png) + +Because controler.getSkin() receives a javafx.scene.control.Skin and not a SkinBase you have to cast it if you need the Behavior: + +```java((SkinBase)getSkin()).getBehavior(); ``` + +## Workaround for css haters + +For some of you this mechanism seems to be a little to oversized. Maybe you only need a specific control once in your application and you do not plan to skin it with css and doing all this stuff. For this use case there is a nice workaround in the JavaFX API. You can ignore all the css stuff and set the skin class to your control in code: + +```javapublic class MyCustomControl extends Control { + public MyCustomControl() { + setSkinClassName(MyCustomControlSkin.class.getName()); + } +} ``` + +The benefit of this workflow is that refactoring of packages or classnames don't break your code and you don't need a extra css file. On the other side there is a great handicap. You can't use css defined skins in any extension of this control. __I think that every public API like gridfx should use the css way__. In some internal use cases the hard coded way could be faster. + +## Conclusion + +Now we created a control, a skin and a behavior that are working fine and can be added to your UI tree. But like in swing when simply extending the JComponent you don't see anything on screen. So the next step is to style and layout your component. I will handle this topic in my next post. + +If you want to look at some code of existing custom components check out [jgridfx](https://github.com/guigarage/gridfx) or [JFXtras](https://github.com/JFXtras/jfxtras-labs). At jgridfx the following files match with this article: + +* `com.guigarage.fx.grid.GridView` (control) +* `com.guigarage.fx.grid.skin.GridViewSkin` (skin) +* `com.guigarage.fx.grid.behavior.GridViewBehavior` (behavior) +* `/src/main/resources/com/guigarage/fx/grid/gridview.css` (css) diff --git a/content/posts/2012-11-17-swing-is-dead.md b/content/posts/2012-11-17-swing-is-dead.md index 737e1ea6..283ab824 100644 --- a/content/posts/2012-11-17-swing-is-dead.md +++ b/content/posts/2012-11-17-swing-is-dead.md @@ -10,10 +10,10 @@ preview_image: "/posts/preview-images/software-development-green.svg" --- JavaFX is the new UI API for Java Desktop applications and since summer it is part of Java 7. So every new Java Runtime is shipped with JavaFX. I think it's the perfect time to play with this great API and learn how to create cool applications with a better user experience than ever before. -To learn JavaFX I started the [gridFX]({{< ref "/posts/2012-11-14-gridfx-is-hosted-at-github" >}}) project that provide a grid based control for JavaFX. While programming this control I learned a lot about JavaFX and all the different included APIs. +To learn JavaFX I started the [gridFX](/posts/2012-11-14-gridfx-is-hosted-at-github) project that provide a grid based control for JavaFX. While programming this control I learned a lot about JavaFX and all the different included APIs. By doing so it was sometimes hard to understand some of the paradigms that are used in JavaFX. It took a lot of research and the help of some guys ([Gerrit Grunwald](http://harmoniccode.blogspot.de), [Jonathan Giles](http://jonathangiles.net)) to understand this approaches and use them to create a better code and integration to the given APIs. After understanding all this stuff I can say that JavaFX is a really powerful API with a lot of potential. After all I think that the learning curve for JavaFX is much different to the one for swing. The basics are currently hard to understand and learn but ones you decode them everything is so much easier. -I want to use my blog to provide some basic tutorials about the different APIs and technics of JavaFX. All this tutorials are based on [demos]({{< ref "/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices" >}}) that will use gridFX. Hope you like them. +I want to use my blog to provide some basic tutorials about the different APIs and technics of JavaFX. All this tutorials are based on [demos](/posts/2012-11-14-playing-with-gridfx-and-itunes-webservices) that will use gridFX. Hope you like them. diff --git a/content/posts/2012-11-21-gridfx-pagination.md b/content/posts/2012-11-21-gridfx-pagination.md index 3d302bb5..c3b11c95 100644 --- a/content/posts/2012-11-21-gridfx-pagination.md +++ b/content/posts/2012-11-21-gridfx-pagination.md @@ -1,41 +1,39 @@ ---- -outdated: true -showInBlog: false -title: 'gridfx & pagination' -date: "2012-11-21" -author: hendrik -categories: [JavaFX] -excerpt: 'With the JavaFX API for pagination it is possible to combine GridFX with the given features and create a grid that supports pagination.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -The [GridFX]({{ site.baseurl }}{% link pages/projects/gridfx.md %}) component has currently one big problem. For every item inside the model a new cell is created inside the scene graph. So if you have 100.000 items in the item list of a the grid it will contain 100.000 cells. If every cell contains 4 nodes the scene graph will contain 400.000 nodes. In swing you used [renderer classes](http://docs.oracle.com/javase/tutorial/uiswing/components/combobox.html#renderer) to avoid this problem. But JavaFX work different. Instead of using only one renderer instance and painting this again and again JavaFX uses cell factories to create new instances for every cell. The trick is, that only the cells you currently see on screen are added to the scene graph. Old cells will be dropped from the scene graph when the view is scrolling and recycled or reused later. This logic is implemented in com.sun.javafx.scene.control.skin.VirtualFlow and used by the JavaFX [ListView](http://docs.oracle.com/javafx/2/ui_controls/list-view.htm) for example. I will implement this behavior to gridfx as soon as possible. - -![pagination1](/posts/guigarage-legacy/pagination1.png) - -While playing with other JavaFX features and APIs I found another solution for this problem. With the [Pagination](http://docs.oracle.com/javafx/2/ui_controls/pagination.htm) class you can split your UI in different pages. An example for this method is the iOS home screen. Here all your apps are split in different pages and you can flip through them. - -![Pagnation2](/posts/guigarage-legacy/Pagnation2.png) - -I tried to imitate this behavior for the GridView and will share my experience here. - -A list of items should be cut in little peaces so that each part fits exactly into one page of the Pagination Control and the page count depends on the item count. Thanks to the [JavaFX property binding](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm) this plan wasn't to hard to implement. I created a helper class ([GridPaginationHelper.java](https://github.com/guigarage/gridfx/blob/master/src/main/java/com/guigarage/fx/grid/util/GridPaginationHelper.java)) that takes care of all the bindings and calculations. You only need a cell factory for the GridView. There are some problems with Pagination so that not everything is working as it should. Will later talk about this. First a short movie that shows the current state: - -{{< vimeo 54046675 >}} - -You can see how the Pagination animates through all pages and the page count is computed every time the size of the cells is changing. You can see the problems that the implementation has at this stage, too. Sometimes the navigation bar flickers or disappear for a moment. This a very strange behavior and I can't find the cause. Then I can not access the size of the page inside the Pagination Control. At the moment I'm working with the following hack: - -{{< highlight java >}} -pageHeight = pagination.getHeight() - 64; -{{< / highlight >}} - -There is another limitation of the Pagination Control. You can not deactivate the animation. When the cell size changes the current page index may change while showing the same cells. At this moment the animation should be deactivated. - -Maybe I have done something wrong or don't understand the complete usage of Pagination but I think that behavior I want to copy is used in a lot of modern applications: - -![pagination-demo1-150x150](/posts/guigarage-legacy/pagination-demo1.jpg) - -![pagination-demo2-150x150](/posts/guigarage-legacy/pagination-demo2.png) - -![pagination-demo3-150x150](/posts/guigarage-legacy/pagination-demo3.png) - -I will open some tickets at Jira for this issues. Maybe someone has I idea how to do this a better way in the meantime. I'm open for better and new ideas. +--- +outdated: true +showInBlog: false +title: 'gridfx & pagination' +date: "2012-11-21" +author: hendrik +categories: [JavaFX] +excerpt: 'With the JavaFX API for pagination it is possible to combine GridFX with the given features and create a grid that supports pagination.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +The [GridFX]({{ site.baseurl }}{% link pages/projects/gridfx.md %}) component has currently one big problem. For every item inside the model a new cell is created inside the scene graph. So if you have 100.000 items in the item list of a the grid it will contain 100.000 cells. If every cell contains 4 nodes the scene graph will contain 400.000 nodes. In swing you used [renderer classes](http://docs.oracle.com/javase/tutorial/uiswing/components/combobox.html#renderer) to avoid this problem. But JavaFX work different. Instead of using only one renderer instance and painting this again and again JavaFX uses cell factories to create new instances for every cell. The trick is, that only the cells you currently see on screen are added to the scene graph. Old cells will be dropped from the scene graph when the view is scrolling and recycled or reused later. This logic is implemented in com.sun.javafx.scene.control.skin.VirtualFlow and used by the JavaFX [ListView](http://docs.oracle.com/javafx/2/ui_controls/list-view.htm) for example. I will implement this behavior to gridfx as soon as possible. + +![pagination1](/posts/guigarage-legacy/pagination1.png) + +While playing with other JavaFX features and APIs I found another solution for this problem. With the [Pagination](http://docs.oracle.com/javafx/2/ui_controls/pagination.htm) class you can split your UI in different pages. An example for this method is the iOS home screen. Here all your apps are split in different pages and you can flip through them. + +![Pagnation2](/posts/guigarage-legacy/Pagnation2.png) + +I tried to imitate this behavior for the GridView and will share my experience here. + +A list of items should be cut in little peaces so that each part fits exactly into one page of the Pagination Control and the page count depends on the item count. Thanks to the [JavaFX property binding](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm) this plan wasn't to hard to implement. I created a helper class ([GridPaginationHelper.java](https://github.com/guigarage/gridfx/blob/master/src/main/java/com/guigarage/fx/grid/util/GridPaginationHelper.java)) that takes care of all the bindings and calculations. You only need a cell factory for the GridView. There are some problems with Pagination so that not everything is working as it should. Will later talk about this. First a short movie that shows the current state: + + + +You can see how the Pagination animates through all pages and the page count is computed every time the size of the cells is changing. You can see the problems that the implementation has at this stage, too. Sometimes the navigation bar flickers or disappear for a moment. This a very strange behavior and I can't find the cause. Then I can not access the size of the page inside the Pagination Control. At the moment I'm working with the following hack: + +```javapageHeight = pagination.getHeight() - 64; ``` + +There is another limitation of the Pagination Control. You can not deactivate the animation. When the cell size changes the current page index may change while showing the same cells. At this moment the animation should be deactivated. + +Maybe I have done something wrong or don't understand the complete usage of Pagination but I think that behavior I want to copy is used in a lot of modern applications: + +![pagination-demo1-150x150](/posts/guigarage-legacy/pagination-demo1.jpg) + +![pagination-demo2-150x150](/posts/guigarage-legacy/pagination-demo2.png) + +![pagination-demo3-150x150](/posts/guigarage-legacy/pagination-demo3.png) + +I will open some tickets at Jira for this issues. Maybe someone has I idea how to do this a better way in the meantime. I'm open for better and new ideas. diff --git a/content/posts/2012-11-25-maven-support-for-datafx.md b/content/posts/2012-11-25-maven-support-for-datafx.md index 84eb35cf..0b9e23af 100644 --- a/content/posts/2012-11-25-maven-support-for-datafx.md +++ b/content/posts/2012-11-25-maven-support-for-datafx.md @@ -1,31 +1,29 @@ ---- -outdated: true -showInBlog: false -title: 'Maven support for DataFX' -date: "2012-11-25" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'Since DataFX is one of the big JavaFX related libraries out there it makes sense to use it in modern Maven based applications. This post gives an overview how you can use DataFX in a Maven based project.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -At [Devoxx](http://www.devoxx.com/display/DV12/Home) I met [Johan Vos](https://twitter.com/johanvos) and we talked about [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) and Maven support. Now two weeks later we released DataFX 1.0 with Maven support and I am a official contributor of this great project. Thanks to Johan and Jonathan! - -With this post I will give a short "Getting Started" documentation for everyone who wants to use DataFX in a Maven based project. - -You can easily add a dependency to DataFX 1.0 to any Maven project. With doing so a JavaFX 2.2 dependency is added to your project, too. Just add the following dependency to your Maven pom: - -{{< highlight xml >}} - - org.javafxdata - datafx-core - 1.0 - -{{< / highlight >}} - -Since all datafx-core artifacts are available at [Maven Central Repository](http://search.maven.org) the provision of DataFX will work automatically. - -If you are interested in how we added JavaFX as a dependency to DataFX you can read [here]({{< ref "/posts/2012-10-13-building-javafx-applications-with-maven" >}}) about it. - -This description shows how to add DataFX to your dependency hierarchy. If you want to run or distribute your application we recommend this [Maven plugin](https://github.com/zonski/javafx-maven-plugin) that provides a lot of functionality to a JavaFX Maven project. You can find a tutorial about this plugin [here](http://www.zenjava.com/2012/11/24/from-zero-to-javafx-in-5-minutes/). - -I hope you like the way how we integrated Maven support to DataFX and it this will help you creating cool JavaFX apps. +--- +outdated: true +showInBlog: false +title: 'Maven support for DataFX' +date: "2012-11-25" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'Since DataFX is one of the big JavaFX related libraries out there it makes sense to use it in modern Maven based applications. This post gives an overview how you can use DataFX in a Maven based project.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +At [Devoxx](http://www.devoxx.com/display/DV12/Home) I met [Johan Vos](https://twitter.com/johanvos) and we talked about [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) and Maven support. Now two weeks later we released DataFX 1.0 with Maven support and I am a official contributor of this great project. Thanks to Johan and Jonathan! + +With this post I will give a short "Getting Started" documentation for everyone who wants to use DataFX in a Maven based project. + +You can easily add a dependency to DataFX 1.0 to any Maven project. With doing so a JavaFX 2.2 dependency is added to your project, too. Just add the following dependency to your Maven pom: + +```xml + org.javafxdata + datafx-core + 1.0 + ``` + +Since all datafx-core artifacts are available at [Maven Central Repository](http://search.maven.org) the provision of DataFX will work automatically. + +If you are interested in how we added JavaFX as a dependency to DataFX you can read [here](/posts/2012-10-13-building-javafx-applications-with-maven) about it. + +This description shows how to add DataFX to your dependency hierarchy. If you want to run or distribute your application we recommend this [Maven plugin](https://github.com/zonski/javafx-maven-plugin) that provides a lot of functionality to a JavaFX Maven project. You can find a tutorial about this plugin [here](http://www.zenjava.com/2012/11/24/from-zero-to-javafx-in-5-minutes/). + +I hope you like the way how we integrated Maven support to DataFX and it this will help you creating cool JavaFX apps. diff --git a/content/posts/2012-11-29-custom-ui-controls-with-javafx-part-2.md b/content/posts/2012-11-29-custom-ui-controls-with-javafx-part-2.md index 94afcaa3..28d76be1 100644 --- a/content/posts/2012-11-29-custom-ui-controls-with-javafx-part-2.md +++ b/content/posts/2012-11-29-custom-ui-controls-with-javafx-part-2.md @@ -1,246 +1,222 @@ ---- -outdated: true -showInBlog: false -title: 'Custom UI Controls with JavaFX (Part 2)' -date: "2012-11-29" -author: hendrik -categories: [JavaFX] -excerpt: 'One thing I often done is Swing was customization of components and the creation of new components types. With the last release of JavaFX you can easily create custom controls with this new UI toolkit, too. This post gives a first overview about the JavaFX APIs to create custom controls.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -I started a [series of JavaFX tutorials last week]({{< ref "/posts/2012-11-17-custom-ui-controls-with-javafx-part-1" >}}). In this second part I will explain how to layout custom controls and measure their bounds. Like in my last post I will try to show the differences and benefits of JavaFX compared to Swing. - -## Floating Point Bounds - -JavaFX uses a Scene Graph as the structure for all graphical nodes. This graph supports transformations like scaling or rotation for all its children in a very easy way. - -In Swing it is easy to translate a panel with all its children in x or y direction. In JavaFX you can now translate, scale or rotate a parent node with all children according to the x, y an z axes. But before we take a look at this transformations I will show you the simple way of setting bounds in JavaFX. There are three important methodes that every Node in JavaFX provides: - -{{< highlight java >}} -void relocate(double x, double y) - -void resize(double width, double height) - -void resizeRelocate(double x, double y, double width, double height) -{{< / highlight >}} - -This methods are equivalent to the following ones that a provided by JComponent: - -{{< highlight java >}} -void setLocation(int x, int y) - -void setSize(int width, int height) - -void setBounds(int x, int y, int width, int height) -{{< / highlight >}} - -The difference between them is that JavaFX uses "double" as parameter type. The methods of JComponent have it's historical background in AWT. I think at the time of implementation no one thought about rectangles that were arranged between pixels and were drawn with antialiasing. The JavaFX methods provides this functionality and once transformation comes into play everyone should understand why this is essential. - -## Let's do a basic layout - -When layouting a swing UI you normally do not call the methods mentioned above in your code. Layout managers do all the work for you. In most cases you have a container like the JPanel with LayoutManager like BorderLayout that layouts all children of the container within it's bounds. - -JavaFX don't know LayoutManagers. All layouting is done directly by the containers. The basic layout container is called Pane and when looking at the type hierarchy of this class you will find containers with different layout algorithms like the VBox or HBox. - -![layout1](/posts/guigarage-legacy/layout1.png) - -You can read about the different layout containers and their special scopes [here](http://docs.oracle.com/javafx/2/layout/builtin_layouts.htm). When creation custom panes there are a few more points to take care of. I will talk about this in a later post. - -## Preparing custom control for layout - -A LayoutManager in Swing computes the bounds of all children by three properties: - -{{< highlight java >}} -Dimension getMaximumSize() - -Dimension getPreferredSize() - -Dimension getMinimumSize() -{{< / highlight >}} - -A LayoutManager can use this properties of every child to compute its bounds inside the layout. When using FlowLayout for example every child has exactly its preferred dimension. So when you created your custom JComponent you needed to override this methods. This mechanism has one big problem: You can not calculate a dynamic aspect ratio of the children. Ever asked yourself why JLabel do not support automatic word wrapping? I think the leak of aspect ratio calculation in swing is the reason for this limitation. You can only calculate the preferred bounds of a component but you can not calculate the preferred width dependent to its height by using the default Swing workflow and APIs. - -With JavaFX you can do this calculations. Each Node in JavaFX provides the following methodes: - -{{< highlight java >}} -double computeMinHeight(double width) - -double computeMinWidth(double height) - -double computeMaxHeight(double width) - -double computeMaxWidth(double height) - -double computePrefHeight(double width) - -double computePrefWidth(double height) -{{< / highlight >}} - -By overriding this methods you can control how your custom control will be layouted in a pane. At the first moment everything looks right and easy. You can calculate the components height by its width and vice versa. But to use this calculations JavaFX needs a hint how the bias of a component is working. This is the point where the content bias comes into play. With this property every node can define if its width depends on the height or in opposite way. The current value is defined by this method: - -{{< highlight java >}} -Orientation getContentBias() -{{< / highlight >}} - -If the node is resizable and its height depends on its width the method should return Orientation.HORIZONTAL. If its width depends on its height return Orientation.VERTICAL. If your custom component do not need a width/height dependency you can even return null for its content bias. In this case -1 will always be passed to all methodes (computePrefWidth, etc.). Now your calculations will not depend on this value and we will have the same behavior as in Swing. The component do not use aspect ratio. - -So it is no problem anymore to provide a word wrap in a Textcomponent when using JavaFX. I will explain the usage of this methodes with a more easy example. Let's assume that we need a component that always has a surface area of 24 pixels. - -![layout2](/posts/guigarage-legacy/layout2.png) - -With swing we would only have a few different ways/dimensions to create such a component: - -{{< highlight java >}} -Dimension getPreferredSize() { - return new Dimension(24,1); - //All other different versions - //return new Dimension(1,24); - //return new Dimension(2,12); - //return new Dimension(12,2); - //return new Dimension(8,3); - //return new Dimension(3,8); - //return new Dimension(6,4); - //return new Dimension(4,6); -} -{{< / highlight >}} - -In reality there is a unlimited count of rectangles that have a area of 24 pixels. For example a rectangle with a width of 4,7 and a height of 5,105... has exact this area. - -![layout3](/posts/guigarage-legacy/layout3.png) - -With JavaFX and the extended ways to calculate the dimension of components and the use of double values we can create all of this rectangles (this is only limited by the range of double values). First of all we need to implement all this different methods for dimension calculation: - -{{< highlight java >}} -@Override -protected double computeMaxHeight(double width) { - if (width > 0) { - return Double.MAX_VALUE; - } else { - return 24.0 / width; - } -} - -@Override -protected double computeMaxWidth(double height) { - if (height > 0) { - return Double.MAX_VALUE; - } else { - return 24.0 / height; - } -} - -@Override -protected double computeMinHeight(double width) { - if (width > 0) { - return Double.MIN_VALUE; - } else { - return 24.0 / width; - } -} - -@Override -protected double computeMinWidth(double height) { - if (height > 0) { - return Double.MIN_VALUE; - } else { - return 24.0 / height; - } -} - -@Override -protected double computePrefHeight(double width) { - if (width > 0) { - return 4; - } else { - return 24.0 / width; - } -} - -@Override -protected double computePrefWidth(double height) { - if (height > 0) { - return 6; - } else { - return 24.0 / height; - } -} -{{< / highlight >}} - -All methodes can handle -1 as parameter and returns a default value in that case. - -Here is a movie showing the layout with Orientation.HORIZONTAL. Because a 24 pixel area would be very small. So I changed it to 240.000 for this movie: - -{{< vimeo 54478790 >}} - -And here is the movie with Orientation.VERTICAL: - -{{< vimeo 54479737 >}} - -As mentioned in my last post each Custom Control needs a Skin. To do things right you have to override all compute...-methods in your Skin and not in the Control class. Only getContentBias() needs to be overridden in the Control. - -## Useful hints - -If your component should has a constant dimension you can easily set all properties instead of overriding all the methods: - -{{< highlight java >}} -myControl.setPrefWidth(4); -myControl.setPrefHeight(6); - -//myControl.setPrefSize(4, 6); -{{< / highlight >}} - -By default Control.USE_COMPUTED_SIZE is set for this properties. This indicates JavaFX to calculate the dimension by using mecanisms mentioned above. - -Another hint is to set Control.USE_PREF_SIZE to max/min size instead of overriding all methods. This will use the preferred size for min/max size: - -{{< highlight java >}} -myControl.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); -{{< / highlight >}} - -Once a component is layouted you can access it's current bounds with: - -{{< highlight java >}} -myControl.getBoundsInLocal() -{{< / highlight >}} - -You can find more information about JavaFX layout [here](http://amyfowlersblog.wordpress.com/2011/06/02/javafx2-0-layout-a-class-tour/). - -### But I need a control that is not resizable - -The JavaFX layout mechanism even supports this feature. Every Node has this method: - -{{< highlight java >}} -boolean isResizable() -{{< / highlight >}} - -When this method returns false all (official) layout panes will not resize your control. In this case the layout only handles the location of your control. - -## Transformation - -As I mentioned before Nodes support transformation. In special translation, rotation and scaling are currently supported. Very important is to not equalize transformation with layouting. A transform in JavaFX changes the visual bounds of a layouted component. Every component needs to be layouted as descripted above. Once this is done the control can be transformed by a mouse event, for example. Here is a short example that rotates a node by mouse over event: - -{{< highlight java >}} -myControl.setOnMouseEntered(new EventHandler() { - - @Override - public void handle(MouseEvent arg0) { - setRotate(15.0); - } -}); - -myControl.setOnMouseExited(new EventHandler() { - - @Override - public void handle(MouseEvent arg0) { - setRotate(0); - } -}); -{{< / highlight >}} - -To measure the current visual bounds of a components once it is transformed we have the additional method getBoundsInParent(). Other than getBoundsInLocal() this method returns the bounds which include the transforms of the component. - -![layout4](/posts/guigarage-legacy/layout4.png) - -You can find more informations about transformations in JavaFX [here](http://docs.oracle.com/javafx/2/transformations/jfxpub-transformations.htm). - -You can download the example I used for this post [here](https://github.com/downloads/guigarage/gridfx-demos/layout-demo.zip). +--- +outdated: true +showInBlog: false +title: 'Custom UI Controls with JavaFX (Part 2)' +date: "2012-11-29" +author: hendrik +categories: [JavaFX] +excerpt: 'One thing I often done is Swing was customization of components and the creation of new components types. With the last release of JavaFX you can easily create custom controls with this new UI toolkit, too. This post gives a first overview about the JavaFX APIs to create custom controls.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +I started a [series of JavaFX tutorials last week](/posts/2012-11-17-custom-ui-controls-with-javafx-part-1). In this second part I will explain how to layout custom controls and measure their bounds. Like in my last post I will try to show the differences and benefits of JavaFX compared to Swing. + +## Floating Point Bounds + +JavaFX uses a Scene Graph as the structure for all graphical nodes. This graph supports transformations like scaling or rotation for all its children in a very easy way. + +In Swing it is easy to translate a panel with all its children in x or y direction. In JavaFX you can now translate, scale or rotate a parent node with all children according to the x, y an z axes. But before we take a look at this transformations I will show you the simple way of setting bounds in JavaFX. There are three important methodes that every Node in JavaFX provides: + +```javavoid relocate(double x, double y) + +void resize(double width, double height) + +void resizeRelocate(double x, double y, double width, double height) ``` + +This methods are equivalent to the following ones that a provided by JComponent: + +```javavoid setLocation(int x, int y) + +void setSize(int width, int height) + +void setBounds(int x, int y, int width, int height) ``` + +The difference between them is that JavaFX uses "double" as parameter type. The methods of JComponent have it's historical background in AWT. I think at the time of implementation no one thought about rectangles that were arranged between pixels and were drawn with antialiasing. The JavaFX methods provides this functionality and once transformation comes into play everyone should understand why this is essential. + +## Let's do a basic layout + +When layouting a swing UI you normally do not call the methods mentioned above in your code. Layout managers do all the work for you. In most cases you have a container like the JPanel with LayoutManager like BorderLayout that layouts all children of the container within it's bounds. + +JavaFX don't know LayoutManagers. All layouting is done directly by the containers. The basic layout container is called Pane and when looking at the type hierarchy of this class you will find containers with different layout algorithms like the VBox or HBox. + +![layout1](/posts/guigarage-legacy/layout1.png) + +You can read about the different layout containers and their special scopes [here](http://docs.oracle.com/javafx/2/layout/builtin_layouts.htm). When creation custom panes there are a few more points to take care of. I will talk about this in a later post. + +## Preparing custom control for layout + +A LayoutManager in Swing computes the bounds of all children by three properties: + +```javaDimension getMaximumSize() + +Dimension getPreferredSize() + +Dimension getMinimumSize() ``` + +A LayoutManager can use this properties of every child to compute its bounds inside the layout. When using FlowLayout for example every child has exactly its preferred dimension. So when you created your custom JComponent you needed to override this methods. This mechanism has one big problem: You can not calculate a dynamic aspect ratio of the children. Ever asked yourself why JLabel do not support automatic word wrapping? I think the leak of aspect ratio calculation in swing is the reason for this limitation. You can only calculate the preferred bounds of a component but you can not calculate the preferred width dependent to its height by using the default Swing workflow and APIs. + +With JavaFX you can do this calculations. Each Node in JavaFX provides the following methodes: + +```javadouble computeMinHeight(double width) + +double computeMinWidth(double height) + +double computeMaxHeight(double width) + +double computeMaxWidth(double height) + +double computePrefHeight(double width) + +double computePrefWidth(double height) ``` + +By overriding this methods you can control how your custom control will be layouted in a pane. At the first moment everything looks right and easy. You can calculate the components height by its width and vice versa. But to use this calculations JavaFX needs a hint how the bias of a component is working. This is the point where the content bias comes into play. With this property every node can define if its width depends on the height or in opposite way. The current value is defined by this method: + +```javaOrientation getContentBias() ``` + +If the node is resizable and its height depends on its width the method should return Orientation.HORIZONTAL. If its width depends on its height return Orientation.VERTICAL. If your custom component do not need a width/height dependency you can even return null for its content bias. In this case -1 will always be passed to all methodes (computePrefWidth, etc.). Now your calculations will not depend on this value and we will have the same behavior as in Swing. The component do not use aspect ratio. + +So it is no problem anymore to provide a word wrap in a Textcomponent when using JavaFX. I will explain the usage of this methodes with a more easy example. Let's assume that we need a component that always has a surface area of 24 pixels. + +![layout2](/posts/guigarage-legacy/layout2.png) + +With swing we would only have a few different ways/dimensions to create such a component: + +```javaDimension getPreferredSize() { + return new Dimension(24,1); + //All other different versions + //return new Dimension(1,24); + //return new Dimension(2,12); + //return new Dimension(12,2); + //return new Dimension(8,3); + //return new Dimension(3,8); + //return new Dimension(6,4); + //return new Dimension(4,6); +} ``` + +In reality there is a unlimited count of rectangles that have a area of 24 pixels. For example a rectangle with a width of 4,7 and a height of 5,105... has exact this area. + +![layout3](/posts/guigarage-legacy/layout3.png) + +With JavaFX and the extended ways to calculate the dimension of components and the use of double values we can create all of this rectangles (this is only limited by the range of double values). First of all we need to implement all this different methods for dimension calculation: + +```java@Override +protected double computeMaxHeight(double width) { + if (width > 0) { + return Double.MAX_VALUE; + } else { + return 24.0 / width; + } +} + +@Override +protected double computeMaxWidth(double height) { + if (height > 0) { + return Double.MAX_VALUE; + } else { + return 24.0 / height; + } +} + +@Override +protected double computeMinHeight(double width) { + if (width > 0) { + return Double.MIN_VALUE; + } else { + return 24.0 / width; + } +} + +@Override +protected double computeMinWidth(double height) { + if (height > 0) { + return Double.MIN_VALUE; + } else { + return 24.0 / height; + } +} + +@Override +protected double computePrefHeight(double width) { + if (width > 0) { + return 4; + } else { + return 24.0 / width; + } +} + +@Override +protected double computePrefWidth(double height) { + if (height > 0) { + return 6; + } else { + return 24.0 / height; + } +} ``` + +All methodes can handle -1 as parameter and returns a default value in that case. + +Here is a movie showing the layout with Orientation.HORIZONTAL. Because a 24 pixel area would be very small. So I changed it to 240.000 for this movie: + + + +And here is the movie with Orientation.VERTICAL: + + + +As mentioned in my last post each Custom Control needs a Skin. To do things right you have to override all compute...-methods in your Skin and not in the Control class. Only getContentBias() needs to be overridden in the Control. + +## Useful hints + +If your component should has a constant dimension you can easily set all properties instead of overriding all the methods: + +```javamyControl.setPrefWidth(4); +myControl.setPrefHeight(6); + +//myControl.setPrefSize(4, 6); ``` + +By default Control.USE_COMPUTED_SIZE is set for this properties. This indicates JavaFX to calculate the dimension by using mecanisms mentioned above. + +Another hint is to set Control.USE_PREF_SIZE to max/min size instead of overriding all methods. This will use the preferred size for min/max size: + +```javamyControl.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); ``` + +Once a component is layouted you can access it's current bounds with: + +```javamyControl.getBoundsInLocal() ``` + +You can find more information about JavaFX layout [here](http://amyfowlersblog.wordpress.com/2011/06/02/javafx2-0-layout-a-class-tour/). + +### But I need a control that is not resizable + +The JavaFX layout mechanism even supports this feature. Every Node has this method: + +```javaboolean isResizable() ``` + +When this method returns false all (official) layout panes will not resize your control. In this case the layout only handles the location of your control. + +## Transformation + +As I mentioned before Nodes support transformation. In special translation, rotation and scaling are currently supported. Very important is to not equalize transformation with layouting. A transform in JavaFX changes the visual bounds of a layouted component. Every component needs to be layouted as descripted above. Once this is done the control can be transformed by a mouse event, for example. Here is a short example that rotates a node by mouse over event: + +```javamyControl.setOnMouseEntered(new EventHandler() { + + @Override + public void handle(MouseEvent arg0) { + setRotate(15.0); + } +}); + +myControl.setOnMouseExited(new EventHandler() { + + @Override + public void handle(MouseEvent arg0) { + setRotate(0); + } +}); ``` + +To measure the current visual bounds of a components once it is transformed we have the additional method getBoundsInParent(). Other than getBoundsInLocal() this method returns the bounds which include the transforms of the component. + +![layout4](/posts/guigarage-legacy/layout4.png) + +You can find more informations about transformations in JavaFX [here](http://docs.oracle.com/javafx/2/transformations/jfxpub-transformations.htm). + +You can download the example I used for this post [here](https://github.com/downloads/guigarage/gridfx-demos/layout-demo.zip). diff --git a/content/posts/2012-11-29-the-future-of-vagrant-binding.md b/content/posts/2012-11-29-the-future-of-vagrant-binding.md index 699f8cd1..e1033b20 100644 --- a/content/posts/2012-11-29-the-future-of-vagrant-binding.md +++ b/content/posts/2012-11-29-the-future-of-vagrant-binding.md @@ -8,6 +8,6 @@ categories: [General, Vagrant-Binding] excerpt: "Currently I'm working on Chef support for the Vagrant-Binding. So you can choose between Puppet and Chef as provisioner." preview_image: "/posts/preview-images/software-development-green.svg" --- -The last weeks my top priority was [JavaFX]({{< ref "/posts/2012-11-17-custom-ui-controls-with-javafx-part-1" >}}) and [GridFX]({{< ref "/posts/2012-11-29-gridfx-is-moving-forward" >}}). But this doesn't mean that [Vagrant-Binding]({{< ref "/posts/2012-11-01-introducing-vagrant-binding" >}}) stands still. Currently I'm working on [Chef](http://www.opscode.com/chef/) support for the API. So you can choose between [Puppet](http://puppetlabs.com) and Chef as provisioner. I have some great plans for the future and I think that [Vagrants future and the founding of HashiCorp](http://www.hashicorp.com/blog/announcing-hashicorp.html) will be very positive for Vagrant-Binding. I was always thinking how to add [AWS](http://aws.amazon.com) as a second provider next to [VirtualBox](https://www.virtualbox.org). I hope that Vagrants further development will provide exactly this features :) +The last weeks my top priority was [JavaFX](/posts/2012-11-17-custom-ui-controls-with-javafx-part-1) and [GridFX](/posts/2012-11-29-gridfx-is-moving-forward). But this doesn't mean that [Vagrant-Binding](/posts/2012-11-01-introducing-vagrant-binding) stands still. Currently I'm working on [Chef](http://www.opscode.com/chef/) support for the API. So you can choose between [Puppet](http://puppetlabs.com) and Chef as provisioner. I have some great plans for the future and I think that [Vagrants future and the founding of HashiCorp](http://www.hashicorp.com/blog/announcing-hashicorp.html) will be very positive for Vagrant-Binding. I was always thinking how to add [AWS](http://aws.amazon.com) as a second provider next to [VirtualBox](https://www.virtualbox.org). I hope that Vagrants further development will provide exactly this features :) I am, by the way, not a Chef expert. So maybe anyone out there wants to help me with the Chef integration and provides some Chef projects that covers all the support Chef features in Vagrant. It would be a superb help for a full functional and great API. diff --git a/content/posts/2012-12-10-access-puppet-forge-repository-with-java.md b/content/posts/2012-12-10-access-puppet-forge-repository-with-java.md index 75d73330..2641e621 100644 --- a/content/posts/2012-12-10-access-puppet-forge-repository-with-java.md +++ b/content/posts/2012-12-10-access-puppet-forge-repository-with-java.md @@ -1,25 +1,23 @@ ---- -outdated: true -showInBlog: false -title: 'Access Puppet Forge Repository with Java' -date: "2012-12-10" -author: hendrik -categories: [Vagrant-Binding] -excerpt: 'To integrate Puppet modules more easy to Vagrant-Binding projects I created a Java API that access Puppet Forge by REST.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -To integrate Puppet modules more easy to [Vagrant-Binding]({{< ref "/posts/2012-11-01-introducing-vagrant-binding" >}}) projects I created a Java API that access Puppet Forge by REST. You can search for modules or download any module to your locale disc. So you can easily add modules from the repository to your own Vagrant environments. The API is currently not part of Vagrant-Binding and can be found [here](https://github.com/guigarage/puppet-forge-ws) but I plan to integrate the API in the future. Additionally I added a demo to the [Vagrant-Binding-Demos]({{ site.baseurl }}{% post_url 2012-11-03-vagrant-binding-demos %} -) project that can be found at [github](https://github.com/guigarage/vagrant-binding-demos/blob/master/src/main/java/com/guigarage/vagrant/tutorials/PuppetTutorial2.java). Here is a short example that uses the API: - -{{< highlight java >}} -PuppetForgeClient client = new PuppetForgeClient(); -File moduleFolder = new File("/path/to/my/modules"); - -ListallDescriptions = client.findModules("mongodb"); - -for(PuppetForgeModuleDescription description : allDescriptions) { - System.out.println("Installing " + description.getFullName()); - PuppetForgeModule module = client.findModule(description); - client.installToModulesDir(moduleFolder, module); -} -{{< / highlight >}} +--- +outdated: true +showInBlog: false +title: 'Access Puppet Forge Repository with Java' +date: "2012-12-10" +author: hendrik +categories: [Vagrant-Binding] +excerpt: 'To integrate Puppet modules more easy to Vagrant-Binding projects I created a Java API that access Puppet Forge by REST.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +To integrate Puppet modules more easy to [Vagrant-Binding](/posts/2012-11-01-introducing-vagrant-binding) projects I created a Java API that access Puppet Forge by REST. You can search for modules or download any module to your locale disc. So you can easily add modules from the repository to your own Vagrant environments. The API is currently not part of Vagrant-Binding and can be found [here](https://github.com/guigarage/puppet-forge-ws) but I plan to integrate the API in the future. Additionally I added a demo to the [Vagrant-Binding-Demos]({{ site.baseurl }}{% post_url 2012-11-03-vagrant-binding-demos %} +) project that can be found at [github](https://github.com/guigarage/vagrant-binding-demos/blob/master/src/main/java/com/guigarage/vagrant/tutorials/PuppetTutorial2.java). Here is a short example that uses the API: + +```javaPuppetForgeClient client = new PuppetForgeClient(); +File moduleFolder = new File("/path/to/my/modules"); + +ListallDescriptions = client.findModules("mongodb"); + +for(PuppetForgeModuleDescription description : allDescriptions) { + System.out.println("Installing " + description.getFullName()); + PuppetForgeModule module = client.findModule(description); + client.installToModulesDir(moduleFolder, module); +} ``` diff --git a/content/posts/2012-12-12-some-news-about-vagrant-binding.md b/content/posts/2012-12-12-some-news-about-vagrant-binding.md index 4fad5c8c..bb0a7593 100644 --- a/content/posts/2012-12-12-some-news-about-vagrant-binding.md +++ b/content/posts/2012-12-12-some-news-about-vagrant-binding.md @@ -8,4 +8,4 @@ categories: [General, Vagrant-Binding] excerpt: 'Yesterday I gave a Vagrant-Binding talk at JUG Dortmund. I received really great feedback and thanks to some productive discussions the future of the API is getting more clearly to me.' preview_image: "/posts/preview-images/software-development-green.svg" --- -Yesterday I gave a [Vagrant-Binding]({{< ref "/posts/2012-11-01-introducing-vagrant-binding" >}}) presentation at [JUG Dortmund](http://www.jugdo.de/?p=75). I received really great feedback and thanks to some productive discussions the future of the API is getting more clearly to me. You can download the slides of the talk [here](http://de.slideshare.net/HendrikEbbers/vagrant-puppet-jug-2). The German jaxenter has published an [article about Vagrant-Binding](http://it-republik.de/jaxenter/artikel/Einweg-VM-zur-Runtime-erstellen-5516.html) today. So it seems that there are really people out there wanting to use this stuff. Thanks for all the positive feedback and support. +Yesterday I gave a [Vagrant-Binding](/posts/2012-11-01-introducing-vagrant-binding) presentation at [JUG Dortmund](http://www.jugdo.de/?p=75). I received really great feedback and thanks to some productive discussions the future of the API is getting more clearly to me. You can download the slides of the talk [here](http://de.slideshare.net/HendrikEbbers/vagrant-puppet-jug-2). The German jaxenter has published an [article about Vagrant-Binding](http://it-republik.de/jaxenter/artikel/Einweg-VM-zur-Runtime-erstellen-5516.html) today. So it seems that there are really people out there wanting to use this stuff. Thanks for all the positive feedback and support. diff --git a/content/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi.md b/content/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi.md index 551a0c34..927a8942 100644 --- a/content/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi.md +++ b/content/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi.md @@ -12,7 +12,7 @@ Today I started playing with my Pi & JavaFX. Even after a few hours I can say th I created a screensaver as a first demo. The programm loads random pictures (2848 × 4288 pixel) and animates them on the screen: -{{< youtube r0GEm1pEhoE >}} + I used a experimental build of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) for loading and scaling all the pictures. In my point of view the performance of all animations is awesome. Oracle has really done a great work! diff --git a/content/posts/2012-12-29-gridfx-on-raspberry-pi-javafx-8.md b/content/posts/2012-12-29-gridfx-on-raspberry-pi-javafx-8.md index 0053ec32..1fc0d441 100644 --- a/content/posts/2012-12-29-gridfx-on-raspberry-pi-javafx-8.md +++ b/content/posts/2012-12-29-gridfx-on-raspberry-pi-javafx-8.md @@ -1,35 +1,31 @@ ---- -outdated: true -showInBlog: false -title: 'GridFX on Raspberry Pi & JavaFX 8' -date: "2012-12-29" -author: hendrik -categories: [IoT, JavaFX] -excerpt: 'For my first JFX demo I created a experimental GridFX version for JavaFX 8.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -For my [first JFX demo]({{< ref "/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi" >}}) I tried to create a experimental [GridFX]({{ site.baseurl }}{% link pages/projects/gridfx.md %}) version for JavaFX 8. As I mentioned in an [earlier post]({{< ref "/posts/2012-11-17-custom-ui-controls-with-javafx-part-1" >}}) there are some API changed between JavaFX 2 and 8. For GridFX I had to change two things: - -* The BaseSkin class is now public -* The CSS support that I used in GridFX seams to work completely different in JavaFX 8. - -Both issues were fixed very fast and an GridFX demo was running on my Mac where I have JDK "build 1.8.0-ea-b69" installed ([download it here](http://jdk8.java.net/download.html)). But after deploying the demo on my Pi nothing happened... - -So I created a log file for the java output by adding some parameters to the shell command: - -{{< highlight shell >}} -java -cp myApp.jar com.guigarage.Demo >log 2>&1 -{{< / highlight >}} - -The logging showed me the cause of my problems: - -{{< highlight shell >}} -java.lang.NoSuchMethodError: javafx.scene.control.SkinBase.(Ljavafx/scene/control/Control;)V at com.guigarage.fx.grid.skin.GridViewSkin.(GridViewSkin.java:24) -{{< / highlight >}} - -While everything compiled perfectly on my Mac, the Pi has a different JavaFX compilation / version ("build 1.8.0-ea-b36e"). As a first solution I downloaded the "jfxrt.jar" file from my Pi and included it to the GridFX project on my Mac. After doing so I saw all the compilation problems in Eclipse and had a change to resolve them. It would be very nice to have a real 1.8.0-ea-b36 build installed on my Mac. Any idea where to get it? - -But after fixing all this problems the GridFX demo is running on my Pi: - -{{< youtube OZv6WUpEzS8 >}} - +--- +outdated: true +showInBlog: false +title: 'GridFX on Raspberry Pi & JavaFX 8' +date: "2012-12-29" +author: hendrik +categories: [IoT, JavaFX] +excerpt: 'For my first JFX demo I created a experimental GridFX version for JavaFX 8.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +For my [first JFX demo](/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi) I tried to create a experimental [GridFX]({{ site.baseurl }}{% link pages/projects/gridfx.md %}) version for JavaFX 8. As I mentioned in an [earlier post](/posts/2012-11-17-custom-ui-controls-with-javafx-part-1) there are some API changed between JavaFX 2 and 8. For GridFX I had to change two things: + +* The BaseSkin class is now public +* The CSS support that I used in GridFX seams to work completely different in JavaFX 8. + +Both issues were fixed very fast and an GridFX demo was running on my Mac where I have JDK "build 1.8.0-ea-b69" installed ([download it here](http://jdk8.java.net/download.html)). But after deploying the demo on my Pi nothing happened... + +So I created a log file for the java output by adding some parameters to the shell command: + +```shelljava -cp myApp.jar com.guigarage.Demo >log 2>&1 ``` + +The logging showed me the cause of my problems: + +```shelljava.lang.NoSuchMethodError: javafx.scene.control.SkinBase.(Ljavafx/scene/control/Control;)V at com.guigarage.fx.grid.skin.GridViewSkin.(GridViewSkin.java:24) ``` + +While everything compiled perfectly on my Mac, the Pi has a different JavaFX compilation / version ("build 1.8.0-ea-b36e"). As a first solution I downloaded the "jfxrt.jar" file from my Pi and included it to the GridFX project on my Mac. After doing so I saw all the compilation problems in Eclipse and had a change to resolve them. It would be very nice to have a real 1.8.0-ea-b36 build installed on my Mac. Any idea where to get it? + +But after fixing all this problems the GridFX demo is running on my Pi: + + + diff --git a/content/posts/2012-12-31-bindabletransition.md b/content/posts/2012-12-31-bindabletransition.md index 8803cebc..7b68809f 100644 --- a/content/posts/2012-12-31-bindabletransition.md +++ b/content/posts/2012-12-31-bindabletransition.md @@ -1,42 +1,38 @@ ---- -outdated: true -showInBlog: false -title: 'BindableTransition' -date: "2012-12-31" -author: hendrik -categories: [General, JavaFX] -excerpt: 'JavaFX supports a lot of transition and animation classes. But sometimes you need a special animation for that no default transition is provided by JavaFX.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -JavaFX supports a lot of [transition and animation](http://docs.oracle.com/javafx/2/animations/basics.htm#CJAJJAGI) classes for Node properties like the [`javafx.animation.ScaleTransition`](http://docs.oracle.com/javafx/2/api/javafx/animation/ScaleTransition.html). But sometimes you need a special animation for that no default transition is provided by JavaFX. Currently the best pratice is to extend [`javafx.animation.Transition`](http://docs.oracle.com/javafx/2/api/javafx/animation/Transition.html) and override the `interpolate(double frac)` method: - -{{< highlight java >}} -protected void interpolate(double frac) { - myProperty.set(frac); -} -{{< / highlight >}} - -Because JavaFX offers [PropertyBinding](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm) I created a `BindableTransition` which current fraction is a `Property` and can be bind to any other `NumberProperty`. Here is an example: - -{{< highlight java >}} -Button button = new Button("BindableTransition"); -DropShadow shadow = DropShadowBuilder.create().build(); -button.setEffect(shadow); - -final Duration duration = Duration.millis(1200); -BindableTransition transition = new BindableTransition(duration); -transition.setCycleCount(1000); -transition.setAutoReverse(true); - -shadow.offsetXProperty().bind(transition.fractionProperty().multiply(32)); -shadow.offsetYProperty().bind(transition.fractionProperty().multiply(32)); -button.translateXProperty().bind(transition.fractionProperty().multiply(-32)); - -transition.play(); -{{< / highlight >}} - -The `fractionProperty` of the `BindableTransition` is bound to three different properties and the result will look like this: - -{{< vimeo 56550389 >}} - -The [BindableTransition](https://github.com/JFXtras/jfxtras-labs/blob/master/src/main/java/jfxtras/labs/animation/BindableTransition.java) class and a [demo](https://github.com/JFXtras/jfxtras-labs/blob/master/src/test/java/jfxtras/labs/animation/BindableTransitionTrial.java)are commited to the [JFXtras project](http://jfxtras.org). +--- +outdated: true +showInBlog: false +title: 'BindableTransition' +date: "2012-12-31" +author: hendrik +categories: [General, JavaFX] +excerpt: 'JavaFX supports a lot of transition and animation classes. But sometimes you need a special animation for that no default transition is provided by JavaFX.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +JavaFX supports a lot of [transition and animation](http://docs.oracle.com/javafx/2/animations/basics.htm#CJAJJAGI) classes for Node properties like the [`javafx.animation.ScaleTransition`](http://docs.oracle.com/javafx/2/api/javafx/animation/ScaleTransition.html). But sometimes you need a special animation for that no default transition is provided by JavaFX. Currently the best pratice is to extend [`javafx.animation.Transition`](http://docs.oracle.com/javafx/2/api/javafx/animation/Transition.html) and override the `interpolate(double frac)` method: + +```javaprotected void interpolate(double frac) { + myProperty.set(frac); +} ``` + +Because JavaFX offers [PropertyBinding](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm) I created a `BindableTransition` which current fraction is a `Property` and can be bind to any other `NumberProperty`. Here is an example: + +```javaButton button = new Button("BindableTransition"); +DropShadow shadow = DropShadowBuilder.create().build(); +button.setEffect(shadow); + +final Duration duration = Duration.millis(1200); +BindableTransition transition = new BindableTransition(duration); +transition.setCycleCount(1000); +transition.setAutoReverse(true); + +shadow.offsetXProperty().bind(transition.fractionProperty().multiply(32)); +shadow.offsetYProperty().bind(transition.fractionProperty().multiply(32)); +button.translateXProperty().bind(transition.fractionProperty().multiply(-32)); + +transition.play(); ``` + +The `fractionProperty` of the `BindableTransition` is bound to three different properties and the result will look like this: + + + +The [BindableTransition](https://github.com/JFXtras/jfxtras-labs/blob/master/src/main/java/jfxtras/labs/animation/BindableTransition.java) class and a [demo](https://github.com/JFXtras/jfxtras-labs/blob/master/src/test/java/jfxtras/labs/animation/BindableTransitionTrial.java)are commited to the [JFXtras project](http://jfxtras.org). diff --git a/content/posts/2013-01-01-invokeandwait-for-javafx.md b/content/posts/2013-01-01-invokeandwait-for-javafx.md index 3b02080c..3d297b26 100644 --- a/content/posts/2013-01-01-invokeandwait-for-javafx.md +++ b/content/posts/2013-01-01-invokeandwait-for-javafx.md @@ -1,84 +1,82 @@ ---- -outdated: true -showInBlog: false -title: 'invokeAndWait for JavaFX' -date: "2013-01-01" -author: hendrik -categories: [DataFX, General, JavaFX] -excerpt: "Swing offers the two methods SwingUtilities.invokeAndWait(...) and SwingUtilities.invokeLater(...) to execute a Runnable object on Swings event dispatching thread. Let's have a look how we can have the same functionallity in JavaFX" -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Swing offers the two methods `SwingUtilities.invokeAndWait(...)` and `SwingUtilities.invokeLater(...) to execute a Runnable object on Swings event dispatching thread. You can read more about this methods [http://javarevisited.blogspot.de/2011/09/invokeandwait-invokelater-swing-example.html](here). - -As I currently know JavaFX provides only `Platform.runLater(...)` that is the equivalent of SwingUtilities.invokeLater(...). A "runAndWait" method doesn't exist at the moment. While developing some [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) stuff and my [first Raspberry Pi demo]({{< ref "/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi" >}}) I needed this feature in JavaFX. So I created a `runAndWait` method that will hopefully be part of DataFX in some future. Until then you can use this code in your project: - -{{< highlight java >}} -import java.util.concurrent.ExecutionException; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import javafx.application.Platform; - -public class FXUtilities { - - private static class ThrowableWrapper { - Throwable t; - } - - /** - * Invokes a Runnable in JFX Thread and waits while it's finished. Like - * SwingUtilities.invokeAndWait does for EDT. - * - * @param run - * The Runnable that has to be called on JFX thread. - * @throws InterruptedException - * f the execution is interrupted. - * @throws ExecutionException - * If a exception is occurred in the run method of the Runnable - */ - public static void runAndWait(final Runnable run) - throws InterruptedException, ExecutionException { - if (Platform.isFxApplicationThread()) { - try { - run.run(); - } catch (Exception e) { - throw new ExecutionException(e); - } - } else { - final Lock lock = new ReentrantLock(); - final Condition condition = lock.newCondition(); - final ThrowableWrapper throwableWrapper = new ThrowableWrapper(); - lock.lock(); - try { - Platform.runLater(new Runnable() { - - @Override - public void run() { - lock.lock(); - try { - run.run(); - } catch (Throwable e) { - throwableWrapper.t = e; - } finally { - try { - condition.signal(); - } finally { - lock.unlock(); - } - } - } - }); - condition.await(); - if (throwableWrapper.t != null) { - throw new ExecutionException(throwableWrapper.t); - } - } finally { - lock.unlock(); - } - } - } -} -{{< / highlight >}} - -It's working for all my needs. Please give me some feedback if there are any problems or bug. +--- +outdated: true +showInBlog: false +title: 'invokeAndWait for JavaFX' +date: "2013-01-01" +author: hendrik +categories: [DataFX, General, JavaFX] +excerpt: "Swing offers the two methods SwingUtilities.invokeAndWait(...) and SwingUtilities.invokeLater(...) to execute a Runnable object on Swings event dispatching thread. Let's have a look how we can have the same functionallity in JavaFX" +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Swing offers the two methods `SwingUtilities.invokeAndWait(...)` and `SwingUtilities.invokeLater(...) to execute a Runnable object on Swings event dispatching thread. You can read more about this methods [http://javarevisited.blogspot.de/2011/09/invokeandwait-invokelater-swing-example.html](here). + +As I currently know JavaFX provides only `Platform.runLater(...)` that is the equivalent of SwingUtilities.invokeLater(...). A "runAndWait" method doesn't exist at the moment. While developing some [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) stuff and my [first Raspberry Pi demo](/posts/2012-12-28-my-first-steps-with-javafx-on-raspberry-pi) I needed this feature in JavaFX. So I created a `runAndWait` method that will hopefully be part of DataFX in some future. Until then you can use this code in your project: + +```javaimport java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javafx.application.Platform; + +public class FXUtilities { + + private static class ThrowableWrapper { + Throwable t; + } + + /** + * Invokes a Runnable in JFX Thread and waits while it's finished. Like + * SwingUtilities.invokeAndWait does for EDT. + * + * @param run + * The Runnable that has to be called on JFX thread. + * @throws InterruptedException + * f the execution is interrupted. + * @throws ExecutionException + * If a exception is occurred in the run method of the Runnable + */ + public static void runAndWait(final Runnable run) + throws InterruptedException, ExecutionException { + if (Platform.isFxApplicationThread()) { + try { + run.run(); + } catch (Exception e) { + throw new ExecutionException(e); + } + } else { + final Lock lock = new ReentrantLock(); + final Condition condition = lock.newCondition(); + final ThrowableWrapper throwableWrapper = new ThrowableWrapper(); + lock.lock(); + try { + Platform.runLater(new Runnable() { + + @Override + public void run() { + lock.lock(); + try { + run.run(); + } catch (Throwable e) { + throwableWrapper.t = e; + } finally { + try { + condition.signal(); + } finally { + lock.unlock(); + } + } + } + }); + condition.await(); + if (throwableWrapper.t != null) { + throw new ExecutionException(throwableWrapper.t); + } + } finally { + lock.unlock(); + } + } + } +} ``` + +It's working for all my needs. Please give me some feedback if there are any problems or bug. diff --git a/content/posts/2013-01-10-custom-ui-controls-with-javafx-part3.md b/content/posts/2013-01-10-custom-ui-controls-with-javafx-part3.md index acbd0678..1b467302 100644 --- a/content/posts/2013-01-10-custom-ui-controls-with-javafx-part3.md +++ b/content/posts/2013-01-10-custom-ui-controls-with-javafx-part3.md @@ -1,222 +1,198 @@ ---- -outdated: true -showInBlog: false -title: 'Custom UI Controls with JavaFX (Part3)' -date: "2013-01-10" -author: hendrik -categories: [JavaFX] -excerpt: 'In this post I will explain the basic JavaFX Property API. I will first explain the old Swing way to better understand the idea of the new API and all problems that are solved by it.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In this post I will explain the basic JavaFX Property API because most of the planed future tutorials will depend on Properties. I will first explain the old Swing way to better understand the idea of the new API and all problems that are solved by it. - -## Properties in Swing - -In Swing every Component is a [JavaBean](http://docs.oracle.com/javase/tutorial/javabeans/) and its fields (bean properties) are accessible by getters and setters. If you want to change the text of a button you have to call the setter method: - -{{< highlight java >}} -jbutton.setText("click me"); - -You can receive the current text by calling the getter method: - -String currentText = jbutton.getText(); -{{< / highlight >}} - -With this methods you can easily modify and control your user interface in a static way. For example you can receive and check all entered data by calling the required getter methods on your components once the save button is pressed. - -But in the last years user experience has changed. Data should be validated in the moment it has been entered by the user and the background color of a application should change on the fly while the user is dropping the mouse over the colorchooser. All this things can be done with Swing and [PropertyChangeSupport](http://docs.oracle.com/javase/6/docs/api/java/beans/PropertyChangeSupport.html). In a well coded Swing Control every change of a bean property fires a PropertyChangeEvent to all its listeners. The official Swing components offer this feature for all properties. Here is a short example that shows `PropertyChangeSupport` in a custom JComponent: - -{{< highlight java >}} -public class JCustomComponent extends JComponent { - - private int volume; - - public int getVolume() { - return volume; - } - - public void setVolume(int volume) { - int oldValue = this.volume; - this.volume = volume; - firePropertyChange("volume", oldValue, this.volume); - } -} -{{< / highlight >}} - -It's quite simple to access this feature outside of the component: - -{{< highlight java >}} -JCustomComponent customComponent = new JCustomComponent(); -customComponent.addPropertyChangeListener("volume", new PropertyChangeListener() { - - @Override - public void propertyChange(PropertyChangeEvent evt) { - System.out.println("Volume changed to " + evt.getNewValue()); - } -}); -{{< / highlight >}} - -The problem is that all this stuff produces a lot of redundant code and do not last for modern and big applications. - -## Glue code and BeansBinding - -Most of the time `PropertyChangeSupport` is used in Swing applications to connect the view to a data model. By doing so a lot of code looks like this: - -{{< highlight java >}} -//example code from http://goo.gl/T6Iqg -slider.addValueListener(new ChangeListener() { - public void stateChanged(ChangeEvent e) { - selectedObject.setFoo(slider.getValue()); - } -}); - -selectedObject.addPropertyChangeListener(new PropertyChangeListener() { - public void propertyChanged(PropertyChangeEvent e) { - if (e.getPropertyName() == null || "foo".equals(e.getPropertyName())) { - slider.setValue(selectedObject.getFoo()); - } - } -}); -{{< / highlight >}} - -This code is really painful and vulnerable to (copy & paste) errors. Thanks to the BeansBinding API the modern Swing developer do not need this glue code and simply bind properties. The API allow to bind two different properties with each other so that all changes are automatically adopted by the opposite side. Here is a short example of a binding: - -{{< highlight java >}} -//example code from http://goo.gl/KmGz1 -Property slideValue = BeanProperty.create("value"); -Property tintValue = BeanProperty.create("tint"); -Binding tintBinding = Bindings.createAutoBinding(UpdateStrategy.READ, tintSlider, slideValue, tintedPanel, tintValue); -tintBinding.bind(); -tintSlider.setValue(0); -{{< / highlight >}} - -With the API you can create one way or bidirectional bindings. The difference is that in a one way binding only one side is a observer. In a bidirectional binding every change on any side is adopted by the opposite side. - -If you are interested in BeansBinding with Swing I suggest you to [read](http://today.java.net/pub/a/2008/03/20/synchronizing-properties-with-beans-binding.html) [this](http://weblogs.java.net/blog/zixle/archive/2006/05/ease_of_swing_d.html) [articles](https://blogs.oracle.com/geertjan/entry/beans_binding_via_the_road). - -## JavaFX Properties - -JavaFX includes the [`javafx.beans.property.Property`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/Property.html) Interface that extend property handling and binding with some great features and a very simple but powerful API. All JavaFX controls use the property API to grant access to their fields. Normally next to the getter & setter methods there is a new method to access the property. Here is a example for the "double cellWidth" attribute of [GridView]({{< ref "/posts/2012-11-29-gridfx-is-moving-forward" >}}): - -{{< highlight java >}} -private DoubleProperty cellWidth; - -public final DoubleProperty cellWidthProperty() { - if (cellWidth == null) { - cellWidth = new SimpleDoubleProperty(64); - } - return cellWidth; -} - -public void setCellWidth(double value) { - cellWidthProperty().set(value); -} - -public double getCellWidth() { - return cellWidth == null ? 64.0 : cellWidth.get(); -} -{{< / highlight >}} - -As you can see there is no "double cellWidth" field in the code. Instead of this the attribute is wrapped in a Property. JavaFX offers a set of basic property classes for primitive datatypes like String or double. All this basic implementations are part of the package [`javafx.beans.property.*`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/package-summary.html). - -The getter and setter methods work directly with the Property instance and set or request the current value from the property. Next to all this `Simple*Property` classes there are some special implementations like read only implementations that can be used if you want to prevent your field from external changes. In this case only removing the setter-method is not enough because you can access the Property instance. It's recommend to use `ReadOnly**Property` classes (like [`ReadOnlyDoubleProperty`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/ReadOnlyDoubleProperty.html)) in this case. - -## The benefit of Properties - -By using the above described design for bean properties in JavaFX you will get a lot of pros in your code. First of all JavaFX properties offer support for [`ChangeListener`](http://docs.oracle.com/javafx/2/api/javafx/beans/value/ChangeListener.html). So you can add listeners to every property: - -{{< highlight java >}} -SimpleStringProperty textProp = new SimpleStringProperty(); -textProp.addListener(new ChangeListener() { - - @Override - public void changed(ObservableValue observableValue, String oldValue, String newValue) { - System.out.println("Value changed: " + oldValue + " -> " + newValue); - } -}); - -Slider mySlider = new Slider(); -mySlider.valueProperty().addListener(new ChangeListener() { - - @Override - public void changed(ObservableValue observableValue, Number oldValue, Number newValue) { - System.out.println("Value changed!"); - } -}); -{{< / highlight >}} - -The usage of `ChangeListener` in JavaFX is equivalent to PropertyChangeSupport in Swing. But in my eyes there are some benefits. The code looks much cleaner and it's very easy to add a listener to one specific field / property. In Swing you have to add the field name as a String parameter and produce plenty of refactoring risk. Next to the ChangeLister you can register [`InvalidationListener`](http://docs.oracle.com/javafx/2/api/javafx/beans/InvalidationListener.html) to your properties. You can read more about the difference between this two listener types [here](http://blog.netopyr.com/2012/02/08/when-to-use-a-changelistener-or-an-invalidationlistener/). - -## Let's bind this stuff - -Another and in my opinion the best feature of JavaFX properties is the binding function. For this the Property interface offers the following methods: - -{{< highlight java >}} -void bind(javafx.beans.value.ObservableValue other); - -void unbind(); - -boolean isBound(); - -void bindBidirectional(javafx.beans.property.Property other); - -void unbindBidirectional(javafx.beans.property.Property other); -{{< / highlight >}} - -By using this methods you can create bindings between JavaFX properties very easy and with much cleaner code than in Swing. In the following example the value of a slider will be bound to the cell width of a grid: - -{{< highlight java >}} -GridView myGrid = new GridView<>(); -Slider columnWidthSlider = SliderBuilder.create().min(10).max(512).build(); -myGrid.cellWidthProperty().bind(columnWidthSlider.valueProperty()); -{{< / highlight >}} - -By doing so the change of the slider will directly change the cell width of the grid because this property is bound to the value property of the slider. You can see the result in a video (0:20): - -{{< vimeo 53462905 >}} - -By using the `bind(..)` method a change of the column width will not influence the slider value because we have a one way binding. But creating a bidirectional binding is easy as pie: - -{{< highlight java >}} -Slider mySlider1 = SliderBuilder.create().min(10).max(512).build(); -Slider mySlider2 = SliderBuilder.create().min(10).max(512).build(); -mySlider1.valueProperty().bindBidirectional(mySlider2.valueProperty()); -{{< / highlight >}} - -Now whatever slider is changed, the other one will adopt it's value: - -{{< vimeo 57128737 >}} - -You can simply remove a binding by calling `property.unbind()` in your code. - -With this methods you can easily bind two or more properties with the same value type (String, double, etc.). But sometimes you need a more complex binding. Maybe you need to bind a slider value to the visible property of a label. The label should appear once the slider value reaches a maximum. The JavaFX Property API offers some conversion methods for this needs. Most property types provides specific methods that create a new binding. Here is an sample that uses some of this methods: - -{{< highlight java >}} -Slider mySlider1 = new Slider(); -Label myLabel = LabelBuilder.create().text("ALERT!").visible(false).build(); -myLabel.visibleProperty().bind(mySlider1.valueProperty().multiply(2).greaterThan(100)); -{{< / highlight >}} - -In line 3 the valueProperty in converted to a new double binding that is always double the size of the wrapped property. Now by calling the greaterThan(..) method we create a boolean binding that is wrapped around the double binding. This bindings value is true while the wrapped value is > 100. So if the value of the slider is greater than 50 (50 * 2 > 100) the label will be visible: - -{{< vimeo 57133467 >}} - -Next to this functions there is the util class [`javafx.beans.binding.Bindings`](http://docs.oracle.com/javafx/2/api/javafx/beans/binding/Bindings.html) that provides a lot of additional functions and support. For example you can add converters to a binding by using this class: - -{{< highlight java >}} -SimpleIntegerProperty intProp = new SimpleIntegerProperty(); -SimpleStringProperty textProp = new SimpleStringProperty(); -StringConverter converter = new IntegerStringConverter(); - -Bindings.bindBidirectional(textProp, intProp, (StringConverter)converter); -{{< / highlight >}} - -Once you set the value of the StringProperty to "8" the IntegerProperty will update it's wrapped value to 8 and vice versa. - -You can read more about the Property API & binding [here](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm). - -## Properties for custom controls - -For custom components in JavaFX it is highly recommend to provide properties for all attributes of the control. By doing so you and other developers can easily bind this attributes to other properties or add change listener to them. Many JavaFX APIs (basic & 3rdParty) support the Property API. For example the next release of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) will provide properties for all received data so that you can easily bind your control attributes to the data that will be loaded in background. So you can bind the items inside a ListView to the result of a REST request with only one line of code. - -One of my next tutorials will show how you can bind your custom control properties to CSS properties. +--- +outdated: true +showInBlog: false +title: 'Custom UI Controls with JavaFX (Part3)' +date: "2013-01-10" +author: hendrik +categories: [JavaFX] +excerpt: 'In this post I will explain the basic JavaFX Property API. I will first explain the old Swing way to better understand the idea of the new API and all problems that are solved by it.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In this post I will explain the basic JavaFX Property API because most of the planed future tutorials will depend on Properties. I will first explain the old Swing way to better understand the idea of the new API and all problems that are solved by it. + +## Properties in Swing + +In Swing every Component is a [JavaBean](http://docs.oracle.com/javase/tutorial/javabeans/) and its fields (bean properties) are accessible by getters and setters. If you want to change the text of a button you have to call the setter method: + +```javajbutton.setText("click me"); + +You can receive the current text by calling the getter method: + +String currentText = jbutton.getText(); ``` + +With this methods you can easily modify and control your user interface in a static way. For example you can receive and check all entered data by calling the required getter methods on your components once the save button is pressed. + +But in the last years user experience has changed. Data should be validated in the moment it has been entered by the user and the background color of a application should change on the fly while the user is dropping the mouse over the colorchooser. All this things can be done with Swing and [PropertyChangeSupport](http://docs.oracle.com/javase/6/docs/api/java/beans/PropertyChangeSupport.html). In a well coded Swing Control every change of a bean property fires a PropertyChangeEvent to all its listeners. The official Swing components offer this feature for all properties. Here is a short example that shows `PropertyChangeSupport` in a custom JComponent: + +```javapublic class JCustomComponent extends JComponent { + + private int volume; + + public int getVolume() { + return volume; + } + + public void setVolume(int volume) { + int oldValue = this.volume; + this.volume = volume; + firePropertyChange("volume", oldValue, this.volume); + } +} ``` + +It's quite simple to access this feature outside of the component: + +```javaJCustomComponent customComponent = new JCustomComponent(); +customComponent.addPropertyChangeListener("volume", new PropertyChangeListener() { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + System.out.println("Volume changed to " + evt.getNewValue()); + } +}); ``` + +The problem is that all this stuff produces a lot of redundant code and do not last for modern and big applications. + +## Glue code and BeansBinding + +Most of the time `PropertyChangeSupport` is used in Swing applications to connect the view to a data model. By doing so a lot of code looks like this: + +```java//example code from http://goo.gl/T6Iqg +slider.addValueListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + selectedObject.setFoo(slider.getValue()); + } +}); + +selectedObject.addPropertyChangeListener(new PropertyChangeListener() { + public void propertyChanged(PropertyChangeEvent e) { + if (e.getPropertyName() == null || "foo".equals(e.getPropertyName())) { + slider.setValue(selectedObject.getFoo()); + } + } +}); ``` + +This code is really painful and vulnerable to (copy & paste) errors. Thanks to the BeansBinding API the modern Swing developer do not need this glue code and simply bind properties. The API allow to bind two different properties with each other so that all changes are automatically adopted by the opposite side. Here is a short example of a binding: + +```java//example code from http://goo.gl/KmGz1 +Property slideValue = BeanProperty.create("value"); +Property tintValue = BeanProperty.create("tint"); +Binding tintBinding = Bindings.createAutoBinding(UpdateStrategy.READ, tintSlider, slideValue, tintedPanel, tintValue); +tintBinding.bind(); +tintSlider.setValue(0); ``` + +With the API you can create one way or bidirectional bindings. The difference is that in a one way binding only one side is a observer. In a bidirectional binding every change on any side is adopted by the opposite side. + +If you are interested in BeansBinding with Swing I suggest you to [read](http://today.java.net/pub/a/2008/03/20/synchronizing-properties-with-beans-binding.html) [this](http://weblogs.java.net/blog/zixle/archive/2006/05/ease_of_swing_d.html) [articles](https://blogs.oracle.com/geertjan/entry/beans_binding_via_the_road). + +## JavaFX Properties + +JavaFX includes the [`javafx.beans.property.Property`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/Property.html) Interface that extend property handling and binding with some great features and a very simple but powerful API. All JavaFX controls use the property API to grant access to their fields. Normally next to the getter & setter methods there is a new method to access the property. Here is a example for the "double cellWidth" attribute of [GridView](/posts/2012-11-29-gridfx-is-moving-forward): + +```javaprivate DoubleProperty cellWidth; + +public final DoubleProperty cellWidthProperty() { + if (cellWidth == null) { + cellWidth = new SimpleDoubleProperty(64); + } + return cellWidth; +} + +public void setCellWidth(double value) { + cellWidthProperty().set(value); +} + +public double getCellWidth() { + return cellWidth == null ? 64.0 : cellWidth.get(); +} ``` + +As you can see there is no "double cellWidth" field in the code. Instead of this the attribute is wrapped in a Property. JavaFX offers a set of basic property classes for primitive datatypes like String or double. All this basic implementations are part of the package [`javafx.beans.property.*`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/package-summary.html). + +The getter and setter methods work directly with the Property instance and set or request the current value from the property. Next to all this `Simple*Property` classes there are some special implementations like read only implementations that can be used if you want to prevent your field from external changes. In this case only removing the setter-method is not enough because you can access the Property instance. It's recommend to use `ReadOnly**Property` classes (like [`ReadOnlyDoubleProperty`](http://docs.oracle.com/javafx/2/api/javafx/beans/property/ReadOnlyDoubleProperty.html)) in this case. + +## The benefit of Properties + +By using the above described design for bean properties in JavaFX you will get a lot of pros in your code. First of all JavaFX properties offer support for [`ChangeListener`](http://docs.oracle.com/javafx/2/api/javafx/beans/value/ChangeListener.html). So you can add listeners to every property: + +```javaSimpleStringProperty textProp = new SimpleStringProperty(); +textProp.addListener(new ChangeListener() { + + @Override + public void changed(ObservableValue observableValue, String oldValue, String newValue) { + System.out.println("Value changed: " + oldValue + " -> " + newValue); + } +}); + +Slider mySlider = new Slider(); +mySlider.valueProperty().addListener(new ChangeListener() { + + @Override + public void changed(ObservableValue observableValue, Number oldValue, Number newValue) { + System.out.println("Value changed!"); + } +}); ``` + +The usage of `ChangeListener` in JavaFX is equivalent to PropertyChangeSupport in Swing. But in my eyes there are some benefits. The code looks much cleaner and it's very easy to add a listener to one specific field / property. In Swing you have to add the field name as a String parameter and produce plenty of refactoring risk. Next to the ChangeLister you can register [`InvalidationListener`](http://docs.oracle.com/javafx/2/api/javafx/beans/InvalidationListener.html) to your properties. You can read more about the difference between this two listener types [here](http://blog.netopyr.com/2012/02/08/when-to-use-a-changelistener-or-an-invalidationlistener/). + +## Let's bind this stuff + +Another and in my opinion the best feature of JavaFX properties is the binding function. For this the Property interface offers the following methods: + +```javavoid bind(javafx.beans.value.ObservableValue other); + +void unbind(); + +boolean isBound(); + +void bindBidirectional(javafx.beans.property.Property other); + +void unbindBidirectional(javafx.beans.property.Property other); ``` + +By using this methods you can create bindings between JavaFX properties very easy and with much cleaner code than in Swing. In the following example the value of a slider will be bound to the cell width of a grid: + +```javaGridView myGrid = new GridView<>(); +Slider columnWidthSlider = SliderBuilder.create().min(10).max(512).build(); +myGrid.cellWidthProperty().bind(columnWidthSlider.valueProperty()); ``` + +By doing so the change of the slider will directly change the cell width of the grid because this property is bound to the value property of the slider. You can see the result in a video (0:20): + + + +By using the `bind(..)` method a change of the column width will not influence the slider value because we have a one way binding. But creating a bidirectional binding is easy as pie: + +```javaSlider mySlider1 = SliderBuilder.create().min(10).max(512).build(); +Slider mySlider2 = SliderBuilder.create().min(10).max(512).build(); +mySlider1.valueProperty().bindBidirectional(mySlider2.valueProperty()); ``` + +Now whatever slider is changed, the other one will adopt it's value: + + + +You can simply remove a binding by calling `property.unbind()` in your code. + +With this methods you can easily bind two or more properties with the same value type (String, double, etc.). But sometimes you need a more complex binding. Maybe you need to bind a slider value to the visible property of a label. The label should appear once the slider value reaches a maximum. The JavaFX Property API offers some conversion methods for this needs. Most property types provides specific methods that create a new binding. Here is an sample that uses some of this methods: + +```javaSlider mySlider1 = new Slider(); +Label myLabel = LabelBuilder.create().text("ALERT!").visible(false).build(); +myLabel.visibleProperty().bind(mySlider1.valueProperty().multiply(2).greaterThan(100)); ``` + +In line 3 the valueProperty in converted to a new double binding that is always double the size of the wrapped property. Now by calling the greaterThan(..) method we create a boolean binding that is wrapped around the double binding. This bindings value is true while the wrapped value is > 100. So if the value of the slider is greater than 50 (50 * 2 > 100) the label will be visible: + + + +Next to this functions there is the util class [`javafx.beans.binding.Bindings`](http://docs.oracle.com/javafx/2/api/javafx/beans/binding/Bindings.html) that provides a lot of additional functions and support. For example you can add converters to a binding by using this class: + +```javaSimpleIntegerProperty intProp = new SimpleIntegerProperty(); +SimpleStringProperty textProp = new SimpleStringProperty(); +StringConverter converter = new IntegerStringConverter(); + +Bindings.bindBidirectional(textProp, intProp, (StringConverter)converter); ``` + +Once you set the value of the StringProperty to "8" the IntegerProperty will update it's wrapped value to 8 and vice versa. + +You can read more about the Property API & binding [here](http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm). + +## Properties for custom controls + +For custom components in JavaFX it is highly recommend to provide properties for all attributes of the control. By doing so you and other developers can easily bind this attributes to other properties or add change listener to them. Many JavaFX APIs (basic & 3rdParty) support the Property API. For example the next release of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) will provide properties for all received data so that you can easily bind your control attributes to the data that will be loaded in background. So you can bind the items inside a ListView to the result of a REST request with only one line of code. + +One of my next tutorials will show how you can bind your custom control properties to CSS properties. diff --git a/content/posts/2013-01-16-this-is-for-the-native-ones.md b/content/posts/2013-01-16-this-is-for-the-native-ones.md index b101d681..7d6590fa 100644 --- a/content/posts/2013-01-16-this-is-for-the-native-ones.md +++ b/content/posts/2013-01-16-this-is-for-the-native-ones.md @@ -1,94 +1,90 @@ ---- -outdated: true -showInBlog: false -title: 'This is for the native ones' -date: "2013-01-16" -author: hendrik -categories: [AquaFX, General, JavaFX] -excerpt: 'JavaFX provides the ability to style controls by CSS or code. We are using this functionallity to create native looking controls.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -As you can read in an [earlier post]({{< ref "/posts/2012-11-17-custom-ui-controls-with-javafx-part-1" >}}), JavaFX provides the ability to style controls by CSS or code. Most of the techniques you can use to style components are described in [this great JavaOne talk by Gerrit Grunwald](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=2425). The following graphic shows the basic relationship between all parts that are involved in component styling: - -![nativ1-1](/posts/guigarage-legacy/nativ1-1.png) - -As you can see in the graphic, the CSS file is the main entry point for the Control styling. The CSS defines UI related properties like colors or fonts. In addition to this the CSS specifies the Skin class that will be used to style the Control. - -## Official JavaFX styles - -JavaFX currently offers one system independent style that is named "Caspian". The style is completely defined in one CSS file: - -{{< highlight java >}} -com.sun.javafx.scene.control.skin.caspian.caspian.css -{{< / highlight >}} - -Oracle and the JavaFX team did a really great job by designing this cross platform skin. I my opinion it looks great on every OS. Here is a short extract of the Style: - -![caspian](/posts/guigarage-legacy/caspian.png) - -You can see the complete blueprint [here](http://javafx-jira.kenai.com/secure/attachment/34815/Caspian.png) (you need to be logged in to OpenJFX Jira). The Caspian style offers definitions for all default JavaFX controls. - -The following chart describes the style handling for the JavaFX Button with the official caspian CSS file: - -![nativ2-1](/posts/guigarage-legacy/nativ2-1.png) - -While JavaFX 2.x only offers the caspian style, JavaFX 8 will provide a second one called "Modena". - -![modena](/posts/guigarage-legacy/modena.png) - -Here you can see a preview of Buttons with Modena style. Compared to Caspian it's brighter and the controls look a little flatter. Modena is still in development and can change all the time until JavaFX 8 will be released. - -While the current preview of Modena really looks great (see full preview at OpenJFX Jira [http://javafx-jira.kenai.com/secure/attachment/34814/Modena-v0.1.png](here)) it's still a cross platform Look & Feel. You can compare the "Caspian" and "Modena" styles to the "Metal" and "Nimbus" Look & Feel of Swing. Both are designed for cross platform use, too. But Swing benefit are the system L&Fs. So if you have to develop a application with a native look, you currently have to use Swing because it offers native Look & Feels for Windows, Mac OS and Linux. But since JavaFX 2.x is out we do not want to use Swing anymore :) - -## Native JavaFX styles - -As described above JavaFX officially only offers cross platform skins. According to a [mail by Richard Bair](http://mail.openjdk.java.net/pipermail/openjfx-dev/2013-January/005281.html) from the JavaFX mailing list oracle won't provide native skins for JavaFX in the near future. - -Thanks to the great JavaFX community the first approaches for native styles appeared the last month. Jasper Potts created some [basic CSS styles for buttons](http://fxexperience.com/2011/12/styling-fx-buttons-with-css/) that copy the native style of iOS, Mac OS Lion and Windows 7. - -![native-buttons](/posts/guigarage-legacy/native-buttons.png) - -The second starting point is [JMetro](http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/), a project maintained by [Pedro Duque Vieira](http://twitter.com/P_Duke), that provides Windows 8 Metro skins for JavaFX controls with the support of a dark and a light theme. - -![pushbuttondark-1](/posts/guigarage-legacy/pushbuttondark-1.png) - -![contextmenu-metro](/posts/guigarage-legacy/contextmenu-metro.png) - -JMetro is part of [JFXtras](http://jfxtras.org) and can be found at [github](https://github.com/JFXtras/jfxtras-styles). While writing this post the following controls are supported by JMetro: - -* Button -* ToggleButton -* CheckBox -* RadioButton -* ContextMenu -* MenuItem -* ScrollBar -* ScrollPane -* ComboBox - -The next supported Control will be the JFXtras Calender Control. You can find a cool preview [here](http://pixelduke.wordpress.com/2013/01/14/java-calendar-with-a-metro-style/). - -The last approach I've found is from [software4java](http://blog.software4java.com). On this blog you can find some pictures and videos of [iOS](http://blog.software4java.com/?p=27), [Mac OS and Windows 7 related styles](http://blog.software4java.com/?p=15) for JavaFX. But there is no activity since April 2012 and the sources were never provided. - -## JavaFX goes Aqua - -The current community based projects demonstrate that it's possible to create native looking skins for JavaFX controls. According to this conditions a student I'm currently responsible for is planning the bachelor of science thesis based on JavaFX. Currently it is planned to develop a native JavaFX skin for Mac OS. The project should combine skins for JavaFX controls like it's done with JMetro and provide new Mac specific controls on the other hand like [macwidgets](http://code.google.com/p/macwidgets/) does for Java Swing. - -We have done a first prove of concept and are happy to share the result with you. Do you find out which shutdown window is the native one and which one is developed in JavaFX? - -{{< youtube GbJDg5wsJ9E >}} - - -Our first code is hacked in many ways and needs a lot of refactoring for sure. But it was a lot of fun and we learned a lot about CSS and JavaFX skins. Even if it's hacked at some points we do not use any bitmaps. Everything is done with CSS, JavaFX effects and the SVG support. We created some basic concepts how to create this aqua related skins for JavaFX. The following chart shows the class relations and inheritance for a Button: - -![nativ3-3](/posts/guigarage-legacy/nativ3-3.png) - -As you can see we still use the basic Button class from JavaFX. So every Button can be skinned to a Aqua Button. The Button will be styled by a special Skin class ("AquaButtonSkin") because the usage of CSS isn't enough for some points. As you can see in the video the default button of a dialog has a blinking animation in MacOS. This effect can't be generated by CSS. But because there is only one global CSS file for the Aqua Skin you can style a complete application (once every control type is supported)  by only adding one line to your code. As described at the beginning of this article all Skin classes are defined inside the CSS and will be managed by JavaFX automatically. Here is the code to set the style to your application: - -{{< highlight java >}} -myScene.getStylesheets().add(AquaSkin.getStylesheet()); -{{< / highlight >}} - -## One last hurdle - -We currently defining the proposal of the thesis and plan to submit it next week. The topic of the project is not really typical for an exam and it's not supported by a company. But we like the topic and open source development a lot more than a project that is related to our company so we chose this one. It could be difficult to convince the university of this project. Maybe some feedback from the JavaFX community can help. So if you like this idea (or even if you have some critique) please give us a short review or feedback. We hope that we can confirm the benefit of this project this way. +--- +outdated: true +showInBlog: false +title: 'This is for the native ones' +date: "2013-01-16" +author: hendrik +categories: [AquaFX, General, JavaFX] +excerpt: 'JavaFX provides the ability to style controls by CSS or code. We are using this functionallity to create native looking controls.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +As you can read in an [earlier post](/posts/2012-11-17-custom-ui-controls-with-javafx-part-1), JavaFX provides the ability to style controls by CSS or code. Most of the techniques you can use to style components are described in [this great JavaOne talk by Gerrit Grunwald](https://oracleus.activeevents.com/connect/sessionDetail.ww?SESSION_ID=2425). The following graphic shows the basic relationship between all parts that are involved in component styling: + +![nativ1-1](/posts/guigarage-legacy/nativ1-1.png) + +As you can see in the graphic, the CSS file is the main entry point for the Control styling. The CSS defines UI related properties like colors or fonts. In addition to this the CSS specifies the Skin class that will be used to style the Control. + +## Official JavaFX styles + +JavaFX currently offers one system independent style that is named "Caspian". The style is completely defined in one CSS file: + +```javacom.sun.javafx.scene.control.skin.caspian.caspian.css ``` + +Oracle and the JavaFX team did a really great job by designing this cross platform skin. I my opinion it looks great on every OS. Here is a short extract of the Style: + +![caspian](/posts/guigarage-legacy/caspian.png) + +You can see the complete blueprint [here](http://javafx-jira.kenai.com/secure/attachment/34815/Caspian.png) (you need to be logged in to OpenJFX Jira). The Caspian style offers definitions for all default JavaFX controls. + +The following chart describes the style handling for the JavaFX Button with the official caspian CSS file: + +![nativ2-1](/posts/guigarage-legacy/nativ2-1.png) + +While JavaFX 2.x only offers the caspian style, JavaFX 8 will provide a second one called "Modena". + +![modena](/posts/guigarage-legacy/modena.png) + +Here you can see a preview of Buttons with Modena style. Compared to Caspian it's brighter and the controls look a little flatter. Modena is still in development and can change all the time until JavaFX 8 will be released. + +While the current preview of Modena really looks great (see full preview at OpenJFX Jira [http://javafx-jira.kenai.com/secure/attachment/34814/Modena-v0.1.png](here)) it's still a cross platform Look & Feel. You can compare the "Caspian" and "Modena" styles to the "Metal" and "Nimbus" Look & Feel of Swing. Both are designed for cross platform use, too. But Swing benefit are the system L&Fs. So if you have to develop a application with a native look, you currently have to use Swing because it offers native Look & Feels for Windows, Mac OS and Linux. But since JavaFX 2.x is out we do not want to use Swing anymore :) + +## Native JavaFX styles + +As described above JavaFX officially only offers cross platform skins. According to a [mail by Richard Bair](http://mail.openjdk.java.net/pipermail/openjfx-dev/2013-January/005281.html) from the JavaFX mailing list oracle won't provide native skins for JavaFX in the near future. + +Thanks to the great JavaFX community the first approaches for native styles appeared the last month. Jasper Potts created some [basic CSS styles for buttons](http://fxexperience.com/2011/12/styling-fx-buttons-with-css/) that copy the native style of iOS, Mac OS Lion and Windows 7. + +![native-buttons](/posts/guigarage-legacy/native-buttons.png) + +The second starting point is [JMetro](http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/), a project maintained by [Pedro Duque Vieira](http://twitter.com/P_Duke), that provides Windows 8 Metro skins for JavaFX controls with the support of a dark and a light theme. + +![pushbuttondark-1](/posts/guigarage-legacy/pushbuttondark-1.png) + +![contextmenu-metro](/posts/guigarage-legacy/contextmenu-metro.png) + +JMetro is part of [JFXtras](http://jfxtras.org) and can be found at [github](https://github.com/JFXtras/jfxtras-styles). While writing this post the following controls are supported by JMetro: + +* Button +* ToggleButton +* CheckBox +* RadioButton +* ContextMenu +* MenuItem +* ScrollBar +* ScrollPane +* ComboBox + +The next supported Control will be the JFXtras Calender Control. You can find a cool preview [here](http://pixelduke.wordpress.com/2013/01/14/java-calendar-with-a-metro-style/). + +The last approach I've found is from [software4java](http://blog.software4java.com). On this blog you can find some pictures and videos of [iOS](http://blog.software4java.com/?p=27), [Mac OS and Windows 7 related styles](http://blog.software4java.com/?p=15) for JavaFX. But there is no activity since April 2012 and the sources were never provided. + +## JavaFX goes Aqua + +The current community based projects demonstrate that it's possible to create native looking skins for JavaFX controls. According to this conditions a student I'm currently responsible for is planning the bachelor of science thesis based on JavaFX. Currently it is planned to develop a native JavaFX skin for Mac OS. The project should combine skins for JavaFX controls like it's done with JMetro and provide new Mac specific controls on the other hand like [macwidgets](http://code.google.com/p/macwidgets/) does for Java Swing. + +We have done a first prove of concept and are happy to share the result with you. Do you find out which shutdown window is the native one and which one is developed in JavaFX? + + + + +Our first code is hacked in many ways and needs a lot of refactoring for sure. But it was a lot of fun and we learned a lot about CSS and JavaFX skins. Even if it's hacked at some points we do not use any bitmaps. Everything is done with CSS, JavaFX effects and the SVG support. We created some basic concepts how to create this aqua related skins for JavaFX. The following chart shows the class relations and inheritance for a Button: + +![nativ3-3](/posts/guigarage-legacy/nativ3-3.png) + +As you can see we still use the basic Button class from JavaFX. So every Button can be skinned to a Aqua Button. The Button will be styled by a special Skin class ("AquaButtonSkin") because the usage of CSS isn't enough for some points. As you can see in the video the default button of a dialog has a blinking animation in MacOS. This effect can't be generated by CSS. But because there is only one global CSS file for the Aqua Skin you can style a complete application (once every control type is supported)  by only adding one line to your code. As described at the beginning of this article all Skin classes are defined inside the CSS and will be managed by JavaFX automatically. Here is the code to set the style to your application: + +```javamyScene.getStylesheets().add(AquaSkin.getStylesheet()); ``` + +## One last hurdle + +We currently defining the proposal of the thesis and plan to submit it next week. The topic of the project is not really typical for an exam and it's not supported by a company. But we like the topic and open source development a lot more than a project that is related to our company so we chose this one. It could be difficult to convince the university of this project. Maybe some feedback from the JavaFX community can help. So if you like this idea (or even if you have some critique) please give us a short review or feedback. We hope that we can confirm the benefit of this project this way. diff --git a/content/posts/2013-02-09-datafx-observableexecutor-preview.md b/content/posts/2013-02-09-datafx-observableexecutor-preview.md index 2cec93f3..77c8ce4f 100644 --- a/content/posts/2013-02-09-datafx-observableexecutor-preview.md +++ b/content/posts/2013-02-09-datafx-observableexecutor-preview.md @@ -1,44 +1,40 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX: ObservableExecutor Preview' -date: "2013-02-09" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'Since December we are working on a new DataFX version. DataFX will provide a new low level API for multithreading and background tasks in JavaFX.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Since December we are working on a new [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) version. DataFX will provide a new low level API for multithreading and background tasks in JavaFX. With this API everyone can create new and custom DataSources that will fetch data in background and publish it to JavaFX properties (like you can do with the SwingWorker in Swing). Additionally we will provide some basic concurrency features like the [invokeAndWait-method]({{< ref "/posts/2013-01-01-invokeandwait-for-javafx" >}}). - -Next to this features we created a new [Executor](http://docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html) class that offers some additional features for the use with JavaFX. The `ObservableExecutor` holds a ListProperty with all currently scheduled or running tasks. By using this Property you can easily observe all tasks in your UI. We will provide a ListCell class to visualize the running tasks of the executor with only a few lines of code: - -{{< highlight java >}} -Executor executor = new ObservableExecutor(); -ListView> list = new ListView<>(); -list.setCellFactory(new ServiceListCellFactory()); -list.itemsProperty().bind(executor.currentServicesProperty()); -{{< / highlight >}} - -The ObservableExecutor uses the wrapper pattern to hold any Executor. Because all task are wrapped into [Services](http://docs.oracle.com/javafx/2/api/javafx/concurrent/Service.html) you can easily access the title, message or progress of any task. A short example that shows the current state of the API can be found here: - -{{< youtube eQaVNQKy1U0 >}} - -Because Runnable & Callable normally do not provide title, message and progress properties we created extended interfaces (`DataFXRunnable` & `DataFXCallable`) where all this functions are injected while using them with the ObservableExecutor. - -At the current state tasks can be passed to an `ObservableExecutor` by the following methods: - -{{< highlight java >}} -public Worker submit(Service service); - -public Worker submit(Task task); - -public Worker submit(Callable callable); - -public Worker submit(Runnable runnable); - -public void execute(Runnable runnable); -{{< / highlight >}} - -There are some additional features, too. You can mark a task as not cancelable for example. - -Hope you like this stuff! +--- +outdated: true +showInBlog: false +title: 'DataFX: ObservableExecutor Preview' +date: "2013-02-09" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'Since December we are working on a new DataFX version. DataFX will provide a new low level API for multithreading and background tasks in JavaFX.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Since December we are working on a new [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) version. DataFX will provide a new low level API for multithreading and background tasks in JavaFX. With this API everyone can create new and custom DataSources that will fetch data in background and publish it to JavaFX properties (like you can do with the SwingWorker in Swing). Additionally we will provide some basic concurrency features like the [invokeAndWait-method](/posts/2013-01-01-invokeandwait-for-javafx). + +Next to this features we created a new [Executor](http://docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html) class that offers some additional features for the use with JavaFX. The `ObservableExecutor` holds a ListProperty with all currently scheduled or running tasks. By using this Property you can easily observe all tasks in your UI. We will provide a ListCell class to visualize the running tasks of the executor with only a few lines of code: + +```javaExecutor executor = new ObservableExecutor(); +ListView> list = new ListView<>(); +list.setCellFactory(new ServiceListCellFactory()); +list.itemsProperty().bind(executor.currentServicesProperty()); ``` + +The ObservableExecutor uses the wrapper pattern to hold any Executor. Because all task are wrapped into [Services](http://docs.oracle.com/javafx/2/api/javafx/concurrent/Service.html) you can easily access the title, message or progress of any task. A short example that shows the current state of the API can be found here: + + + +Because Runnable & Callable normally do not provide title, message and progress properties we created extended interfaces (`DataFXRunnable` & `DataFXCallable`) where all this functions are injected while using them with the ObservableExecutor. + +At the current state tasks can be passed to an `ObservableExecutor` by the following methods: + +```javapublic Worker submit(Service service); + +public Worker submit(Task task); + +public Worker submit(Callable callable); + +public Worker submit(Runnable runnable); + +public void execute(Runnable runnable); ``` + +There are some additional features, too. You can mark a task as not cancelable for example. + +Hope you like this stuff! diff --git a/content/posts/2013-03-02-global-stylesheet-for-your-javafx-application.md b/content/posts/2013-03-02-global-stylesheet-for-your-javafx-application.md index 0bcc1c24..958a34d1 100644 --- a/content/posts/2013-03-02-global-stylesheet-for-your-javafx-application.md +++ b/content/posts/2013-03-02-global-stylesheet-for-your-javafx-application.md @@ -1,47 +1,37 @@ ---- -outdated: true -showInBlog: false -title: 'Global Stylesheet for your JavaFX Application' -date: "2013-03-02" -author: hendrik -categories: [General, JavaFX] -excerpt: 'There is a way to set a global Stylesheet to all JavaFX Scenes in your app. By using the JavaFX 8 class StyleManager you can define the default CSS files.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -You can style your JavaFX [Scene](http://docs.oracle.com/javafx/2/api/javafx/scene/Scene.html) by CSS as you can read [here]({{< ref "/posts/2013-01-16-this-is-for-the-native-ones" >}}) and [here](http://docs.oracle.com/javafx/2/css_tutorial/jfxpub-css_tutorial.htm). All this examples show how to apply a specific Stylesheet to one Scene by using - -{{< highlight java >}} -myScene.getStylesheets().add("path/to/custom.css"); -{{< / highlight >}} - -Inside the SceneGraph of this Scene all Nodes will use the defined Stylesheets. But while your Application grows you will normally have more than one Scene. Each [Window](http://docs.oracle.com/javafx/2/api/javafx/stage/Window.html) in JavaFX holds its own Scene. Even if your Application only uses one [Stage](http://docs.oracle.com/javafx/2/api/javafx/stage/Stage.html) you will normally have more than one window while every [ContextMenu](http://docs.oracle.com/javafx/2/api/javafx/scene/control/ContextMenu.html) is Window. So if you use custom ContextMenus or a [ChoiceBox](http://docs.oracle.com/javafx/2/api/javafx/scene/control/ChoiceBox.html) inside your application this Components will technically be displayed in a separate Window with a new Scene and SceneGraph. But with the code mentioned above only "myScene" will use the custom Stylesheet. The Scene of the ContextMenu will not be affected by this. One trick here is to set the Stylesheet manually to the Scene of the Window: - -{{< highlight java >}} -myContextMenu.getScene().getStylesheets().add("path/to/custom.css"); -{{< / highlight >}} - -But this is only a bad workaround and you won't do this for every ContextMenu. When you use a ChoiceBox you even can't access the Scene of the popup because this is defined in private methods of the ChoiceBoxSkin. - -But there is a way to set a global Stylesheet to all Scenes. By using the JavaFX 8 class StyleManager you can define the default CSS files. The class uses the singleton pattern and can easily accepted. The following code will add a CSS file to all Stylesheets of all Scenes: - -{{< highlight java >}} -StyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); -{{< / highlight >}} - -Currently there is one bug with this. The default Stylesheet (currently [caspian]({{< ref "/posts/2013-01-16-this-is-for-the-native-ones" >}})) is defined inside the StyleManger, too. But the default will not be set until a first Node is created. When adding a additional user defined Stylesheet a Exception is thrown. So to avoid problems you have to set the default CSS before adding a custom one. This can currently only done by calling a private API: - -{{< highlight java >}} -PlatformImpl.setDefaultPlatformUserAgentStylesheet(); -StyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); -{{< / highlight >}} - -I will open a issue at [http://javafx-jira.kenai.com](http://javafx-jira.kenai.com) about this behavior. - -## Addition - -With the help of Jonathan Giles I found a better way without using private APIs. You can easily set the default stylesheet by using "Application.setUserAgentStylesheet(String url)". If you use null as parameter value the default stylesheet (currently caspian) will be used. So here is the code without using a private API: - -{{< highlight java >}} -Application.setUserAgentStylesheet(null); -StyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); -{{< / highlight >}} +--- +outdated: true +showInBlog: false +title: 'Global Stylesheet for your JavaFX Application' +date: "2013-03-02" +author: hendrik +categories: [General, JavaFX] +excerpt: 'There is a way to set a global Stylesheet to all JavaFX Scenes in your app. By using the JavaFX 8 class StyleManager you can define the default CSS files.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +You can style your JavaFX [Scene](http://docs.oracle.com/javafx/2/api/javafx/scene/Scene.html) by CSS as you can read [here](/posts/2013-01-16-this-is-for-the-native-ones) and [here](http://docs.oracle.com/javafx/2/css_tutorial/jfxpub-css_tutorial.htm). All this examples show how to apply a specific Stylesheet to one Scene by using + +```javamyScene.getStylesheets().add("path/to/custom.css"); ``` + +Inside the SceneGraph of this Scene all Nodes will use the defined Stylesheets. But while your Application grows you will normally have more than one Scene. Each [Window](http://docs.oracle.com/javafx/2/api/javafx/stage/Window.html) in JavaFX holds its own Scene. Even if your Application only uses one [Stage](http://docs.oracle.com/javafx/2/api/javafx/stage/Stage.html) you will normally have more than one window while every [ContextMenu](http://docs.oracle.com/javafx/2/api/javafx/scene/control/ContextMenu.html) is Window. So if you use custom ContextMenus or a [ChoiceBox](http://docs.oracle.com/javafx/2/api/javafx/scene/control/ChoiceBox.html) inside your application this Components will technically be displayed in a separate Window with a new Scene and SceneGraph. But with the code mentioned above only "myScene" will use the custom Stylesheet. The Scene of the ContextMenu will not be affected by this. One trick here is to set the Stylesheet manually to the Scene of the Window: + +```javamyContextMenu.getScene().getStylesheets().add("path/to/custom.css"); ``` + +But this is only a bad workaround and you won't do this for every ContextMenu. When you use a ChoiceBox you even can't access the Scene of the popup because this is defined in private methods of the ChoiceBoxSkin. + +But there is a way to set a global Stylesheet to all Scenes. By using the JavaFX 8 class StyleManager you can define the default CSS files. The class uses the singleton pattern and can easily accepted. The following code will add a CSS file to all Stylesheets of all Scenes: + +```javaStyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); ``` + +Currently there is one bug with this. The default Stylesheet (currently [caspian](/posts/2013-01-16-this-is-for-the-native-ones)) is defined inside the StyleManger, too. But the default will not be set until a first Node is created. When adding a additional user defined Stylesheet a Exception is thrown. So to avoid problems you have to set the default CSS before adding a custom one. This can currently only done by calling a private API: + +```javaPlatformImpl.setDefaultPlatformUserAgentStylesheet(); +StyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); ``` + +I will open a issue at [http://javafx-jira.kenai.com](http://javafx-jira.kenai.com) about this behavior. + +## Addition + +With the help of Jonathan Giles I found a better way without using private APIs. You can easily set the default stylesheet by using "Application.setUserAgentStylesheet(String url)". If you use null as parameter value the default stylesheet (currently caspian) will be used. So here is the code without using a private API: + +```javaApplication.setUserAgentStylesheet(null); +StyleManager.getInstance().addUserAgentStylesheet(AQUA_CSS_NAME); ``` diff --git a/content/posts/2013-03-02-update-for-the-native-ones.md b/content/posts/2013-03-02-update-for-the-native-ones.md index 42b7e3a6..0735705c 100644 --- a/content/posts/2013-03-02-update-for-the-native-ones.md +++ b/content/posts/2013-03-02-update-for-the-native-ones.md @@ -8,7 +8,7 @@ categories: [AquaFX, General, JavaFX] excerpt: 'I was keen on starting with the first components for the native Look and Feel for JavaFX. Now I want to show you what is the result.' preview_image: "/posts/preview-images/software-development-green.svg" --- -Since [my project]({{< ref "/posts/2013-01-16-this-is-for-the-native-ones" >}}) was accepted, I was keen on starting with the first components. Now I want to show you what is the result. +Since [my project](/posts/2013-01-16-this-is-for-the-native-ones) was accepted, I was keen on starting with the first components. Now I want to show you what is the result. ## Remember the shutdown dialog? @@ -68,7 +68,7 @@ The good news in this context is, that all of this stuff works on retina- and no There are some points, which had to be decided before I really could start and which are not absolutely clear. -Currently all CSS-styling is overriding caspian.css, as it is the default style. But we all look forward to modena replacing caspian for JavaFX 8. This may cause some trouble in future concerning new controls, which do not have Mac OS-Styling yet. For this reason, it is not that clear, how to apply mac_os.css to an application. Hendrik describes the different variations using the StyleManager in [his latest blogpost]({{< ref "/posts/2013-03-02-global-stylesheet-for-your-javafx-application" >}}). Thoughtless usage of style management also causes trouble with PopUp-components. +Currently all CSS-styling is overriding caspian.css, as it is the default style. But we all look forward to modena replacing caspian for JavaFX 8. This may cause some trouble in future concerning new controls, which do not have Mac OS-Styling yet. For this reason, it is not that clear, how to apply mac_os.css to an application. Hendrik describes the different variations using the StyleManager in [his latest blogpost](/posts/2013-03-02-global-stylesheet-for-your-javafx-application). Thoughtless usage of style management also causes trouble with PopUp-components. The main approach in styling the components is, using CSS as much as possible. Everything, that is not possible in CSS e.g. multiple effects on one component or animations is implemented in Aqua*Skins, which simply override the skin of the affected control. In this way, effects and animations are no problem. diff --git a/content/posts/2013-03-17-introducing-marvinfx.md b/content/posts/2013-03-17-introducing-marvinfx.md index c544f40b..48b2ecdf 100644 --- a/content/posts/2013-03-17-introducing-marvinfx.md +++ b/content/posts/2013-03-17-introducing-marvinfx.md @@ -1,125 +1,115 @@ ---- -outdated: true -showInBlog: false -title: 'Introducing MarvinFx' -date: "2013-03-17" -author: hendrik -categories: [JavaFX] -excerpt: "For all my current JavaFX work I need a simple test framework to check the behavior of controls or complete scenes. Since I'm working more and more with the Property API I mainly wanted to to test the properties and their behavior of controls and scenes." -preview_image: "/posts/preview-images/software-development-green.svg" ---- -For all my current JavaFX work I need a simple test framework to check the behavior of controls or complete scenes. Since I'm working more and more with the [Property API]({{< ref "/posts/2013-01-10-custom-ui-controls-with-javafx-part3" >}}) I mainly wanted to to test the properties and their behavior of controls and scenes. - -[FEST](http://fest.easytesting.org) is doing a great job for automated tests in Swing and with [JemmyFX](http://jemmy.java.net/JemmyFXGuide/jemmy-guide.html) a first framework for JavaFX is available. But both of this framework don't have a (good) support for Properties. - -Because of this I started my own testing framework for JavaFX. [MarvinFX](https://github.com/guigarage/MarvinFX) has the goal to easily test JavaFX controls and scenes with a special attention on properties. The framework is currently in a very early state and only a few parts are implemented. But I think that already the current state can point out where the journey will lead. And maybe I will receive some helpful suggestions. - -You can simply use MarvinFX with a JUnit test. By using Marvin you can create a JavaFX Scene of the Application part you want to test: - -{{< highlight java >}} -@Test -public void test1() { - Button b1 = new Button("Test123"); - MarvinFx.show(b1); -} -{{< / highlight >}} - -Marvin will generate a Parent (StackPane) for the Button and put everything in a Scene. The Scene will automatically be shown on the screen. - -To test parts of the ui you need a Robot that will generate user interactions for you. It is planned that MarvinFX will provide a OS based robot (by `AWTRobot`) and a Java based robot (by `JFXRobot`) under the surface. At the current state the mouse handling of the robot is working. - -{{< highlight java >}} -@Test -public void test2() { - Button b1 = new Button("Test123"); - MarvinFx.show(b1); - NodeFixture -{{< / highlight >}} - -If you want to see more code check out the [DataFX 2.0 repository at bitbucket](https://bitbucket.org/datafx/datafx). - -We will provide more samples and tutorials later, and you are very welcome to join us at JavaOne [(CON3202, Wednesday Sep 25, 08:30 am, Hilton - Plaza B)](https://oracleus.activeevents.com/2013/connect/sessionDetail.ww?SESSION_ID=3202). +--- +outdated: true +showInBlog: false +title: 'DataFX Controller API' +date: "2013-09-13" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'The DataFX team will show some cool new APIs at JavaOne this year. Today I will give a short preview to another part of the new DataFX APIs.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +The [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) team will show some cool new APIs at [JavaOne](http://www.oracle.com/javaone/index.html) this year. Johan has written [a post about writeback support in the new DataFX](http://www.lodgon.com/dali/blog/entry/Writeback_support_in_DataFX) version some days ago. Today I will give a short preview to another part of the new DataFX APIs. + +## The Controller API + +The base of the API is the inversion of control for JavaFX controllers. The API offers the annotation `FXMLController` that can be assigned to any controller class. By using this annotation you can define the FXML-View that should be used by this controller. If you don‘t define the name of the FXML-File the API will try to find the view by using a convention over configuration approach. By doing so you do not need to add Java class information in your FXML file. You can use all default FXML annotations that are defined by JavaFX. By doing so you can inject all your nodes that are defined in the fxml-file in the controller by simply adding the `@FXML` annotation. Additionally you can use the `@PostConstruct` annotation. A simple Controller that is created by this API can look like this one: + +```java@FXMLController("Details.fxml") +public class DetailViewController { + @FXML + private TextField myTextfield; + @FXML + private Button backButton; + @PostConstruct + public void init() { + myTextfield.setText("Hello!"); + } +} ``` + +To load the view and the controller DataFX-Controller offers a simple HelperClass called `ViewFactory`. This is the default entry point to the complete DataFX-Controller API. You can create a view and controller by using this class: + +```javaViewFactory.getInstance().createByController(DetailViewController.class); ``` + +The FXML-file that is used by this controller can look like this one: + +```xml + + + + + ``` + +If you want to see more code check out the [DataFX 2.0 repository at bitbucket](https://bitbucket.org/datafx/datafx). + +We will provide more samples and tutorials later, and you are very welcome to join us at JavaOne [(CON3202, Wednesday Sep 25, 08:30 am, Hilton - Plaza B)](https://oracleus.activeevents.com/2013/connect/sessionDetail.ww?SESSION_ID=3202). diff --git a/content/posts/2013-09-24-flatter.md b/content/posts/2013-09-24-flatter.md index c5922d9c..95b44a59 100644 --- a/content/posts/2013-09-24-flatter.md +++ b/content/posts/2013-09-24-flatter.md @@ -8,7 +8,7 @@ categories: [Flatter, JavaFX] excerpt: "For our JavaOne talk Claudine and I created a new JavaFX skin called Flatter. While AquaFX has it's main focus on desktop applications running on a Mac, Flatter is made for touch and embedded devices." preview_image: "/posts/preview-images/software-development-green.svg" --- -For our [JavaOne talk]({{< ref "/posts/2013-09-24-lets-get-wet" >}}) Claudine and I created a new JavaFX skin called Flatter. While [AquaFX](http://aquafx-project.com) has it's main focus on desktop applications running on a Mac, Flatter is made for touch and embedded devices. +For our [JavaOne talk](/posts/2013-09-24-lets-get-wet) Claudine and I created a new JavaFX skin called Flatter. While [AquaFX](http://aquafx-project.com) has it's main focus on desktop applications running on a Mac, Flatter is made for touch and embedded devices. Flatter is a skin that is as simple as it can be. There are no gradients or shiny effects in the skin of all the controls. Next to this the controls of flatter have a bigger appearance than all controls in the desktop skins. By doing so, they are prepared for touch. Here is a first screenshot of some of the default JavaFX controls skinned by Flatter: @@ -26,4 +26,4 @@ As I mentioned before we created some special skins. This skins can be used for ![tags](/posts/guigarage-legacy/tags.png) -Flatter is still in progress, but we will open source it in the next weeks after JavaOne. You can find more information about Flatter in our ["Let's get wet" JavaOne presentation]({{< ref "/posts/2013-09-24-lets-get-wet" >}}). So if you want a flat user experience stay tuned for this one ;) +Flatter is still in progress, but we will open source it in the next weeks after JavaOne. You can find more information about Flatter in our ["Let's get wet" JavaOne presentation](/posts/2013-09-24-lets-get-wet). So if you want a flat user experience stay tuned for this one ;) diff --git a/content/posts/2013-09-24-make-your-app-smile-d.md b/content/posts/2013-09-24-make-your-app-smile-d.md index b6212782..2cd10e20 100644 --- a/content/posts/2013-09-24-make-your-app-smile-d.md +++ b/content/posts/2013-09-24-make-your-app-smile-d.md @@ -14,4 +14,4 @@ I think most of you know the emoji icons that are used in WhatsApp and other soc We took this control and added Emoji support to it. All the Emojis are part of the unicode standard and can be defined by simple using a char. The alien emoji is defined by char 0xF47D for example. -You can find out more about the emoji text flow in our [JavaOne slides]({{< ref "/posts/2013-09-24-lets-get-wet" >}}). Because this is still in progress we will post a more detailed description later. +You can find out more about the emoji text flow in our [JavaOne slides](/posts/2013-09-24-lets-get-wet). Because this is still in progress we will post a more detailed description later. diff --git a/content/posts/2013-12-27-datafx-controller-framework-preview.md b/content/posts/2013-12-27-datafx-controller-framework-preview.md index fb2f12aa..eaf99aa9 100644 --- a/content/posts/2013-12-27-datafx-controller-framework-preview.md +++ b/content/posts/2013-12-27-datafx-controller-framework-preview.md @@ -1,438 +1,390 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX Controller Framework Preview' -date: "2013-12-27" -author: hendrik -categories: [DataFX, General, JavaFX] -excerpt: 'Today we released the version 2.0 of DataFX. As a next step we will work on DataFX 8.0 that will use Java 8 and JavaFX 8.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Today we released the version 2.0 of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). Thanks for all the feedback that we received with the last release candidates. As a next step we will work on DataFX 8.0 that will use Java 8 and JavaFX 8. One of the new features that will be part of the next release are currently in development. Today I will show a first preview to DataFX 8.0. - -The last month I added a new framework to DataFX. This framework should help to create the views of applications, define actions on them and create flows that contain a subset of views. - -To show the features of the framework I will create a simple JavaFX application. The app should manage persons. Persons can be loaded, created, edited and deleted by the app. Let's have a first look on the general concept of the application: - -![datafx1](/posts/guigarage-legacy/datafx1.png) - -As you can see the app contains 3 different views: - -* a master view that shows all persons in a list -* a create view that can add a new person -* a edit view to edit a person - -All this views are linked by actions ("save", "add", etc.) that will manipulate the data or show another view. First of all we want to take a look on the data model. Here a simple Person class is defined: - -{{< highlight java >}} -public class Person { - private StringProperty name; - private StringProperty notes; - public Person() { - } - public Person (String name, String notes) { - setName(name); - setNotes(notes); - } - public String getName() { - return nameProperty().get(); - } - public StringProperty nameProperty() { - if(name == null) { - name = new SimpleStringProperty(); - } - return name; - } - public final void setName(String name) { - this.nameProperty().set(name); - } - public String getNotes() { - return notesProperty().get(); - } - public StringProperty notesProperty() { - if(notes == null) { - notes = new SimpleStringProperty(); - } - return notes; - } - public final void setNotes(String notes) { - this.notesProperty().set(notes); - } - @Override - public String toString() { - return getName(); - } -} -{{< / highlight >}} - -This class defines a person. Because we want to handle a list of persons we need another class that in our case defines the global data model: - -{{< highlight java >}} -public class DataModel { - private ListProperty persons; - private IntegerProperty selectedPersonIndex; - public ListProperty getPersons() { - if (persons == null) { - ObservableList innerList = FXCollections.observableArrayList(); - persons = new SimpleListProperty<>(innerList); - } - return persons; - } - public int getSelectedPersonIndex() { - return selectedPersonIndexProperty().get(); - } - public void setSelectedPersonIndex(int selectedPersonIndex) { - this.selectedPersonIndex.set(selectedPersonIndex); - } - public IntegerProperty selectedPersonIndexProperty() { - if (selectedPersonIndex == null) { - selectedPersonIndex = new SimpleIntegerProperty(); - } - return selectedPersonIndex; - } -} -{{< / highlight >}} - -This class defines a list of persons and the currently selected person by an index. To create a first default set of persons that can be loaded we define a additional class. In a real world application this class could wrap a database connection, for example: - -{{< highlight java >}} -public class LoadPersonsTask implements Runnable { - Person[] persons = { - new Person("Johan Vos", "Johan is CTO at LodgON, a Java Champion, a member of the BeJUG steering group, the Devoxx steering group and he is a JCP member."), - new Person("Jonathan Giles", "Jonathan Giles is the JavaFX UI controls technical lead at Oracle, where he has been involved with JavaFX since 2009."), - new Person("Hendrik Ebbers", "Hendrik Ebbers is Senior Java Architect at Materna GmbH in Dortmund, Germany.")}; - @Inject - private DataModel model; - @Override - public void run() { - model.getPersons().clear(); - ListDataProvider ldp = ListDataProviderBuilder - .create() - .dataReader(new ArrayDataReader(persons)) - .resultList(model.getPersons()) - .build(); - ldp.retrieve(); - } -} -{{< / highlight >}} - -After the data model is defined we can create the first view. Let's start with the master view. To create the view Scene Builder can be used. Here we can easily design the following view: - -![datafx21](/posts/guigarage-legacy/datafx21.png) - -For all needed controls IDs are defined in the FXML. Normally you need to define a controller class in FXML. This is not needed for the DataFX Controller API. Instead of this we can bind a controller and a FXML view by the use of an annotation. As the next step a controller is needed. As a first step we create a small controller with some additional annotations: - -{{< highlight java >}} -@FXMLController("listView.fxml") -public class MasterViewController { -@FXML -private Button editButton; -@FXML -private Button removeButton; -@FXML -private Button addButton; -@FXML -private Button loadButton; -@FXML -private ListView dataList; -} -{{< / highlight >}} - -In this first version there is only one difference to the default JavaFX APIs: The FXMLController annotation is added. This annotation defines the link between the controller class and the FXML file. As a next step we want to create a data model. Here the next benefit of the framework can be used: Context Dependency Injection. To add a model to the the controller we can simple inject it: - -{{< highlight java >}} -@Inject -private DataModel model; -{{< / highlight >}} - -To explain what happens here the CDI module in DataFX need to be described a little bit more. As in JEE CDI different scopes are supported in DataFX: - -* ViewScope -* FlowScope -* ApplicationScope - -All this scopes have a different context is is managed by the framework. All items that are part of the ViewScope have a lifetime of one view. A view is for example the master view in our example. The Application scope is defined as a global scope. All items in this scopes are singletons. The Singleton scope that is already defined in javax.inject can be used here, too. The flow scope defines a flow of views. In our example we will create one flow that handles all the defines views. In a more complex applications different flows can be handled. You can easily create a flow for each tap in a business application, for example. Additionally DataFX supports the dependent scope as it is defined in JEE. - -The data model in our application need to be defined in the flow scope. It should be accessed from all views in this scope. To do so a scope annotation need to be added to the class: - -{{< highlight java >}} -@FlowScoped -public class DataModel { -... -} -{{< / highlight >}} - -Once this is done we can easily inject the data model in our view: - -{{< highlight java >}} -@FXMLController("listView.fxml") -public class MasterViewController { - ... - @Inject - private DataModel model; -} -{{< / highlight >}} - -As a next step some initial setup is needed. To do so the PostConstruct annotation is supported by the DataFX framework: - -{{< highlight java >}} -@FXMLController("listView.fxml") -public class MasterViewController { - .... - @PostConstruct - public void init() { - dataList.itemsProperty().bind(model.getPersons()); - model.selectedPersonIndexProperty().bind(dataList.getSelectionModel().selectedIndexProperty()); - } -} -{{< / highlight >}} - -Now the ListView is bounded to the data model. To create some basic data a action is needed. This action should fire when the "load" button is pressed. First we create a simple class that handles the action: - -{{< highlight java >}} -public class LoadPersonsTask implements Runnable { - Person[] persons = { - new Person("Johan Vos", "Johan is CTO at LodgON, a Java Champion, a member of the BeJUG steering group, the Devoxx steering group and he is a JCP member."), - new Person("Jonathan Giles", "Jonathan Giles is the JavaFX UI controls technical lead at Oracle, where he has been involved with JavaFX since 2009."), - new Person("Hendrik Ebbers", "Hendrik Ebbers is Senior Java Architect at Materna GmbH in Dortmund, Germany.")}; - @Inject - private DataModel model; - @Override - public void run() { - model.getPersons().clear(); - ListDataProvider ldp = ListDataProviderBuilder - .create() - .dataReader(new ArrayDataReader(persons)) - .resultList(model.getPersons()) - .build(); - ldp.retrieve(); - } -} -{{< / highlight >}} - -As you can see the injected model is used here, too. This task can be added to the button by the use of the Flow API. This API defines a flow through all views. The first very simply version of our flow looks like this: - -{{< highlight java >}} -Flow flow = new Flow(MasterViewController.class). - withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); -{{< / highlight >}} - -This defines a flow that starts with the master view and adds a task action to this view. The action is defined by the id "load". To bind this action to the load button only a additional annotation is needed in the controller: - -{{< highlight java >}} -@FXML -@FXMLFlowAction("load") -private Button loadButton; -{{< / highlight >}} - -Now the first version of the application can be started. To do so we need a main class that adds the flow to a JavaFX scene: - -{{< highlight java >}} -public class DataFXDemo extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage stage) throws Exception { - Flow flow = new Flow(MasterViewController.class). - withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); - DefaultFlowContainer container = new DefaultFlowContainer(); - flow.createHandler().start(container); - Scene scene = new Scene(container.getPane()); - stage.setScene(scene); - stage.show(); - } -} -{{< / highlight >}} - -The DefaultFlowContainer class is used in the code. This class is a default implementation of a Pane that wraps a flow. When you start the application the "load" button can be used to load the list of persons. Because of the JavaFX binding the result will be shown directly: - -![datafx3](/posts/guigarage-legacy/datafx3.png) - -As a next step we want to add the edit action to the application. Here an additional view need to be created by Scene Builder: - -![datafx4](/posts/guigarage-legacy/datafx4.png) - -Additionally a controller class is needed. This class uses the described features: - -{{< highlight java >}} -@FXMLController("detailView.fxml") -public class EditViewController { - @FXML - @FXMLFlowAction("save") - private Button saveButton; - @FXML - private TextField nameField; - @FXML - private TextArea notesTextArea; - @Inject - private DataModel model; - @PostConstruct - public void init() { - Person p = model.getPersons().get(model.getSelectedPersonIndex()); - nameField.textProperty().bindBidirectional(p.nameProperty()); - notesTextArea.textProperty().bindBidirectional(p.notesProperty()); - } -} -{{< / highlight >}} - -The data model is injected to the controller. Because it is defined in the flow scope it will be the same instance as in the master view. Additionally some bindings will be created to bind the UI controls to the data model. A flow action is added to the save button. This action is defined by the "save" ID. To add this view to the flow only some additional code is needed: - -{{< highlight java >}} -Flow flow = new Flow(MasterViewController.class). - withLink(MasterViewController.class, "edit", EditViewController.class). - withLink(EditViewController.class, "save", MasterViewController.class). - withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); -{{< / highlight >}} - -As you can see two links are added to the flow. This links are actions that will change the current view of the flow. In this cases we want to link from the master page to the edit page and vice versa. When you start the application now you can edit all persons that are part of the list: - -![datafx5](/posts/guigarage-legacy/datafx5.png) - -As a next step we want to add the remove action to the master view. This can be easily done by adding another action: - -{{< highlight java >}} -public class RemoveActionTask implements Runnable { - @Inject - private DataModel model; - @Override - public void run() { - model.getPersons().remove(model.getSelectedPersonIndex()); - } -} -{{< / highlight >}} - -As the import action this action need to be defined in the flow and bound to a button: - -{{< highlight java >}} -@FXML -@FXMLFlowAction("remove") -private Button removeButton; -{{< / highlight >}} - -Additionally the flow need to be changed: - -{{< highlight java >}} -Flow flow = new Flow(MasterViewController.class). - withLink(MasterViewController.class, "edit", EditViewController.class). - withLink(EditViewController.class, "save", MasterViewController.class). - withTaskAction(MasterViewController.class, "remove", RemoveActionTask.class). - withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); -{{< / highlight >}} - -The Flow API of DataFX supports different types of actions. The link action and the task action are used in this example until now. As a next step we want to add the view to create new persons. Here we will use some additional features of the framework. - -Because the view should look like the edit view we can reuse the FXML here. Additonally a controller is needed. Here is a first basic version: - -{{< highlight java >}} -@FXMLController("detailView.fxml") -public class AddViewController { - @FXML - @FXMLFlowAction("save") - private Button saveButton; - @FXML - private TextField nameField; - @FXML - private TextArea notesTextArea; - private StringProperty nameProperty = new SimpleStringProperty(); - private StringProperty noteProperty = new SimpleStringProperty(); - @Inject - private DataModel model; - @PostConstruct - public void init() { - nameField.textProperty().bindBidirectional(nameProperty); - notesTextArea.textProperty().bindBidirectional(noteProperty); - } -} -{{< / highlight >}} - -The data that is added in the view will be stored in the two properties that are defined in the view. Once everything is fine a new person should be created and added to the data model. To do so we use a new action type: The MethodAction. With this type a method of the controller can easily bound to an button. To do so we add a method with the needed annotation in the controller class: - -{{< highlight java >}} -@FXMLController("detailView.fxml") -public class AddViewController { -@FXML -@FXMLFlowAction("save") -private Button saveButton; -... -@ActionMethod("addPerson") - public void addPerson() { - Person p = new Person(); - p.setName(nameProperty.get()); - model.getPersons().add(p); - } -} -{{< / highlight >}} - -Like all other actions this action need to be added to the flow. Because we want to add the person to the data model and then jump back to the master view a action chain is used here: - -{{< highlight java >}} -Flow flow = new Flow(MasterViewController.class). -               withLink(MasterViewController.class, "edit", EditViewController.class). -               withLink(MasterViewController.class, "add", AddViewController.class). -               withLink(EditViewController.class, "save", MasterViewController.class). -               withTaskAction(MasterViewController.class, "remove", RemoveActionTask.class). -               withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class). -               withAction(AddViewController.class, "save", new FlowActionChain(new FlowMethodAction("addPerson"), new FlowLink(MasterViewController.class))); -{{< / highlight >}} - -A action chain defines a list of actions that will be handled. In this example the "save" button is bound to an action chain that first calls the "addPerson" method and then links to the master view. By doing so new persons can be created. - -Next to all the action types that are shown in this example DataFX will provide additional ones and the ability to add custom action classes. - -As a last step we want to add validation. When a new person is created we want to check if the name is not null. The DataFX API supports the default Java Bean Validation and adds to support for JavaFX properties. Because of this we can easily add a NotNull annotation to the name property: - -{{< highlight java >}} -@NotNull -private StringProperty nameProperty = new SimpleStringProperty(); -{{< / highlight >}} - -To validate the data of the view a validation action can be added to the action chain that is bound to the "save" button: - -{{< highlight java >}} -Flow flow = new Flow(MasterViewController.class). -               ... -               withAction(AddViewController.class, "save", new FlowActionChain(new ValidationFlowAction(), new FlowMethodAction("addPerson"), new FlowLink(MasterViewController.class))); -{{< / highlight >}} - -The validation action automatically validates all validatable fields that are defined in the controller. Groups, as defined in the Java Bean Valdidation, are supported, too. When any data is not valid the action chain will stop. - -To provide feedback to the user some additional code is needed. The validator can be injected to the controller: - -{{< highlight java >}} -@Validator -private ValidatorFX validator; -{{< / highlight >}} - -Now we can add a event handler to the validator that will show violations on screen: - -{{< highlight java >}} -@FXMLController("detailView.fxml") -public class AddViewController { -... - @FXML - private Label violationLabel; - @Validator - private ValidatorFX validator; - @PostConstruct - public void init() { - ... - validator.setOnValidationFinished(event -> handleViolations(event.getViolations()); - } - private void handleViolations(Set> violations) { - if(violations.isEmpty()) { - violationLabel.setVisible(false); - } else { - ConstraintViolation violation = violations.iterator().next(); - violationLabel.setText(violation.getPropertyPath() + " " + violation.getMessage()); - violationLabel.setVisible(true); - } - } -} -{{< / highlight >}} - -Once this is done the view will show violations on the screen: - -![datafx6](/posts/guigarage-legacy/datafx6.png) - -This example shows some of the DataFX Controller features. The complete API is not finished yet and can be found in a branch of the DataFX repository. I hope to receive some feedback about this example. +--- +outdated: true +showInBlog: false +title: 'DataFX Controller Framework Preview' +date: "2013-12-27" +author: hendrik +categories: [DataFX, General, JavaFX] +excerpt: 'Today we released the version 2.0 of DataFX. As a next step we will work on DataFX 8.0 that will use Java 8 and JavaFX 8.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Today we released the version 2.0 of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). Thanks for all the feedback that we received with the last release candidates. As a next step we will work on DataFX 8.0 that will use Java 8 and JavaFX 8. One of the new features that will be part of the next release are currently in development. Today I will show a first preview to DataFX 8.0. + +The last month I added a new framework to DataFX. This framework should help to create the views of applications, define actions on them and create flows that contain a subset of views. + +To show the features of the framework I will create a simple JavaFX application. The app should manage persons. Persons can be loaded, created, edited and deleted by the app. Let's have a first look on the general concept of the application: + +![datafx1](/posts/guigarage-legacy/datafx1.png) + +As you can see the app contains 3 different views: + +* a master view that shows all persons in a list +* a create view that can add a new person +* a edit view to edit a person + +All this views are linked by actions ("save", "add", etc.) that will manipulate the data or show another view. First of all we want to take a look on the data model. Here a simple Person class is defined: + +```javapublic class Person { + private StringProperty name; + private StringProperty notes; + public Person() { + } + public Person (String name, String notes) { + setName(name); + setNotes(notes); + } + public String getName() { + return nameProperty().get(); + } + public StringProperty nameProperty() { + if(name == null) { + name = new SimpleStringProperty(); + } + return name; + } + public final void setName(String name) { + this.nameProperty().set(name); + } + public String getNotes() { + return notesProperty().get(); + } + public StringProperty notesProperty() { + if(notes == null) { + notes = new SimpleStringProperty(); + } + return notes; + } + public final void setNotes(String notes) { + this.notesProperty().set(notes); + } + @Override + public String toString() { + return getName(); + } +} ``` + +This class defines a person. Because we want to handle a list of persons we need another class that in our case defines the global data model: + +```javapublic class DataModel { + private ListProperty persons; + private IntegerProperty selectedPersonIndex; + public ListProperty getPersons() { + if (persons == null) { + ObservableList innerList = FXCollections.observableArrayList(); + persons = new SimpleListProperty<>(innerList); + } + return persons; + } + public int getSelectedPersonIndex() { + return selectedPersonIndexProperty().get(); + } + public void setSelectedPersonIndex(int selectedPersonIndex) { + this.selectedPersonIndex.set(selectedPersonIndex); + } + public IntegerProperty selectedPersonIndexProperty() { + if (selectedPersonIndex == null) { + selectedPersonIndex = new SimpleIntegerProperty(); + } + return selectedPersonIndex; + } +} ``` + +This class defines a list of persons and the currently selected person by an index. To create a first default set of persons that can be loaded we define a additional class. In a real world application this class could wrap a database connection, for example: + +```javapublic class LoadPersonsTask implements Runnable { + Person[] persons = { + new Person("Johan Vos", "Johan is CTO at LodgON, a Java Champion, a member of the BeJUG steering group, the Devoxx steering group and he is a JCP member."), + new Person("Jonathan Giles", "Jonathan Giles is the JavaFX UI controls technical lead at Oracle, where he has been involved with JavaFX since 2009."), + new Person("Hendrik Ebbers", "Hendrik Ebbers is Senior Java Architect at Materna GmbH in Dortmund, Germany.")}; + @Inject + private DataModel model; + @Override + public void run() { + model.getPersons().clear(); + ListDataProvider ldp = ListDataProviderBuilder + .create() + .dataReader(new ArrayDataReader(persons)) + .resultList(model.getPersons()) + .build(); + ldp.retrieve(); + } +} ``` + +After the data model is defined we can create the first view. Let's start with the master view. To create the view Scene Builder can be used. Here we can easily design the following view: + +![datafx21](/posts/guigarage-legacy/datafx21.png) + +For all needed controls IDs are defined in the FXML. Normally you need to define a controller class in FXML. This is not needed for the DataFX Controller API. Instead of this we can bind a controller and a FXML view by the use of an annotation. As the next step a controller is needed. As a first step we create a small controller with some additional annotations: + +```java@FXMLController("listView.fxml") +public class MasterViewController { +@FXML +private Button editButton; +@FXML +private Button removeButton; +@FXML +private Button addButton; +@FXML +private Button loadButton; +@FXML +private ListView dataList; +} ``` + +In this first version there is only one difference to the default JavaFX APIs: The FXMLController annotation is added. This annotation defines the link between the controller class and the FXML file. As a next step we want to create a data model. Here the next benefit of the framework can be used: Context Dependency Injection. To add a model to the the controller we can simple inject it: + +```java@Inject +private DataModel model; ``` + +To explain what happens here the CDI module in DataFX need to be described a little bit more. As in JEE CDI different scopes are supported in DataFX: + +* ViewScope +* FlowScope +* ApplicationScope + +All this scopes have a different context is is managed by the framework. All items that are part of the ViewScope have a lifetime of one view. A view is for example the master view in our example. The Application scope is defined as a global scope. All items in this scopes are singletons. The Singleton scope that is already defined in javax.inject can be used here, too. The flow scope defines a flow of views. In our example we will create one flow that handles all the defines views. In a more complex applications different flows can be handled. You can easily create a flow for each tap in a business application, for example. Additionally DataFX supports the dependent scope as it is defined in JEE. + +The data model in our application need to be defined in the flow scope. It should be accessed from all views in this scope. To do so a scope annotation need to be added to the class: + +```java@FlowScoped +public class DataModel { +... +} ``` + +Once this is done we can easily inject the data model in our view: + +```java@FXMLController("listView.fxml") +public class MasterViewController { + ... + @Inject + private DataModel model; +} ``` + +As a next step some initial setup is needed. To do so the PostConstruct annotation is supported by the DataFX framework: + +```java@FXMLController("listView.fxml") +public class MasterViewController { + .... + @PostConstruct + public void init() { + dataList.itemsProperty().bind(model.getPersons()); + model.selectedPersonIndexProperty().bind(dataList.getSelectionModel().selectedIndexProperty()); + } +} ``` + +Now the ListView is bounded to the data model. To create some basic data a action is needed. This action should fire when the "load" button is pressed. First we create a simple class that handles the action: + +```javapublic class LoadPersonsTask implements Runnable { + Person[] persons = { + new Person("Johan Vos", "Johan is CTO at LodgON, a Java Champion, a member of the BeJUG steering group, the Devoxx steering group and he is a JCP member."), + new Person("Jonathan Giles", "Jonathan Giles is the JavaFX UI controls technical lead at Oracle, where he has been involved with JavaFX since 2009."), + new Person("Hendrik Ebbers", "Hendrik Ebbers is Senior Java Architect at Materna GmbH in Dortmund, Germany.")}; + @Inject + private DataModel model; + @Override + public void run() { + model.getPersons().clear(); + ListDataProvider ldp = ListDataProviderBuilder + .create() + .dataReader(new ArrayDataReader(persons)) + .resultList(model.getPersons()) + .build(); + ldp.retrieve(); + } +} ``` + +As you can see the injected model is used here, too. This task can be added to the button by the use of the Flow API. This API defines a flow through all views. The first very simply version of our flow looks like this: + +```javaFlow flow = new Flow(MasterViewController.class). + withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); ``` + +This defines a flow that starts with the master view and adds a task action to this view. The action is defined by the id "load". To bind this action to the load button only a additional annotation is needed in the controller: + +```java@FXML +@FXMLFlowAction("load") +private Button loadButton; ``` + +Now the first version of the application can be started. To do so we need a main class that adds the flow to a JavaFX scene: + +```javapublic class DataFXDemo extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage stage) throws Exception { + Flow flow = new Flow(MasterViewController.class). + withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); + DefaultFlowContainer container = new DefaultFlowContainer(); + flow.createHandler().start(container); + Scene scene = new Scene(container.getPane()); + stage.setScene(scene); + stage.show(); + } +} ``` + +The DefaultFlowContainer class is used in the code. This class is a default implementation of a Pane that wraps a flow. When you start the application the "load" button can be used to load the list of persons. Because of the JavaFX binding the result will be shown directly: + +![datafx3](/posts/guigarage-legacy/datafx3.png) + +As a next step we want to add the edit action to the application. Here an additional view need to be created by Scene Builder: + +![datafx4](/posts/guigarage-legacy/datafx4.png) + +Additionally a controller class is needed. This class uses the described features: + +```java@FXMLController("detailView.fxml") +public class EditViewController { + @FXML + @FXMLFlowAction("save") + private Button saveButton; + @FXML + private TextField nameField; + @FXML + private TextArea notesTextArea; + @Inject + private DataModel model; + @PostConstruct + public void init() { + Person p = model.getPersons().get(model.getSelectedPersonIndex()); + nameField.textProperty().bindBidirectional(p.nameProperty()); + notesTextArea.textProperty().bindBidirectional(p.notesProperty()); + } +} ``` + +The data model is injected to the controller. Because it is defined in the flow scope it will be the same instance as in the master view. Additionally some bindings will be created to bind the UI controls to the data model. A flow action is added to the save button. This action is defined by the "save" ID. To add this view to the flow only some additional code is needed: + +```javaFlow flow = new Flow(MasterViewController.class). + withLink(MasterViewController.class, "edit", EditViewController.class). + withLink(EditViewController.class, "save", MasterViewController.class). + withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); ``` + +As you can see two links are added to the flow. This links are actions that will change the current view of the flow. In this cases we want to link from the master page to the edit page and vice versa. When you start the application now you can edit all persons that are part of the list: + +![datafx5](/posts/guigarage-legacy/datafx5.png) + +As a next step we want to add the remove action to the master view. This can be easily done by adding another action: + +```javapublic class RemoveActionTask implements Runnable { + @Inject + private DataModel model; + @Override + public void run() { + model.getPersons().remove(model.getSelectedPersonIndex()); + } +} ``` + +As the import action this action need to be defined in the flow and bound to a button: + +```java@FXML +@FXMLFlowAction("remove") +private Button removeButton; ``` + +Additionally the flow need to be changed: + +```javaFlow flow = new Flow(MasterViewController.class). + withLink(MasterViewController.class, "edit", EditViewController.class). + withLink(EditViewController.class, "save", MasterViewController.class). + withTaskAction(MasterViewController.class, "remove", RemoveActionTask.class). + withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class); ``` + +The Flow API of DataFX supports different types of actions. The link action and the task action are used in this example until now. As a next step we want to add the view to create new persons. Here we will use some additional features of the framework. + +Because the view should look like the edit view we can reuse the FXML here. Additonally a controller is needed. Here is a first basic version: + +```java@FXMLController("detailView.fxml") +public class AddViewController { + @FXML + @FXMLFlowAction("save") + private Button saveButton; + @FXML + private TextField nameField; + @FXML + private TextArea notesTextArea; + private StringProperty nameProperty = new SimpleStringProperty(); + private StringProperty noteProperty = new SimpleStringProperty(); + @Inject + private DataModel model; + @PostConstruct + public void init() { + nameField.textProperty().bindBidirectional(nameProperty); + notesTextArea.textProperty().bindBidirectional(noteProperty); + } +} ``` + +The data that is added in the view will be stored in the two properties that are defined in the view. Once everything is fine a new person should be created and added to the data model. To do so we use a new action type: The MethodAction. With this type a method of the controller can easily bound to an button. To do so we add a method with the needed annotation in the controller class: + +```java@FXMLController("detailView.fxml") +public class AddViewController { +@FXML +@FXMLFlowAction("save") +private Button saveButton; +... +@ActionMethod("addPerson") + public void addPerson() { + Person p = new Person(); + p.setName(nameProperty.get()); + model.getPersons().add(p); + } +} ``` + +Like all other actions this action need to be added to the flow. Because we want to add the person to the data model and then jump back to the master view a action chain is used here: + +```javaFlow flow = new Flow(MasterViewController.class). +               withLink(MasterViewController.class, "edit", EditViewController.class). +               withLink(MasterViewController.class, "add", AddViewController.class). +               withLink(EditViewController.class, "save", MasterViewController.class). +               withTaskAction(MasterViewController.class, "remove", RemoveActionTask.class). +               withTaskAction(MasterViewController.class, "load", LoadPersonsTask.class). +               withAction(AddViewController.class, "save", new FlowActionChain(new FlowMethodAction("addPerson"), new FlowLink(MasterViewController.class))); ``` + +A action chain defines a list of actions that will be handled. In this example the "save" button is bound to an action chain that first calls the "addPerson" method and then links to the master view. By doing so new persons can be created. + +Next to all the action types that are shown in this example DataFX will provide additional ones and the ability to add custom action classes. + +As a last step we want to add validation. When a new person is created we want to check if the name is not null. The DataFX API supports the default Java Bean Validation and adds to support for JavaFX properties. Because of this we can easily add a NotNull annotation to the name property: + +```java@NotNull +private StringProperty nameProperty = new SimpleStringProperty(); ``` + +To validate the data of the view a validation action can be added to the action chain that is bound to the "save" button: + +```javaFlow flow = new Flow(MasterViewController.class). +               ... +               withAction(AddViewController.class, "save", new FlowActionChain(new ValidationFlowAction(), new FlowMethodAction("addPerson"), new FlowLink(MasterViewController.class))); ``` + +The validation action automatically validates all validatable fields that are defined in the controller. Groups, as defined in the Java Bean Valdidation, are supported, too. When any data is not valid the action chain will stop. + +To provide feedback to the user some additional code is needed. The validator can be injected to the controller: + +```java@Validator +private ValidatorFX validator; ``` + +Now we can add a event handler to the validator that will show violations on screen: + +```java@FXMLController("detailView.fxml") +public class AddViewController { +... + @FXML + private Label violationLabel; + @Validator + private ValidatorFX validator; + @PostConstruct + public void init() { + ... + validator.setOnValidationFinished(event -> handleViolations(event.getViolations()); + } + private void handleViolations(Set> violations) { + if(violations.isEmpty()) { + violationLabel.setVisible(false); + } else { + ConstraintViolation violation = violations.iterator().next(); + violationLabel.setText(violation.getPropertyPath() + " " + violation.getMessage()); + violationLabel.setVisible(true); + } + } +} ``` + +Once this is done the view will show violations on the screen: + +![datafx6](/posts/guigarage-legacy/datafx6.png) + +This example shows some of the DataFX Controller features. The complete API is not finished yet and can be found in a branch of the DataFX repository. I hope to receive some feedback about this example. diff --git a/content/posts/2014-01-23-datafx-8-preview-2-processchain.md b/content/posts/2014-01-23-datafx-8-preview-2-processchain.md index c4651c4b..641586a6 100644 --- a/content/posts/2014-01-23-datafx-8-preview-2-processchain.md +++ b/content/posts/2014-01-23-datafx-8-preview-2-processchain.md @@ -1,57 +1,55 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX 8 Preview 2: The ProcessChain' -date: "2014-01-23" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'This DataFX 8 preview introduces the ProcessChain. This uses Java 8 features like Lambda to provide multi threaded functionality in JavaFX' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Some time ago I gave a [first preview of the new APIs and functions]({{< ref "/posts/2013-12-27-datafx-controller-framework-preview" >}}) in [DataFX 8]({{ site.baseurl }}{% link pages/projects/datafx.md %}). We are currently plan to release DataFX 8 once JavaFX 8 is released. I plan to blog about the new features in DataFX in the next weeks. By doing so we hope to receive some useful feedback. In the last preview I described the [controller and flow API]({{< ref "/posts/2013-12-27-datafx-controller-framework-preview" >}}) of DataFX. Today I will show you only only small class :) - -Next to new APIs we added some helpful classes to the DataFX core packages. The class that I want to show you today is one of these new classes: The ProcessChain. - -The ProcessChain can be used to create a task chain. A task can be run on the "JavaFX Application Thread" or on a background thread. Some of you may know the SwingWorker class that was a important tool when working with background tasks in Swing. With the ProcessChain you can do similar stuff but thanks to Java 8, Lambdas and functional interfaces this class is more powerful that SwingWorker ever was. - -Let's think about a dialog that loads some data from a server. This can be done by clicking a button. Whenever the button is pressed the following workflow should be executed: - -* Disable Button -* Communicate with Server -* Enable Button - -The button must enabled and disabled on the "JavaFX Application Thread" but you should not communicate with the server on this thread cause it will freeze the application. Because of that the communication must be executed on a extra thread: - -* Disable Button (Application Thread) -* Communicate with Server (Background-Thread) -* Enable Button (Application Thread) - -The ProcessChain is a fluent API that can be used to create this workflows in JavaFX. Because its a chain you can add as many task as you want. The ProcessChain can use Lambdas and pass task results to the next Task. The following default interfaces can be used here: - -* java.util.function.Function -* java.util.function.Supplier -* java.util.function.Consumer -* java.lang.Runnable - -Here is a simple example: - -{{< highlight java >}} -Label label = new Label("No data"); -Button button = new Button("Press me“); -button.setOnAction(new EventHandler() { - public void handle(ActionEvent event) { - new ProcessChain().inPlatformThread(() -> button.setDisable(true)) - .inExecutor(() -> communicateWithServer()) - .inExecutor(() -> {return "Time in Millis: " + System.currentTimeMillis();}) - .inPlatformThread((Consumer) (t) -> label.setText(t.toString())) - .inPlatformThread(() -> button.setDisable(false)) - .run(); - } -}); -{{< / highlight >}} - -As you can see in the example it is very easy to create a chain with the API. By using the `inExecutor(...)` or `inPlatformThread(...)` method a developer can choose if a task should run on the "JavaFX Application Thread" or in background. - -In addition tasks can publish its result and use the result of the previous one. Here the new Java 8 interfaces Function, Supplier and Consumer can be used to define tasks with an input or output value. - -The DataReader API is still part of DataFX of course. If you use DataReaders you shouldn't need the shown API or can add all tasks to the Platform thread. But sometimes there are issues where you need to create your own custom background tasks. In these cases the ProcessChain is your friend ;) +--- +outdated: true +showInBlog: false +title: 'DataFX 8 Preview 2: The ProcessChain' +date: "2014-01-23" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'This DataFX 8 preview introduces the ProcessChain. This uses Java 8 features like Lambda to provide multi threaded functionality in JavaFX' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Some time ago I gave a [first preview of the new APIs and functions](/posts/2013-12-27-datafx-controller-framework-preview) in [DataFX 8]({{ site.baseurl }}{% link pages/projects/datafx.md %}). We are currently plan to release DataFX 8 once JavaFX 8 is released. I plan to blog about the new features in DataFX in the next weeks. By doing so we hope to receive some useful feedback. In the last preview I described the [controller and flow API](/posts/2013-12-27-datafx-controller-framework-preview) of DataFX. Today I will show you only only small class :) + +Next to new APIs we added some helpful classes to the DataFX core packages. The class that I want to show you today is one of these new classes: The ProcessChain. + +The ProcessChain can be used to create a task chain. A task can be run on the "JavaFX Application Thread" or on a background thread. Some of you may know the SwingWorker class that was a important tool when working with background tasks in Swing. With the ProcessChain you can do similar stuff but thanks to Java 8, Lambdas and functional interfaces this class is more powerful that SwingWorker ever was. + +Let's think about a dialog that loads some data from a server. This can be done by clicking a button. Whenever the button is pressed the following workflow should be executed: + +* Disable Button +* Communicate with Server +* Enable Button + +The button must enabled and disabled on the "JavaFX Application Thread" but you should not communicate with the server on this thread cause it will freeze the application. Because of that the communication must be executed on a extra thread: + +* Disable Button (Application Thread) +* Communicate with Server (Background-Thread) +* Enable Button (Application Thread) + +The ProcessChain is a fluent API that can be used to create this workflows in JavaFX. Because its a chain you can add as many task as you want. The ProcessChain can use Lambdas and pass task results to the next Task. The following default interfaces can be used here: + +* java.util.function.Function +* java.util.function.Supplier +* java.util.function.Consumer +* java.lang.Runnable + +Here is a simple example: + +```javaLabel label = new Label("No data"); +Button button = new Button("Press me“); +button.setOnAction(new EventHandler() { + public void handle(ActionEvent event) { + new ProcessChain().inPlatformThread(() -> button.setDisable(true)) + .inExecutor(() -> communicateWithServer()) + .inExecutor(() -> {return "Time in Millis: " + System.currentTimeMillis();}) + .inPlatformThread((Consumer) (t) -> label.setText(t.toString())) + .inPlatformThread(() -> button.setDisable(false)) + .run(); + } +}); ``` + +As you can see in the example it is very easy to create a chain with the API. By using the `inExecutor(...)` or `inPlatformThread(...)` method a developer can choose if a task should run on the "JavaFX Application Thread" or in background. + +In addition tasks can publish its result and use the result of the previous one. Here the new Java 8 interfaces Function, Supplier and Consumer can be used to define tasks with an input or output value. + +The DataReader API is still part of DataFX of course. If you use DataReaders you shouldn't need the shown API or can add all tasks to the Platform thread. But sometimes there are issues where you need to create your own custom background tasks. In these cases the ProcessChain is your friend ;) diff --git a/content/posts/2014-01-24-javafx-meets-javaee.md b/content/posts/2014-01-24-javafx-meets-javaee.md index ef9a7135..df8e8ccc 100644 --- a/content/posts/2014-01-24-javafx-meets-javaee.md +++ b/content/posts/2014-01-24-javafx-meets-javaee.md @@ -12,7 +12,7 @@ At [JavaOne](http://www.oracle.com/javaone/) [Arun Gupta](https://www.java.net// ![book](/posts/guigarage-legacy/javaee-book.jpg) -After reading the book you are ready to develop a web application that is based on JEE 7. Most of this applications will have a web frontend that is based on JSF as it is shown in the book, too. But as I mentioned in a [earlier post]({{< ref "/posts/2013-05-11-designing-javafx-business-applications-part-1" >}}), a modern business applications often needs support for mobile or desktop applications: +After reading the book you are ready to develop a web application that is based on JEE 7. Most of this applications will have a web frontend that is based on JSF as it is shown in the book, too. But as I mentioned in a [earlier post](/posts/2013-05-11-designing-javafx-business-applications-part-1), a modern business applications often needs support for mobile or desktop applications: ![3tier](/posts/guigarage-legacy/3tier.png) @@ -61,7 +61,7 @@ With DataFX 2.0 we will introduce the flow API. By using this API you can simply ## Contexts and Dependency Injection -As shown in [one of my last posts]({{< ref "/posts/2013-12-27-datafx-controller-framework-preview" >}}) DataFX supports context dependency injection in controllers. Currently Providers, etc. are not supported. This will be one of the next features that we want to add. DataFX currently support the following scopes: +As shown in [one of my last posts](/posts/2013-12-27-datafx-controller-framework-preview) DataFX supports context dependency injection in controllers. Currently Providers, etc. are not supported. This will be one of the next features that we want to add. DataFX currently support the following scopes: * viewScope * flowScope @@ -69,12 +69,12 @@ As shown in [one of my last posts]({{< ref "/posts/2013-12-27-datafx-controller- ## Concurrency Utilities -With JEE 7 you can use Executor instances in your business app. In DataFX we provide different classes and utility functions to provide a great support for concurrency. Next to invokeLater(...) method that is part of JavaFX you can find [invokeAndWait(...) utility methods in DataFX-core]({{< ref "/posts/2013-01-01-invokeandwait-for-javafx" >}}). These can be used to create your own background task API. In addition DataFX provides a [new Executor class]({{< ref "/posts/2013-02-09-datafx-observableexecutor-preview" >}}) that supports JavaFX properties. In most cases you can use the DataReader API of DataFX to handle background tasks. In some special cases you can use the [ProcessChain class]({{ site.baseurl }}{% post_url 2014-01-23-datafx-8-preview-2-processchain %} +With JEE 7 you can use Executor instances in your business app. In DataFX we provide different classes and utility functions to provide a great support for concurrency. Next to invokeLater(...) method that is part of JavaFX you can find [invokeAndWait(...) utility methods in DataFX-core](/posts/2013-01-01-invokeandwait-for-javafx). These can be used to create your own background task API. In addition DataFX provides a [new Executor class](/posts/2013-02-09-datafx-observableexecutor-preview) that supports JavaFX properties. In most cases you can use the DataReader API of DataFX to handle background tasks. In some special cases you can use the [ProcessChain class]({{ site.baseurl }}{% post_url 2014-01-23-datafx-8-preview-2-processchain %} ) that will be new in DataFX 8. ## Bean Validation -Thanks to JEE we have a [default specification](http://docs.oracle.com/javaee/6/tutorial/doc/gircz.html) for bean validation. DataFX uses this specification, too. By doing so a developer doesn't need to learn new APIs. All the annotations and interfaces that are part of [JSR 303](http://beanvalidation.org/1.0/spec/) can be used in DataFX. A first example how bean validation works in DataFX can be found [here]({{< ref "/posts/2013-12-27-datafx-controller-framework-preview" >}}). +Thanks to JEE we have a [default specification](http://docs.oracle.com/javaee/6/tutorial/doc/gircz.html) for bean validation. DataFX uses this specification, too. By doing so a developer doesn't need to learn new APIs. All the annotations and interfaces that are part of [JSR 303](http://beanvalidation.org/1.0/spec/) can be used in DataFX. A first example how bean validation works in DataFX can be found [here](/posts/2013-12-27-datafx-controller-framework-preview). ## Java Persistence diff --git a/content/posts/2014-03-27-datafx-8-nighthacking.md b/content/posts/2014-03-27-datafx-8-nighthacking.md index 6de4c7d2..2d9f7c8b 100644 --- a/content/posts/2014-03-27-datafx-8-nighthacking.md +++ b/content/posts/2014-03-27-datafx-8-nighthacking.md @@ -10,5 +10,5 @@ preview_image: "/posts/preview-images/software-development-green.svg" --- [Stephen Chin](https://twitter.com/steveonjava) has interviewed me yesterday for [http://nighthacking.com](http://nighthacking.com) about DataFX 8. In the interview I show a lot of new features and APIs of DataFX. Have fun :) -{{< youtube 94oUnzlPUyQ >}} + diff --git a/content/posts/2014-03-28-reactive-programming-javafx.md b/content/posts/2014-03-28-reactive-programming-javafx.md index fd146724..1c72c9b2 100644 --- a/content/posts/2014-03-28-reactive-programming-javafx.md +++ b/content/posts/2014-03-28-reactive-programming-javafx.md @@ -1,55 +1,49 @@ ---- -outdated: true -showInBlog: false -title: 'Reactive Programming with JavaFX' -date: "2014-03-28" -author: hendrik -categories: [JavaFX] -excerpt: 'Because the JavaFX API was designed for Java 8 it provides a lot of Lambda support and callbacks are used a lot. But next to the default JavaFX APIs there are currently an open source projects that adds a lot of reactive design and architecture to the JavaFX basics: ReactFX.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Java 8 is finally released and Lambda expression are an official part of Java. Thanks to this it's much easier to write applications in a reactive way. One of the main concepts in reactive architecture is an __event driven design__. [The reactive manifesto](http://www.reactivemanifesto.org/) contains the following about event driven design: - -> In an event-driven application, the components interact with each other through the production and consumption of events—discrete pieces of information describing facts. These events are sent and received in an asynchronous and non-blocking fashion. - -## Reactive Programming with Java - -By using Lambdas it's very easy to define callbacks that can react on specific events. Here is a short example about an event driven design without the usage of Lambda expressions: - -{{< highlight java >}} -public static void hello(String... names) { - Observable.from(names).subscribe(new Action() { - @Override - public void call(String s) { - System.out.println("Hello " + s + "!"); - } - }); -} -{{< / highlight >}} - -Thanks to Lambda expressions the same functionallity can be coded in Java 8 this way: - -{{< highlight java >}} -public static void hello(String... names) { - Observable.from(names).subscribe((s) -> System.out.println("Hello " + s + "!")); -} -{{< / highlight >}} - -The example is part of the [RxJava tutorial](https://github.com/Netflix/RxJava/wiki/Getting-Started). - -## Reactive Programming with JavaFX - -Let's take a look at JavaFX. Because the JavaFX API was designed for Java 8 it provides a lot of Lambda support and callbacks are used a lot. But next to the default JavaFX APIs there are currently an open source projects that adds a lot of reactive design and architecture to the JavaFX basics: [ReactFX](https://github.com/TomasMikula/ReactFX). - -By using ReactFX you can do a lot of cool event driven stuff with only a few lines of code. Here is an example how event handlers can be designed to react on user inputs: - -{{< highlight java >}} -EventStream clicks = EventStreams.eventsOf(node, MouseEvent.MOUSE_CLICKED); -clicks.subscribe(click -> System.out.println("Click!")); -{{< / highlight >}} - -I think the API provides a lot of cool functionallity that let you design JavaFX applications that are more reactive and I hope to see a lot of more code like shown in the example above. - -## Summery - -The are currently 2 cool reactive APIs for Java out there: [RxJava](https://github.com/Netflix/RxJava/wiki/Getting-Started) for a basic use in Java and [ReactFX](https://github.com/TomasMikula/ReactFX) that is specialized for JavaFX. Theoretically you can do everything (or most of the stuff) you can do with ReactFXwith the help of RxJava, too. But here you need to concern about the JavaFX Application Thread. Because ReactFX is implementated for JavaFX (or a single threaded environment) you don't need to handle this. A first comparison of this two libraries can be found [here](https://gist.github.com/timyates/fd6904dcca366d50729c#comment-1198536). +--- +outdated: true +showInBlog: false +title: 'Reactive Programming with JavaFX' +date: "2014-03-28" +author: hendrik +categories: [JavaFX] +excerpt: 'Because the JavaFX API was designed for Java 8 it provides a lot of Lambda support and callbacks are used a lot. But next to the default JavaFX APIs there are currently an open source projects that adds a lot of reactive design and architecture to the JavaFX basics: ReactFX.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Java 8 is finally released and Lambda expression are an official part of Java. Thanks to this it's much easier to write applications in a reactive way. One of the main concepts in reactive architecture is an __event driven design__. [The reactive manifesto](http://www.reactivemanifesto.org/) contains the following about event driven design: + +> In an event-driven application, the components interact with each other through the production and consumption of events—discrete pieces of information describing facts. These events are sent and received in an asynchronous and non-blocking fashion. + +## Reactive Programming with Java + +By using Lambdas it's very easy to define callbacks that can react on specific events. Here is a short example about an event driven design without the usage of Lambda expressions: + +```javapublic static void hello(String... names) { + Observable.from(names).subscribe(new Action() { + @Override + public void call(String s) { + System.out.println("Hello " + s + "!"); + } + }); +} ``` + +Thanks to Lambda expressions the same functionallity can be coded in Java 8 this way: + +```javapublic static void hello(String... names) { + Observable.from(names).subscribe((s) -> System.out.println("Hello " + s + "!")); +} ``` + +The example is part of the [RxJava tutorial](https://github.com/Netflix/RxJava/wiki/Getting-Started). + +## Reactive Programming with JavaFX + +Let's take a look at JavaFX. Because the JavaFX API was designed for Java 8 it provides a lot of Lambda support and callbacks are used a lot. But next to the default JavaFX APIs there are currently an open source projects that adds a lot of reactive design and architecture to the JavaFX basics: [ReactFX](https://github.com/TomasMikula/ReactFX). + +By using ReactFX you can do a lot of cool event driven stuff with only a few lines of code. Here is an example how event handlers can be designed to react on user inputs: + +```javaEventStream clicks = EventStreams.eventsOf(node, MouseEvent.MOUSE_CLICKED); +clicks.subscribe(click -> System.out.println("Click!")); ``` + +I think the API provides a lot of cool functionallity that let you design JavaFX applications that are more reactive and I hope to see a lot of more code like shown in the example above. + +## Summery + +The are currently 2 cool reactive APIs for Java out there: [RxJava](https://github.com/Netflix/RxJava/wiki/Getting-Started) for a basic use in Java and [ReactFX](https://github.com/TomasMikula/ReactFX) that is specialized for JavaFX. Theoretically you can do everything (or most of the stuff) you can do with ReactFXwith the help of RxJava, too. But here you need to concern about the JavaFX Application Thread. Because ReactFX is implementated for JavaFX (or a single threaded environment) you don't need to handle this. A first comparison of this two libraries can be found [here](https://gist.github.com/timyates/fd6904dcca366d50729c#comment-1198536). diff --git a/content/posts/2014-03-29-javafx-css-utilities.md b/content/posts/2014-03-29-javafx-css-utilities.md index 49efe152..9ba4e41b 100644 --- a/content/posts/2014-03-29-javafx-css-utilities.md +++ b/content/posts/2014-03-29-javafx-css-utilities.md @@ -1,106 +1,100 @@ ---- -outdated: true -showInBlog: false -title: 'JavaFX CSS Utilities' -date: "2014-03-29" -author: hendrik -categories: [JavaFX] -excerpt: 'Ever tried to add a Styleable property to a JavaFX Control or Skin? By doing so you can add additional CSS support to a Control type.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Ever tried to add a Styleable property to a JavaFX Control or Skin? By doing so you can add additional CSS support to a Control type. [Gerrit Grunwald](https://twitter.com/hansolo_) has described the benefits of styleable properties in a [blog post](http://harmoniccode.blogspot.de/2013/05/css-confusing-style-sheets.html). One big problem is the boilerplate code that will be created when implementing these properties in a Control class. Here is an example how a Control with only one property will look like: - -{{< highlight java >}} -public class MyControl extends Control { - private StyleableObjectProperty backgroundFill; - public Paint getBackgroundFill() { - return backgroundFill == null ? Color.GRAY : backgroundFill.get(); - } - public void setBackgroundFill(Paint backgroundFill) { - this.backgroundFill.set(backgroundFill); - } - public StyleableObjectProperty backgroundFillProperty() { - if (backgroundFill == null) { - backgroundFill = new SimpleStyleableObjectProperty(StyleableProperties.BACKGROUND_FILL, MyControl.this, "backgroundFill", Color.GRAY); - } - return backgroundFill; - } - private static class StyleableProperties { - private static final CssMetaData< MyControl, Paint> BACKGROUND_FILL = - new CssMetaData< MyControl, Paint>("-fx-background-fill", - PaintConverter.getInstance(), Color.GRAY) { - @Override - public boolean isSettable(MyControl control) { - return control.backgroundFill == null || !control.backgroundFill.isBound(); - } - @Override - public StyleableProperty getStyleableProperty(MyControl control) { - return control.backgroundFillProperty(); - } - }; - private static final List> STYLEABLES; - static { - final List> styleables = - new ArrayList>(Control.getClassCssMetaData()); - Collections.addAll(styleables, - BACKGROUND_FILL - ); - STYLEABLES = Collections.unmodifiableList(styleables); - } - } - @Override - public List> getControlCssMetaData() { - return getClassCssMetaData(); - } - public static List> getClassCssMetaData() { - return StyleableProperties.STYLEABLES; - } -} -{{< / highlight >}} - -That's a lot of code for only one property. Therefore I created some helper classes to do all the work. These classes are part of the "css-helper" library that I released today. - -Here is an example how the Control will look like when using the "css-helper" library: - -{{< highlight java >}} -public class MyControl extends Control { - private StyleableObjectProperty backgroundFill; - public Paint getBackgroundFill() { - return backgroundFill == null ? Color.GRAY : backgroundFill.get(); - } - public void setBackgroundFill(Paint backgroundFill) { - this.backgroundFill.set(backgroundFill); - } - public StyleableObjectProperty backgroundFillProperty() { - if (backgroundFill == null) { - backgroundFill = CssHelper.createProperty(StyleableProperties.BACKGROUND_FILL, MyControl); - } - return backgroundFill; - } - private static class StyleableProperties { - private static final CssHelper.PropertyBasedCssMetaData BACKGROUND_FILL = CssHelper.createMetaData("-fx-background-fill", PaintConverter.getInstance(), "backgroundFill", Color.LIGHTGREEN); - private static final List> STYLEABLES = CssHelper.createCssMetaDataList(Control.getClassCssMetaData(), BACKGROUND_FILL); - } - @Override - public List> getControlCssMetaData() { - return getClassCssMetaData(); - } - public static List> getClassCssMetaData() { - return StyleableProperties.STYLEABLES; - } -} -{{< / highlight >}} - -By using the static methods of the CssHelper class the code is much more readable. - -But there is one problem with the API: It uses reflection internally and because of this the CSS algorithm will be slower as when using the first aproach. So the CssHelper should only be used for Controls that should not be part of an open source library and don't appear often in the scene graph. If you need a special Control in your application or add a CSS property to an existing one you can use these classes to minimize the source code. - -The Library is deployed to [Maven Central](http://search.maven.org/#artifactdetails%7Ccom.guigarage%7Ccss-helper%7C0.1%7Cjar) and can be easily added to a Maven project: - -{{< highlight xml >}} - -    com.guigarage -    css-helper -    0.1 - -{{< / highlight >}} +--- +outdated: true +showInBlog: false +title: 'JavaFX CSS Utilities' +date: "2014-03-29" +author: hendrik +categories: [JavaFX] +excerpt: 'Ever tried to add a Styleable property to a JavaFX Control or Skin? By doing so you can add additional CSS support to a Control type.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Ever tried to add a Styleable property to a JavaFX Control or Skin? By doing so you can add additional CSS support to a Control type. [Gerrit Grunwald](https://twitter.com/hansolo_) has described the benefits of styleable properties in a [blog post](http://harmoniccode.blogspot.de/2013/05/css-confusing-style-sheets.html). One big problem is the boilerplate code that will be created when implementing these properties in a Control class. Here is an example how a Control with only one property will look like: + +```javapublic class MyControl extends Control { + private StyleableObjectProperty backgroundFill; + public Paint getBackgroundFill() { + return backgroundFill == null ? Color.GRAY : backgroundFill.get(); + } + public void setBackgroundFill(Paint backgroundFill) { + this.backgroundFill.set(backgroundFill); + } + public StyleableObjectProperty backgroundFillProperty() { + if (backgroundFill == null) { + backgroundFill = new SimpleStyleableObjectProperty(StyleableProperties.BACKGROUND_FILL, MyControl.this, "backgroundFill", Color.GRAY); + } + return backgroundFill; + } + private static class StyleableProperties { + private static final CssMetaData< MyControl, Paint> BACKGROUND_FILL = + new CssMetaData< MyControl, Paint>("-fx-background-fill", + PaintConverter.getInstance(), Color.GRAY) { + @Override + public boolean isSettable(MyControl control) { + return control.backgroundFill == null || !control.backgroundFill.isBound(); + } + @Override + public StyleableProperty getStyleableProperty(MyControl control) { + return control.backgroundFillProperty(); + } + }; + private static final List> STYLEABLES; + static { + final List> styleables = + new ArrayList>(Control.getClassCssMetaData()); + Collections.addAll(styleables, + BACKGROUND_FILL + ); + STYLEABLES = Collections.unmodifiableList(styleables); + } + } + @Override + public List> getControlCssMetaData() { + return getClassCssMetaData(); + } + public static List> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } +} ``` + +That's a lot of code for only one property. Therefore I created some helper classes to do all the work. These classes are part of the "css-helper" library that I released today. + +Here is an example how the Control will look like when using the "css-helper" library: + +```javapublic class MyControl extends Control { + private StyleableObjectProperty backgroundFill; + public Paint getBackgroundFill() { + return backgroundFill == null ? Color.GRAY : backgroundFill.get(); + } + public void setBackgroundFill(Paint backgroundFill) { + this.backgroundFill.set(backgroundFill); + } + public StyleableObjectProperty backgroundFillProperty() { + if (backgroundFill == null) { + backgroundFill = CssHelper.createProperty(StyleableProperties.BACKGROUND_FILL, MyControl); + } + return backgroundFill; + } + private static class StyleableProperties { + private static final CssHelper.PropertyBasedCssMetaData BACKGROUND_FILL = CssHelper.createMetaData("-fx-background-fill", PaintConverter.getInstance(), "backgroundFill", Color.LIGHTGREEN); + private static final List> STYLEABLES = CssHelper.createCssMetaDataList(Control.getClassCssMetaData(), BACKGROUND_FILL); + } + @Override + public List> getControlCssMetaData() { + return getClassCssMetaData(); + } + public static List> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } +} ``` + +By using the static methods of the CssHelper class the code is much more readable. + +But there is one problem with the API: It uses reflection internally and because of this the CSS algorithm will be slower as when using the first aproach. So the CssHelper should only be used for Controls that should not be part of an open source library and don't appear often in the scene graph. If you need a special Control in your application or add a CSS property to an existing one you can use these classes to minimize the source code. + +The Library is deployed to [Maven Central](http://search.maven.org/#artifactdetails%7Ccom.guigarage%7Ccss-helper%7C0.1%7Cjar) and can be easily added to a Maven project: + +```xml +    com.guigarage +    css-helper +    0.1 + ``` diff --git a/content/posts/2014-05-15-javafx-8-interview-jax-2014.md b/content/posts/2014-05-15-javafx-8-interview-jax-2014.md index 12d2e549..715cfaf7 100644 --- a/content/posts/2014-05-15-javafx-8-interview-jax-2014.md +++ b/content/posts/2014-05-15-javafx-8-interview-jax-2014.md @@ -10,5 +10,5 @@ preview_image: "/posts/preview-images/software-development-green.svg" --- I was interviewed at [JAX Conference](http://jax.de/2014/) this week. The [interview](http://jaxenter.de/videos/JavaFX-8-wo-stehen-wir-wo-geht-hin-173516) is in German and I talk about the current state of JavaFX and if it's ready for the use in enterprise: -{{< youtube 43wcPNo1U4Y >}} + diff --git a/content/posts/2014-05-19-datafx-8-0-tutorials.md b/content/posts/2014-05-19-datafx-8-0-tutorials.md index 058ce35e..eda25549 100644 --- a/content/posts/2014-05-19-datafx-8-0-tutorials.md +++ b/content/posts/2014-05-19-datafx-8-0-tutorials.md @@ -16,10 +16,10 @@ I will introduce all DataFX tutorials first here in my blog. Later I will add th The first tutorials are only. Here is a short list: -* [How to set up a DataFX application]({{< ref "/posts/2015-01-28-set-datafx-application" >}}) -* [Tutorial 1]({{< ref "/posts/2014-05-20-datafx-tutorial-1" >}}) -* [Tutorial 2]({{< ref "/posts/2014-05-22-datafx-tutorial-2" >}}) -* [Tutorial 3]({{< ref "/posts/2014-05-31-datafx-tutorial-3" >}}) -* [Tutorial 4]({{< ref "/posts/2014-06-08-datafx-tutorial-4" >}}) -* [Tutorial 5]({{< ref "/posts/2014-06-27-datafx-tutorial-5" >}}) -* [Tutorial 6]({{< ref "/posts/2015-01-22-datafx-tutorial-6" >}}) +* [How to set up a DataFX application](/posts/2015-01-28-set-datafx-application) +* [Tutorial 1](/posts/2014-05-20-datafx-tutorial-1) +* [Tutorial 2](/posts/2014-05-22-datafx-tutorial-2) +* [Tutorial 3](/posts/2014-05-31-datafx-tutorial-3) +* [Tutorial 4](/posts/2014-06-08-datafx-tutorial-4) +* [Tutorial 5](/posts/2014-06-27-datafx-tutorial-5) +* [Tutorial 6](/posts/2015-01-22-datafx-tutorial-6) diff --git a/content/posts/2014-05-20-datafx-tutorial-1.md b/content/posts/2014-05-20-datafx-tutorial-1.md index dce12b5a..772d6424 100644 --- a/content/posts/2014-05-20-datafx-tutorial-1.md +++ b/content/posts/2014-05-20-datafx-tutorial-1.md @@ -1,151 +1,137 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX 8 Tutorial 1' -date: "2014-05-20" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'In this post I want to show you the first simple tutorial for DataFX 8. In thetutorial we want to create a simple view with only one point of interaction.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -As mentioned [here]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}) I started a series of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) tutorials. In this post I want to show you the first simple tutorial. - -In this tutorial we want to create a simple view with only one point of interaction. The source of the tutorial can be found [here](https://bitbucket.org/datafx/datafx/src/940c9c9556c6/datafx-tutorial1/?at=default). The following figure shows how the example will look like: - -![example](/posts/guigarage-legacy/exasmple1.png) - -Every time the button in the view is clicked the text in the top label should change. To create this application we will start with the layout of the view. To do so we need a [FXML](http://docs.oracle.com/javafx/2/fxml_get_started/jfxpub-fxml_get_started.htm) file that defines the JavaFX view of the application. This can easily be created with [Scene Builder](http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html). The FXML file ([simpleView.fxml](https://bitbucket.org/datafx/datafx/src/940c9c9556c662760a39830d05c9a4519eea3832/datafx-tutorial1/src/main/resources/org/datafx/tutorial/simpleView.fxml?at=default)) of the tutorial will look like this: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - - -{{< / highlight >}} - -Once this is created we need a controller class for the view. A first simple version that only uses JavaFX basic API will look like this: - -{{< highlight java >}} -public class SimpleController { - @FXML - private Label resultLabel; - @FXML - private Button actionButton; -} -{{< / highlight >}} - -Like it is defined in the JavaFX basic API the two fields `resultLabel` and `actionButton` are named as the components defined in the FXML file by its `fx:id` attribute. BY adding the `@FXML` annotation to these fields they will be automatically injected once the controller will be instantiated. -Until now only JavaFX basic APIs are used. Now we want to add some DataFX magic. As mentioned we need some interaction every time the button is pressed. Because of that we want to trigger an action by the button. DataFX provides two annotations to handle this approach: `@ActionTrigger` and `@ActionMethod`. - -The `@ActionTrigger` annotation can be added to a field of the controller that defines a Node in the view. The DataFX container will automatically add an action handler to the Node. Any method that should be called once an action is triggered must be annotated with the `@ActionMethod` annotation. For both Annotations you must specify a value that defines the unique action id of the action that is handled by the annotation. In this case I used the id `myAction` and added the annotations to the class: - -{{< highlight java >}} -public class SimpleController { - @FXML - private Label resultLabel; - @FXML - @ActionTrigger("myAction") - private Button actionButton; - @ActionMethod("myAction") - public void onAction() { - // DO some action... - } -} -{{< / highlight >}} - -So whenever the `actionButton` will be pressed the `onAction()` method will be called because they are bound by the same id. The id must be unique in view controller. As you will see in following tutorials there are other types of actions than simply calling a method. For now the most important point is that a component that is annotated with `@ActionTrigger` can trigger a method that is annotated with `@ActionMethod` if both annotations define the same unique action id. - -As a next step we will implement the action and define a default text for the label: - -{{< highlight java >}} -public class SimpleController { - @FXML - private Label resultLabel; - @FXML - @ActionTrigger("myAction") - private Button actionButton; - private int clickCount = 0; - @PostConstruct - public void init() { - resultLabel.setText("Button was clicked " + clickCount + " times"); - } - @ActionMethod("myAction") - public void onAction() { - clickCount++; - resultLabel.setText("Button was clicked " + clickCount + " times"); - } -} -{{< / highlight >}} - -As you can see in the code a new method called `init()` was added. This method should be called after the controller has been initialized and the fields that are annotated with `@FXML` are injected. In DataFX this can be done by adding the `@PostConstruct` Annotation to the method. By doing so the DataFX flow container will call this method once all injectable values of the controller instance are injected. There three different types of values / fields that can be injected: - -* UI components that are annotated by `@FXML` -* DataFX objects. Here DataFX provides several annotations -* Custom implementations. These will be injected by using the `@Inject` annotation - -The method that is annotated by `@PostContruct` will be called when all injections are finished. In this first example we will only use `@FXML` to inject FXML UI components to the controller. - -One thing that is still missing is the binding between the controller class and the FXML file. DataFX provides the `@FXMLController` annotation for this purpose. By using this annotation the controller class defines its FXML file that contains the layout of the view. After adding the annotation the final controller class will look like this: - -{{< highlight java >}} -@FXMLController("simpleView.fxml") -public class SimpleController { - @FXML - private Label resultLabel; - @FXML - @ActionTrigger("myAction") - private Button actionButton; - private int clickCount = 0; - @PostConstruct - public void init() { - resultLabel.setText("Button was clicked " + clickCount + " times"); - } - @ActionMethod("myAction") - public void onAction() { - clickCount++; - resultLabel.setText("Button was clicked " + clickCount + " times"); - } -} -{{< / highlight >}} - -Once this is done we need a main class to start the application and show the view on screen. We can't use the basic FXMLLoader class here because we used some annotations that must be handled by the DataFX container. But since the last preview of DataFX 8.0 the Flow API provides a very simple way to show the view on screen. Here is the complete main class that will start the application: - -{{< highlight java >}} -public class Tutorial1Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - new Flow(SimpleController.class).startInStage(primaryStage); - } -} -{{< / highlight >}} - - The shown way is the most simple way to start a flow: the controller class of the start view is always passed as parameter to the constructor of the `Flow` class. Because in this demo we have only one view we simply pass the `SimpleController` class to the flow. The `Flow` class provides a utility method called `startInStage()` that renders the Flow in a `Stage`. By doing so the `Scene` will be created automatically and the `Stage` will contain a `Scene` that only contains the defined flow. In this first tutorial the flow will only contain one view. - -This first example is quite easy and normally you could define the action binding by only one line of Java code in the `init()` method: - -{{< highlight java >}} -actionButton.setOnAction((e) -> onAction()); -{{< / highlight >}} - -So why are all these annotations used here? As you will see in further tutorials that are more complex than this one it will make sense to use the annotations to provide more readable code. - -I hope you liked this first tutorial. I plan to add the second one at the end of the week. +--- +outdated: true +showInBlog: false +title: 'DataFX 8 Tutorial 1' +date: "2014-05-20" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'In this post I want to show you the first simple tutorial for DataFX 8. In thetutorial we want to create a simple view with only one point of interaction.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +As mentioned [here](/posts/2014-05-19-datafx-8-0-tutorials) I started a series of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) tutorials. In this post I want to show you the first simple tutorial. + +In this tutorial we want to create a simple view with only one point of interaction. The source of the tutorial can be found [here](https://bitbucket.org/datafx/datafx/src/940c9c9556c6/datafx-tutorial1/?at=default). The following figure shows how the example will look like: + +![example](/posts/guigarage-legacy/exasmple1.png) + +Every time the button in the view is clicked the text in the top label should change. To create this application we will start with the layout of the view. To do so we need a [FXML](http://docs.oracle.com/javafx/2/fxml_get_started/jfxpub-fxml_get_started.htm) file that defines the JavaFX view of the application. This can easily be created with [Scene Builder](http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html). The FXML file ([simpleView.fxml](https://bitbucket.org/datafx/datafx/src/940c9c9556c662760a39830d05c9a4519eea3832/datafx-tutorial1/src/main/resources/org/datafx/tutorial/simpleView.fxml?at=default)) of the tutorial will look like this: + +```xml + + + + + + + + + + + + + + + + + ``` + +Once this is created we need a controller class for the view. A first simple version that only uses JavaFX basic API will look like this: + +```javapublic class SimpleController { + @FXML + private Label resultLabel; + @FXML + private Button actionButton; +} ``` + +Like it is defined in the JavaFX basic API the two fields `resultLabel` and `actionButton` are named as the components defined in the FXML file by its `fx:id` attribute. BY adding the `@FXML` annotation to these fields they will be automatically injected once the controller will be instantiated. +Until now only JavaFX basic APIs are used. Now we want to add some DataFX magic. As mentioned we need some interaction every time the button is pressed. Because of that we want to trigger an action by the button. DataFX provides two annotations to handle this approach: `@ActionTrigger` and `@ActionMethod`. + +The `@ActionTrigger` annotation can be added to a field of the controller that defines a Node in the view. The DataFX container will automatically add an action handler to the Node. Any method that should be called once an action is triggered must be annotated with the `@ActionMethod` annotation. For both Annotations you must specify a value that defines the unique action id of the action that is handled by the annotation. In this case I used the id `myAction` and added the annotations to the class: + +```javapublic class SimpleController { + @FXML + private Label resultLabel; + @FXML + @ActionTrigger("myAction") + private Button actionButton; + @ActionMethod("myAction") + public void onAction() { + // DO some action... + } +} ``` + +So whenever the `actionButton` will be pressed the `onAction()` method will be called because they are bound by the same id. The id must be unique in view controller. As you will see in following tutorials there are other types of actions than simply calling a method. For now the most important point is that a component that is annotated with `@ActionTrigger` can trigger a method that is annotated with `@ActionMethod` if both annotations define the same unique action id. + +As a next step we will implement the action and define a default text for the label: + +```javapublic class SimpleController { + @FXML + private Label resultLabel; + @FXML + @ActionTrigger("myAction") + private Button actionButton; + private int clickCount = 0; + @PostConstruct + public void init() { + resultLabel.setText("Button was clicked " + clickCount + " times"); + } + @ActionMethod("myAction") + public void onAction() { + clickCount++; + resultLabel.setText("Button was clicked " + clickCount + " times"); + } +} ``` + +As you can see in the code a new method called `init()` was added. This method should be called after the controller has been initialized and the fields that are annotated with `@FXML` are injected. In DataFX this can be done by adding the `@PostConstruct` Annotation to the method. By doing so the DataFX flow container will call this method once all injectable values of the controller instance are injected. There three different types of values / fields that can be injected: + +* UI components that are annotated by `@FXML` +* DataFX objects. Here DataFX provides several annotations +* Custom implementations. These will be injected by using the `@Inject` annotation + +The method that is annotated by `@PostContruct` will be called when all injections are finished. In this first example we will only use `@FXML` to inject FXML UI components to the controller. + +One thing that is still missing is the binding between the controller class and the FXML file. DataFX provides the `@FXMLController` annotation for this purpose. By using this annotation the controller class defines its FXML file that contains the layout of the view. After adding the annotation the final controller class will look like this: + +```java@FXMLController("simpleView.fxml") +public class SimpleController { + @FXML + private Label resultLabel; + @FXML + @ActionTrigger("myAction") + private Button actionButton; + private int clickCount = 0; + @PostConstruct + public void init() { + resultLabel.setText("Button was clicked " + clickCount + " times"); + } + @ActionMethod("myAction") + public void onAction() { + clickCount++; + resultLabel.setText("Button was clicked " + clickCount + " times"); + } +} ``` + +Once this is done we need a main class to start the application and show the view on screen. We can't use the basic FXMLLoader class here because we used some annotations that must be handled by the DataFX container. But since the last preview of DataFX 8.0 the Flow API provides a very simple way to show the view on screen. Here is the complete main class that will start the application: + +```javapublic class Tutorial1Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + new Flow(SimpleController.class).startInStage(primaryStage); + } +} ``` + + The shown way is the most simple way to start a flow: the controller class of the start view is always passed as parameter to the constructor of the `Flow` class. Because in this demo we have only one view we simply pass the `SimpleController` class to the flow. The `Flow` class provides a utility method called `startInStage()` that renders the Flow in a `Stage`. By doing so the `Scene` will be created automatically and the `Stage` will contain a `Scene` that only contains the defined flow. In this first tutorial the flow will only contain one view. + +This first example is quite easy and normally you could define the action binding by only one line of Java code in the `init()` method: + +```javaactionButton.setOnAction((e) -> onAction()); ``` + +So why are all these annotations used here? As you will see in further tutorials that are more complex than this one it will make sense to use the annotations to provide more readable code. + +I hope you liked this first tutorial. I plan to add the second one at the end of the week. diff --git a/content/posts/2014-05-22-datafx-tutorial-2.md b/content/posts/2014-05-22-datafx-tutorial-2.md index 682b7390..1c88353b 100644 --- a/content/posts/2014-05-22-datafx-tutorial-2.md +++ b/content/posts/2014-05-22-datafx-tutorial-2.md @@ -1,145 +1,131 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX 8 Tutorial 2' -date: "2014-05-22" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'In this DataFX tutorial I will show how navigation between different views can easily be managed with DataFX and its Flow API.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -As I mentioned [here]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}) I started a series of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) tutorials. The first tutorial can be found [here]({{< ref "/posts/2014-05-20-datafx-tutorial-1" >}}). - -In this second tutorial I will show you the basics about __navigation in a DataFX flow__. In this example we will define two different views that are part of a flow and then navigate from one view to the other one. The source of the tutorial can be found[here](https://bitbucket.org/datafx/datafx/src/a92bddc1904a905be89205d5edf3a39015149227/datafx-tutorial2/?at=default). - -The following pictures shows the two views of the tutorial and its interaction: - -![tutorial2](/posts/guigarage-legacy/tutorial2.png) - -As shown in the first tutorial we want to start by defining the views in __FXML__. Here is the __FXML__ file for the first view: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - - - - - - -{{< / highlight >}} - -This defines a view that looks like this: - -![pic1](/posts/guigarage-legacy/pic1.png) - -The second view differ only in a few points from the first one. Here is the FXML definition of the second view: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - - - - - - -{{< / highlight >}} - -As you can see in the code there is a style addition in the __VBox__ tag. Here the red background color of the view is defined by __CSS__. The rendered view will look like this: - -![pic2](/posts/guigarage-legacy/pic2.png) - -As a next step we create __view controllers__ for the views. As shown in the last tutorial you have to define a class for each view that acts as its controller. In a first step we create some empty controller classes that are only annotated by `@FXMLController`: - -{{< highlight java >}} -@FXMLController("view1.fxml") -public class View1Controller { - @FXML - private Button actionButton; -} -{{< / highlight >}} - -Only the button that is defined by the `fx:id` attribute in the FXML file is part of the first controller class. The second one will look similar: - -{{< highlight java >}} -@FXMLController("view2.fxml") -public class View2Controller { - @FXML - private Button actionButton; -} -{{< / highlight >}} - -Once this is done an application that is based on a flow can be created. As learned in tutorial 1 there is an easy way to create an application that only contains one flow. We will use it here, too. The main class of this example will look like this: - -{{< highlight java >}} -public class Tutorial2Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - new Flow(View1Controller.class).startInStage(primaryStage); - } -} -{{< / highlight >}} - -By doing so a new flow is created that defines `view1` as its start view. If you start the application you will already see the first view but the button won't trigger any action. To add an action that links to the second view DataFX provides a special annotation called <`@LinkAction`. This annotation is working like the `@ActionTrigger` annotation that was introduced in tutorial 1. Once the button is clicked an action event will be fired and handled by the DataFX container. When using the `@LinkAction` annotation the controller of the target view must be specified. Therefore the Annotations provides a value that can be defined as the controller class. Here is the updated code of the first controller class: - -{{< highlight java >}} -@FXMLController("view1.fxml") -public class View1Controller { - @FXML - @LinkAction(View2Controller.class) - private Button actionButton; -} -{{< / highlight >}} - -Whenever the button is pressed the flow will navigate to the second view that is defined by the `View2Controller` class. The `@LinkAction` can be added to any JavaFX Node. If the component extends the `ButtonBase` class or the `MenuItem` class a handler for action events will be added to the control. Otherwise the action will be called once the control is clicked by mouse. By using the annotation a developer doesn't need to handle the complete navigation like changing the view or create a new data model. DataFX will handle all these steps automatically and the defined view will appear on screen once the action is triggered. - -Maybe you can already imagine how the final code for the second view controller will look like ;) - -{{< highlight java >}} -@FXMLController("view2.fxml") -public class View2Controller { - @FXML - @LinkAction(View1Controller.class) - private Button actionButton; -} -{{< / highlight >}} - -Once this is done you have created the first simply flow with DataFX. By using the `@LinkAction` annotation you can create big flows that contain a lot different views. In later tutorials you will that DataFX provides some other cool features to manage the navigation in a flow. - -As said in the first example each action in DataFX is defined by an unique ID. In the case of a LinkAction, the developer doesn't need to define an ID on its own. DataFX will create a unique ID once the controller will be initialized. By doing so the source code is much shorter and cleaner. As you will see later in the tutorials there are several other ways to define a navigation in a flow. Some of these work with an unique ID as shown in tutorial 1. +--- +outdated: true +showInBlog: false +title: 'DataFX 8 Tutorial 2' +date: "2014-05-22" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'In this DataFX tutorial I will show how navigation between different views can easily be managed with DataFX and its Flow API.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +As I mentioned [here](/posts/2014-05-19-datafx-8-0-tutorials) I started a series of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) tutorials. The first tutorial can be found [here](/posts/2014-05-20-datafx-tutorial-1). + +In this second tutorial I will show you the basics about __navigation in a DataFX flow__. In this example we will define two different views that are part of a flow and then navigate from one view to the other one. The source of the tutorial can be found[here](https://bitbucket.org/datafx/datafx/src/a92bddc1904a905be89205d5edf3a39015149227/datafx-tutorial2/?at=default). + +The following pictures shows the two views of the tutorial and its interaction: + +![tutorial2](/posts/guigarage-legacy/tutorial2.png) + +As shown in the first tutorial we want to start by defining the views in __FXML__. Here is the __FXML__ file for the first view: + +```xml + + + + + + + + + + + + + + + + + + + + + ``` + +This defines a view that looks like this: + +![pic1](/posts/guigarage-legacy/pic1.png) + +The second view differ only in a few points from the first one. Here is the FXML definition of the second view: + +```xml + + + + + + + + + + + + + + + + + + + + + ``` + +As you can see in the code there is a style addition in the __VBox__ tag. Here the red background color of the view is defined by __CSS__. The rendered view will look like this: + +![pic2](/posts/guigarage-legacy/pic2.png) + +As a next step we create __view controllers__ for the views. As shown in the last tutorial you have to define a class for each view that acts as its controller. In a first step we create some empty controller classes that are only annotated by `@FXMLController`: + +```java@FXMLController("view1.fxml") +public class View1Controller { + @FXML + private Button actionButton; +} ``` + +Only the button that is defined by the `fx:id` attribute in the FXML file is part of the first controller class. The second one will look similar: + +```java@FXMLController("view2.fxml") +public class View2Controller { + @FXML + private Button actionButton; +} ``` + +Once this is done an application that is based on a flow can be created. As learned in tutorial 1 there is an easy way to create an application that only contains one flow. We will use it here, too. The main class of this example will look like this: + +```javapublic class Tutorial2Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + new Flow(View1Controller.class).startInStage(primaryStage); + } +} ``` + +By doing so a new flow is created that defines `view1` as its start view. If you start the application you will already see the first view but the button won't trigger any action. To add an action that links to the second view DataFX provides a special annotation called <`@LinkAction`. This annotation is working like the `@ActionTrigger` annotation that was introduced in tutorial 1. Once the button is clicked an action event will be fired and handled by the DataFX container. When using the `@LinkAction` annotation the controller of the target view must be specified. Therefore the Annotations provides a value that can be defined as the controller class. Here is the updated code of the first controller class: + +```java@FXMLController("view1.fxml") +public class View1Controller { + @FXML + @LinkAction(View2Controller.class) + private Button actionButton; +} ``` + +Whenever the button is pressed the flow will navigate to the second view that is defined by the `View2Controller` class. The `@LinkAction` can be added to any JavaFX Node. If the component extends the `ButtonBase` class or the `MenuItem` class a handler for action events will be added to the control. Otherwise the action will be called once the control is clicked by mouse. By using the annotation a developer doesn't need to handle the complete navigation like changing the view or create a new data model. DataFX will handle all these steps automatically and the defined view will appear on screen once the action is triggered. + +Maybe you can already imagine how the final code for the second view controller will look like ;) + +```java@FXMLController("view2.fxml") +public class View2Controller { + @FXML + @LinkAction(View1Controller.class) + private Button actionButton; +} ``` + +Once this is done you have created the first simply flow with DataFX. By using the `@LinkAction` annotation you can create big flows that contain a lot different views. In later tutorials you will that DataFX provides some other cool features to manage the navigation in a flow. + +As said in the first example each action in DataFX is defined by an unique ID. In the case of a LinkAction, the developer doesn't need to define an ID on its own. DataFX will create a unique ID once the controller will be initialized. By doing so the source code is much shorter and cleaner. As you will see later in the tutorials there are several other ways to define a navigation in a flow. Some of these work with an unique ID as shown in tutorial 1. diff --git a/content/posts/2014-05-31-datafx-tutorial-3.md b/content/posts/2014-05-31-datafx-tutorial-3.md index 4a688316..7732dc37 100644 --- a/content/posts/2014-05-31-datafx-tutorial-3.md +++ b/content/posts/2014-05-31-datafx-tutorial-3.md @@ -1,173 +1,157 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX Tutorial 3' -date: "2014-05-31" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'In this tutorial I want to show how a wizard dialog can be created with DataFX.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -In this tutorial I want to show how a wizard dialog can be created with [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). This example depends on 2 other tutorials that can be found [here]({{< ref "/posts/2014-05-20-datafx-tutorial-1" >}}) and [here]({{< ref "/posts/2014-05-22-datafx-tutorial-2" >}}). - -The wizard that will be created in this tutorial contains 5 different views that are linked to each other: - -![flow1](/posts/guigarage-legacy/flow1.png) - -Next to the "next" action that navigates to the following view the wizard should support a "back" action that navigates to the last displayed view and a "finish" action that directly navigates to the last view: - -![flow3](/posts/guigarage-legacy/flow3.png) - -__Note:__ The last diagram doesn't contain all possible links. Because the "back" action navigates always the last visible view you could navigate directly from the last view to the first view if the "finish" action was triggered in the first view and then the back action is triggered. - -As shown in the other tutorials we will start the the view layout and generate all views by using __FXML__ and Scene Builder. All the views of the wizard will contain a toolbar with some buttons to trigger the defined action. Here are some previews how the views will look like: - -![views](/posts/guigarage-legacy/views.png) - -Thanks to FXML we don't need to implement the toolbar for every view. For this purpose FXML provides the `fx:include` tag that can be used to interleave vxml defined views. So we can define the toolbar as a separate FXML file: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - -{{< / highlight >}} - -As you can see the buttons for the 3 defined actions ("back", "next" and "finish") are defined in the toolbar. As described in the earlier tutorials these components can be injected in the controller class instances by using the `@FXML` annotation and a field name that matches the value of the `fx:id` attribute. - -The FXML of the toolbar (actionBar.fxml) can now included in all the FXML files that defines the different views of the wizard. Here is the code of the first view as an example: - -{{< highlight xml >}} - - - - - - - - - - -
- - - - - -
-
-{{< / highlight >}} - -As you can see the toolbar is integrated in the bottom of the central `BorderPane`. -Once all FXML files are created we can start to create the view controller as described in the earlier tutorials. Therefore we create a Java class for each view and bind the class to the corresponding FXML file by using the `@FXMLController` annotation: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller { -} -{{< / highlight >}} - -When looking at the `@FXMLController` annotation of the class you can find a new feature. Next to the fxml file that defines the view of the wizard step a `title` attribute is added. This defines the title of the view. Because the wizard will be added to a `Stage` by using the `Flow.startInStage()` method (see [tutorial 1]({{< ref "/posts/2014-05-20-datafx-tutorial-1" >}})) the title of the flow is automatically bound to the window title of the `Stage`. So whenever the view in the flow changes the title of the application window will change to the defined title of the view. As you will learn in future tutorial you can easily change the title of a view in code. In addition to the title other metadata like a icon can be defined for a view or flow. -As a next step the buttons of the toolbar should be injected in the controller classes and the specific actions for them should be defined. Here a new annotation will be introduced: By using the `@BackAction` annotation the flow will automatically handle an action that navigates to the last visible view. The annotation can be used like the `@ActionTrigger` and `@LinkAction` annotations that were introduced in tutorial 1 and 2. Therefore the controller class for the a view in the wizard could be defined like this: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller { - @FXML - @LinkAction(Wizard2Controller.class) - private Button nextButton; - @FXML - @BackAction - private Button backButton; - @FXML - @LinkAction(WizardDoneController.class) - private Button finishButton; -} -{{< / highlight >}} - -When all controllers would be designed like this we would create some duplicate code. The definition of the back button and the finish button would look the same in each controller class. Therefore we will create an abstract class that contains these definitions and all other view controllers will depend on it: - -{{< highlight java >}} -public class AbstractWizardController { - @FXML - @BackAction - private Button backButton; - @FXML - @LinkAction(WizardDoneController.class) - private Button finishButton; - public Button getBackButton() { - return backButton; - } - public Button getFinishButton() { - return finishButton; - } -} -{{< / highlight >}} - -Now a controller for the wizard will look like this: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller extends AbstractWizardController { - @FXML - @LinkAction(Wizard2Controller.class) - private Button nextButton; -} -{{< / highlight >}} - -Note: The injection of private nodes in super classes is a feature of DataFX. So if you will try this by using the default `FXMLLoader` of JavaFX this won't work. In addition the `FXMLLoader` doesn't support the injection of FXML nodes that are defined in a sub-fxml that is included by using the `fx:include` tag. As a limitation this nodes must not have a CSS id defined because this will override the `fx:id` in the java object and in that case DataFX can't inject them. I plan to open a issue at OpenJFX for this. -As a last step we need to disable the "back" button on the first view and the "next" and "finish" buttons on the last view. This can be done in the view controller by defining a method with the `@PostConstruct` annotation that will be called once the controller instance is created: - -{{< highlight java >}} -@FXMLController(value="wizardDone.fxml", title = "Wizard: Finish") -public class WizardDoneController extends AbstractWizardController { - @FXML - private Button nextButton; - @PostConstruct - public void init() { - nextButton.setDisable(true); - getFinishButton().setDisable(true); - } -} -{{< / highlight >}} - -Once this is done the wizard is completed an can be displayed in a JavaFX application. Therefore we define the following main class: - -{{< highlight java >}} -public class Tutorial3Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - new Flow(WizardStartController.class).startInStage(primaryStage); - } -} -{{< / highlight >}} - -The complete source of this tutorial can be found [here](https://bitbucket.org/datafx/datafx/src/b25aa30116e80c83d02a4b2a46c76fd603c0c7f4/datafx-tutorial3). - -Here is an movie of the finished wizard application: - -{{< youtube zGjc4VfcM9A >}} - +--- +outdated: true +showInBlog: false +title: 'DataFX Tutorial 3' +date: "2014-05-31" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'In this tutorial I want to show how a wizard dialog can be created with DataFX.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +In this tutorial I want to show how a wizard dialog can be created with [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). This example depends on 2 other tutorials that can be found [here](/posts/2014-05-20-datafx-tutorial-1) and [here](/posts/2014-05-22-datafx-tutorial-2). + +The wizard that will be created in this tutorial contains 5 different views that are linked to each other: + +![flow1](/posts/guigarage-legacy/flow1.png) + +Next to the "next" action that navigates to the following view the wizard should support a "back" action that navigates to the last displayed view and a "finish" action that directly navigates to the last view: + +![flow3](/posts/guigarage-legacy/flow3.png) + +__Note:__ The last diagram doesn't contain all possible links. Because the "back" action navigates always the last visible view you could navigate directly from the last view to the first view if the "finish" action was triggered in the first view and then the back action is triggered. + +As shown in the other tutorials we will start the the view layout and generate all views by using __FXML__ and Scene Builder. All the views of the wizard will contain a toolbar with some buttons to trigger the defined action. Here are some previews how the views will look like: + +![views](/posts/guigarage-legacy/views.png) + +Thanks to FXML we don't need to implement the toolbar for every view. For this purpose FXML provides the `fx:include` tag that can be used to interleave vxml defined views. So we can define the toolbar as a separate FXML file: + +```xml + + + + + + + + + + + + + + + + ``` + +As you can see the buttons for the 3 defined actions ("back", "next" and "finish") are defined in the toolbar. As described in the earlier tutorials these components can be injected in the controller class instances by using the `@FXML` annotation and a field name that matches the value of the `fx:id` attribute. + +The FXML of the toolbar (actionBar.fxml) can now included in all the FXML files that defines the different views of the wizard. Here is the code of the first view as an example: + +```xml + + + + + + + + + +
+ + + + + +
+
``` + +As you can see the toolbar is integrated in the bottom of the central `BorderPane`. +Once all FXML files are created we can start to create the view controller as described in the earlier tutorials. Therefore we create a Java class for each view and bind the class to the corresponding FXML file by using the `@FXMLController` annotation: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller { +} ``` + +When looking at the `@FXMLController` annotation of the class you can find a new feature. Next to the fxml file that defines the view of the wizard step a `title` attribute is added. This defines the title of the view. Because the wizard will be added to a `Stage` by using the `Flow.startInStage()` method (see [tutorial 1](/posts/2014-05-20-datafx-tutorial-1)) the title of the flow is automatically bound to the window title of the `Stage`. So whenever the view in the flow changes the title of the application window will change to the defined title of the view. As you will learn in future tutorial you can easily change the title of a view in code. In addition to the title other metadata like a icon can be defined for a view or flow. +As a next step the buttons of the toolbar should be injected in the controller classes and the specific actions for them should be defined. Here a new annotation will be introduced: By using the `@BackAction` annotation the flow will automatically handle an action that navigates to the last visible view. The annotation can be used like the `@ActionTrigger` and `@LinkAction` annotations that were introduced in tutorial 1 and 2. Therefore the controller class for the a view in the wizard could be defined like this: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller { + @FXML + @LinkAction(Wizard2Controller.class) + private Button nextButton; + @FXML + @BackAction + private Button backButton; + @FXML + @LinkAction(WizardDoneController.class) + private Button finishButton; +} ``` + +When all controllers would be designed like this we would create some duplicate code. The definition of the back button and the finish button would look the same in each controller class. Therefore we will create an abstract class that contains these definitions and all other view controllers will depend on it: + +```javapublic class AbstractWizardController { + @FXML + @BackAction + private Button backButton; + @FXML + @LinkAction(WizardDoneController.class) + private Button finishButton; + public Button getBackButton() { + return backButton; + } + public Button getFinishButton() { + return finishButton; + } +} ``` + +Now a controller for the wizard will look like this: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller extends AbstractWizardController { + @FXML + @LinkAction(Wizard2Controller.class) + private Button nextButton; +} ``` + +Note: The injection of private nodes in super classes is a feature of DataFX. So if you will try this by using the default `FXMLLoader` of JavaFX this won't work. In addition the `FXMLLoader` doesn't support the injection of FXML nodes that are defined in a sub-fxml that is included by using the `fx:include` tag. As a limitation this nodes must not have a CSS id defined because this will override the `fx:id` in the java object and in that case DataFX can't inject them. I plan to open a issue at OpenJFX for this. +As a last step we need to disable the "back" button on the first view and the "next" and "finish" buttons on the last view. This can be done in the view controller by defining a method with the `@PostConstruct` annotation that will be called once the controller instance is created: + +```java@FXMLController(value="wizardDone.fxml", title = "Wizard: Finish") +public class WizardDoneController extends AbstractWizardController { + @FXML + private Button nextButton; + @PostConstruct + public void init() { + nextButton.setDisable(true); + getFinishButton().setDisable(true); + } +} ``` + +Once this is done the wizard is completed an can be displayed in a JavaFX application. Therefore we define the following main class: + +```javapublic class Tutorial3Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + new Flow(WizardStartController.class).startInStage(primaryStage); + } +} ``` + +The complete source of this tutorial can be found [here](https://bitbucket.org/datafx/datafx/src/b25aa30116e80c83d02a4b2a46c76fd603c0c7f4/datafx-tutorial3). + +Here is an movie of the finished wizard application: + + + diff --git a/content/posts/2014-06-08-datafx-tutorial-4.md b/content/posts/2014-06-08-datafx-tutorial-4.md index 849a3b23..65e7610b 100644 --- a/content/posts/2014-06-08-datafx-tutorial-4.md +++ b/content/posts/2014-06-08-datafx-tutorial-4.md @@ -1,214 +1,192 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX Tutorial 4' -date: "2014-06-08" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'This is the 4th tutorial about DataFX. In this tutorial I will show how you can manage central actions and navigation of a flow.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -This is the 4th tutorial about navigation with [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) and the DataFX flow API. An overview of all tutorials can be found [here]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}). In this tutorial I will show how you can manage the action handling and navigation of a flow from outside of the flow. To do so we will refactor the wizard that was created in [tutorial 3]({{< ref "/posts/2014-05-31-datafx-tutorial-3" >}}). - -As described in tutorial 3 the wizard will be composed of some views that define the steps of the wizard. In addition a toolbar with some buttons is placed on the bottom. The views will look like this: - -![views](/posts/guigarage-legacy/views.png) - -All views of the wizard are linked by a navigation model. In tutorial 3 this was created by directly in the view controller classes so each view defines its navigation and actions. In this tutorial we will use the second approach that DataFX provides: All views doesn't know anything about there action and navigation model. Instead of this the actions are defined extern. The navigation and action behavior will be the same as in tutorial 3. Here is a short overview about the links between the views of the wizard: - -![flow3](/posts/guigarage-legacy/flow3.png) - -As always we want to start by defining the views in FXML. Because the toolbar will look the same on each view we can extract it in a seperate FXML file. As shown in tutorial 3 a FXML file can included in another one by using the `fx:include` tag. Here is the FXML definition of the toolbar: - -{{< highlight xml >}} - - - - - - - - - - - - - - -{{< / highlight >}} - -The definition of the toolbar is the same as in the last tutorial. The definition of the wizard steps is the same, too. Here is a FXML definition of one step: - -{{< highlight xml >}} - - - - - - - - - - - - -
- - - - - -
-
-{{< / highlight >}} - -As a next step we need view controller classes for all views in the wizard. As a first step we will create empty classes that are annoted with the `FXMLController` annotation: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller { -} -{{< / highlight >}} - -All the actions of the wizard will be triggered by the toolbar. Because the toolbar is defined on each view we can create an abstract class for the toolbar components that can be used as a superclass for the view controller classes: - -{{< highlight java >}} -public class AbstractWizardController { - @FXML - @ActionTrigger("back") - private Button backButton; - @FXML - @ActionTrigger("finish") - private Button finishButton; - @FXML - @ActionTrigger("next") - private Button nextButton; - public Button getBackButton() { - return backButton; - } - public Button getFinishButton() { - return finishButton; - } - public Button getNextButton() { - return nextButton; - } -} -{{< / highlight >}} - -All view controller classes can now extend the class: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller extends AbstractWizardController { -} -{{< / highlight >}} - -Mainly the same structure was created in tutorial 3 but here we have one big different. In Chapter 3 the next button can't be defined in the super class because it was annotated with the `@LinkAction` annotation. This must be done in each controller class because the target of the navigation must be defined as an parameter of the annotation definition (see tutorial 3 for a more detailed description). As already mentioned we want to extract the action handling in this tutorial. So all buttons can be defined and injected in the `AbstractWizardController` class. As you can see in the code the buttons are annoted by the `@ActionTrigger` annotation that was already used in some other tutorials. By using this annotation an action id is defined and the flow will trigger the action that is specified by this id each time the button will be pressed. - -The last thing that is missing is the main class. This class will look different to the main classes of the first three tutorials. Here we want to define all the actions of the wizard and link its views. To do so we start with a simple class and show the flow. You should know this kind of class from the other tutorials: - -{{< highlight java >}} -public class Tutorial4Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - new Flow(WizardStartController.class).startInStage(primaryStage); - } -} -{{< / highlight >}} - -As a next step we want to bind all the different views and create a navigation model. To do so the `Flow` class contains some methods that can be used to define links between views of the flow. The `Flow` class is defined as a fluent API and navigation links or action can be simply added to it. Here is a example for a flow with one link: - -{{< highlight java >}} -new Flow(View1Controller.class). -withLink(View1Controller.class, "link-id", View2Controller.class). -startInStage(primaryStage); -{{< / highlight >}} - -In the flow a link from view 1 to view 2 is defined. Both views are specified by their controller classes (View1Controller.class and View2Controller.class) and the link is defined by the unique id "link-id". Once this is done you can simply add the `@ActionTrigger("link-id")` annotation to a defined node in the `View1Controller` class. DataFX will register the navigation to view 2 for this node. So whenever the node is clicked the navigation will be transformed. - -For the current example the code of the main class will look like this: - -{{< highlight java >}} -public class Tutorial4Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - new Flow(WizardStartController.class). - withLink(WizardStartController.class, "next", Wizard1Controller.class). - withLink(Wizard1Controller.class, "next", Wizard2Controller.class). - withLink(Wizard2Controller.class, "next", Wizard3Controller.class). - withLink(Wizard3Controller.class, "next", WizardDoneController.class). - withGlobalBackAction("back"). - withGlobalLink("finish", WizardDoneController.class). - startInStage(primaryStage); - } -} -{{< / highlight >}} - -Next to the `withLink(...)` method two additional methods of the `Flow` class are used in the code. The withGlobalLink(...) method defines a navigation action that will be registered for each view in the flow. So the `@ActionTrigger("finish")` annotation can be used in each view and will navigate to the last view of the wizard. For each action type that can be registered to a DataFX flow the `Flow` class provides methods to register the action for only one view or as a global action for all views. This is done for the back action, too. The "back" button is visible on each view of the wizard and therefore the `withGlobalBackAction("back")` method is used here. So whenever a action with the id "back" is triggered in any view of the flow a back action will be called. This is exactly the same as adding a @BackAction annotation to the node that should trigger the back action. - -All these methods add a action to the DataFX flow. A action is defined by the interface org.datafx.controller.flow.action.FlowAction and all the shown methods will internally call the `Flow.addActionToView(Class controllerClass, String actionId, FlowAction action)` method that will add a action instance for the defined id to a specific view. Methods that add global actions will call `Flow.addGlobalAction(String actionId, FlowAction action)` internally. As you can see even custom actions can be added to a flow by simply implementing the `FlowAction` interface. DataFX contains a set with all the most important actions types that can be added to a flow or a specific view. The following figure shows the inheritance of the `FlowAction` interface: - -![ACTION-uml](/posts/guigarage-legacy/ACTION-uml.png) - -Some of the actions in the diagram will be handled in future tutorials. First I will only explain the basic action types: - -* `FlowLink` defines a link to another view in the flow. In the example instances of this class will be registered to the flow whenever the `withLink(...)` method is called. -* `FlowBackAction` handles a back navigation in the flow. This is the same as using the `@BackAction` annotation in a view controller -* `FlowTaskAction` will execute a task that is defined as a `Runnable` on the Platform Application Thread. We will see an example of the action type after this overview. -* `FlowAsyncTaskAction` will execute a task that is defined as a `Runnable` on a background thread. -* `FlowMethodAction` will call a method in the given view. The same result can be created by using the `@ActionMethod` annotation as shown in the first tutorial. - -As you can see in this overview all the actions that were created by using annotations in the previous tutorials can be defined directly for the flow. By doing so a controller class doesn't need to now how an action is implemented. It must only now the specific id of the action and which node should trigger the action. This structure can be very helpful if default views should be used in multiple flows or if controller classes and / or action classes are part of different modules that don't depend on each other. Let's think about the following structure: - -![pic](/posts/guigarage-legacy/Bildschirmfoto-2014-06-08-um-22.03.20.tiff) - -In this example the ViewController1.class, ViewController2.class and CustomAction.class don't know each other. With the help of the DataFX flow API you can simply combine them by using: - -{{< highlight java >}} -new Flow(View1Controller.class). -withLink(View1Controller.class, "link-id", View2Controller.class). -withAction(View2Controller.class "callAction", new CustomAction()). -startInStage(primaryStage); -{{< / highlight >}} - -As a last step I want to extent the example application and add a help output. This should be a global action that prints some help on the console. To do so the action is registered for the flow: - -{{< highlight java >}} -new Flow(WizardStartController.class). -withLink(WizardStartController.class, "next", Wizard1Controller.class). -... -withGlobalTaskAction("help", () -> System.out.println("## There is no help for the application :( ##")). -startInStage(primaryStage); -{{< / highlight >}} - -As you can see in the code you can simply pass a lambda expression as a action to the flow because the `FlowTaskAction` class that is used internally here defines the action as a `Runnable` that is a function interface since Java8. -Once this is done the action can be triggered in any view: - -{{< highlight java >}} -@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") -public class Wizard1Controller extends AbstractWizardController { - @FXML - @ActionTrigger("help") - private Hyperlink helpLink; -} -{{< / highlight >}} - -When looking at the [source code of the tutorial](https://bitbucket.org/datafx/datafx/src/7c6009a86ac83709855bd75e9f795b68747756f4/datafx-tutorial4/?at=default) you will see that the "help" action isn't triggered in all views. That is no problem for DataFX. A global action mustn't be called in each view and even a normal action mustn't be called in the defined controller. The API only defines that there is an action with the given id that could be called. For this last step a hyperlink tag is added to the FXML files. Here is a screenshot of the final wizard: - -![dialog-desc](/posts/guigarage-legacy/dialog-desc.png) +--- +outdated: true +showInBlog: false +title: 'DataFX Tutorial 4' +date: "2014-06-08" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'This is the 4th tutorial about DataFX. In this tutorial I will show how you can manage central actions and navigation of a flow.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +This is the 4th tutorial about navigation with [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}) and the DataFX flow API. An overview of all tutorials can be found [here](/posts/2014-05-19-datafx-8-0-tutorials). In this tutorial I will show how you can manage the action handling and navigation of a flow from outside of the flow. To do so we will refactor the wizard that was created in [tutorial 3](/posts/2014-05-31-datafx-tutorial-3). + +As described in tutorial 3 the wizard will be composed of some views that define the steps of the wizard. In addition a toolbar with some buttons is placed on the bottom. The views will look like this: + +![views](/posts/guigarage-legacy/views.png) + +All views of the wizard are linked by a navigation model. In tutorial 3 this was created by directly in the view controller classes so each view defines its navigation and actions. In this tutorial we will use the second approach that DataFX provides: All views doesn't know anything about there action and navigation model. Instead of this the actions are defined extern. The navigation and action behavior will be the same as in tutorial 3. Here is a short overview about the links between the views of the wizard: + +![flow3](/posts/guigarage-legacy/flow3.png) + +As always we want to start by defining the views in FXML. Because the toolbar will look the same on each view we can extract it in a seperate FXML file. As shown in tutorial 3 a FXML file can included in another one by using the `fx:include` tag. Here is the FXML definition of the toolbar: + +```xml + + + + + + + + + + + + + ``` + +The definition of the toolbar is the same as in the last tutorial. The definition of the wizard steps is the same, too. Here is a FXML definition of one step: + +```xml + + + + + + + + + + + +
+ + + + + +
+
``` + +As a next step we need view controller classes for all views in the wizard. As a first step we will create empty classes that are annoted with the `FXMLController` annotation: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller { +} ``` + +All the actions of the wizard will be triggered by the toolbar. Because the toolbar is defined on each view we can create an abstract class for the toolbar components that can be used as a superclass for the view controller classes: + +```javapublic class AbstractWizardController { + @FXML + @ActionTrigger("back") + private Button backButton; + @FXML + @ActionTrigger("finish") + private Button finishButton; + @FXML + @ActionTrigger("next") + private Button nextButton; + public Button getBackButton() { + return backButton; + } + public Button getFinishButton() { + return finishButton; + } + public Button getNextButton() { + return nextButton; + } +} ``` + +All view controller classes can now extend the class: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller extends AbstractWizardController { +} ``` + +Mainly the same structure was created in tutorial 3 but here we have one big different. In Chapter 3 the next button can't be defined in the super class because it was annotated with the `@LinkAction` annotation. This must be done in each controller class because the target of the navigation must be defined as an parameter of the annotation definition (see tutorial 3 for a more detailed description). As already mentioned we want to extract the action handling in this tutorial. So all buttons can be defined and injected in the `AbstractWizardController` class. As you can see in the code the buttons are annoted by the `@ActionTrigger` annotation that was already used in some other tutorials. By using this annotation an action id is defined and the flow will trigger the action that is specified by this id each time the button will be pressed. + +The last thing that is missing is the main class. This class will look different to the main classes of the first three tutorials. Here we want to define all the actions of the wizard and link its views. To do so we start with a simple class and show the flow. You should know this kind of class from the other tutorials: + +```javapublic class Tutorial4Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + new Flow(WizardStartController.class).startInStage(primaryStage); + } +} ``` + +As a next step we want to bind all the different views and create a navigation model. To do so the `Flow` class contains some methods that can be used to define links between views of the flow. The `Flow` class is defined as a fluent API and navigation links or action can be simply added to it. Here is a example for a flow with one link: + +```javanew Flow(View1Controller.class). +withLink(View1Controller.class, "link-id", View2Controller.class). +startInStage(primaryStage); ``` + +In the flow a link from view 1 to view 2 is defined. Both views are specified by their controller classes (View1Controller.class and View2Controller.class) and the link is defined by the unique id "link-id". Once this is done you can simply add the `@ActionTrigger("link-id")` annotation to a defined node in the `View1Controller` class. DataFX will register the navigation to view 2 for this node. So whenever the node is clicked the navigation will be transformed. + +For the current example the code of the main class will look like this: + +```javapublic class Tutorial4Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + new Flow(WizardStartController.class). + withLink(WizardStartController.class, "next", Wizard1Controller.class). + withLink(Wizard1Controller.class, "next", Wizard2Controller.class). + withLink(Wizard2Controller.class, "next", Wizard3Controller.class). + withLink(Wizard3Controller.class, "next", WizardDoneController.class). + withGlobalBackAction("back"). + withGlobalLink("finish", WizardDoneController.class). + startInStage(primaryStage); + } +} ``` + +Next to the `withLink(...)` method two additional methods of the `Flow` class are used in the code. The withGlobalLink(...) method defines a navigation action that will be registered for each view in the flow. So the `@ActionTrigger("finish")` annotation can be used in each view and will navigate to the last view of the wizard. For each action type that can be registered to a DataFX flow the `Flow` class provides methods to register the action for only one view or as a global action for all views. This is done for the back action, too. The "back" button is visible on each view of the wizard and therefore the `withGlobalBackAction("back")` method is used here. So whenever a action with the id "back" is triggered in any view of the flow a back action will be called. This is exactly the same as adding a @BackAction annotation to the node that should trigger the back action. + +All these methods add a action to the DataFX flow. A action is defined by the interface org.datafx.controller.flow.action.FlowAction and all the shown methods will internally call the `Flow.addActionToView(Class controllerClass, String actionId, FlowAction action)` method that will add a action instance for the defined id to a specific view. Methods that add global actions will call `Flow.addGlobalAction(String actionId, FlowAction action)` internally. As you can see even custom actions can be added to a flow by simply implementing the `FlowAction` interface. DataFX contains a set with all the most important actions types that can be added to a flow or a specific view. The following figure shows the inheritance of the `FlowAction` interface: + +![ACTION-uml](/posts/guigarage-legacy/ACTION-uml.png) + +Some of the actions in the diagram will be handled in future tutorials. First I will only explain the basic action types: + +* `FlowLink` defines a link to another view in the flow. In the example instances of this class will be registered to the flow whenever the `withLink(...)` method is called. +* `FlowBackAction` handles a back navigation in the flow. This is the same as using the `@BackAction` annotation in a view controller +* `FlowTaskAction` will execute a task that is defined as a `Runnable` on the Platform Application Thread. We will see an example of the action type after this overview. +* `FlowAsyncTaskAction` will execute a task that is defined as a `Runnable` on a background thread. +* `FlowMethodAction` will call a method in the given view. The same result can be created by using the `@ActionMethod` annotation as shown in the first tutorial. + +As you can see in this overview all the actions that were created by using annotations in the previous tutorials can be defined directly for the flow. By doing so a controller class doesn't need to now how an action is implemented. It must only now the specific id of the action and which node should trigger the action. This structure can be very helpful if default views should be used in multiple flows or if controller classes and / or action classes are part of different modules that don't depend on each other. Let's think about the following structure: + +![pic](/posts/guigarage-legacy/Bildschirmfoto-2014-06-08-um-22.03.20.tiff) + +In this example the ViewController1.class, ViewController2.class and CustomAction.class don't know each other. With the help of the DataFX flow API you can simply combine them by using: + +```javanew Flow(View1Controller.class). +withLink(View1Controller.class, "link-id", View2Controller.class). +withAction(View2Controller.class "callAction", new CustomAction()). +startInStage(primaryStage); ``` + +As a last step I want to extent the example application and add a help output. This should be a global action that prints some help on the console. To do so the action is registered for the flow: + +```javanew Flow(WizardStartController.class). +withLink(WizardStartController.class, "next", Wizard1Controller.class). +... +withGlobalTaskAction("help", () -> System.out.println("## There is no help for the application :( ##")). +startInStage(primaryStage); ``` + +As you can see in the code you can simply pass a lambda expression as a action to the flow because the `FlowTaskAction` class that is used internally here defines the action as a `Runnable` that is a function interface since Java8. +Once this is done the action can be triggered in any view: + +```java@FXMLController(value="wizard1.fxml", title = "Wizard: Step 1") +public class Wizard1Controller extends AbstractWizardController { + @FXML + @ActionTrigger("help") + private Hyperlink helpLink; +} ``` + +When looking at the [source code of the tutorial](https://bitbucket.org/datafx/datafx/src/7c6009a86ac83709855bd75e9f795b68747756f4/datafx-tutorial4/?at=default) you will see that the "help" action isn't triggered in all views. That is no problem for DataFX. A global action mustn't be called in each view and even a normal action mustn't be called in the defined controller. The API only defines that there is an action with the given id that could be called. For this last step a hyperlink tag is added to the FXML files. Here is a screenshot of the final wizard: + +![dialog-desc](/posts/guigarage-legacy/dialog-desc.png) diff --git a/content/posts/2014-06-17-aerofx-getting-closer.md b/content/posts/2014-06-17-aerofx-getting-closer.md index cc227167..55bdbd16 100644 --- a/content/posts/2014-06-17-aerofx-getting-closer.md +++ b/content/posts/2014-06-17-aerofx-getting-closer.md @@ -8,7 +8,7 @@ categories: [AeroFX, JavaFX] excerpt: 'This is a new preview of the AeroFX Theme for JavaFX. The theme creates a native look and feel for JavaFX applications on Windows.' preview_image: "/posts/preview-images/software-development-green.svg" --- -As shown in [the sneak peek last week]({{< ref "/posts/2014-06-10-sneak-peek-aerofx" >}}) a new JavaFX theme is in development. [Matthias Meidinger](http://sigpwr.de) is creating this skin as part of his bachelor thesis and since the last week he made some big improvements. So I think it's time for a new preview of the upcoming JavaFX theme. The following pics show dialogs that use the __AeroFX__ skin: +As shown in [the sneak peek last week](/posts/2014-06-10-sneak-peek-aerofx) a new JavaFX theme is in development. [Matthias Meidinger](http://sigpwr.de) is creating this skin as part of his bachelor thesis and since the last week he made some big improvements. So I think it's time for a new preview of the upcoming JavaFX theme. The following pics show dialogs that use the __AeroFX__ skin: ![1](/posts/guigarage-legacy/j1.jpg) diff --git a/content/posts/2014-06-27-datafx-tutorial-5.md b/content/posts/2014-06-27-datafx-tutorial-5.md index dc010a6f..eb29faa3 100644 --- a/content/posts/2014-06-27-datafx-tutorial-5.md +++ b/content/posts/2014-06-27-datafx-tutorial-5.md @@ -1,276 +1,256 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX Tutorial 5' -date: "2014-06-27" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'This is the last tutorial about the basic flow and action API of DataFX.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -This is the last tutorial about the basic flow and action API of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). I plan to start a second tutorial series about injection and resources in DataFX. But in those parts of DataFX 8 the APIs aren't final at the moment. So it will sadly take one or two month before I can continue the tutorials. An overview of all tutorials can be found [here]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}). - -In this tutorial I want to show how flows can be interlaced. Therefore we create a new variation of the wizard app that was shown in [tutorial 4]({{< ref "/posts/2014-06-08-datafx-tutorial-4" >}}). The application should provide a simple wizard with some steps. Here is an overview how the dialogs and the flow where defined in tutorial 4: - -![flow3](/posts/guigarage-legacy/flow3.png) - -Once the application is finished it will look like the own that was created last time: - -![views](/posts/guigarage-legacy/views.png) - -The last implementation of the wizard has one weak point in its architecture: Each step of the wizard is defined as a view in a flow. The view contains the complete view of the application and so also the action toolbar is part of each view. In the last tutorial we extracted the toolbar in a separate FXML file but the controls were created for each view individually. But the toolbar has the same functionality on each view and normally it should be one single instance and only the view in the center of the application should change: - -![tut5-2](/posts/guigarage-legacy/tut5-2.png) - -This more logical structure is part of this tutorial and by using interlaced flows the favored behavior can be created. As always we will start by creating the FXML files for the application. Because the top views of the wizard should be defined in a separate flow there will be FXML files for the views that only contain the content of one wizard step. Here is an example for the first step: - -{{< highlight xml >}} - - - - - - -{{< / highlight >}} - -As you can see the FXML is much shorter than in the other tutorials. It only contains the Label that will be shown on the first view of the wizard. - -For the toolbar we can use the same FXML file as in tutorial 4: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - -{{< / highlight >}} - -The FXML defines a HBox that include the 3 action buttons. - -Once this is done the application is defined by the following FXML files: - -![tut5-3](/posts/guigarage-legacy/tut5-3.png) - -As a next step a master FXML file is needed that includes the different components. As shown in then last tutorial the fx:include tag can be used to include FXML files within each other. Because the toolbar will be static component and the bottom of the application it can be included in the main FXML file. The wizard views on the top will change. Therefore we add a StackPane to the main FXML file that will hold the views. Here is the FXML file: - -{{< highlight xml >}} - - - - - - - - - - - - - - - - - - - - - -
- -
-
-{{< / highlight >}} - -The StackPane that will hold the central views of the wizard is defined by the unique id "centerPane". Once this is done all FXML files are finished and we can start to code in JavaFX. Let's take a look at the general structure of the application before we start to define the needed flows. As said the application will contain two flows that are interlaced. Here is a graphic that shows the structure: - -![tut5-1](/posts/guigarage-legacy/tut5-1.png) - -We will start with the view controller classes for all the wizard steps. Because the action toolbars isn't part of this views anymore and they contain only a label the code is very short :) Here is the class for the first view controller: - -{{< highlight java >}} -@FXMLController("wizardView1.fxml") -public class WizardView1Controller { -} -{{< / highlight >}} - -Easy, isn't it? The class is only needed to specify the view in the Java code and create the binding between the class and the FXML file. As you can see in the code of the example all other controller classes will look like this. - -As a next step we will create the flow of for these views. Here we only need to link the view for navigation. Therefore the definition of the flow will look like this: - -{{< highlight java >}} -Flow flow = new Flow(WizardView1Controller.class). - withLink(WizardView1Controller.class, "next", WizardView2Controller.class). - withLink(WizardView2Controller.class, "next", WizardView3Controller.class). - withLink(WizardView3Controller.class, "next", WizardView4Controller.class). - withLink(WizardView4Controller.class, "next", WizardView5Controller.class); -{{< / highlight >}} - -Ok, but where should this flow be defined. Until now all flows were defined in the main class of the application but this flow is an inner flow that will be part of another flow. To better understand how this can be achieved we should have a look at the main class of the application: - -{{< highlight java >}} -public class Tutorial5Main extends Application { - public static void main(String[] args) { - launch(args); - } - @Override - public void start(Stage primaryStage) throws Exception { - Flow flow = new Flow(WizardController.class); - FlowHandler flowHandler = flow.createHandler(); - StackPane pane = flowHandler.start(new DefaultFlowContainer()); - primaryStage.setScene(new Scene(pane)); - primaryStage.show(); - } -} -{{< / highlight >}} - -As in all other tutorials a flow is defined and started in the application class. But as you can see this flow defines only one start view that is defined by its controller class (WizardController). This class defines the controller of the main view of the application that is defined by the main FXML file as already shown. The flow that defines the steps of the wizard will be an inner flow in this view. Therefore the inner flow is defined in the WizardController controller class: - -{{< highlight java >}} -@FXMLController("wizard.fxml") -public class WizardController { - @FXML - private StackPane centerPane; - private FlowHandler flowHandler; - @PostConstruct - public void init() throws FlowException { - Flow innerFlow = new Flow(WizardView1Controller.class). - withLink(WizardView1Controller.class, "next", WizardView2Controller.class). - withLink(WizardView2Controller.class, "next", WizardView3Controller.class). - withLink(WizardView3Controller.class, "next", WizardView4Controller.class). - withLink(WizardView4Controller.class, "next", WizardView5Controller.class); - flowHandler = innerFlow.createHandler(); - centerPane.getChildren().add(flowHandler.start(new AnimatedFlowContainer(Duration.millis(320), ContainerAnimations.ZOOM_IN))); - } -} -{{< / highlight >}} - -Here all the steps of the wizard are added and linked to the inner flow. As you can see in the code the flow is started in a difference way than it was done several times before. A FlowHandler instance is created and this instances is started. This is wrapped in the startInStage() method of the Flow class that was used in all the other tutorials. The FlowHandler instance represents a running flow. The Flow class only defines the structure of a flow and can be started several times in parallel. Each running flow instance is defined by a FlowHandler instance and The FlowHandler instance can be used to interact with the flow. In addition the start(...) method of the FlowHandler class needs a FlowContainer as parameter. The FlowContainer provides a parent node in that the flow will be shown. In this case we use an AnimatedFlowContainer that provides animations whenever the flow navigates to a different view. We will see the animations at the end of the tutorial. The flow will be placed in the centerPane that is defined by FXML and injected in the controller by using the @FXML annotation. - -As said the FlowHandler class provides methods to interact with the flow. We will use this methods in the tutorial to call the "next", "back" and "finish" actions of the flow. To do so we need the action buttons that can easily be injected in the controller class: - -{{< highlight java >}} -@FXMLController("wizard.fxml") -public class WizardController { - @FXML - @ActionTrigger("back") - private Button backButton; - @FXML - @ActionTrigger("finish") - private Button finishButton; - @FXML - @ActionTrigger("next") - private Button nextButton; - @FXML - private StackPane centerPane; - //init()... -} -{{< / highlight >}} - -All the Buttons are annotated by the @ActionTrigger annotation and will therefore trigger actions in the flow. But they won't trigger actions in the inner flow because they are defined in the global flow that is defined in the main class. To handle this actions we will create some methods that are annoted by the @ActionMethod() annotation. As seen in [tutorial 1]({{< ref "/posts/2014-05-20-datafx-tutorial-1" >}}) these methods will be called whenever the action with the matching unique id is called: - -{{< highlight java >}} -@FXMLController("wizard.fxml") -public class WizardController { - @FXML - @ActionTrigger("back") - private Button backButton; - @FXML - @ActionTrigger("finish") - private Button finishButton; - @FXML - @ActionTrigger("next") - private Button nextButton; - //init()... - @ActionMethod("back") - public void onBack() throws VetoException, FlowException { - } - @ActionMethod("next") - public void onNext() throws VetoException, FlowException { - } - @ActionMethod("finish") - public void onFinish() throws VetoException, FlowException { - } -} -{{< / highlight >}} - -In these methods the FlowHandler instance of the inner flow can be used to interact with the inner flow and navigate to new views or trigger actions. In addition some logic is needed to enable and disable the action buttons depending on the state of the inner flow. Here is the complete code of the WizardController class with the final implementation of these methods: - -{{< highlight java >}} -@FXMLController("wizard.fxml") -public class WizardController { - @FXML - @ActionTrigger("back") - private Button backButton; - @FXML - @ActionTrigger("finish") - private Button finishButton; - @FXML - @ActionTrigger("next") - private Button nextButton; - @FXML - private StackPane centerPane; - private FlowHandler flowHandler; - @PostConstruct - public void init() throws FlowException { - Flow flow = new Flow(WizardView1Controller.class). - withLink(WizardView1Controller.class, "next", WizardView2Controller.class). - withLink(WizardView2Controller.class, "next", WizardView3Controller.class). - withLink(WizardView3Controller.class, "next", WizardView4Controller.class). - withLink(WizardView4Controller.class, "next", WizardView5Controller.class); - flowHandler = flow.createHandler(); - centerPane.getChildren().add(flowHandler.start(new AnimatedFlowContainer(Duration.millis(320), ContainerAnimations.ZOOM_IN))); - backButton.setDisable(true); - } - @ActionMethod("back") - public void onBack() throws VetoException, FlowException { - flowHandler.navigateBack(); - if(flowHandler.getCurrentViewControllerClass().equals(WizardView1Controller.class)) { - backButton.setDisable(true); - } else { - backButton.setDisable(false); - } - finishButton.setDisable(false); - nextButton.setDisable(false); - } - @ActionMethod("next") - public void onNext() throws VetoException, FlowException { - flowHandler.handle("next"); - if(flowHandler.getCurrentViewControllerClass().equals(WizardView5Controller.class)) { - nextButton.setDisable(true); - finishButton.setDisable(true); - } else { - nextButton.setDisable(false); - } - backButton.setDisable(false); - } - @ActionMethod("finish") - public void onFinish() throws VetoException, FlowException { - flowHandler.navigateTo(WizardView5Controller.class); - finishButton.setDisable(true); - nextButton.setDisable(true); - backButton.setDisable(false); - } -} -{{< / highlight >}} - -Once this is done you can start the flow and navigate through all the steps of the wizard. Thanks to the AnimatedFlowContainer container each link will be animated: - -{{< youtube uNX7VGtL2PY >}} - -As I said this is the final tutorial about the Flow API. From our point of view the API is finshed and we only plan to polish some stuff and add documentation. But we plan to release DataFX 8 at JavaOne and so there is still some time to change things. So if you have any questions, feedback or improvements please let us now. The next step is the CDI part of DataFX and once the APIs are stable I will continue this tutorial series. +--- +outdated: true +showInBlog: false +title: 'DataFX Tutorial 5' +date: "2014-06-27" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'This is the last tutorial about the basic flow and action API of DataFX.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +This is the last tutorial about the basic flow and action API of [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). I plan to start a second tutorial series about injection and resources in DataFX. But in those parts of DataFX 8 the APIs aren't final at the moment. So it will sadly take one or two month before I can continue the tutorials. An overview of all tutorials can be found [here](/posts/2014-05-19-datafx-8-0-tutorials). + +In this tutorial I want to show how flows can be interlaced. Therefore we create a new variation of the wizard app that was shown in [tutorial 4](/posts/2014-06-08-datafx-tutorial-4). The application should provide a simple wizard with some steps. Here is an overview how the dialogs and the flow where defined in tutorial 4: + +![flow3](/posts/guigarage-legacy/flow3.png) + +Once the application is finished it will look like the own that was created last time: + +![views](/posts/guigarage-legacy/views.png) + +The last implementation of the wizard has one weak point in its architecture: Each step of the wizard is defined as a view in a flow. The view contains the complete view of the application and so also the action toolbar is part of each view. In the last tutorial we extracted the toolbar in a separate FXML file but the controls were created for each view individually. But the toolbar has the same functionality on each view and normally it should be one single instance and only the view in the center of the application should change: + +![tut5-2](/posts/guigarage-legacy/tut5-2.png) + +This more logical structure is part of this tutorial and by using interlaced flows the favored behavior can be created. As always we will start by creating the FXML files for the application. Because the top views of the wizard should be defined in a separate flow there will be FXML files for the views that only contain the content of one wizard step. Here is an example for the first step: + +```xml + + + + + ``` + +As you can see the FXML is much shorter than in the other tutorials. It only contains the Label that will be shown on the first view of the wizard. + +For the toolbar we can use the same FXML file as in tutorial 4: + +```xml + + + + + + + + + + + + + + + + ``` + +The FXML defines a HBox that include the 3 action buttons. + +Once this is done the application is defined by the following FXML files: + +![tut5-3](/posts/guigarage-legacy/tut5-3.png) + +As a next step a master FXML file is needed that includes the different components. As shown in then last tutorial the fx:include tag can be used to include FXML files within each other. Because the toolbar will be static component and the bottom of the application it can be included in the main FXML file. The wizard views on the top will change. Therefore we add a StackPane to the main FXML file that will hold the views. Here is the FXML file: + +```xml + + + + + + + + + + + + + + + + + + + + +
+ +
+
``` + +The StackPane that will hold the central views of the wizard is defined by the unique id "centerPane". Once this is done all FXML files are finished and we can start to code in JavaFX. Let's take a look at the general structure of the application before we start to define the needed flows. As said the application will contain two flows that are interlaced. Here is a graphic that shows the structure: + +![tut5-1](/posts/guigarage-legacy/tut5-1.png) + +We will start with the view controller classes for all the wizard steps. Because the action toolbars isn't part of this views anymore and they contain only a label the code is very short :) Here is the class for the first view controller: + +```java@FXMLController("wizardView1.fxml") +public class WizardView1Controller { +} ``` + +Easy, isn't it? The class is only needed to specify the view in the Java code and create the binding between the class and the FXML file. As you can see in the code of the example all other controller classes will look like this. + +As a next step we will create the flow of for these views. Here we only need to link the view for navigation. Therefore the definition of the flow will look like this: + +```javaFlow flow = new Flow(WizardView1Controller.class). + withLink(WizardView1Controller.class, "next", WizardView2Controller.class). + withLink(WizardView2Controller.class, "next", WizardView3Controller.class). + withLink(WizardView3Controller.class, "next", WizardView4Controller.class). + withLink(WizardView4Controller.class, "next", WizardView5Controller.class); ``` + +Ok, but where should this flow be defined. Until now all flows were defined in the main class of the application but this flow is an inner flow that will be part of another flow. To better understand how this can be achieved we should have a look at the main class of the application: + +```javapublic class Tutorial5Main extends Application { + public static void main(String[] args) { + launch(args); + } + @Override + public void start(Stage primaryStage) throws Exception { + Flow flow = new Flow(WizardController.class); + FlowHandler flowHandler = flow.createHandler(); + StackPane pane = flowHandler.start(new DefaultFlowContainer()); + primaryStage.setScene(new Scene(pane)); + primaryStage.show(); + } +} ``` + +As in all other tutorials a flow is defined and started in the application class. But as you can see this flow defines only one start view that is defined by its controller class (WizardController). This class defines the controller of the main view of the application that is defined by the main FXML file as already shown. The flow that defines the steps of the wizard will be an inner flow in this view. Therefore the inner flow is defined in the WizardController controller class: + +```java@FXMLController("wizard.fxml") +public class WizardController { + @FXML + private StackPane centerPane; + private FlowHandler flowHandler; + @PostConstruct + public void init() throws FlowException { + Flow innerFlow = new Flow(WizardView1Controller.class). + withLink(WizardView1Controller.class, "next", WizardView2Controller.class). + withLink(WizardView2Controller.class, "next", WizardView3Controller.class). + withLink(WizardView3Controller.class, "next", WizardView4Controller.class). + withLink(WizardView4Controller.class, "next", WizardView5Controller.class); + flowHandler = innerFlow.createHandler(); + centerPane.getChildren().add(flowHandler.start(new AnimatedFlowContainer(Duration.millis(320), ContainerAnimations.ZOOM_IN))); + } +} ``` + +Here all the steps of the wizard are added and linked to the inner flow. As you can see in the code the flow is started in a difference way than it was done several times before. A FlowHandler instance is created and this instances is started. This is wrapped in the startInStage() method of the Flow class that was used in all the other tutorials. The FlowHandler instance represents a running flow. The Flow class only defines the structure of a flow and can be started several times in parallel. Each running flow instance is defined by a FlowHandler instance and The FlowHandler instance can be used to interact with the flow. In addition the start(...) method of the FlowHandler class needs a FlowContainer as parameter. The FlowContainer provides a parent node in that the flow will be shown. In this case we use an AnimatedFlowContainer that provides animations whenever the flow navigates to a different view. We will see the animations at the end of the tutorial. The flow will be placed in the centerPane that is defined by FXML and injected in the controller by using the @FXML annotation. + +As said the FlowHandler class provides methods to interact with the flow. We will use this methods in the tutorial to call the "next", "back" and "finish" actions of the flow. To do so we need the action buttons that can easily be injected in the controller class: + +```java@FXMLController("wizard.fxml") +public class WizardController { + @FXML + @ActionTrigger("back") + private Button backButton; + @FXML + @ActionTrigger("finish") + private Button finishButton; + @FXML + @ActionTrigger("next") + private Button nextButton; + @FXML + private StackPane centerPane; + //init()... +} ``` + +All the Buttons are annotated by the @ActionTrigger annotation and will therefore trigger actions in the flow. But they won't trigger actions in the inner flow because they are defined in the global flow that is defined in the main class. To handle this actions we will create some methods that are annoted by the @ActionMethod() annotation. As seen in [tutorial 1](/posts/2014-05-20-datafx-tutorial-1) these methods will be called whenever the action with the matching unique id is called: + +```java@FXMLController("wizard.fxml") +public class WizardController { + @FXML + @ActionTrigger("back") + private Button backButton; + @FXML + @ActionTrigger("finish") + private Button finishButton; + @FXML + @ActionTrigger("next") + private Button nextButton; + //init()... + @ActionMethod("back") + public void onBack() throws VetoException, FlowException { + } + @ActionMethod("next") + public void onNext() throws VetoException, FlowException { + } + @ActionMethod("finish") + public void onFinish() throws VetoException, FlowException { + } +} ``` + +In these methods the FlowHandler instance of the inner flow can be used to interact with the inner flow and navigate to new views or trigger actions. In addition some logic is needed to enable and disable the action buttons depending on the state of the inner flow. Here is the complete code of the WizardController class with the final implementation of these methods: + +```java@FXMLController("wizard.fxml") +public class WizardController { + @FXML + @ActionTrigger("back") + private Button backButton; + @FXML + @ActionTrigger("finish") + private Button finishButton; + @FXML + @ActionTrigger("next") + private Button nextButton; + @FXML + private StackPane centerPane; + private FlowHandler flowHandler; + @PostConstruct + public void init() throws FlowException { + Flow flow = new Flow(WizardView1Controller.class). + withLink(WizardView1Controller.class, "next", WizardView2Controller.class). + withLink(WizardView2Controller.class, "next", WizardView3Controller.class). + withLink(WizardView3Controller.class, "next", WizardView4Controller.class). + withLink(WizardView4Controller.class, "next", WizardView5Controller.class); + flowHandler = flow.createHandler(); + centerPane.getChildren().add(flowHandler.start(new AnimatedFlowContainer(Duration.millis(320), ContainerAnimations.ZOOM_IN))); + backButton.setDisable(true); + } + @ActionMethod("back") + public void onBack() throws VetoException, FlowException { + flowHandler.navigateBack(); + if(flowHandler.getCurrentViewControllerClass().equals(WizardView1Controller.class)) { + backButton.setDisable(true); + } else { + backButton.setDisable(false); + } + finishButton.setDisable(false); + nextButton.setDisable(false); + } + @ActionMethod("next") + public void onNext() throws VetoException, FlowException { + flowHandler.handle("next"); + if(flowHandler.getCurrentViewControllerClass().equals(WizardView5Controller.class)) { + nextButton.setDisable(true); + finishButton.setDisable(true); + } else { + nextButton.setDisable(false); + } + backButton.setDisable(false); + } + @ActionMethod("finish") + public void onFinish() throws VetoException, FlowException { + flowHandler.navigateTo(WizardView5Controller.class); + finishButton.setDisable(true); + nextButton.setDisable(true); + backButton.setDisable(false); + } +} ``` + +Once this is done you can start the flow and navigate through all the steps of the wizard. Thanks to the AnimatedFlowContainer container each link will be animated: + + + +As I said this is the final tutorial about the Flow API. From our point of view the API is finshed and we only plan to polish some stuff and add documentation. But we plan to release DataFX 8 at JavaOne and so there is still some time to change things. So if you have any questions, feedback or improvements please let us now. The next step is the CDI part of DataFX and once the APIs are stable I will continue this tutorial series. diff --git a/content/posts/2014-09-09-javaone-2014-preview.md b/content/posts/2014-09-09-javaone-2014-preview.md index 23a11033..65b0cdf3 100644 --- a/content/posts/2014-09-09-javaone-2014-preview.md +++ b/content/posts/2014-09-09-javaone-2014-preview.md @@ -8,9 +8,9 @@ categories: [JavaFX] excerpt: 'I recorded a short video in that I introduce the talks and show a short preview of some JavaFX demos' preview_image: "/posts/preview-images/software-development-green.svg" --- -As I mentioned [last week]({{< ref "/posts/2014-08-26-javaone-2014-sessions" >}}) I will give 6 talks at [JavaOne](https://www.oracle.com/javaone/) this year. To get a better overview of this talks I recorded a short video in that I introduce the talks and show a short preview of some JavaFX demos: +As I mentioned [last week](/posts/2014-08-26-javaone-2014-sessions) I will give 6 talks at [JavaOne](https://www.oracle.com/javaone/) this year. To get a better overview of this talks I recorded a short video in that I introduce the talks and show a short preview of some JavaFX demos: -{{< youtube 6Qp5mezk4dA >}} + Most of the talks will be hold with other speakers. Here is a list of all the people I'm currently working on cool stuff for JavaOne: diff --git a/content/posts/2014-09-11-javaone-preview-enterprise-javafx.md b/content/posts/2014-09-11-javaone-preview-enterprise-javafx.md index ac82bbf2..949dcc6d 100644 --- a/content/posts/2014-09-11-javaone-preview-enterprise-javafx.md +++ b/content/posts/2014-09-11-javaone-preview-enterprise-javafx.md @@ -19,5 +19,5 @@ Date and Time: 9/29/14, 16:00 - 17:00 Because I think a good talk must contain cool demos, I created some examples especially for my talks. Today I will introduce one of them with a short video. The video shows how you can login with Twitter in JavaFX. You can use this as an alternative workflow for users to login or register in your application. If you want to know how this is done you should visit my "Enterprise JavaFX" talk :) -{{< youtube RTdQjxaH_wY >}} + diff --git a/content/posts/2014-09-30-enrich-list-ui-using-medialistcell.md b/content/posts/2014-09-30-enrich-list-ui-using-medialistcell.md index eb138563..eb8b23b4 100644 --- a/content/posts/2014-09-30-enrich-list-ui-using-medialistcell.md +++ b/content/posts/2014-09-30-enrich-list-ui-using-medialistcell.md @@ -1,118 +1,110 @@ ---- -outdated: true -showInBlog: false -title: 'Enrich your List UI by using the MediaListCell' -date: "2014-09-30" -author: hendrik -categories: [JavaFX] -excerpt: 'For JavaOne I created some JavaFX APIs that contains basic utilities and controls that can be easily integrated in any JavaFX application. The ui-basics module contains some custom list cells that can be used to enrich your JavaFX application.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -By default the cells of a `ListView` will show only a string representation of your data on screen. This is ok for a basic implementation and will work for a lot of use cases. But if you have a application that contains a big list like a news feed or a chat you need a better skin for the cells. For JavaOne I created some JavaFX APIs that contains basic utilities and controls that can be easily integrated in any JavaFX application. The `ui-basics` module contains some custom list cells that can be used to enrich your JavaFX application. By using this cells we can achieve the following UI change with only a few lines of Java code: - -![media-cell](/posts/guigarage-legacy/media-cell.png) - -The following graphics shows the inheritance of the new cell classes: - -![ext-257x300](/posts/guigarage-legacy/ext-257x300.png) - -The `StructuredListCell` defines a cell that splits a cell in 3 regions: left, center and right. The center region will grow by default: - -![cell-layout](/posts/guigarage-legacy/cell-layout.png) - -You can add any content to the cell by calling the following methods: - -{{< highlight java >}} -setRightContent(Node node) -setRightContent(Node node) -setRightContent(Node node) -{{< / highlight >}} - -In addition the cell provides some new CSS attributes to style it: - -* `-fx-left-alignment`: Defines the vertical alignment of the left content -* `-fx-center-alignment`: Defines the vertical alignment of the center content -* `-fx-right-alignment`: Defines the vertical alignment of the right content -* `-fx-spacing`: Defines the spacing between the 3 regions -* `-fx-height-rule`: Defines which region should be used for the height calculation. By default the center content is used. This means that the cell will be as high as the center content. - -The `MediaListCell` extends this cell definition. It sets the center content to a a title and a description label. If you want to use the cell you only need to call `setTitle(...)` and `setDescription(...)` to define the center content: - -![tite-desc](/posts/guigarage-legacy/tite-desc.png) - -The class provides default style classes for both labels: - -* `media-cell-title` -* `media-cell-description` - -In addition the class provides two new CSS properties: - -* `-fx-show-description`: Defines if the description label should be visible -* `-fx-text-spacing`: Defines the spacing between the title and description label - -If you want the UI as shown in the demo you should use the `SimpleMediaListCell` class. This adds a rounded image view as the left content. By using this cell its very easy to create a list view like you know from many modern applications. To make the use of the cell even easier I introduced the `Media` interface. The `SimpleMediaListCell` is defined as `SimpleMediaListCell` and therefore it can only used with data that implements the `Media` interface. This interface is quite simple as you can see in its source: - -{{< highlight java >}} -public interface Media { - - StringProperty titleProperty(); - - StringProperty descriptionProperty(); - - ObjectProperty imageProperty(); -} -{{< / highlight >}} - -The properties of the nodes in the cell are automatically bound to the properties that are provided by the interface and therefore the `SimpleMediaListCell` class can be used like shown in the following example: - -{{< highlight java >}} -public class Album implements Media { - - private String artist; - - private String coverUrl; - - private String name; - - //getter & setter - - @Override - public StringProperty titleProperty() { - return new SimpleStringProperty(getName()); - } - - @Override - public StringProperty descriptionProperty() { - return new SimpleStringProperty(getArtist()); - } - - @Override - public ObjectProperty imageProperty() { - return new SimpleObjectProperty<>(new Image(getCoverUrl(), true)); - } -} - -//In View class... - -ListViewlistView = new ListView<>(); -listView.setCellFactory(v -> new SimpleMediaListCell<>()); -listView.setItems(dataModel.getAlbums()); -{{< / highlight >}} - -All the cell classes are part of the `ui-basics` module that can be found at github: - -{{< highlight xml >}} - - com.guigarage - ui-basics - X.Y - -{{< / highlight >}} - -## further development - -At the moment I plan some new features for the cells. As you might have registered the right region wasn't used in this example. In most UIs this is used to define a user action or hint like shown in this image: - -![showroom](/posts/guigarage-legacy/showroom.png) - -I plan to add this as a default feature. In addition I will provide different styles for the image view. Maybe you don't want a rounded view. In this case it would be perfect to define a style by css. Please ping me if you have some other cool improvements :) +--- +outdated: true +showInBlog: false +title: 'Enrich your List UI by using the MediaListCell' +date: "2014-09-30" +author: hendrik +categories: [JavaFX] +excerpt: 'For JavaOne I created some JavaFX APIs that contains basic utilities and controls that can be easily integrated in any JavaFX application. The ui-basics module contains some custom list cells that can be used to enrich your JavaFX application.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +By default the cells of a `ListView` will show only a string representation of your data on screen. This is ok for a basic implementation and will work for a lot of use cases. But if you have a application that contains a big list like a news feed or a chat you need a better skin for the cells. For JavaOne I created some JavaFX APIs that contains basic utilities and controls that can be easily integrated in any JavaFX application. The `ui-basics` module contains some custom list cells that can be used to enrich your JavaFX application. By using this cells we can achieve the following UI change with only a few lines of Java code: + +![media-cell](/posts/guigarage-legacy/media-cell.png) + +The following graphics shows the inheritance of the new cell classes: + +![ext-257x300](/posts/guigarage-legacy/ext-257x300.png) + +The `StructuredListCell` defines a cell that splits a cell in 3 regions: left, center and right. The center region will grow by default: + +![cell-layout](/posts/guigarage-legacy/cell-layout.png) + +You can add any content to the cell by calling the following methods: + +```javasetRightContent(Node node) +setRightContent(Node node) +setRightContent(Node node) ``` + +In addition the cell provides some new CSS attributes to style it: + +* `-fx-left-alignment`: Defines the vertical alignment of the left content +* `-fx-center-alignment`: Defines the vertical alignment of the center content +* `-fx-right-alignment`: Defines the vertical alignment of the right content +* `-fx-spacing`: Defines the spacing between the 3 regions +* `-fx-height-rule`: Defines which region should be used for the height calculation. By default the center content is used. This means that the cell will be as high as the center content. + +The `MediaListCell` extends this cell definition. It sets the center content to a a title and a description label. If you want to use the cell you only need to call `setTitle(...)` and `setDescription(...)` to define the center content: + +![tite-desc](/posts/guigarage-legacy/tite-desc.png) + +The class provides default style classes for both labels: + +* `media-cell-title` +* `media-cell-description` + +In addition the class provides two new CSS properties: + +* `-fx-show-description`: Defines if the description label should be visible +* `-fx-text-spacing`: Defines the spacing between the title and description label + +If you want the UI as shown in the demo you should use the `SimpleMediaListCell` class. This adds a rounded image view as the left content. By using this cell its very easy to create a list view like you know from many modern applications. To make the use of the cell even easier I introduced the `Media` interface. The `SimpleMediaListCell` is defined as `SimpleMediaListCell` and therefore it can only used with data that implements the `Media` interface. This interface is quite simple as you can see in its source: + +```javapublic interface Media { + + StringProperty titleProperty(); + + StringProperty descriptionProperty(); + + ObjectProperty imageProperty(); +} ``` + +The properties of the nodes in the cell are automatically bound to the properties that are provided by the interface and therefore the `SimpleMediaListCell` class can be used like shown in the following example: + +```javapublic class Album implements Media { + + private String artist; + + private String coverUrl; + + private String name; + + //getter & setter + + @Override + public StringProperty titleProperty() { + return new SimpleStringProperty(getName()); + } + + @Override + public StringProperty descriptionProperty() { + return new SimpleStringProperty(getArtist()); + } + + @Override + public ObjectProperty imageProperty() { + return new SimpleObjectProperty<>(new Image(getCoverUrl(), true)); + } +} + +//In View class... + +ListViewlistView = new ListView<>(); +listView.setCellFactory(v -> new SimpleMediaListCell<>()); +listView.setItems(dataModel.getAlbums()); ``` + +All the cell classes are part of the `ui-basics` module that can be found at github: + +```xml + com.guigarage + ui-basics + X.Y + ``` + +## further development + +At the moment I plan some new features for the cells. As you might have registered the right region wasn't used in this example. In most UIs this is used to define a user action or hint like shown in this image: + +![showroom](/posts/guigarage-legacy/showroom.png) + +I plan to add this as a default feature. In addition I will provide different styles for the image view. Maybe you don't want a rounded view. In this case it would be perfect to define a style by css. Please ping me if you have some other cool improvements :) diff --git a/content/posts/2014-10-01-dialog-objects-pattern-automated-tests-testfx.md b/content/posts/2014-10-01-dialog-objects-pattern-automated-tests-testfx.md index 20e732e3..e1f55824 100644 --- a/content/posts/2014-10-01-dialog-objects-pattern-automated-tests-testfx.md +++ b/content/posts/2014-10-01-dialog-objects-pattern-automated-tests-testfx.md @@ -1,102 +1,94 @@ ---- -outdated: true -showInBlog: false -title: 'The View Objects Pattern & automated tests with TestFX' -date: "2014-10-01" -author: hendrik -categories: [JavaFX] -excerpt: 'When developing an application you should add automated tests. Oh, sorry - I mean you MUST add automated test. This post introduces a pattern that help to create reuaseable and maintainable tests' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -When developing an application you should add automated tests. Oh, sorry - I mean you MUST add automated test. So when developing a JavaFX application there will come the moment when you ask yourself "How should I test this UI? A normal JUnit tests won't work here..." - -## TestFX basics - -That's right but the JavaFX community is prepared for this problem by offering [TestFX](https://github.com/TestFX/TestFX). With TestFX you can create unit tests for JavaFX applications. Let's imagine you have an application that only contains a login dialog: - -![login](/posts/guigarage-legacy/login.png) - -You can automatically test this dialog by using the TestFX API. I coded test might look like this: - -{{< highlight java >}} -click(".text-field").type("steve"); -click(".password-field").type("duke4ever"); -click(".button:default"); - -assertNodeExists( ".dialog" ); -{{< / highlight >}} - -As you can see you can control and fake all user events by using TestFX. At [github](https://github.com/TestFX/TestFX/wiki) you can find a general documentation of the API. - -## Dialog Objects Pattern - -Mostly your application will contain more than a simple login dialog and in that case a test could become confusing: - -{{< highlight java >}} -click("#user-field").type("steve"); -click("#password-field").type("duke4ever"); -click("#login-button"); -click("#menu-button"); -click("#action-35"); -click("#tab-5"); -click("#next"); -click("#next"); -click("#next"); -click("#details"); -assertNodeExists( "#user-picture" ); -{{< / highlight >}} - -Web developers already know this problem and introduced a pattern to avoid it: [PageObject](http://martinfowler.com/bliki/PageObject.html) - -Since we don't have pages in JavaFX applications I would call it __"View Objects Pattern"__ instead. By using this pattern you will define a class / object for each view in your application. Let's image we have a music application with the following workflow: - -![workflow](/posts/guigarage-legacy/test-workflow.png) - -The applications contains 4 different views. To write tests for the application we should create a view object for each view. Here is an pseudo code example for the album overview: - -{{< highlight java >}} -public class AlbumOverviewView extends ViewObject { - - public AlbumDetailView openAlbum(String name) { - click((Text t) -> t.getText().contains(name)); - return new AlbumDetailView(getTestHandler()); - } - - public AlbumOverviewView checkAlbumCount(int count) { - assertEquals(count, getList().size()); - return this; - } - - - public AlbumOverviewView assertContainsAlbum(String name) { - assertTrue(getAlbums().filtered(a -> a.getName().equals(name)).isEmpty()); - return this; - } -} -{{< / highlight >}} - -You can see some important facts in the code: - -* Each user interaction is defined as a method -* The class provides methods to check important states -* Each method returns the view object for the page that is visible after the method has been executed -* If the view won't change by calling a method the method will return `this` - -By doing so it is very easy to write understandable tests. Because all the methods will return a view object you can use it as a fluent API: - -{{< highlight java >}} -@Test -public void checkSearchResult() { - new SearchView(this).search("Rise Against").assertContainsAlbum("The Black Market"); -} - -@Test -public void checkTrackCount() { - new SearchView(this).search("Rise Against").openAlbum("The Black Market").checkTrackCountOfSelectedAlbum(12); -} - -@Test -public void checkPlayWorkflow() { - new SearchView(this).search("Rise Against").openAlbum("The Black Market").play(1); -} -{{< / highlight >}} +--- +outdated: true +showInBlog: false +title: 'The View Objects Pattern & automated tests with TestFX' +date: "2014-10-01" +author: hendrik +categories: [JavaFX] +excerpt: 'When developing an application you should add automated tests. Oh, sorry - I mean you MUST add automated test. This post introduces a pattern that help to create reuaseable and maintainable tests' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +When developing an application you should add automated tests. Oh, sorry - I mean you MUST add automated test. So when developing a JavaFX application there will come the moment when you ask yourself "How should I test this UI? A normal JUnit tests won't work here..." + +## TestFX basics + +That's right but the JavaFX community is prepared for this problem by offering [TestFX](https://github.com/TestFX/TestFX). With TestFX you can create unit tests for JavaFX applications. Let's imagine you have an application that only contains a login dialog: + +![login](/posts/guigarage-legacy/login.png) + +You can automatically test this dialog by using the TestFX API. I coded test might look like this: + +```javaclick(".text-field").type("steve"); +click(".password-field").type("duke4ever"); +click(".button:default"); + +assertNodeExists( ".dialog" ); ``` + +As you can see you can control and fake all user events by using TestFX. At [github](https://github.com/TestFX/TestFX/wiki) you can find a general documentation of the API. + +## Dialog Objects Pattern + +Mostly your application will contain more than a simple login dialog and in that case a test could become confusing: + +```javaclick("#user-field").type("steve"); +click("#password-field").type("duke4ever"); +click("#login-button"); +click("#menu-button"); +click("#action-35"); +click("#tab-5"); +click("#next"); +click("#next"); +click("#next"); +click("#details"); +assertNodeExists( "#user-picture" ); ``` + +Web developers already know this problem and introduced a pattern to avoid it: [PageObject](http://martinfowler.com/bliki/PageObject.html) + +Since we don't have pages in JavaFX applications I would call it __"View Objects Pattern"__ instead. By using this pattern you will define a class / object for each view in your application. Let's image we have a music application with the following workflow: + +![workflow](/posts/guigarage-legacy/test-workflow.png) + +The applications contains 4 different views. To write tests for the application we should create a view object for each view. Here is an pseudo code example for the album overview: + +```javapublic class AlbumOverviewView extends ViewObject { + + public AlbumDetailView openAlbum(String name) { + click((Text t) -> t.getText().contains(name)); + return new AlbumDetailView(getTestHandler()); + } + + public AlbumOverviewView checkAlbumCount(int count) { + assertEquals(count, getList().size()); + return this; + } + + + public AlbumOverviewView assertContainsAlbum(String name) { + assertTrue(getAlbums().filtered(a -> a.getName().equals(name)).isEmpty()); + return this; + } +} ``` + +You can see some important facts in the code: + +* Each user interaction is defined as a method +* The class provides methods to check important states +* Each method returns the view object for the page that is visible after the method has been executed +* If the view won't change by calling a method the method will return `this` + +By doing so it is very easy to write understandable tests. Because all the methods will return a view object you can use it as a fluent API: + +```java@Test +public void checkSearchResult() { + new SearchView(this).search("Rise Against").assertContainsAlbum("The Black Market"); +} + +@Test +public void checkTrackCount() { + new SearchView(this).search("Rise Against").openAlbum("The Black Market").checkTrackCountOfSelectedAlbum(12); +} + +@Test +public void checkPlayWorkflow() { + new SearchView(this).search("Rise Against").openAlbum("The Black Market").play(1); +} ``` diff --git a/content/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css.md b/content/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css.md index a17fc6a0..2acb4af7 100644 --- a/content/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css.md +++ b/content/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css.md @@ -1,78 +1,68 @@ ---- -outdated: true -showInBlog: false -title: 'How to integrate custom fonts in your JavaFX application by using CSS' -date: "2014-10-01" -author: hendrik -categories: [JavaFX] -excerpt: 'This CSS trick will show how you can change to font for a complete application or only a specific control by using CSS.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -This is one of the CSS tips that were part of my "Extreme Guimaker" talk at JavaOne this year and will show how you can change the font for a complete application or only a specific control by using CSS. - -Before explaining the CSS solution I want to show a short example in Java code: - -{{< highlight java >}} -Button b = new Button("Text"); -b.setFont(new Font("Arial", 24)); -{{< / highlight >}} - -In the code, the font of a button is set to "Arial" with a size of 24. All basic nodes in JavaFX that contains a text provide a font property that can be simply used to define a new font for the node. I don't think that this is a best practice because the font is an attribute that styles the application and therefore it should be separated from the application code. - -## Set fonts by CSS - -Fortunately JavaFX supports CSS and therefore we can extract the font specification from the Java code and add it to CSS. I won't discuss how IDs and style classes in CSS work in this post (If you not familiar with CSS you should have a look in [my book]({{ site.baseurl }}{% link pages/mastering-javafx-controls.md %})). The font of a node can be defined by using the `-fx-font-*` attributes in CSS. You can find a documentation of these attributes [here](http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html#typefont). Here is an example that defines the font for a button: - -{{< highlight css >}} -#my-button { - -fx-font-family: "Arial"; - -fx-font-size: 24; -} -{{< / highlight >}} - -If you want to define a global font for all controls in your application you can simply set the font to the `.text` style class. The Text shape in JavaFX contains this pseudo class by default and all nodes that render a text on the screen use the Text shape internally. Here is the css rule that will set the font for a complete application: - -{{< highlight css >}} -.text { - -fx-font-family: "Asap"; -} -{{< / highlight >}} - -## Adding custom fonts - -In the examples "Arial" is used as the custom font. Normally you can assume that this font will be installed on a client system. But sometimes you want to use a custom font to create a unique UI. In the following example, I want to use the Roboto font that is the official font of [Googles Material Design](http://www.google.com/design/spec/style/typography.html#typography-roboto). Normally this font won't be installed on a client system. So if you define the font by CSS and a customer will run the application without installing the specific font on the OS JavaFX will select a system font as a fallback and the cool UI of the app is broken. But thankfully there is a good solution for this problem. Since Java 8 JavaFX supports the `@font-face` rule that can be used to add fonts. As a first step, the font file must be added to the application. As a best practice the file should be added to the resources folder: - -![font](/posts/guigarage-legacy/font.png) - -Once this is done the font can be defined in CSS by using the `@font-face` rule: - -{{< highlight css >}} -@font-face { - font-family: 'Roboto'; - src: url('Roboto-Medium.ttf'); -} - -.text { - -fx-font-family: "Roboto"; -} -{{< / highlight >}} - -Now the font will be used in our application even if it isn't installed on the OS: - -![font-loaded](/posts/guigarage-legacy/font-loaded.png) - -## Update - -As I learned today the shown code isn't working in Java versions >= 1.8u60. Starting with this version the attribute “font-family” is ignored and you have to use the real name of the TTF. - -If you want to use the font “Birds of Paradis” that is contained in the file `demo.ttf`, for example, you have to use this CSS file: - -{{< highlight css >}} -@font-face { - src: url(“/ui/font/demo.ttf”); -} - -.label { - -fx-font-family: “Birds of Paradise”; -} -{{< / highlight >}} +--- +outdated: true +showInBlog: false +title: 'How to integrate custom fonts in your JavaFX application by using CSS' +date: "2014-10-01" +author: hendrik +categories: [JavaFX] +excerpt: 'This CSS trick will show how you can change to font for a complete application or only a specific control by using CSS.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +This is one of the CSS tips that were part of my "Extreme Guimaker" talk at JavaOne this year and will show how you can change the font for a complete application or only a specific control by using CSS. + +Before explaining the CSS solution I want to show a short example in Java code: + +```javaButton b = new Button("Text"); +b.setFont(new Font("Arial", 24)); ``` + +In the code, the font of a button is set to "Arial" with a size of 24. All basic nodes in JavaFX that contains a text provide a font property that can be simply used to define a new font for the node. I don't think that this is a best practice because the font is an attribute that styles the application and therefore it should be separated from the application code. + +## Set fonts by CSS + +Fortunately JavaFX supports CSS and therefore we can extract the font specification from the Java code and add it to CSS. I won't discuss how IDs and style classes in CSS work in this post (If you not familiar with CSS you should have a look in [my book]({{ site.baseurl }}{% link pages/mastering-javafx-controls.md %})). The font of a node can be defined by using the `-fx-font-*` attributes in CSS. You can find a documentation of these attributes [here](http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html#typefont). Here is an example that defines the font for a button: + +```css#my-button { + -fx-font-family: "Arial"; + -fx-font-size: 24; +} ``` + +If you want to define a global font for all controls in your application you can simply set the font to the `.text` style class. The Text shape in JavaFX contains this pseudo class by default and all nodes that render a text on the screen use the Text shape internally. Here is the css rule that will set the font for a complete application: + +```css.text { + -fx-font-family: "Asap"; +} ``` + +## Adding custom fonts + +In the examples "Arial" is used as the custom font. Normally you can assume that this font will be installed on a client system. But sometimes you want to use a custom font to create a unique UI. In the following example, I want to use the Roboto font that is the official font of [Googles Material Design](http://www.google.com/design/spec/style/typography.html#typography-roboto). Normally this font won't be installed on a client system. So if you define the font by CSS and a customer will run the application without installing the specific font on the OS JavaFX will select a system font as a fallback and the cool UI of the app is broken. But thankfully there is a good solution for this problem. Since Java 8 JavaFX supports the `@font-face` rule that can be used to add fonts. As a first step, the font file must be added to the application. As a best practice the file should be added to the resources folder: + +![font](/posts/guigarage-legacy/font.png) + +Once this is done the font can be defined in CSS by using the `@font-face` rule: + +```css@font-face { + font-family: 'Roboto'; + src: url('Roboto-Medium.ttf'); +} + +.text { + -fx-font-family: "Roboto"; +} ``` + +Now the font will be used in our application even if it isn't installed on the OS: + +![font-loaded](/posts/guigarage-legacy/font-loaded.png) + +## Update + +As I learned today the shown code isn't working in Java versions >= 1.8u60. Starting with this version the attribute “font-family” is ignored and you have to use the real name of the TTF. + +If you want to use the font “Birds of Paradis” that is contained in the file `demo.ttf`, for example, you have to use this CSS file: + +```css@font-face { + src: url(“/ui/font/demo.ttf”); +} + +.label { + -fx-font-family: “Birds of Paradise”; +} ``` diff --git a/content/posts/2014-10-05-iconify-application-resolution-independent-way.md b/content/posts/2014-10-05-iconify-application-resolution-independent-way.md index ea9daf1b..86732883 100644 --- a/content/posts/2014-10-05-iconify-application-resolution-independent-way.md +++ b/content/posts/2014-10-05-iconify-application-resolution-independent-way.md @@ -1,81 +1,71 @@ ---- -outdated: true -showInBlog: false -title: 'Iconify your application the resolution independent way' -date: "2014-10-05" -author: hendrik -categories: [JavaFX] -excerpt: 'Often icons are very important for a good UI. They will create a modern and professional look and will help the user to understand the meaning of actions.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Often icons are very important for a good UI. They will create a modern and professional look and will help the user to understand the meaning of actions. Especially on small screens icons are important. For a mobile application you won't create a toolbar with 5 actions that are described by a long text. In the following screenshot 3 actions are defined in the toolbar by using only icon: - -![Bildschirmfoto](/posts/guigarage-legacy/Bildschirmfoto-2014-09-16-um-21.21.04-744x1024.png) - -I think that mostly all users will understand that these actions defines a back action and 2 actions to change the volume. - -Today applications will be developed for different hardware and therefore for different screen resolutions. Let's think about the retina displays. Here you will have a 4x bigger resolution than on normal screens. In an ideal case an applications supports this devices and will fit its size and content. But that means, that all controls will have a 4x bigger size. Often you will use images for your icons. But resizing them will create a pixelated view. For a Mac retina display you can use the ["@2"-syntax](https://developer.apple.com/library/ios/qa/qa1686/_index.html) to provide retina images. But sometimes you want to scale an icon even bigger (Maybe you want to scale it in an animation, for example). Thats why you should use vector based icons: - -![pvv-300x141](/posts/guigarage-legacy/pvv-300x141.png) - -Ok, that sounds reasonable but where can we find vector based icons and how can we integrate them in JavaFX? - -For me the best resource for vector based icons is [Font Awesome](http://fortawesome.github.io/Font-Awesome/) that is a font which contains over 450 vector based icons. Here is a short extract: - -![awe](/posts/guigarage-legacy/video-player-icons.png) - -Because it is a font it can simply be integrated to any JavaFX application ([see this post]({{< ref "/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css" >}})). Once the font is assigned to a control you can define an icon by setting the text of the control. Here a special unicode character need to be set as the text. The following example describes how to set the pen icon to a button: - -{{< highlight java >}} -button.setText('\uf040' + ""); -// \uf040 is the unicode char for the icon as defines here: http://fortawesome.github.io/Font-Awesome/icon/pencil/ -{{< / highlight >}} - -Once you now this trick you still need to do some steps to display a vector based icon: - -* add the font to the resources folder -* define the font in CSS by using `@font-face` -* set the font to the specific control (in CSS) -* define the specific icon in java code - -Especially the last point isn't what I want. Icons are part of the style of an application and therefore it would be perfect if we could define them in CSS. There fore I created a new Skin for the JavaFX Button called `IconifiedButtonSkin`. By using the skin the handling of vector based icons in your JavaFX application is much easier. To use the skin you only need one line of Java code: - -{{< highlight java >}} -IconifiedButtonSkin.addStyle(myButton); -{{< / highlight >}} - -Once this is done the new skin for the button is set. This automatically contains the following steps: - -* add the font to the resources folder -* define the font in CSS by using `@font-face` -* set the font to the specific control in CSS by adding the `iconified-button` style class - -The last think that need to be done is setting the text of the button to define the wanted icon. Thankfully the new skin provides an additional CSS attribute that can be used. By using the -fx-icon-text attribute you can define the wanted icon directly in CSS: - -{{< highlight css >}} -#myButton { - -fx-icon-text: "\uf0a9"; -} -{{< / highlight >}} - -The `IconifiedButtonSkin` class is part of the `ui-basics` module that will be found at Maven Central the next days: - -{{< highlight xml >}} - - com.guigarage - ui-basics - X.Y - -{{< / highlight >}} - -## further development - -I plan to add a special CSS Converter in Java to provide a better definition of the icons in CSS. Wouldn't it be cool if you could do the following: - -{{< highlight css >}} -#myButton { - -fx-icon: "fa fa-pencil"; -} -{{< / highlight >}} - -Once this is done it would be cool to support more fonts like [ionicons](http://ionicons.com) by default. +--- +outdated: true +showInBlog: false +title: 'Iconify your application the resolution independent way' +date: "2014-10-05" +author: hendrik +categories: [JavaFX] +excerpt: 'Often icons are very important for a good UI. They will create a modern and professional look and will help the user to understand the meaning of actions.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Often icons are very important for a good UI. They will create a modern and professional look and will help the user to understand the meaning of actions. Especially on small screens icons are important. For a mobile application you won't create a toolbar with 5 actions that are described by a long text. In the following screenshot 3 actions are defined in the toolbar by using only icon: + +![Bildschirmfoto](/posts/guigarage-legacy/Bildschirmfoto-2014-09-16-um-21.21.04-744x1024.png) + +I think that mostly all users will understand that these actions defines a back action and 2 actions to change the volume. + +Today applications will be developed for different hardware and therefore for different screen resolutions. Let's think about the retina displays. Here you will have a 4x bigger resolution than on normal screens. In an ideal case an applications supports this devices and will fit its size and content. But that means, that all controls will have a 4x bigger size. Often you will use images for your icons. But resizing them will create a pixelated view. For a Mac retina display you can use the ["@2"-syntax](https://developer.apple.com/library/ios/qa/qa1686/_index.html) to provide retina images. But sometimes you want to scale an icon even bigger (Maybe you want to scale it in an animation, for example). Thats why you should use vector based icons: + +![pvv-300x141](/posts/guigarage-legacy/pvv-300x141.png) + +Ok, that sounds reasonable but where can we find vector based icons and how can we integrate them in JavaFX? + +For me the best resource for vector based icons is [Font Awesome](http://fortawesome.github.io/Font-Awesome/) that is a font which contains over 450 vector based icons. Here is a short extract: + +![awe](/posts/guigarage-legacy/video-player-icons.png) + +Because it is a font it can simply be integrated to any JavaFX application ([see this post](/posts/2014-10-01-integrate-custom-fonts-javafx-application-using-css)). Once the font is assigned to a control you can define an icon by setting the text of the control. Here a special unicode character need to be set as the text. The following example describes how to set the pen icon to a button: + +```javabutton.setText('\uf040' + ""); +// \uf040 is the unicode char for the icon as defines here: http://fortawesome.github.io/Font-Awesome/icon/pencil/ ``` + +Once you now this trick you still need to do some steps to display a vector based icon: + +* add the font to the resources folder +* define the font in CSS by using `@font-face` +* set the font to the specific control (in CSS) +* define the specific icon in java code + +Especially the last point isn't what I want. Icons are part of the style of an application and therefore it would be perfect if we could define them in CSS. There fore I created a new Skin for the JavaFX Button called `IconifiedButtonSkin`. By using the skin the handling of vector based icons in your JavaFX application is much easier. To use the skin you only need one line of Java code: + +```javaIconifiedButtonSkin.addStyle(myButton); ``` + +Once this is done the new skin for the button is set. This automatically contains the following steps: + +* add the font to the resources folder +* define the font in CSS by using `@font-face` +* set the font to the specific control in CSS by adding the `iconified-button` style class + +The last think that need to be done is setting the text of the button to define the wanted icon. Thankfully the new skin provides an additional CSS attribute that can be used. By using the -fx-icon-text attribute you can define the wanted icon directly in CSS: + +```css#myButton { + -fx-icon-text: "\uf0a9"; +} ``` + +The `IconifiedButtonSkin` class is part of the `ui-basics` module that will be found at Maven Central the next days: + +```xml + com.guigarage + ui-basics + X.Y + ``` + +## further development + +I plan to add a special CSS Converter in Java to provide a better definition of the icons in CSS. Wouldn't it be cool if you could do the following: + +```css#myButton { + -fx-icon: "fa fa-pencil"; +} ``` + +Once this is done it would be cool to support more fonts like [ionicons](http://ionicons.com) by default. diff --git a/content/posts/2014-10-22-datafx-8-released.md b/content/posts/2014-10-22-datafx-8-released.md index 1c840590..1094a373 100644 --- a/content/posts/2014-10-22-datafx-8-released.md +++ b/content/posts/2014-10-22-datafx-8-released.md @@ -1,80 +1,76 @@ ---- -outdated: true -showInBlog: false -title: 'DataFX 8 has been released & DataFX core overview' -date: "2014-10-22" -author: hendrik -categories: [DataFX, JavaFX] -excerpt: 'I''m proud to announce that we have released DataFX 8.0 last week. This post will give you an overview of all the cool new features.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -I'm proud to announce that we have released DataFX 8.0 last week. With DataFX 2.0 we created an API to get real world data in your JavaFX 2 application by using data readers for REST, WebSocket, SSE and many more endpoints. With DataFX 8.0 we introduce a lot of more content for your JavaFX 8 applications. Next to the data reader APIs we included flow and injection API to DataFX to create MVC based views and complex workflows. By doing so we lifted DataFX from a data reader API to a (small) application framework. DataFX 8.0 contains 5 modules: - -* core -* datasources -* websocket -* flow -* injection - -I think one of the big benefits of DataFX is that it has hardly any external dependency. The following graph shows the internal and external dependencies of DataFX 8: - -![datafx-dep.016](/posts/guigarage-legacy/datafx-dep.016.png) - -As you can see in the picture next to the javassist dependencies all other dependencies are Java specs. - -Ok let's talk about the content of the modules. As a first step of the DataFX 8 development we extracted all APIs that provide general support for multithreading and added them to the core module. Next to this some cool new APIs are part of the core module that will help you to define background tasks and solve concurrent problems the easy ways. Today I want to introduce two of these features. If you are interested in all features of DataFX 8 you should read the [tutorials]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}) and have a look in our [JavaOne slides](http://de.slideshare.net/HendrikEbbers/datafx-8-javaone-2014). - -## The Observable Executor - -When working with background tasks you will need a thread pool to manage all the concurrent operations. JavaSE provides several different of them with the Executors class. In DataFX 8 we introduce the [ObservableExecutor]({{< ref "/posts/2013-02-09-datafx-observableexecutor-preview" >}}) that is an implementation of the [Executor](http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) interface and provides some JavaFX specific additional functionality. By using the ObservableExecutor you can bind the capacity of the executor to any JavaFX property. In addition all the task interfaces and classes of JavaSE, JavaFX and DataFX are supported by the ObservableExecutor. By doing so it is very easy to define titles, messages or progress updates for all your background tasks and show them on screen. A demo of the ObservableExecutor can be found [here]({{< ref "/posts/2013-02-09-datafx-observableexecutor-preview" >}}). As a next step we will combine the ObservableExecutor with the cool Task Progress View by Dirk Lemmermann. It looks like this one is made for the ObservableExecutor ;) - -{{< youtube As-ahnLR_Dw >}} - -## The ProcessChain - -When developing an enterprise application with JavaFX you will need to define background tasks to call some server endpoints or start batch processes. Normally you will react to the answer of the tasks and update the UI. For example if you call a REST endpoint to receive some data you want to display the data on screen once the call is done. Doing this in the JavaFX Application thread isn't the best idea. You don't know how long the task will need to execute and therefore the application can't be repainted while the call is executing. This will end in a frozen application and frustrated users. - -![frozen](/posts/guigarage-legacy/frozen.png) - -It's import to execute the server call (as any long running action) to a background thread. Doing this with the basic JavaSE concurrency tools will blow up your code and create methods that aren't readable. Here is a simple example of a function that will call a background task and show it's result on screen: - -{{< highlight java >}} -Runnable backgroundRunnable = () -> { - try { - data = loadFromServer(); - Platform.runLater(() -> { - updateUI(data); - }); - } catch(Exception e) { - Platform.runLater(() -> { - handleException(e); - }); - } finally { - Platform.runLater(() -> { - unblockUI(); - }); - } -} -{{< / highlight >}} - -I hope you are with me when saying that this code isn't as readable as it should be. In Swing Java contains a good helper class called the [SwingWorker](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/simple.html). By using this class it was easier to create background tasks that provide data for the fronted. - -![background-thread](/posts/guigarage-legacy/background-thread.png) - -It's still a lot of code that is needed to create a working SwingWorker because anonymous classes are needed. But today we have Lambdas, functional interfaces and all this cool language features and therefore you wouldn't code a background tasks this way. In DataFX 8 we introduce the ProcessChain class that is like a SwingWorker on steroids. Here is a small example that shows how the top code can be refactored by using the ProcessChain: - -{{< highlight java >}} -ProcessChain.create(). -addRunnableInPlatformThread(() -> blockUI()). -addSupplierInExecutor(() -> loadFromServer()). -addConsumerInPlatformThread(d -> updateUI(d)). -onException(e -> handleException(e)). -withFinal(() -> unblockUI()). -run(); -{{< / highlight >}} - -Cool, isn't it. Now we can read the code and understand what's going on here. The ProcessChain uses all the new functional interfaces like Supplier or Consumer to define a chain of tasks that can be called on a background thread or on the JavaFX Application Thread. In addition the exception handling is directly included in the ProcessChain API. If you want to learn more about the ProcessChain you should check out our [slides](http://de.slideshare.net/HendrikEbbers/datafx-8-javaone-2014) or my [JavaFX Enterprise talk](http://de.slideshare.net/HendrikEbbers/javafx-enterprise-javaone-2014?related=1). - -I hope you like these features. In the next posts I will introduce the other DataFX 8 modules. - -{% include posts/slideshare.html id="39687394" %} +--- +outdated: true +showInBlog: false +title: 'DataFX 8 has been released & DataFX core overview' +date: "2014-10-22" +author: hendrik +categories: [DataFX, JavaFX] +excerpt: 'I''m proud to announce that we have released DataFX 8.0 last week. This post will give you an overview of all the cool new features.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +I'm proud to announce that we have released DataFX 8.0 last week. With DataFX 2.0 we created an API to get real world data in your JavaFX 2 application by using data readers for REST, WebSocket, SSE and many more endpoints. With DataFX 8.0 we introduce a lot of more content for your JavaFX 8 applications. Next to the data reader APIs we included flow and injection API to DataFX to create MVC based views and complex workflows. By doing so we lifted DataFX from a data reader API to a (small) application framework. DataFX 8.0 contains 5 modules: + +* core +* datasources +* websocket +* flow +* injection + +I think one of the big benefits of DataFX is that it has hardly any external dependency. The following graph shows the internal and external dependencies of DataFX 8: + +![datafx-dep.016](/posts/guigarage-legacy/datafx-dep.016.png) + +As you can see in the picture next to the javassist dependencies all other dependencies are Java specs. + +Ok let's talk about the content of the modules. As a first step of the DataFX 8 development we extracted all APIs that provide general support for multithreading and added them to the core module. Next to this some cool new APIs are part of the core module that will help you to define background tasks and solve concurrent problems the easy ways. Today I want to introduce two of these features. If you are interested in all features of DataFX 8 you should read the [tutorials](/posts/2014-05-19-datafx-8-0-tutorials) and have a look in our [JavaOne slides](http://de.slideshare.net/HendrikEbbers/datafx-8-javaone-2014). + +## The Observable Executor + +When working with background tasks you will need a thread pool to manage all the concurrent operations. JavaSE provides several different of them with the Executors class. In DataFX 8 we introduce the [ObservableExecutor](/posts/2013-02-09-datafx-observableexecutor-preview) that is an implementation of the [Executor](http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) interface and provides some JavaFX specific additional functionality. By using the ObservableExecutor you can bind the capacity of the executor to any JavaFX property. In addition all the task interfaces and classes of JavaSE, JavaFX and DataFX are supported by the ObservableExecutor. By doing so it is very easy to define titles, messages or progress updates for all your background tasks and show them on screen. A demo of the ObservableExecutor can be found [here](/posts/2013-02-09-datafx-observableexecutor-preview). As a next step we will combine the ObservableExecutor with the cool Task Progress View by Dirk Lemmermann. It looks like this one is made for the ObservableExecutor ;) + + + +## The ProcessChain + +When developing an enterprise application with JavaFX you will need to define background tasks to call some server endpoints or start batch processes. Normally you will react to the answer of the tasks and update the UI. For example if you call a REST endpoint to receive some data you want to display the data on screen once the call is done. Doing this in the JavaFX Application thread isn't the best idea. You don't know how long the task will need to execute and therefore the application can't be repainted while the call is executing. This will end in a frozen application and frustrated users. + +![frozen](/posts/guigarage-legacy/frozen.png) + +It's import to execute the server call (as any long running action) to a background thread. Doing this with the basic JavaSE concurrency tools will blow up your code and create methods that aren't readable. Here is a simple example of a function that will call a background task and show it's result on screen: + +```javaRunnable backgroundRunnable = () -> { + try { + data = loadFromServer(); + Platform.runLater(() -> { + updateUI(data); + }); + } catch(Exception e) { + Platform.runLater(() -> { + handleException(e); + }); + } finally { + Platform.runLater(() -> { + unblockUI(); + }); + } +} ``` + +I hope you are with me when saying that this code isn't as readable as it should be. In Swing Java contains a good helper class called the [SwingWorker](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/simple.html). By using this class it was easier to create background tasks that provide data for the fronted. + +![background-thread](/posts/guigarage-legacy/background-thread.png) + +It's still a lot of code that is needed to create a working SwingWorker because anonymous classes are needed. But today we have Lambdas, functional interfaces and all this cool language features and therefore you wouldn't code a background tasks this way. In DataFX 8 we introduce the ProcessChain class that is like a SwingWorker on steroids. Here is a small example that shows how the top code can be refactored by using the ProcessChain: + +```javaProcessChain.create(). +addRunnableInPlatformThread(() -> blockUI()). +addSupplierInExecutor(() -> loadFromServer()). +addConsumerInPlatformThread(d -> updateUI(d)). +onException(e -> handleException(e)). +withFinal(() -> unblockUI()). +run(); ``` + +Cool, isn't it. Now we can read the code and understand what's going on here. The ProcessChain uses all the new functional interfaces like Supplier or Consumer to define a chain of tasks that can be called on a background thread or on the JavaFX Application Thread. In addition the exception handling is directly included in the ProcessChain API. If you want to learn more about the ProcessChain you should check out our [slides](http://de.slideshare.net/HendrikEbbers/datafx-8-javaone-2014) or my [JavaFX Enterprise talk](http://de.slideshare.net/HendrikEbbers/javafx-enterprise-javaone-2014?related=1). + +I hope you like these features. In the next posts I will introduce the other DataFX 8 modules. + +{% include posts/slideshare.html id="39687394" %} diff --git a/content/posts/2014-11-01-new-desktop-application-framework-datafx.md b/content/posts/2014-11-01-new-desktop-application-framework-datafx.md index de79578c..e013d76e 100644 --- a/content/posts/2014-11-01-new-desktop-application-framework-datafx.md +++ b/content/posts/2014-11-01-new-desktop-application-framework-datafx.md @@ -1,181 +1,167 @@ ---- -outdated: true -showInBlog: false -title: 'New Desktop Application Framework & DataFX' -date: "2014-11-01" -author: hendrik -categories: [DataFX, Desktop Application Framework (JSR 377), JavaFX] -excerpt: 'If DataFX should become an implementation of the JSR specification it must implement general interfaces and support a toolkit independent architecture.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Maybe you have mentioned that [Andres Almiray](https://twitter.com/aalmiray) is planing [a new desktop application framework JSR](http://www.jroller.com/aalmiray/entry/new_desktop_application_framework_jsr). I had a chat with him some days ago at the canoo hq and we discussed some points of this project. In addition Andres gave me an introduction to [Griffon](http://griffon.codehaus.org) and I showed him [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). - -One of the core features of the framework should be UI toolkit independency. By doing so the framework will only contain general definitions and JavaFX or Swing specific implementations will be loaded by SPI, for example. - -Griffon already contains this abstraction but DataFX is hardly coded against JavaFX. I think this is absolutely ok and at the moment there eis no plan to support other UI toolkits that JavaFX with DataFX. As said the application framework will define general classes and interfaces and maybe DataFX will be one of the JavaFX implementation. We will see what happens in the future. - -## Generalizing the DataFX concepts - -If DataFX should become an implementation of the JSR specification it must implement general interfaces and support a toolkit independent architecture. Therefore I did some tests and create a small platform independent framework based on DataFX architecture and APIs. I chose the concurrent API and the controller API of DataFX and created more general versions. As a benefit I created some cool code and features that will be integrated in DataFX 8.1. Let's have a look at the framework that is called "JWrap". You can find [the sources at GitHub](https://github.com/guigarage/jwrap). Because this was only a test there isn't any javadoc at the moment but the project contains a [Swing](https://github.com/guigarage/jwrap/tree/master/src/main/java/com/guigarage/uif/examples/swing) and a [JavaFX](https://github.com/guigarage/jwrap/tree/master/src/main/java/com/guigarage/uif/examples/javafx) example. JWrap has zero dependencies and defines a MVC and a concurrency API. Both API are platform independent and you don't need dependencies to Swing or JavaFX to use them. - -## JWrap concurrency utils - -JWrap contains a [UIToolkit class](https://github.com/guigarage/jwrap/blob/master/src/main/java/com/guigarage/uif/concurrent/UIToolkit.java) that can be used to work with the event and rendering thread of the underlying UI toolkit. Here are the methods that are defined in the class: - -{{< highlight java >}} -void runAndWait(Runnable runnable) - -void runLater(Runnable runnable) - -boolean isToolkitThread() - - T runCallableAndWait(Callable callable) -{{< / highlight >}} - -By using these methods you can interact with the event and rendering thread of the used UI toolkit. To do so you must configure JWrap. This can be done by only one line of code. Here is an example how you configure JWrap to use the Swing EDT: - -{{< highlight java >}} -UIToolkit.setPlatform(SwingPlatform.getInstance()); -{{< / highlight >}} - -There are several other concurrency classes in JWrap that all depend on the UIToolkit class. By doing so you can now use all the concurrency helpers in JWrap and automatically the EDT will be used as application thread. I ported the [ProcessChain]({{< ref "/posts/2014-10-22-datafx-8-released" >}}) of DataFX to JWarp and now you can code the following in your Swing application: - -{{< highlight java >}} -ProcessChain.create(). - addSupplierInPlatformThread(() -> myTextField.getText()). - addFunctionInExecutor((t) -> WeatherService.getWeather(t)). - addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { - myLabel.setText("Error"); - e.printStackTrace(); - }).run(); -{{< / highlight >}} - -I think this code is much better than using the SwingWorker. You can easily use the `ProcessChain` in any Swing application that supports Java 8. - -## JWrap MVC API - -DataFX contains the [controller and flow API]({{< ref "/posts/2014-05-19-datafx-8-0-tutorials" >}}) that can be used to define MVC based views in JavaFX. I ported some parts of this API to JWarp and created a UI toolkit independent way to define MVC based controllers. JWrap contains some annotations that can be used to create a link between the view and the controller of a dialog. - -Let's start with the swim example. As a first step we define the view and define names for all the UI components: - -{{< highlight java >}} -public class SwingDemoView extends JPanel { - - public SwingDemoView() { - setLayout(new BorderLayout()); - - JButton myButton = new JButton("Get weather by city"); - myButton.setName("myButton"); - - JTextField myTextField = new JTextField(); - myTextField.setName("myTextField"); - - JLabel myLabel = new JLabel("Result..."); - myLabel.setName("myLabel"); - - add(myTextField, BorderLayout.NORTH); - add(myButton, BorderLayout.CENTER); - add(myLabel, BorderLayout.SOUTH); - } -} -{{< / highlight >}} - -The second class of the dialog will be the controller class. In this class JWrap annotations can be sued to inject view components in the controller and define interaction: - -{{< highlight java >}} -public class SwingDemoController { - - @ViewNode - @ActionTrigger("copy-action") - private JButton myButton; - - @ViewNode - private JTextField myTextField; - - @ViewNode - private JLabel myLabel; - - @ActionMethod("copy-action") - private void copy() { - ProcessChain.create(). - addSupplierInPlatformThread(() -> myTextField.getText()). - addFunctionInExecutor((t) -> WeatherService.getWeather(t)). - addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { - myLabel.setText("Error"); - e.printStackTrace(); - }).run(); - } - - @PostConstruct - private void init() { - System.out.println("TADA"); - } -} -{{< / highlight >}} - -The `@ViewNode` annotation can be compared to the `@FXML` annotation that is used in JavaFX and DataFX to inject view nodes that are defined in FXML in a controller. The `@ViewNode` annotation has some benefits because it can be used for FXML based view and for coded view (this is one of the features that I will integrate in DataFX 8.1). - -The JavaFX version looks mainly the same. Here is the code for the view class: - -{{< highlight java >}} -public class JavaFXDemoView extends VBox { - - public JavaFXDemoView() { - setSpacing(12); - setPadding(new Insets(12)); - - Button myButton = new Button("Get weather by city"); - myButton.setId("myButton"); - - TextField myTextField = new TextField(); - myTextField.setId("myTextField"); - - Label myLabel = new Label("Result..."); - myLabel.setId("myLabel"); - - getChildren().addAll(myTextField, myButton, myLabel); - - } -} -{{< / highlight >}} - -And here we have the controller class: - -{{< highlight java >}} -public class JavaFXDemoController { - - @ViewNode - @ActionTrigger("copy-action") - private Button myButton; - - @ViewNode - private TextField myTextField; - - @ViewNode - private Label myLabel; - - @ActionMethod("copy-action") - private void copy() { - ProcessChain.create(). - addSupplierInPlatformThread(() -> myTextField.getText()). - addFunctionInExecutor((t) -> WeatherService.getWeather(t)). - addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { - myLabel.setText("Error"); - e.printStackTrace(); - }).run(); - } - - @PostConstruct - private void init() { - System.out.println("TADA"); - } -} -{{< / highlight >}} - -As you can see the Swing controller class and the JavaFX controller looks mainly the same. Annotations like `@ViewNode` can be used in Swing and JavaFX. - -## The future of JWrap - -I created this project to test of a UI independent API can look like. I don't plan to continue working on the library. Maybe I will use it when checking some other ideas for the application framework JSR. - -I think that the library can be a benefit for Swing developers. By using JWrap they will get some lambda based concurrency APIs and a MVC framework that can be used to structure the code or prepare a migration to JavaFX. +--- +outdated: true +showInBlog: false +title: 'New Desktop Application Framework & DataFX' +date: "2014-11-01" +author: hendrik +categories: [DataFX, Desktop Application Framework (JSR 377), JavaFX] +excerpt: 'If DataFX should become an implementation of the JSR specification it must implement general interfaces and support a toolkit independent architecture.' +preview_image: "/posts/preview-images/software-development-green.svg" +--- +Maybe you have mentioned that [Andres Almiray](https://twitter.com/aalmiray) is planing [a new desktop application framework JSR](http://www.jroller.com/aalmiray/entry/new_desktop_application_framework_jsr). I had a chat with him some days ago at the canoo hq and we discussed some points of this project. In addition Andres gave me an introduction to [Griffon](http://griffon.codehaus.org) and I showed him [DataFX]({{ site.baseurl }}{% link pages/projects/datafx.md %}). + +One of the core features of the framework should be UI toolkit independency. By doing so the framework will only contain general definitions and JavaFX or Swing specific implementations will be loaded by SPI, for example. + +Griffon already contains this abstraction but DataFX is hardly coded against JavaFX. I think this is absolutely ok and at the moment there eis no plan to support other UI toolkits that JavaFX with DataFX. As said the application framework will define general classes and interfaces and maybe DataFX will be one of the JavaFX implementation. We will see what happens in the future. + +## Generalizing the DataFX concepts + +If DataFX should become an implementation of the JSR specification it must implement general interfaces and support a toolkit independent architecture. Therefore I did some tests and create a small platform independent framework based on DataFX architecture and APIs. I chose the concurrent API and the controller API of DataFX and created more general versions. As a benefit I created some cool code and features that will be integrated in DataFX 8.1. Let's have a look at the framework that is called "JWrap". You can find [the sources at GitHub](https://github.com/guigarage/jwrap). Because this was only a test there isn't any javadoc at the moment but the project contains a [Swing](https://github.com/guigarage/jwrap/tree/master/src/main/java/com/guigarage/uif/examples/swing) and a [JavaFX](https://github.com/guigarage/jwrap/tree/master/src/main/java/com/guigarage/uif/examples/javafx) example. JWrap has zero dependencies and defines a MVC and a concurrency API. Both API are platform independent and you don't need dependencies to Swing or JavaFX to use them. + +## JWrap concurrency utils + +JWrap contains a [UIToolkit class](https://github.com/guigarage/jwrap/blob/master/src/main/java/com/guigarage/uif/concurrent/UIToolkit.java) that can be used to work with the event and rendering thread of the underlying UI toolkit. Here are the methods that are defined in the class: + +```javavoid runAndWait(Runnable runnable) + +void runLater(Runnable runnable) + +boolean isToolkitThread() + + T runCallableAndWait(Callable callable) ``` + +By using these methods you can interact with the event and rendering thread of the used UI toolkit. To do so you must configure JWrap. This can be done by only one line of code. Here is an example how you configure JWrap to use the Swing EDT: + +```javaUIToolkit.setPlatform(SwingPlatform.getInstance()); ``` + +There are several other concurrency classes in JWrap that all depend on the UIToolkit class. By doing so you can now use all the concurrency helpers in JWrap and automatically the EDT will be used as application thread. I ported the [ProcessChain](/posts/2014-10-22-datafx-8-released) of DataFX to JWarp and now you can code the following in your Swing application: + +```javaProcessChain.create(). + addSupplierInPlatformThread(() -> myTextField.getText()). + addFunctionInExecutor((t) -> WeatherService.getWeather(t)). + addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { + myLabel.setText("Error"); + e.printStackTrace(); + }).run(); ``` + +I think this code is much better than using the SwingWorker. You can easily use the `ProcessChain` in any Swing application that supports Java 8. + +## JWrap MVC API + +DataFX contains the [controller and flow API](/posts/2014-05-19-datafx-8-0-tutorials) that can be used to define MVC based views in JavaFX. I ported some parts of this API to JWarp and created a UI toolkit independent way to define MVC based controllers. JWrap contains some annotations that can be used to create a link between the view and the controller of a dialog. + +Let's start with the swim example. As a first step we define the view and define names for all the UI components: + +```javapublic class SwingDemoView extends JPanel { + + public SwingDemoView() { + setLayout(new BorderLayout()); + + JButton myButton = new JButton("Get weather by city"); + myButton.setName("myButton"); + + JTextField myTextField = new JTextField(); + myTextField.setName("myTextField"); + + JLabel myLabel = new JLabel("Result..."); + myLabel.setName("myLabel"); + + add(myTextField, BorderLayout.NORTH); + add(myButton, BorderLayout.CENTER); + add(myLabel, BorderLayout.SOUTH); + } +} ``` + +The second class of the dialog will be the controller class. In this class JWrap annotations can be sued to inject view components in the controller and define interaction: + +```javapublic class SwingDemoController { + + @ViewNode + @ActionTrigger("copy-action") + private JButton myButton; + + @ViewNode + private JTextField myTextField; + + @ViewNode + private JLabel myLabel; + + @ActionMethod("copy-action") + private void copy() { + ProcessChain.create(). + addSupplierInPlatformThread(() -> myTextField.getText()). + addFunctionInExecutor((t) -> WeatherService.getWeather(t)). + addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { + myLabel.setText("Error"); + e.printStackTrace(); + }).run(); + } + + @PostConstruct + private void init() { + System.out.println("TADA"); + } +} ``` + +The `@ViewNode` annotation can be compared to the `@FXML` annotation that is used in JavaFX and DataFX to inject view nodes that are defined in FXML in a controller. The `@ViewNode` annotation has some benefits because it can be used for FXML based view and for coded view (this is one of the features that I will integrate in DataFX 8.1). + +The JavaFX version looks mainly the same. Here is the code for the view class: + +```javapublic class JavaFXDemoView extends VBox { + + public JavaFXDemoView() { + setSpacing(12); + setPadding(new Insets(12)); + + Button myButton = new Button("Get weather by city"); + myButton.setId("myButton"); + + TextField myTextField = new TextField(); + myTextField.setId("myTextField"); + + Label myLabel = new Label("Result..."); + myLabel.setId("myLabel"); + + getChildren().addAll(myTextField, myButton, myLabel); + + } +} ``` + +And here we have the controller class: + +```javapublic class JavaFXDemoController { + + @ViewNode + @ActionTrigger("copy-action") + private Button myButton; + + @ViewNode + private TextField myTextField; + + @ViewNode + private Label myLabel; + + @ActionMethod("copy-action") + private void copy() { + ProcessChain.create(). + addSupplierInPlatformThread(() -> myTextField.getText()). + addFunctionInExecutor((t) -> WeatherService.getWeather(t)). + addConsumerInPlatformThread((t) -> myLabel.setText(t)).onException((e) -> { + myLabel.setText("Error"); + e.printStackTrace(); + }).run(); + } + + @PostConstruct + private void init() { + System.out.println("TADA"); + } +} ``` + +As you can see the Swing controller class and the JavaFX controller looks mainly the same. Annotations like `@ViewNode` can be used in Swing and JavaFX. + +## The future of JWrap + +I created this project to test of a UI independent API can look like. I don't plan to continue working on the library. Maybe I will use it when checking some other ideas for the application framework JSR. + +I think that the library can be a benefit for Swing developers. By using JWrap they will get some lambda based concurrency APIs and a MVC framework that can be used to structure the code or prepare a migration to JavaFX. diff --git a/content/posts/2014-11-04-responsive-design-javafx.md b/content/posts/2014-11-04-responsive-design-javafx.md index 62591c37..71cf2a7d 100644 --- a/content/posts/2014-11-04-responsive-design-javafx.md +++ b/content/posts/2014-11-04-responsive-design-javafx.md @@ -1,149 +1,135 @@ ---- -outdated: true -showInBlog: false -title: 'Responsive Design for JavaFX' -date: "2014-11-04" -author: hendrik -categories: [JavaFX] -excerpt: 'At JavaOne I introduced ResponsiveFX as a lib that adds responsive design to JavaFX. This post describes the core concepts of responsive design and the API.' -preview_image: "/posts/preview-images/software-development-green.svg" ---- -Once of the new APIs that I have shown at JavaOne was __ResponsiveFX__ that can be used to add responsive design to your JavaFX application. ResponsiveFX is an open source project maintained by Canoo and will be published to Maven Central the next days. - -## Responsive Design - -Today software must fit a wide range of devices. When developing an application customers often want to use it on a desktop pc and on a tablet. In addition a subset of the features should be useable with a mobile phone. Oh, and maybe next year the first customers want to check information on a smart watch. Even if web apps and JavaFX applications can be distributed and used on most of this devices you can't simply use the same UI on them. - -![responsive1-1024x544](/posts/guigarage-legacy/responsive1-1024x544.png) - -All these devices provide different screen sizes and resolutions. In addition the user interaction is in some parts completely different. While using a mouse and keyboard on a desktop pc you want to use touch and gestures on your mobile to navigate through the app. - -One approach to handle these issues is responsive design that can be used to provide an optimal viewing experience—easy reading and navigation with a minimum of resizing, panning, and scrolling—across a wide range of devices. Responsive design was first introduced by web applications and influenced the development trends in this area. By defining different layout for specific screen sizes the fronted of a web application will look good on mostly all devices. - -![responsive2-1024x460](/posts/guigarage-legacy/responsive2-1024x460.png) - -## ResponsiveFX - -The core concept of ReponsiveFX is copied from [Twitter Boostrap](http://getbootstrap.com) that provides responsive design for HTML. Boostrap provides several CSS style classes that can be used to hide or show components on different screen sizes. Here is a short overview of all the style classes: - -![responsive-twitter](/posts/guigarage-legacy/responsive-twitter.png) - -By adding one of these style classes to a component the visibility of the component depends on the current frame size. Here is a small example how this can be used in HTML: - -{{< highlight xml >}} -