In the middle of updating pit-swing.
This commit is contained in:
parent
a3f9f4b291
commit
cd8cdf02a4
29
issues/pit-swing/0017fn4.rst
Normal file
29
issues/pit-swing/0017fn4.rst
Normal 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
|
27
issues/pit-swing/0018fn7.rst
Normal file
27
issues/pit-swing/0018fn7.rst
Normal 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
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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 = [:]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
BIN
pit-swing/griffon-app/resources/cancel.png
Executable file
BIN
pit-swing/griffon-app/resources/cancel.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 587 B |
BIN
pit-swing/griffon-app/resources/shutdown.png
Executable file
BIN
pit-swing/griffon-app/resources/shutdown.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 688 B |
@ -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))
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
10
pit-swing/test/integration/NewIssueDialogTests.groovy
Normal file
10
pit-swing/test/integration/NewIssueDialogTests.groovy
Normal file
@ -0,0 +1,10 @@
|
||||
import griffon.util.IGriffonApplication
|
||||
|
||||
class NewIssueDialogTests extends GroovyTestCase {
|
||||
|
||||
IGriffonApplication app
|
||||
|
||||
void testSomething() {
|
||||
|
||||
}
|
||||
}
|
10
pit-swing/test/integration/ProjectPanelTests.groovy
Normal file
10
pit-swing/test/integration/ProjectPanelTests.groovy
Normal file
@ -0,0 +1,10 @@
|
||||
import griffon.util.IGriffonApplication
|
||||
|
||||
class ProjectPanelTests extends GroovyTestCase {
|
||||
|
||||
IGriffonApplication app
|
||||
|
||||
void testSomething() {
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user