From a3f9f4b2911e8bfab9d5051a728432250d6e993c Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Tue, 2 Mar 2010 11:59:33 -0600 Subject: [PATCH] Restructered pit-swing to better follow MVC paradigm. Adding extensibility features for pit-swing. --- issues/pit-swing/0014fn7.rst | 3 +- issues/pit-swing/0015fs3.rst | 2 + issues/pit-swing/0016fs3.rst | 2 + libpit/src/com/jdbernard/pit/Issue.groovy | 12 + .../jdbernard/pit/swing/PITController.groovy | 155 +++++++- .../griffon-app/lifecycle/Initialize.groovy | 12 +- .../com/jdbernard/pit/swing/PITModel.groovy | 26 ++ .../com/jdbernard/pit/swing/PITView.groovy | 331 +++++++----------- 8 files changed, 329 insertions(+), 214 deletions(-) create mode 100644 issues/pit-swing/0015fs3.rst create mode 100644 issues/pit-swing/0016fs3.rst diff --git a/issues/pit-swing/0014fn7.rst b/issues/pit-swing/0014fn7.rst index 537190b..3250987 100644 --- a/issues/pit-swing/0014fn7.rst +++ b/issues/pit-swing/0014fn7.rst @@ -1 +1,2 @@ -Load only N project deep at a time, lazy load any more. \ No newline at end of file +Load only N project deep at a time, lazy load any more. +======================================================= \ No newline at end of file diff --git a/issues/pit-swing/0015fs3.rst b/issues/pit-swing/0015fs3.rst new file mode 100644 index 0000000..b42ac8e --- /dev/null +++ b/issues/pit-swing/0015fs3.rst @@ -0,0 +1,2 @@ +Add a runtime configuration file. +================================= \ No newline at end of file diff --git a/issues/pit-swing/0016fs3.rst b/issues/pit-swing/0016fs3.rst new file mode 100644 index 0000000..8d18ef3 --- /dev/null +++ b/issues/pit-swing/0016fs3.rst @@ -0,0 +1,2 @@ +Add templates for new issues. +============================= \ No newline at end of file diff --git a/libpit/src/com/jdbernard/pit/Issue.groovy b/libpit/src/com/jdbernard/pit/Issue.groovy index 5e53523..688b1e8 100644 --- a/libpit/src/com/jdbernard/pit/Issue.groovy +++ b/libpit/src/com/jdbernard/pit/Issue.groovy @@ -9,6 +9,8 @@ public abstract class Issue { protected Status status protected int priority protected String text + protected Date deliveryDate + protected Date creationDate Issue(String id, Category c = Category.TASK, Status s = Status.NEW, int p = 9) { @@ -16,6 +18,8 @@ public abstract class Issue { this.category = c this.status = s this.priority = p + this.creationDate = new Date() + this.deliveryDate = null } public String getId() { return id; } @@ -48,6 +52,14 @@ public abstract class Issue { public void setText(String t) { text = t } + public boolean hasDelivery() { return deliveryDate == null } + + public Date getCreationDate() { return creationDate } + + public Date getDeliveryDate() { return deliveryDate } + + public void setDeliveryDate(Date dd) { deliveryDate = dd } + @Override public String toString() { return "${id}(${priority}-${status}): ${category} ${title}" } 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 02c8e72..31508cd 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,17 +1,166 @@ 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.SwingUtilities +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel class PITController { + // these will be injected by Griffon def model def view void mvcGroupInit(Map args) { + + SwingUtilities.invokeAndWait { + model.issueListRenderer = new IssueListCellRenderer() + model.issueListRenderer.categoryIcons = view.categoryIcons + model.issueListRenderer.statusIcons = view.statusIcons + + def config = new File(System.getProperty('user.home'), '.pit') + config = new File(config, 'pit_swing.groovy') + + if (config.exists() && config.isFile()) { + def loader = new GroovyClassLoader(PITController.classLoader) + def configBinding = new Binding() + + // add default values + configBinding.templates = model.templates + configBinding.issueListRenderer = model.issueListRenderer + + def configScript = loader.parseClass(config) + .newInstance(configBinding) + + configScript.invokeMethod("run", null) + + model.templates = configBinding.templates ?: [:] + if (configBinding.issueListRenderer && + configBinding.issueListRenderer != model.issueListRenderer) + model.issueListRenderer = configBinding.issueListRenderer + } + } + } - /* - def action = { evt = null -> + /** + * 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) { 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) { 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/lifecycle/Initialize.groovy b/pit-swing/griffon-app/lifecycle/Initialize.groovy index 4d2b3fd..0a46b4f 100644 --- a/pit-swing/griffon-app/lifecycle/Initialize.groovy +++ b/pit-swing/griffon-app/lifecycle/Initialize.groovy @@ -19,6 +19,16 @@ import groovy.swing.SwingBuilder import griffon.util.GriffonPlatformHelper +import griffon.util.GriffonApplicationHelper GriffonPlatformHelper.tweakForNativePlatform(app) -SwingBuilder.lookAndFeel('gtk', 'mac', 'org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel', 'nimbus', ['metal', [boldFonts: false]]) +SwingBuilder.lookAndFeel('mac', 'org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel', 'nimbus', ['metal', [boldFonts: false]]) + +// make config directory +def confDir = new File(System.getProperty('user.home'), '.pit') +if (!confDir.exists()) confDir.mkdirs() +// find or create configuration file +def swingConf = new File(confDir, 'pit-swing.groovy') +if (!swingConf.exists()) swingConf.createNewFile() +// run config +GriffonApplicationHelper.runScriptInsideEDT(swingConf.canonicalPath, app) 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 48205af..1ea2e1a 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 @@ -1,8 +1,34 @@ package com.jdbernard.pit.swing +import com.jdbernard.pit.Category +import com.jdbernard.pit.Filter +import com.jdbernard.pit.Issue import com.jdbernard.pit.Project +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 + + // map of category -> issue template + def templates = [:] + } diff --git a/pit-swing/griffon-app/views/com/jdbernard/pit/swing/PITView.groovy b/pit-swing/griffon-app/views/com/jdbernard/pit/swing/PITView.groovy index f9935d2..5c08101 100644 --- a/pit-swing/griffon-app/views/com/jdbernard/pit/swing/PITView.groovy +++ b/pit-swing/griffon-app/views/com/jdbernard/pit/swing/PITView.groovy @@ -27,87 +27,77 @@ import javax.swing.tree.TreeSelectionModel import net.miginfocom.swing.MigLayout import java.awt.Color -/* ******************** - * VIEW-Specific data - * ********************/ -// cache the ListModels -projectListModels = [:] +actions { + action ( + id: 'newIssue', + name: 'New Issue', + icon: imageIcon("/add.png"), + accelerator: shortcut('N'), + closure: controller.newIssue, + enabled: bind { model.selectedProject != null } + ) -// map of category -> list icon + 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 } + ) +} + +/* **************** + * GUI components + * ****************/ categoryIcons = [:] - statusIcons = [:] -// filter for projects and issues -filter = new Filter(categories: [], - status: [Status.NEW, Status.VALIDATION_REQUIRED]) - -popupProject = null -selectedProject = model.rootProject - -popupIssue = null - // initialize category-related view data Category.values().each { categoryIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png") - filter.categories.add(it) + model.filter.categories.add(it) } Status.values().each { statusIcons[(it)] = imageIcon("/${it.name().toLowerCase()}.png") } -/* *************** - * event methods - * ***************/ -/** - * displayProject - * @param project Project to display. - * - */ -displayProject = { project = null -> - issueTextArea.text = "" - if (!project) return - - if (!projectListModels[(project.name)]) { - def model = new DefaultListModel() - project.eachIssue(filter) { model.addElement(it) } - projectListModels[(project.name)] = model - } - - issueList.setModel(projectListModels[(project.name)]) -} - -displayIssue = { issue = null -> - if (issue) issueTextArea.text = issue.text -} - -showProjectPopup = { project, x, y -> - popupProject = project - projectPopupMenu[1].enabled = project != null - projectPopupMenu.show(projectTree, x, y) -} - -showIssuePopup = { issue, x, y -> - popupIssue = issue - issuePopupMenu.eachWithIndex { menuItem, idx -> - if (idx != 0) menuItem.enabled = issue != null } - issuePopupMenu.show(issueList, x, y) -} - -newIssue = { evt = null -> - titleTextField.text = "" - categoryComboBox.selectedItem = Category.BUG - statusComboBox.selectedItem = Status.NEW - prioritySpinner.setValue(5) - newIssueDialog.visible = true -} - -/* **************** - * GUI components - * ****************/ openDialog = fileChooser(fileSelectionMode: JFileChooser.DIRECTORIES_ONLY) newIssueDialog = dialog(title: 'New Task...', modal: true, pack: true, @@ -149,64 +139,28 @@ newIssueDialog = dialog(title: 'New Task...', modal: true, pack: true, button('Cancel', actionPerformed: { newIssueDialog.visible = false }, constraints: gbc(gridx: 0, gridy: 5, insets: [5, 5, 5, 5], anchor: GBC.EAST)) - button('Create Issue', + button(createIssue, constraints: gbc(gridx: 1, gridy: 5, insets: [5, 5, 5, 5], - anchor: GBC.WEST), - actionPerformed: { - def issue = selectedProject.createNewIssue( - category: categoryComboBox.selectedItem, - status: statusComboBox.selectedItem, - priority: prioritySpinner.value, - text: titleTextField.text) - projectListModels[(selectedProject.name)] = null - displayProject(selectedProject) - newIssueDialog.visible = false - }) + anchor: GBC.WEST)) } projectPopupMenu = popupMenu() { - menuItem('New Project...', icon: imageIcon("/add.png"), - actionPerformed: { - def name = JOptionPane.showInputDialog(frame, 'Project name:', - 'New Project...', JOptionPane.QUESTION_MESSAGE) - - if (!popupProject) popupProject = model.rootProject - def newProject = popupProject.createNewProject(name) - - popupProject.projects[(newProject.name)] = newProject - projectTree.model = new DefaultTreeModel( - makeNodes(model.rootProject)) - }) - menuItem('Delete Project', icon: imageIcon("/delete.png"), - actionPerformed: { - if (!popupProject) return - popupProject.delete() - // do not like, tied to Project implementation - model.rootProject = new FileProject(model.rootProject.source) - }) + menuItem(newProject) + menuItem(deleteProjectPop) } issuePopupMenu = popupMenu() { - menuItem('New Issue...', icon: imageIcon("/add.png"), - actionPerformed: newIssue) - - menuItem('Delete Issue', icon: imageIcon("/delete.png"), - actionPerformed: { - if (!popupIssue) return - selectedProject.issues.remove(popupIssue.id) - projectListModels[(selectedProject.name)].removeElement(popupIssue) - popupIssue.delete() - }) - + menuItem(newIssue) + menuItem(deleteIssuePop) separator() menu('Change Category') { Category.values().each { category -> menuItem(category.toString(), icon: categoryIcons[(category)], + enabled: bind { model.popupIssue != null }, actionPerformed: { - if (!popupIssue) return - popupIssue.category = category + model.popupIssue.category = category issueList.invalidate() issueList.repaint() }) @@ -217,9 +171,9 @@ issuePopupMenu = popupMenu() { Status.values().each { status -> menuItem(status.toString(), icon: statusIcons[(status)], + enabled: bind { model.popupIssue != null }, actionPerformed: { - if (!popupIssue) return - popupIssue.status = status + model.popupIssue.status = status issueList.invalidate() issueList.repaint() }) @@ -227,12 +181,12 @@ issuePopupMenu = popupMenu() { } menuItem('Change Priority...', + enabled: bind { model.popupIssue != null }, actionPerformed: { - if (!popupIssue) return def newPriority = JOptionPane.showInputDialog(frame, 'New priority (0-9)', 'Change Priority...', JOptionPane.QUESTION_MESSAGE) - try { popupIssue.priority = newPriority.toInteger() } + try { model.popupIssue.priority = newPriority.toInteger() } catch (exception) { JOptionPane.showMessage(frame, 'The priority value must ' + 'be an integer in [0-9].', 'Change Priority...', @@ -271,39 +225,37 @@ frame = application(title:'Personal Issue Tracker', menu('View') { menu('Category') { - Category.values().each { - checkBoxMenuItem(it.toString(), - selected: filter.categories.contains(it), + Category.values().each { cat -> + checkBoxMenuItem(cat.toString(), + selected: model.filter.categories.contains(cat), actionPerformed: { evt -> - def cat = Category.toCategory(evt.source.text) - if (filter.categories.contains(cat)) { - filter.categories.remove(cat) + if (model.filter.categories.contains(cat)) { + model.filter.categories.remove(cat) evt.source.selected = false } else { - filter.categories.add(cat) + model.filter.categories.add(cat) evt.source.selected = true } - projectListModels.clear() - displayProject(selectedProject) + model.projectListModels.clear() + controller.displayProject(model.selectedProject) }) } } menu('Status') { - Status.values().each { - checkBoxMenuItem(it.toString(), - selected: filter.status.contains(it), + Status.values().each { st -> + checkBoxMenuItem(st.toString(), + selected: model.filter.status.contains(st), actionPerformed: { evt -> - def st = Status.toStatus(evt.source.text[0..5]) - if (filter.status.contains(st)) { - filter.status.remove(st) + if (model.filter.status.contains(st)) { + model.filter.status.remove(st) evt.source.selected = false } else { - filter.status.add(st) + model.filter.status.add(st) evt.source.selected = true } - projectListModels.clear() - displayProject(selectedProject) + model.projectListModels.clear() + controller.displayProject(model.selectedProject) }) } } @@ -315,37 +267,37 @@ frame = application(title:'Personal Issue Tracker', checkBoxMenuItem('By ID', buttonGroup: sortMenuButtonGroup, actionPerformed: { - filter.issueSorter = { it.id } - projectListModels.clear() - displayProject(selectedProject) + model.filter.issueSorter = { it.id } + model.projectListModels.clear() + controller.displayProject(selectedProject) }) checkBoxMenuItem('By Category', buttonGroup: sortMenuButtonGroup, actionPerformed: { - filter.issueSorter = { it.category } - projectListModels.clear() - displayProject(selectedProject) + model.filter.issueSorter = { it.category } + model.projectListModels.clear() + controller.displayProject(selectedProject) }) checkBoxMenuItem('By Status', buttonGroup: sortMenuButtonGroup, actionPerformed: { - filter.issueSorter = { it.status } - projectListModels.clear() - displayProject(selectedProject) + model.filter.issueSorter = { it.status } + model.projectListModels.clear() + controller.displayProject(selectedProject) }) checkBoxMenuItem('By Priority', buttonGroup: sortMenuButtonGroup, actionPerformed: { - filter.issueSorter = { it.priority } - projectListModels.clear() - displayProject(selectedProject) + model.filter.issueSorter = { it.priority } + model.projectListModels.clear() + controller.displayProject(selectedProject) }) checkBoxMenuItem('By Title', buttonGroup: sortMenuButtonGroup, actionPerformed: { - filter.issueSorter = { it.title } - projectListModels.clear() - displayProject(selectedProject) + model.filter.issueSorter = { it.title } + model.projectListModels.clear() + controller.displayProject(selectedProject) }) } } @@ -374,21 +326,21 @@ frame = application(title:'Personal Issue Tracker', if (model.rootProject) { projectTree.rootVisible = model.rootProject.issues.size() - new DefaultTreeModel(makeNodes(model.rootProject)) + new DefaultTreeModel(controller.makeNodes( + model.rootProject)) } else { projectTree.rootVisible = false new DefaultTreeModel(new DefaultMutableTreeNode()) } }), valueChanged: { evt -> - selectedProject = evt?.newLeadSelectionPath?. + model.selectedProject = evt?.newLeadSelectionPath?. lastPathComponent?.userObject ?: model.rootProject - displayProject(selectedProject) - //deleteProjectButton.enabled = selectedProject != null + controller.displayProject(model.selectedProject) }, mouseClicked: { evt -> if (evt.button == MouseEvent.BUTTON3) { - showProjectPopup( + controller.showProjectPopup( projectTree.getPathForLocation(evt.x, evt.y) ?.lastPathComponent?.userObject, evt.x, evt.y) @@ -403,35 +355,12 @@ frame = application(title:'Personal Issue Tracker', } // project buttons - button('New Project', icon: imageIcon("/add.png"), + newProjectButton = button(newProject, constraints: gbc(fill: GBC.NONE, gridx: 0, gridy: 1, - anchor: GBC.WEST), - actionPerformed: { - def name = JOptionPane.showInputDialog(frame, - 'Project name:', 'New Project...', - JOptionPane.QUESTION_MESSAGE) - - if (!selectedProject) selectedProject = model.rootProject - def newProject = selectedProject.createNewProject(name) - - selectedProject.projects[(newProject.name)] = newProject - projectTree.model = new DefaultTreeModel( - makeNodes(model.rootProject)) - - }) - deleteProjectButton = button('Delete Project', - icon: imageIcon("/delete.png"), + anchor: GBC.WEST)) + deleteProjectButton = button(deleteProject, constraints: gbc(fill: GBC.NONE, gridx: 1, gridy: 1, - anchor: GBC.WEST), - enabled: bind(source: projectTree, sourceEvent: 'valueChanged', - sourceValue: { projectTree?.lastSelectedPathComponent != null}), - actionPerformed: { - if (!selectedProject) return - selectedProject.delete() - // do not like, tied to Project implementation - model.rootProject = new FileProject( - model.rootProject.source) - }) + anchor: GBC.WEST)) } // split between issue list and issue details @@ -445,42 +374,33 @@ frame = application(title:'Personal Issue Tracker', weighty: 2, gridx: 0, gridy: 0, gridwidth: 3)) { issueList = list( - cellRenderer: new IssueListCellRenderer( - categoryIcons: categoryIcons, - statusIcons: statusIcons), + cellRenderer: bind(source: model, + sourceProperty: 'issueListRenderer'), selectionMode: ListSelectionModel.SINGLE_SELECTION, - valueChanged: { displayIssue(issueList.selectedValue) }, + valueChanged: { evt -> + controller.displayIssue(issueList.selectedValue) + }, mouseClicked: { evt -> if (evt.button == MouseEvent.BUTTON3) { issueList.selectedIndex = issueList.locationToIndex( [evt.x, evt.y] as Point) - showIssuePopup(issueList.selectedValue, - evt.x, evt.y) + 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('New Issue', - constraints: gbc(gridx: 1, gridy: 1, anchor: GBC.EAST), - icon: imageIcon("/add.png"), actionPerformed: newIssue) + anchor: GBC.WEST), selected: true) + button(newIssue, + constraints: gbc(gridx: 1, gridy: 1, anchor: GBC.EAST)) - deleteIssueButton = button('Delete Issue', + deleteIssueButton = button(deleteIssue, constraints: gbc(gridx: 2, gridy: 1, anchor: GBC.EAST), enabled: bind(source: issueList, sourceEvent: 'valueChanged', - sourceValue: { issueList.selectedValue != null }), - icon: imageIcon("/delete.png"), - actionPerformed: { - if (!issueList?.selectedIssue) return - selectedProject.issues.remove(issueList.selectedValue) - projectListModels[(selectedProject.name)] - .removeElement(issueList.selectedValue) - issueList.selectedIssue.delete() - }) + sourceValue: { issueList.selectedValue != null })) } scrollPane(constraints: "bottom") { @@ -488,6 +408,8 @@ frame = application(title:'Personal Issue Tracker', 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: { @@ -504,12 +426,3 @@ frame = application(title:'Personal Issue Tracker', } } } - -/* ****************** - * Auxilary methods - * ******************/ -def makeNodes(Project project) { - def rootNode = new DefaultMutableTreeNode(project) - project.eachProject(filter) { rootNode.add(makeNodes(it)) } - return rootNode -}