In the middle of updating pit-swing.

This commit is contained in:
Jonathan Bernard 2010-03-08 21:33:49 -06:00
parent a3f9f4b291
commit cd8cdf02a4
16 changed files with 660 additions and 427 deletions

View File

@ -0,0 +1,29 @@
Add the ability to load multiple project roots simultaneously.
==============================================================
Description
-----------
I often need to switch between different issue repositories (project-specific,
main work repo, etc.) and have to close the current repo and open a new one.
I would like to be able to keep multiple repositories open at the same time.
Implementation Notes
--------------------
The interface should use tabbed panes to represent the different project roots.
The main view will need to split into seperate MVC components. This may simplify
the initial configuration, as the view data structures for the lists will no
longer be initialized at application startup.
Solution
--------
TBD
Resolution
----------
Date Created: 2010-02-08
Date Resolved: YYYY-MM-DD
Delivery: 0017

View File

@ -0,0 +1,27 @@
Add a default, "all-projects" view.
===================================
Description
-----------
Add a tab that simply lists all issues for all project in one giant
list to the left and a the text area on the right.
Implementation Notes
--------------------
I will need to create a new Project implementation that contains
arbitratily many projects and flattens all the issues under the multiple
projects into one list.
Solution
--------
TBD
Resolution
----------
Date Created: 2010-03-08
Date Resolved: YYYY-MM-DD
Delivery: 0018

View File

@ -9,6 +9,20 @@ application {
//frameClass = 'javax.swing.JFrame'
}
mvcGroups {
// MVC Group for "com.jdbernard.pit.swing.NewIssueDialog"
'NewIssueDialog' {
model = 'com.jdbernard.pit.swing.NewIssueDialogModel'
controller = 'com.jdbernard.pit.swing.NewIssueDialogController'
view = 'com.jdbernard.pit.swing.NewIssueDialogView'
}
// MVC Group for "com.jdbernard.pit.swing.ProjectPanel"
'ProjectPanel' {
model = 'com.jdbernard.pit.swing.ProjectPanelModel'
view = 'com.jdbernard.pit.swing.ProjectPanelView'
controller = 'com.jdbernard.pit.swing.ProjectPanelController'
}
// MVC Group for "com.jdbernard.pit.swing.PIT"
'PIT' {
model = 'com.jdbernard.pit.swing.PITModel'

View File

@ -0,0 +1,19 @@
package com.jdbernard.pit.swing
class NewIssueDialogController {
// these will be injected by Griffon
def model
def view
void mvcGroupInit(Map args) {
// this method is called after model and view are injected
}
def show = {
view.titleTextField.text = ""
view.categoryComboBox.selectedItem = Category.BUG
view.statusComboBox.selectedItem = Status.NEW
view.prioritySpinner.setValue(5)
view.dialog.visible = true
}
}

View File

@ -1,15 +1,8 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.FileProject
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import com.jdbernard.pit.Status
import javax.swing.DefaultListModel
import javax.swing.JOptionPane
import javax.swing.JFileChooser
import javax.swing.SwingUtilities
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
class PITController {
@ -21,8 +14,8 @@ class PITController {
SwingUtilities.invokeAndWait {
model.issueListRenderer = new IssueListCellRenderer()
model.issueListRenderer.categoryIcons = view.categoryIcons
model.issueListRenderer.statusIcons = view.statusIcons
model.issueListRenderer.categoryIcons = model.categoryIcons
model.issueListRenderer.statusIcons = model.statusIcons
def config = new File(System.getProperty('user.home'), '.pit')
config = new File(config, 'pit_swing.groovy')
@ -47,120 +40,53 @@ class PITController {
}
}
//
model.newIssueDialogMVC = buildMVCGroup('NewIssueDialog')
}
/**
* displayProject
* @param project Project to display.
*
*/
void displayProject(Project project) {
view.issueTextArea.text = ""
if (!project) return
def openProject = { evt = null ->
def projectDir
def newMVC
if (view.openDialog.showOpenDialog(view.frame) !=
JFileChooser.APPROVE_OPTION) return
if (!model.projectListModels[(project.name)]) {
def dlm = new DefaultListModel()
project.eachIssue(model.filter) { dlm.addElement(it) }
model.projectListModels[(project.name)] = dlm
projectDir = view.openDialog.selectedFile
// create new ProjectPanel MVC
newMVC = buildMVCGroup('ProjectPanel',
mainMVC: [model: model, view: view, controller: this],
newIssueDialogMVC: model.newIssueDialogMVC,
issueCellRenderer: model.issueListRenderer,
rootProject: new FileProject(projectDir))
newMVC.model.id = projectDir.name
// if we already have a tab with this id
if (model.projectPanelMVCs[(newMVC.model.id)]) {
// try using the canonical path
newMVC.model.id = projectDir.canonicalPath
// still not unique?
if (projectPanelMVC[(newMVC.model.id)]) {
// first time this has happened?
if (!projectIdMap[(newMVC.model.id)])
projectIdMap[(newMVC.model.id)] = 0
// no? increment
else projectIdMap[(newMVC.model.id)] =
projectIdMap[(newMVC.model.id)] + 1
// use our new, unique id
newMVC.model.id = projectDir.name +
projectIdMap[(newMVC.model.id)]
}
}
view.issueList.setModel(model.projectListModels[(project.name)])
model.projectPanelMVCs[newMVC.model.id] = newMVC
view.mainTabbedPane.addTab(newMVC.model.id, newMVC.view.panel)
}
void displayIssue(Issue issue) {
if (!issue) return
view.issueTextArea.text = issue.text
view.issueTextArea.caretPosition = 0
def shutdown = { evt = null ->
app.shutdown()
}
void showProjectPopup(Project project, def x, def y) {
model.popupProject = project
view.projectPopupMenu.show(view.projectTree, x, y)
}
void showIssuePopup(Issue issue, def x, def y) {
model.popupIssue = issue
view.issuePopupMenu.show(view.issueList, x, y)
}
def makeNodes(Project project) {
def rootNode = new DefaultMutableTreeNode(project)
project.eachProject(model.filter) { rootNode.add(makeNodes(it)) }
return rootNode
}
def newProject = { evt ->
def name = JOptionPane.showInputDialog(view.frame, 'Project name:',
'New Project...', JOptionPane.QUESTION_MESSAGE)
def project
if (evt.source == view.newProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootProject
def newProject = project.createNewProject(name)
project.projects[(newProject.name)] = newProject
view.projectTree.model = new DefaultTreeModel(
makeNodes(model.rootProject))
}
def deleteProject = { evt ->
def project
if (evt.source == view.deleteProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootModel
project.delete()
model.rootProject = new FileProject(model.rootProject.source)
}
def newIssue = { evt = null ->
view.titleTextField.text = ""
view.categoryComboBox.selectedItem = Category.BUG
view.statusComboBox.selectedItem = Status.NEW
view.prioritySpinner.setValue(5)
view.newIssueDialog.visible = true
}
def createIssue = { evt = null ->
def issueText = ""
if (model.templates[(view.categoryComboBox.selectedItem)]) {
issueText = model.templates[(view.categoryComboBox.selectedItem)]
issueText = issueText.replaceFirst(/TITLE/,
view.titleTextField.text)
}
def issue = model.selectedProject.createNewIssue(
category: view.categoryComboBox.selectedItem,
status: view.statusComboBox.selectedItem,
priority: view.prioritySpinner.value,
text: issueText)
model.projectListModels[(model.selectedProject.name)] = null
displayProject(model.selectedProject)
view.newIssueDialog.visible = false
}
def deleteIssue = { evt ->
def issue
if (evt.source == view.deleteIssueButton)
issue = view.issueList.selectedValue
else issue = model.popupIssue
model.selectedProject.issues.remove(issue.id)
model.projectListModels[(model.selectedProject.name)]
.removeElement(issue)
issue.delete()
}
def changeCategory = { evt ->
model.popupIssue.status = status
view.issueList.invalidate()
view.issueList.repaint()
}
}

View File

@ -0,0 +1,134 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.FileProject
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import com.jdbernard.pit.Status
import javax.swing.DefaultListModel
import javax.swing.JOptionPane
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
class ProjectPanelController {
// these will be injected by Griffon
def model
def view
void mvcGroupInit(Map args) {
view.projectTree.model = new DefaultTreeModel(makeNodes(model.rootProject))
}
/**
* displayProject
* @param project Project to display.
*
*/
void displayProject(Project project) {
view.issueTextArea.text = ""
if (!project) return
if (!model.projectListModels[(project.name)]) {
def dlm = new DefaultListModel()
project.eachIssue(model.filter ?: model.mainMVC.model.filter)
{ dlm.addElement(it) }
model.projectListModels[(project.name)] = dlm
}
view.issueList.setModel(model.projectListModels[(project.name)])
}
void displayIssue(Issue issue) {
if (!issue) return
view.issueTextArea.text = issue.text
view.issueTextArea.caretPosition = 0
}
void showProjectPopup(Project project, def x, def y) {
model.popupProject = project
view.projectPopupMenu.show(view.projectTree, x, y)
}
void showIssuePopup(Issue issue, def x, def y) {
model.popupIssue = issue
view.issuePopupMenu.show(view.issueList, x, y)
}
def makeNodes(Project project) {
def rootNode = new DefaultMutableTreeNode(project)
project.eachProject(model.filter ?: model.mainMVC.model.filter)
{ rootNode.add(makeNodes(it)) }
return rootNode
}
def newProject = { evt ->
def name = JOptionPane.showInputDialog(model.mainMVC.view.frame,
'Project name:', 'New Project...', JOptionPane.QUESTION_MESSAGE)
def project
if (evt.source == view.newProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootProject
def newProject = project.createNewProject(name)
project.projects[(newProject.name)] = newProject
view.projectTree.model = new DefaultTreeModel(
makeNodes(model.rootProject))
}
def deleteProject = { evt ->
def project
if (evt.source == view.deleteProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootModel
project.delete()
model.rootProject = new FileProject(model.rootProject.source)
}
def newIssue = { evt = null ->
newIssueDialogMVC.controller.show()
if (newIssueDialogMVC.model.accept) {
def nidmodel = newIssueDialodMVC.model
def issueText = ""
if (model.templates[(nidModel.category)]) {
issueText = model.templates[(nidModel.category)]
issueText = issueText.replaceFirst(/TITLE/,
nidModel.text)
}
def issue = model.selectedProject.createNewIssue(
category: nidModel.category,
status: nidModel.status,
priority: nidModel.priority,
text: issueText)
model.projectListModels[(model.selectedProject.name)] = null
displayProject(model.selectedProject)
}
}
def deleteIssue = { evt ->
def issue
if (evt.source == view.deleteIssueButton)
issue = view.issueList.selectedValue
else issue = model.popupIssue
model.selectedProject.issues.remove(issue.id)
model.projectListModels[(model.selectedProject.name)]
.removeElement(issue)
issue.delete()
}
def changeCategory = { evt ->
model.popupIssue.status = status
view.issueList.invalidate()
view.issueList.repaint()
}
}

View File

@ -0,0 +1,14 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Status
import groovy.beans.Bindable
class NewIssueDialogModel {
@Bindable boolean accept
String text
Category category
Status status
int priority
}

View File

@ -8,27 +8,20 @@ import com.jdbernard.pit.Status
import groovy.beans.Bindable
class PITModel {
@Bindable Project rootProject
// cache the ListModels
def projectListModels = [:]
// filter for projects and issues
Filter filter = new Filter(categories: [],
status: [Status.NEW, Status.VALIDATION_REQUIRED])
@Bindable Project popupProject = null
@Bindable Project selectedProject = null
@Bindable Issue popupIssue = null
// configurable exntension points
// ==============================
@Bindable def issueListRenderer
def issueListRenderer
// map of category -> issue template
def templates = [:]
def categoryIcons = [:]
def statusIcons = [:]
def newIssueDialogMVC
def projectPanelMVCs = [:]
def projectIdMap = [:]
}

View File

@ -0,0 +1,30 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Filter
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import groovy.beans.Bindable
class ProjectPanelModel {
def mainMVC
@Bindable Project rootProject
// cache the ListModels
def projectListModels = [:]
@Bindable Project popupProject = null
@Bindable Project selectedProject = null
@Bindable Issue popupIssue = null
// filter for projects and issues
Filter filter
def newIssueDialogMVC
String id
def issueCellRenderer
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

View File

@ -0,0 +1,64 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Status
import java.awt.GridBagConstraints as GBC
import javax.swing.DefaultComboBoxModel
dialog = dialog(title: 'New Task...', modal: true, pack: true,
locationRelativeTo: null) {
gridBagLayout()
label('Title/Summary:',
constraints: gbc(gridx: 0, gridy: 0, gridwidth: 3,
insets: [5, 5, 0, 5], fill: GBC.HORIZONTAL))
titleTextField = textField(
constraints: gbc(gridx: 0, gridy: 1, gridwidth: 3,
insets: [0, 10, 0, 5], fill: GBC.HORIZONTAL),
keyTyped: { model.text = titleTextField.text })
label('Category:',
constraints: gbc(gridx: 0, gridy: 2, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
categoryComboBox = comboBox(
constraints: gbc(gridx: 1, gridy: 2, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: new DefaultComboBoxModel(Category.values()),
editable: false,
itemStateChanged: { model.category = categoryComboBox.selectedValue })
label('Status:',
constraints: gbc(gridx: 0, gridy: 3, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
statusComboBox = comboBox(
constraints: gbc(gridx: 1, gridy: 3, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: new DefaultComboBoxModel(Status.values()),
editable: false,
itemStateChanged: { model.status = statusComboBox.selectedValue })
label('Priority (0-9, 0 is highest priority):',
constraints: gbc(gridx: 0, gridy: 4, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
prioritySpinner = spinner(
constraints: gbc( gridx: 1, gridy: 4, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: spinnerNumberModel(maximum: 9, minimum: 0),
stateChanged: { model.priority = prioritySpinner.value })
button('Cancel',
actionPerformed: {
model.accept = false
dialog.visible = false
},
constraints: gbc(gridx: 0, gridy: 5, insets: [5, 5, 5, 5],
anchor: GBC.EAST))
button('Create Issue',
actionPerformed: {
model.accept = true
dialog.visible = false
},
constraints: gbc(gridx: 1, gridy: 5, insets: [5, 5, 5, 5],
anchor: GBC.WEST))
}

View File

@ -9,195 +9,45 @@ import com.jdbernard.pit.FileProject
import groovy.beans.Bindable
import java.awt.BorderLayout as BL
import java.awt.GridBagConstraints as GBC
import java.awt.Font
import java.awt.Point
import java.awt.event.MouseEvent
import javax.swing.DefaultComboBoxModel
import javax.swing.DefaultListModel
import javax.swing.JDialog
import javax.swing.JFileChooser
import javax.swing.JOptionPane
import javax.swing.JSplitPane
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeCellRenderer
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeSelectionModel
import net.miginfocom.swing.MigLayout
import java.awt.Color
actions {
action (
id: 'newIssue',
name: 'New Issue',
icon: imageIcon("/add.png"),
accelerator: shortcut('N'),
closure: controller.newIssue,
enabled: bind { model.selectedProject != null }
action(
id: 'openProject',
name: 'Open...',
icon: imageIcon('/folder.png'),
accelerator: shortcut('O'),
closure: controller.openProject
)
action (
id: 'createIssue',
name: 'Create Issue',
closure: controller.createIssue
)
action (
id: 'newProject',
name: 'New Project...',
icon: imageIcon("/add.png"),
closure: controller.newProject
)
action (
id: 'deleteProject',
name: 'Delete Project',
closure: controller.deleteProject,
enabled: bind {model.selectedProject != null }
)
action (
id: 'deleteProjectPop',
name: 'Delete Project',
icon: imageIcon("/delete.png"),
closure: controller.deleteProject,
enabled: bind { model.popupProject != null }
)
action (
id: 'deleteIssue',
name: 'Delete Issue',
icon: imageIcon("/delete.png"),
closure: controller.deleteIssue,
)
action (
id: 'deleteIssuePop',
name: 'Delete Issue',
icon: imageIcon("/delete.png"),
closure: controller.deleteIssue,
enabled: bind { model.popupIssue != null }
action(
id: 'shutdown',
name: 'Exit',
icon: imageIcon('/shutdown.png'),
accelerator: shortcut('x'),
closure: controller.shutdown
)
}
/* ****************
* GUI components
* ****************/
categoryIcons = [:]
statusIcons = [:]
// initialize category-related view data
Category.values().each {
categoryIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png")
model.categoryIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png")
model.filter.categories.add(it)
}
Status.values().each {
statusIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png")
model.statusIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png")
}
openDialog = fileChooser(fileSelectionMode: JFileChooser.DIRECTORIES_ONLY)
newIssueDialog = dialog(title: 'New Task...', modal: true, pack: true,
locationRelativeTo: null) {
gridBagLayout()
label('Title/Summary:',
constraints: gbc(gridx: 0, gridy: 0, gridwidth: 3,
insets: [5, 5, 0, 5], fill: GBC.HORIZONTAL))
titleTextField = textField(
constraints: gbc(gridx: 0, gridy: 1, gridwidth: 3,
insets: [0, 10, 0, 5], fill: GBC.HORIZONTAL))
label('Category:',
constraints: gbc(gridx: 0, gridy: 2, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
categoryComboBox = comboBox(
constraints: gbc(gridx: 1, gridy: 2, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: new DefaultComboBoxModel(Category.values()))
label('Status:',
constraints: gbc(gridx: 0, gridy: 3, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
statusComboBox = comboBox(
constraints: gbc(gridx: 1, gridy: 3, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: new DefaultComboBoxModel(Status.values()))
label('Priority (0-9, 0 is highest priority):',
constraints: gbc(gridx: 0, gridy: 4, insets: [5, 5, 0, 0],
fill: GBC.HORIZONTAL))
prioritySpinner = spinner(
constraints: gbc( gridx: 1, gridy: 4, insets: [5, 5, 0, 5],
fill: GBC.HORIZONTAL),
model: spinnerNumberModel(maximum: 9, minimum: 0))
button('Cancel', actionPerformed: { newIssueDialog.visible = false },
constraints: gbc(gridx: 0, gridy: 5, insets: [5, 5, 5, 5],
anchor: GBC.EAST))
button(createIssue,
constraints: gbc(gridx: 1, gridy: 5, insets: [5, 5, 5, 5],
anchor: GBC.WEST))
}
projectPopupMenu = popupMenu() {
menuItem(newProject)
menuItem(deleteProjectPop)
}
issuePopupMenu = popupMenu() {
menuItem(newIssue)
menuItem(deleteIssuePop)
separator()
menu('Change Category') {
Category.values().each { category ->
menuItem(category.toString(),
icon: categoryIcons[(category)],
enabled: bind { model.popupIssue != null },
actionPerformed: {
model.popupIssue.category = category
issueList.invalidate()
issueList.repaint()
})
}
}
menu('Change Status') {
Status.values().each { status ->
menuItem(status.toString(),
icon: statusIcons[(status)],
enabled: bind { model.popupIssue != null },
actionPerformed: {
model.popupIssue.status = status
issueList.invalidate()
issueList.repaint()
})
}
}
menuItem('Change Priority...',
enabled: bind { model.popupIssue != null },
actionPerformed: {
def newPriority = JOptionPane.showInputDialog(frame,
'New priority (0-9)', 'Change Priority...',
JOptionPane.QUESTION_MESSAGE)
try { model.popupIssue.priority = newPriority.toInteger() }
catch (exception) {
JOptionPane.showMessage(frame, 'The priority value must ' +
'be an integer in [0-9].', 'Change Priority...',
JOptionPane.ERROR_MESSAGE)
return
}
issueList.invalidate()
issueList.repaint()
})
}
frame = application(title:'Personal Issue Tracker',
minimumSize: [800, 500],
pack:true,
@ -211,16 +61,8 @@ frame = application(title:'Personal Issue Tracker',
// main menu
menuBar() {
menu("File") {
menuItem('Open...', actionPerformed: {
def projectDir
if (openDialog.showOpenDialog(frame) !=
JFileChooser.APPROVE_OPTION) return
projectDir = openDialog.selectedFile
model.rootProject = new FileProject(projectDir)
})
menuItem('Exit', actionPerformed: { app.shutdown() })
menuItem(openProject)
menuItem(shutdown)
}
menu('View') {
@ -302,127 +144,7 @@ frame = application(title:'Personal Issue Tracker',
}
}
gridBagLayout()
mainTabbedPane = tabbedPane() {
// main split view
splitPane(orientation: JSplitPane.HORIZONTAL_SPLIT,
dividerLocation: 280,
constraints: gbc(fill: GBC.BOTH, insets: [10,10,10,10],
weightx: 2, weighty: 2)) {
// left side (projects tree and buttons)
panel(constraints: "left") {
gridBagLayout()
// tree view of projects
scrollPane(constraints: gbc(fill: GBC.BOTH, gridx: 0, gridy:0,
gridwidth: 2, weightx: 2, weighty: 2)) {
treeCellRenderer = new DefaultTreeCellRenderer()
treeCellRenderer.leafIcon = treeCellRenderer.closedIcon
projectTree = tree(cellRenderer: treeCellRenderer,
model: bind(source: model, sourceProperty: 'rootProject',
sourceValue: {
if (model.rootProject) {
projectTree.rootVisible =
model.rootProject.issues.size()
new DefaultTreeModel(controller.makeNodes(
model.rootProject))
} else {
projectTree.rootVisible = false
new DefaultTreeModel(new DefaultMutableTreeNode())
}
}),
valueChanged: { evt ->
model.selectedProject = evt?.newLeadSelectionPath?.
lastPathComponent?.userObject ?: model.rootProject
controller.displayProject(model.selectedProject)
},
mouseClicked: { evt ->
if (evt.button == MouseEvent.BUTTON3) {
controller.showProjectPopup(
projectTree.getPathForLocation(evt.x, evt.y)
?.lastPathComponent?.userObject,
evt.x, evt.y)
}
})
projectTree.model = new DefaultTreeModel(
new DefaultMutableTreeNode())
projectTree.rootVisible = false
projectTree.selectionModel.selectionMode =
TreeSelectionModel.SINGLE_TREE_SELECTION
}
// project buttons
newProjectButton = button(newProject,
constraints: gbc(fill: GBC.NONE, gridx: 0, gridy: 1,
anchor: GBC.WEST))
deleteProjectButton = button(deleteProject,
constraints: gbc(fill: GBC.NONE, gridx: 1, gridy: 1,
anchor: GBC.WEST))
}
// split between issue list and issue details
splitPane(orientation: JSplitPane.VERTICAL_SPLIT,
dividerLocation: 200, constraints: "right") {
panel(constraints: "top") {
gridBagLayout()
scrollPane(constraints: gbc(fill: GBC.BOTH, weightx: 2,
weighty: 2, gridx: 0, gridy: 0, gridwidth: 3)) {
issueList = list(
cellRenderer: bind(source: model,
sourceProperty: 'issueListRenderer'),
selectionMode: ListSelectionModel.SINGLE_SELECTION,
valueChanged: { evt ->
controller.displayIssue(issueList.selectedValue)
},
mouseClicked: { evt ->
if (evt.button == MouseEvent.BUTTON3) {
issueList.selectedIndex = issueList.locationToIndex(
[evt.x, evt.y] as Point)
controller.showIssuePopup(
issueList.selectedValue, evt.x, evt.y)
}
})
}
wordWrapCheckBox = checkBox('Word wrap',
constraints: gbc(gridx: 0, gridy: 1, weightx: 2,
anchor: GBC.WEST), selected: true)
button(newIssue,
constraints: gbc(gridx: 1, gridy: 1, anchor: GBC.EAST))
deleteIssueButton = button(deleteIssue,
constraints: gbc(gridx: 2, gridy: 1, anchor: GBC.EAST),
enabled: bind(source: issueList, sourceEvent: 'valueChanged',
sourceValue: { issueList.selectedValue != null }))
}
scrollPane(constraints: "bottom") {
issueTextArea = textArea(
wrapStyleWord: true,
lineWrap: bind(source: wordWrapCheckBox,
sourceProperty: 'selected'),
editable: bind( source: issueList, sourceEvent: 'valueChanged',
sourceValue: { issueList.selectedValue != null }),
font: new Font(Font.MONOSPACED, Font.PLAIN, 10),
focusGained: {},
focusLost: {
if (!issueList?.selectedValue) return
if (issueTextArea.text != issueList.selectedValue.text)
issueList.selectedValue.text = issueTextArea.text
},
mouseExited: {
if (!issueList?.selectedValue) return
if (issueTextArea.text != issueList.selectedValue.text)
issueList.selectedValue.text = issueTextArea.text
})
}
}
}
}

View File

@ -0,0 +1,241 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Status
import com.jdbernard.pit.Project
import java.awt.Font
import java.awt.GridBagConstraints as GBC
import java.awt.Point
import java.awt.event.MouseEvent
import javax.swing.JOptionPane
import javax.swing.JSplitPane
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeCellRenderer
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeSelectionModel
actions {
action (
id: 'newIssue',
name: 'New Issue',
icon: imageIcon("/add.png"),
accelerator: shortcut('N'),
closure: controller.newIssue,
enabled: bind { model.selectedProject != null }
)
action (
id: 'newProject',
name: 'New Project...',
icon: imageIcon("/add.png"),
closure: controller.newProject
)
action (
id: 'deleteProject',
name: 'Delete Project',
closure: controller.deleteProject,
enabled: bind {model.selectedProject != null }
)
action (
id: 'deleteProjectPop',
name: 'Delete Project',
icon: imageIcon("/delete.png"),
closure: controller.deleteProject,
enabled: bind { model.popupProject != null }
)
action (
id: 'deleteIssue',
name: 'Delete Issue',
icon: imageIcon("/delete.png"),
closure: controller.deleteIssue,
)
action (
id: 'deleteIssuePop',
name: 'Delete Issue',
icon: imageIcon("/delete.png"),
closure: controller.deleteIssue,
enabled: bind { model.popupIssue != null }
)
}
// popup menu for projects
projectPopupMenu = popupMenu() {
menuItem(newProject)
menuItem(deleteProjectPop)
}
// popup menu for isses
issuePopupMenu = popupMenu() {
menuItem(newIssue)
menuItem(deleteIssuePop)
separator()
menu('Change Category') {
Category.values().each { category ->
menuItem(category.toString(),
icon: model.mainMVC.model.categoryIcons[(category)],
enabled: bind { model.popupIssue != null },
actionPerformed: {
model.popupIssue.category = category
issueList.invalidate()
issueList.repaint()
})
}
}
menu('Change Status') {
Status.values().each { status ->
menuItem(status.toString(),
icon: model.mainMVC.model.statusIcons[(status)],
enabled: bind { model.popupIssue != null },
actionPerformed: {
model.popupIssue.status = status
issueList.invalidate()
issueList.repaint()
})
}
}
menuItem('Change Priority...',
enabled: bind { model.popupIssue != null },
actionPerformed: {
def newPriority = JOptionPane.showInputDialog(mainMVC.view.frame,
'New priority (0-9)', 'Change Priority...',
JOptionPane.QUESTION_MESSAGE)
try { model.popupIssue.priority = newPriority.toInteger() }
catch (exception) {
JOptionPane.showMessageDialog(mainMVC.view.frame,
'The priority value must be an integer in [0-9].',
'Change Priority...', JOptionPane.ERROR_MESSAGE)
return
}
issueList.invalidate()
issueList.repaint()
})
}
// main split view
panel = splitPane(orientation: JSplitPane.HORIZONTAL_SPLIT,
// dividerLocation: bind(source: model.mainModel, property: dividerLocation),
constraints: gbc(fill: GBC.BOTH, insets: [10,10,10,10],
weightx: 2, weighty: 2)) {
// left side (projects tree and buttons)
panel(constraints: "left") {
gridBagLayout()
// tree view of projects
scrollPane(constraints: gbc(fill: GBC.BOTH, gridx: 0, gridy:0,
gridwidth: 2, weightx: 2, weighty: 2)) {
treeCellRenderer = new DefaultTreeCellRenderer()
treeCellRenderer.leafIcon = treeCellRenderer.closedIcon
projectTree = tree(cellRenderer: treeCellRenderer,
model: bind(source: model, sourceProperty: 'rootProject',
sourceValue: {
if (model.rootProject) {
projectTree.rootVisible =
model.rootProject.issues.size()
new DefaultTreeModel(controller.makeNodes(
model.rootProject))
} else {
projectTree.rootVisible = false
new DefaultTreeModel(new DefaultMutableTreeNode())
}
}),
valueChanged: { evt ->
model.selectedProject = evt?.newLeadSelectionPath?.
lastPathComponent?.userObject ?: model.rootProject
controller.displayProject(model.selectedProject)
},
mouseClicked: { evt ->
if (evt.button == MouseEvent.BUTTON3) {
controller.showProjectPopup(
projectTree.getPathForLocation(evt.x, evt.y)
?.lastPathComponent?.userObject,
evt.x, evt.y)
}
})
projectTree.model = new DefaultTreeModel(
new DefaultMutableTreeNode())
projectTree.rootVisible = false
projectTree.selectionModel.selectionMode =
TreeSelectionModel.SINGLE_TREE_SELECTION
}
// project buttons
newProjectButton = button(newProject,
constraints: gbc(fill: GBC.NONE, gridx: 0, gridy: 1,
anchor: GBC.WEST))
deleteProjectButton = button(deleteProject,
constraints: gbc(fill: GBC.NONE, gridx: 1, gridy: 1,
anchor: GBC.WEST))
}
// split between issue list and issue details
splitPane(orientation: JSplitPane.VERTICAL_SPLIT,
dividerLocation: 200, constraints: "right") {
panel(constraints: "top") {
gridBagLayout()
scrollPane(constraints: gbc(fill: GBC.BOTH, weightx: 2,
weighty: 2, gridx: 0, gridy: 0, gridwidth: 3)) {
issueList = list(
cellRenderer: model.issueCellRenderer,
selectionMode: ListSelectionModel.SINGLE_SELECTION,
valueChanged: { evt ->
controller.displayIssue(issueList.selectedValue)
},
mouseClicked: { evt ->
if (evt.button == MouseEvent.BUTTON3) {
issueList.selectedIndex = issueList.locationToIndex(
[evt.x, evt.y] as Point)
controller.showIssuePopup(
issueList.selectedValue, evt.x, evt.y)
}
})
}
wordWrapCheckBox = checkBox('Word wrap',
constraints: gbc(gridx: 0, gridy: 1, weightx: 2,
anchor: GBC.WEST), selected: true)
button(newIssue,
constraints: gbc(gridx: 1, gridy: 1, anchor: GBC.EAST))
deleteIssueButton = button(deleteIssue,
constraints: gbc(gridx: 2, gridy: 1, anchor: GBC.EAST),
enabled: bind(source: issueList, sourceEvent: 'valueChanged',
sourceValue: { issueList.selectedValue != null }))
}
scrollPane(constraints: "bottom") {
issueTextArea = textArea(
wrapStyleWord: true,
lineWrap: bind(source: wordWrapCheckBox,
sourceProperty: 'selected'),
editable: bind( source: issueList, sourceEvent: 'valueChanged',
sourceValue: { issueList.selectedValue != null }),
font: new Font(Font.MONOSPACED, Font.PLAIN, 10),
focusGained: {},
focusLost: {
if (!issueList?.selectedValue) return
if (issueTextArea.text != issueList.selectedValue.text)
issueList.selectedValue.text = issueTextArea.text
},
mouseExited: {
if (!issueList?.selectedValue) return
if (issueTextArea.text != issueList.selectedValue.text)
issueList.selectedValue.text = issueTextArea.text
})
}
}
}

View File

@ -0,0 +1,10 @@
import griffon.util.IGriffonApplication
class NewIssueDialogTests extends GroovyTestCase {
IGriffonApplication app
void testSomething() {
}
}

View File

@ -0,0 +1,10 @@
import griffon.util.IGriffonApplication
class ProjectPanelTests extends GroovyTestCase {
IGriffonApplication app
void testSomething() {
}
}