From cd8cdf02a4c2f64ad236f1d9802fc7b35450883c Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Mon, 8 Mar 2010 21:33:49 -0600 Subject: [PATCH] In the middle of updating pit-swing. --- issues/pit-swing/0017fn4.rst | 29 ++ issues/pit-swing/0018fn7.rst | 27 ++ pit-swing/griffon-app/conf/Application.groovy | 14 + .../pit/swing/NewIssueDialogController.groovy | 19 ++ .../jdbernard/pit/swing/PITController.groovy | 160 +++------ .../pit/swing/ProjectPanelController.groovy | 134 ++++++++ .../pit/swing/NewIssueDialogModel.groovy | 14 + .../com/jdbernard/pit/swing/PITModel.groovy | 23 +- .../pit/swing/ProjectPanelModel.groovy | 30 ++ pit-swing/griffon-app/resources/cancel.png | Bin 0 -> 587 bytes pit-swing/griffon-app/resources/shutdown.png | Bin 0 -> 688 bytes .../pit/swing/NewIssueDialogView.groovy | 64 ++++ .../com/jdbernard/pit/swing/PITView.groovy | 312 +----------------- .../pit/swing/ProjectPanelView.groovy | 241 ++++++++++++++ .../integration/NewIssueDialogTests.groovy | 10 + .../test/integration/ProjectPanelTests.groovy | 10 + 16 files changed, 660 insertions(+), 427 deletions(-) create mode 100644 issues/pit-swing/0017fn4.rst create mode 100644 issues/pit-swing/0018fn7.rst create mode 100644 pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/NewIssueDialogController.groovy create mode 100644 pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/ProjectPanelController.groovy create mode 100644 pit-swing/griffon-app/models/com/jdbernard/pit/swing/NewIssueDialogModel.groovy create mode 100644 pit-swing/griffon-app/models/com/jdbernard/pit/swing/ProjectPanelModel.groovy create mode 100755 pit-swing/griffon-app/resources/cancel.png create mode 100755 pit-swing/griffon-app/resources/shutdown.png create mode 100644 pit-swing/griffon-app/views/com/jdbernard/pit/swing/NewIssueDialogView.groovy create mode 100644 pit-swing/griffon-app/views/com/jdbernard/pit/swing/ProjectPanelView.groovy create mode 100644 pit-swing/test/integration/NewIssueDialogTests.groovy create mode 100644 pit-swing/test/integration/ProjectPanelTests.groovy diff --git a/issues/pit-swing/0017fn4.rst b/issues/pit-swing/0017fn4.rst new file mode 100644 index 0000000..77077b0 --- /dev/null +++ b/issues/pit-swing/0017fn4.rst @@ -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 diff --git a/issues/pit-swing/0018fn7.rst b/issues/pit-swing/0018fn7.rst new file mode 100644 index 0000000..c5566cb --- /dev/null +++ b/issues/pit-swing/0018fn7.rst @@ -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 diff --git a/pit-swing/griffon-app/conf/Application.groovy b/pit-swing/griffon-app/conf/Application.groovy index 94ccff0..7c6e0a4 100644 --- a/pit-swing/griffon-app/conf/Application.groovy +++ b/pit-swing/griffon-app/conf/Application.groovy @@ -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' diff --git a/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/NewIssueDialogController.groovy b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/NewIssueDialogController.groovy new file mode 100644 index 0000000..af4833b --- /dev/null +++ b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/NewIssueDialogController.groovy @@ -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 + } +} diff --git a/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/PITController.groovy b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/PITController.groovy index 31508cd..67c4523 100644 --- a/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/PITController.groovy +++ b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/PITController.groovy @@ -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() - } } diff --git a/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/ProjectPanelController.groovy b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/ProjectPanelController.groovy new file mode 100644 index 0000000..583e427 --- /dev/null +++ b/pit-swing/griffon-app/controllers/com/jdbernard/pit/swing/ProjectPanelController.groovy @@ -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() + } + +} diff --git a/pit-swing/griffon-app/models/com/jdbernard/pit/swing/NewIssueDialogModel.groovy b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/NewIssueDialogModel.groovy new file mode 100644 index 0000000..0c7b9bb --- /dev/null +++ b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/NewIssueDialogModel.groovy @@ -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 +} diff --git a/pit-swing/griffon-app/models/com/jdbernard/pit/swing/PITModel.groovy b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/PITModel.groovy index 1ea2e1a..52cb267 100644 --- a/pit-swing/griffon-app/models/com/jdbernard/pit/swing/PITModel.groovy +++ b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/PITModel.groovy @@ -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 = [:] } diff --git a/pit-swing/griffon-app/models/com/jdbernard/pit/swing/ProjectPanelModel.groovy b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/ProjectPanelModel.groovy new file mode 100644 index 0000000..52547f6 --- /dev/null +++ b/pit-swing/griffon-app/models/com/jdbernard/pit/swing/ProjectPanelModel.groovy @@ -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 +} diff --git a/pit-swing/griffon-app/resources/cancel.png b/pit-swing/griffon-app/resources/cancel.png new file mode 100755 index 0000000000000000000000000000000000000000..c149c2bc017d5ce5a8ae9330dd7dbd012482e0f4 GIT binary patch literal 587 zcmV-R0<`^!P)FS^-G}e*;M)Q6>s#cP zI`Y#S($G6W`W@NI5g|L-MKl0Zmu$m^(0~^Lwo5OO~d#(vPfzGS2eE@_I zS~TaE^z1tT1me$mOd>fuB1*9ukjYHe@2!~sjG5tP)N7xSr3G9P+3oKa++V)SLaGru zn`QvjgqvWRa7{oUyOUDA37GDE%9f3r>9Muk`Z$59p<W>iYj7vxikNWw_8sK+%_fnobvCa5%KNOO6e%CReDLPLmVwdHp%H5J z8cVW-n2=oPDz8D+5J{LSmLlCXMPg`l3MX6KVJNMw&n~!g&9zA<=CHFVyLz1l^k+DTiboyDIuKD~MJE&R)Oo;bO6gPHCylVLL*a<{&sTsdkI7k&ZU WO{4dBd(FK70000 - 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 - }) - } - } } } diff --git a/pit-swing/griffon-app/views/com/jdbernard/pit/swing/ProjectPanelView.groovy b/pit-swing/griffon-app/views/com/jdbernard/pit/swing/ProjectPanelView.groovy new file mode 100644 index 0000000..fac6b97 --- /dev/null +++ b/pit-swing/griffon-app/views/com/jdbernard/pit/swing/ProjectPanelView.groovy @@ -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 + }) + } + } +} diff --git a/pit-swing/test/integration/NewIssueDialogTests.groovy b/pit-swing/test/integration/NewIssueDialogTests.groovy new file mode 100644 index 0000000..6dbfb11 --- /dev/null +++ b/pit-swing/test/integration/NewIssueDialogTests.groovy @@ -0,0 +1,10 @@ +import griffon.util.IGriffonApplication + +class NewIssueDialogTests extends GroovyTestCase { + + IGriffonApplication app + + void testSomething() { + + } +} diff --git a/pit-swing/test/integration/ProjectPanelTests.groovy b/pit-swing/test/integration/ProjectPanelTests.groovy new file mode 100644 index 0000000..5a71def --- /dev/null +++ b/pit-swing/test/integration/ProjectPanelTests.groovy @@ -0,0 +1,10 @@ +import griffon.util.IGriffonApplication + +class ProjectPanelTests extends GroovyTestCase { + + IGriffonApplication app + + void testSomething() { + + } +}