import groovy.beans.Bindable;
import groovy.swing.SwingBuilder
import java.awt.BorderLayout
import javax.swing.JFrame
#Bindable def people = [[name:"Mary", age:18],
[name:"Tom", age:25]]
def swingBuilder = new SwingBuilder()
swingBuilder.edt { // edt method makes sure UI is build on Event Dispatch Thread.
lookAndFeel 'system' // Simple change in look and feel.
frame( title: 'Display a table',
size: [400,300],
show: true,
locationRelativeTo: null,
defaultCloseOperation: JFrame.EXIT_ON_CLOSE)
{
menuBar() {
menu(text: "File", mnemonic: 'F'){
menuItem(text: "Add",
actionPerformed: {
people << [name:"Harry", age:17]
println people
}
)}
}
panel(layout: new BorderLayout()) {
scrollPane(constraints: BorderLayout.CENTER) {
table() {
def listTeacher
tableModel(list: people) {
propertyColumn(header: 'Name', propertyName: 'name')
propertyColumn(header: 'Age', propertyName: 'age')
}
}
}
}
}
}
When click "Add" one entry is added to the list people but the JTable is not updated. How can I fix it?
You can make it an ObservableList, and then refresh the table model when the list changes in some way:
import groovy.beans.Bindable;
import groovy.swing.SwingBuilder
import java.awt.BorderLayout
import javax.swing.JFrame
import java.beans.PropertyChangeListener
#Bindable ObservableList people = [ [name:"Mary", age:18],
[name:"Tom", age:25] ]
def swingBuilder = new SwingBuilder()
swingBuilder.edt { // edt method makes sure UI is build on Event Dispatch Thread.
lookAndFeel 'system' // Simple change in look and feel.
frame( title: 'Display a table',
size: [400,300],
show: true,
locationRelativeTo: null,
defaultCloseOperation: JFrame.HIDE_ON_CLOSE) {
menuBar() {
menu(text: "File", mnemonic: 'F'){
menuItem(text: "Add", actionPerformed: {
people << [name:"Harry", age:17]
println people
}
) }
}
panel(layout: new BorderLayout()) {
scrollPane(constraints: BorderLayout.CENTER) {
table {
def listTeacher
tableModel( id:'model', list: people) { m ->
propertyColumn(header: 'Name', propertyName: 'name')
propertyColumn(header: 'Age', propertyName: 'age')
}
}
}
people.addPropertyChangeListener( { e -> model.fireTableDataChanged() } as PropertyChangeListener )
}
}
}
Related
I'm trying out Lit and I'm having trouble figuring out how to initialize a state field from props. Here's a basic contrived pagination component:
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
This fails in render when using this._numPages (throws RangeError for the map because it's NaN), but is fine with numPages. It seems like if the state is using public properties that have defaults, it works, but otherwise it fails. I think this has to do with the fact that when the element is created it doesn't have props yet, so the initial first render doesn't have a value. But what is the right pattern to achieve this then in that case? The documentation here says "In some cases, internal reactive state may be initialized from public properties—for example, if there is a expensive transformation between the user-visible property and the internal state." (https://lit.dev/docs/components/properties/#public-properties-and-internal-state).
For completeness, here's a snippet of output of tsc:
import { __decorate } from "tslib";
import { html } from 'lit';
import { localized, msg } from '#lit/localize';
import { property } from 'lit/decorators/property.js';
import { state } from 'lit/decorators/state.js';
import { BaseElement } from '../BaseElement.js';
let Pagination = class Pagination extends BaseElement {
constructor() {
super(...arguments);
this.selected = 0;
this._selected = this.selected;
this.pageSize = 10;
this._numPages = Math.ceil(this.total / this.pageSize);
}
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(this._selected, this._numPages);
return html `
<ul class="pagination">
<li>${msg("Previous", { id: 'pagination.previous', desc: 'Previous button in pagination' })}</li>
${Array.from(Array(numPages).keys()).map((e, i) => html `<li class=${this.selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", { id: 'pagination.next', desc: 'Previous button in pagination' })}</li>
</ul>
`;
}
};
Pagination.styles = [BaseElement.styles];
__decorate([
property({ type: Number })
], Pagination.prototype, "selected", void 0);
__decorate([
state()
], Pagination.prototype, "_selected", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "total", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "pageSize", void 0);
__decorate([
state()
], Pagination.prototype, "_numPages", void 0);
Pagination = __decorate([
localized()
], Pagination);
export { Pagination };
The #property decorator only declares the property for any lit-element component. To initialize the property with an default value, you need to declare it in Constructor.
I am not clear on your issue, what i understand that _numPages is giving error. So you can declare and initialize it as follow.
Below is your modified code.
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#property({type: Number})
_numPages: number;
constructor() {
super();
this._numPages = 10
}
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
Assigning the values in the class fields feels risky. I would need to review this, but I think this will not use the instance values so it might have unintended side-effects. I would wait until the instance is available (like the constructor), but, setting it in the constructor is also too soon as the property does not yet have the value from the component, rather it has the initial value from the class-field.
So we need to delay this even further.
I've solved this by using the firstUpdated lifecycle hook:
#customElement("my-component")
export class MyComponent extends LitElement {
#property()
myProperty: string = "";
#state()
_internalState: string = "";
override async firstUpdated() {
// avoid unnecessary calls by waiting for any pending updated to complete.
await this.updateComplete;
this._internalState = this.myProperty;
}
...
}
I do not understand why this code does not work to update a list when navigating "back" from a DetailView(). As far as I can tell, I'm calling a new fetchRequest each time I want to update the list and it seems that request should always return object with current properties. But as others have said they are "stale", reflecting whatever was the property BEFORE the update was committed in the DetailView. And tapping a Navigation link from a "Stale" row, opens a DetailView with the current values of the properties, so I know they have been sacved to the context (haven't they?).
First I have a "dataservice" like this:
import CoreData
import SwiftUI
protocol CategoryDataServiceProtocol {
func getCategories() -> [Category]
func getCategoryById(id: NSManagedObjectID) -> Category?
func addCategory(name: String, color: String)
func updateCategory(_ category: Category)
func deleteCategory(_ category: Category)
}
class CategoryDataService: CategoryDataServiceProtocol {
var viewContext: NSManagedObjectContext = PersistenceController.shared.viewContext
///Shouldn't this next function always return an updated version of my list of categories?
func getCategories() -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
let sort: NSSortDescriptor = NSSortDescriptor(keyPath: \Category.name_, ascending: true)
request.sortDescriptors = [sort]
///This line appears to do nothing if I insert it:
request.shouldRefreshRefetchedObjects = true
do {
///A print statement here does run, so it's getting this far...
print("Inside get categories func")
return try viewContext.fetch(request)
} catch {
return []
}
}
func getCategoryById(id: NSManagedObjectID) -> Category? {
do {
return try viewContext.existingObject(with: id) as? Category
} catch {
return nil
}
}
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
func updateCategory(_ category: Category) {
saveContext()
}
func deleteCategory(_ category: Category) {
viewContext.delete(category)
saveContext()
}
func saveContext() {
PersistenceController.shared.save()
}
}
class MockCategoryDataService: CategoryDataService {
override init() {
super .init()
self.viewContext = PersistenceController.preview.viewContext
print("MOCK INIT")
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
}
}
And I have a viewModel like this:
import SwiftUI
extension CategoriesList {
class ViewModel: ObservableObject {
let dataService: CategoryDataServiceProtocol
#Published var categories: [Category] = []
init(dataService: CategoryDataServiceProtocol = CategoryDataService()) {
self.dataService = dataService
}
func getCategories() {
self.categories = dataService.getCategories()
}
func deleteCategories(at offsets: IndexSet) {
offsets.forEach { index in
let category = categories[index]
dataService.deleteCategory(category)
}
}
}
}
Then my view:
import SwiftUI
struct CategoriesList: View {
#StateObject private var viewModel: CategoriesList.ViewModel
init(viewModel: CategoriesList.ViewModel = .init()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
#State private var isShowingSheet = false
var body: some View {
NavigationView {
List {
ForEach(viewModel.categories) { category in
NavigationLink(
destination: CategoryDetail(category: category)) {
CategoryRow(category: category)
.padding(0)
}
}
.onDelete(perform: { index in
viewModel.deleteCategories(at: index)
viewModel.getCategories()
})
}
.listStyle(PlainListStyle())
.onAppear(perform: {
viewModel.getCategories()
})
.navigationBarTitle(Text("Categories"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: { EditButton() })
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: {
isShowingSheet = true
viewModel.getCategories()
},
label: { Image(systemName: "plus.circle").font(.system(size: 20)) }
)
}
}
.sheet(isPresented: $isShowingSheet, onDismiss: {
viewModel.getCategories()
}, content: {
CategoryForm()
})
}
}
}
struct CategoriesList_Previews: PreviewProvider {
static var previews: some View {
let viewModel: CategoriesList.ViewModel = .init(dataService: MockCategoryDataService())
return CategoriesList(viewModel: viewModel)
}
}
So, when I navigate to the DetailView and change the name of the category, all is fine there. But then tapping the back button or swiping to return to the view - and the view still shows the old name.
I understand that the #Published array of [Category] is probably not looking at changes to objects inside the array, only if an object is removed or added, I guess.
But why is my list not updating anyways, since I am calling viewModel.getCategories() and that is triggering the fetch request in the dataservice getCategories function?
And if Combine is the answer, then how? Or what else am I missing? Does request.shouldRefreshRefetchedObjects = true offer anything? Or is it a bug as I read here: https://mjtsai.com/blog/2019/10/17/core-data-derived-attributes/
I am looking for a way to change the text of a button in Groovy when it is clicked. I can't find any documentation on it. I'm using Swingbuilder to lay the buttons out (it is for a Battleship game). I'm fairly new at using this lang.
What I'm using is:
import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.BorderLayout
class FrontEnd {
FrontEnd() {
def builder = new SwingBuilder()
builder.edt {
frame(title: 'Battleship', size: [500, 350], show: true, locationRelativeTo: null, resizable: false,
defaultCloseOperation: WindowConstants.EXIT_ON_CLOSE) {
borderLayout(vgap: 5)
panel(constraints: BorderLayout.CENTER) {
tableLayout {
tr {...
}
tr {
td {
label '1'
}
td {
button(id: 'a1', text: ' ', actionPerformed:)
}
td {
button(id: 'b1', text: ' ', actionPerformed:)
}
I don't know if it is even possible with this setup, so if there is another way I'd be glad to know about it.
Thank you.
The following example assigns a button to the variable myButton which is then used for reference. Clicking on the button will set the text to hello 0, hello 1, etc:
import groovy.swing.SwingBuilder
import groovy.beans.Bindable
import static javax.swing.JFrame.EXIT_ON_CLOSE
import java.awt.*
class Example {
static def count = 0
static void main(String[] args) {
def swingBuilder = new SwingBuilder()
swingBuilder.edt {
frame(title: 'Example', size: [140, 80],
show: true, locationRelativeTo: null,
defaultCloseOperation: EXIT_ON_CLOSE) {
borderLayout(vgap: 5)
panel(constraints: BorderLayout.SOUTH) {
myButton = button text: 'Save', actionPerformed: {
myButton.setText("hello ${count++}")
}
}
}
}
}
}
I'm trying to create an XML based on the data from a .json. So, my .json file looks something like:
{
"fruit1":
{
"name": "apple",
"quantity": "three",
"taste": "good",
"color": { "walmart": "{{red}}","tj": "{{green}}" }
},
"fruit2":
{
"name": "banana",
"quantity": "five",
"taste": "okay",
"color": { "walmart": "{{gmo}}","tj": "{{organic}}" }
}
}
I can create the XML just fine with the below code, from the above json
import groovy.xml.*
import groovy.json.JsonSlurper
def GenerateXML() {
def jsonSlurper = new JsonSlurper();
def fileReader = new BufferedReader(
new FileReader("/home/workspace/sample.json"))
def parsedData = jsonSlurper.parse(fileReader)
def writer = new FileWriter("sample.XML")
def builder = new StreamingMarkupBuilder()
builder.encoding = 'UTF-8'
writer << builder.bind {
mkp.xmlDeclaration()
"friuts"(version:'$number', application: "FunApp"){
delegate.deployables {
parsedData.each { index, obj ->
"fruit"(name:obj.name, quantity:obj.quantity) {
delegate.taste(obj.taste)
delegate.color {
obj.color.each { name, value ->
it.entry(key:name, value)
}
}
}
}
}
}
}
}
I want to extend this code, such that it looks for particular keys. And if they are present, the loop is performed for those maps as well and as such extends the resulting file.
So, if i have the JSON as like so:
{"fruit1":
{
"name": "apple",
"quantity": "three",
"taste": "good",
"color": { "walmart": "{{red}}","tj": "{{green}}" }
},
"fruit2":
{
"name": "banana",
"quantity": "five",
"taste": "okay",
"color": { "walmart": "{{gmo}}","tj": "{{organic}}" }
},
"chip1":
{
"name": "lays",
"quantity": "one",
"type": "baked"
},
"chip2":
{
"name": "somename",
"quantity": "one",
"type": "fried"
}
}
I want to add an IF, so that it check if any key(s) like 'chip*' is there. And if yes, perform another iteration. If not just skip that section of logic, and not throw any err. like this
import groovy.xml.*
import groovy.json.JsonSlurper
def GenerateXML() {
def jsonSlurper = new JsonSlurper();
def fileReader = new BufferedReader(
new FileReader("/home/okram/workspace/objectsRepo/sample.json"))
def parsedData = jsonSlurper.parse(fileReader)
def writer = new FileWriter("sample.XML")
def builder = new StreamingMarkupBuilder()
builder.encoding = 'UTF-8'
writer << builder.bind {
mkp.xmlDeclaration()
"fruits"(version:'$number', application: "FunApp"){
deployables {
parsedData.each { index, obj ->
"fruit"(name:obj.name, quantity:obj.quantity) {
taste(obj.taste)
color {
obj.color.each { name, value ->
it.entry(key:name, value)
}
}
}
}
}
}
if (parsedData.containsKey('chip*')){
//perform the iteration of the chip* maps
//to access the corresponding values
//below code fails, but that is the intent
parsedData.<onlyTheOnesPassing>.each { index1, obj1 ->
"Chips"(name:obj1.name, quantity:obj1.quantity) {
type(obj1.type)
}
}
}
}
}
I found the same dificult, but on Javascript language, if the logic help you, here what I made:
There are two ways:
You can use the library Lodash on the "get" here: Lodash get or the another one "has": Lodash has.
With they you can put the object and the path and check if there is one without getting any error.
Examples:
_.has(object, 'chip1.name');
// => false
_.has(object, 'fruit1');
// => true
Or you can put the code of the methods here:
// Recursively checks the nested properties of an object and returns the
// object property in case it exists.
static get(obj, key) {
return key.split(".").reduce(function (o, x) {
return (typeof o == "undefined" || o === null) ? o : o[x];
}, obj);
}
// Recursively checks the nested properties of an object and returns
//true in case it exists.
static has(obj, key) {
return key.split(".").every(function (x) {
if (typeof obj != "object" || obj === null || !x in obj)
return false;
obj = obj[x];
return true;
});
}
I hope it helps! :)
Why does this code fail ?
I want to change the color of one panel in a series of several panels, dynamically constructed (total number of panels not known beforehand).
For some reason, this code works when referencing the name of a particular panel (for example 'panel2'), but not when I refer to it dynamically ('panelID').
import groovy.swing.SwingBuilder
import javax.swing.WindowConstants as WC
import javax.swing.JOptionPane
import javax.swing.BoxLayout as BXL
swing = new SwingBuilder()
frame = swing.frame(title:'test',
defaultCloseOperation:WC.DISPOSE_ON_CLOSE) {
panel(id:'mainPanel'){
def panelID
(1..6).each {
panelID = 'panel' + it
panel(alignmentX: 0f, id: panelID , opaque:true ,background : java.awt.Color.GREEN){
label('description')
textField(id: "description$it", text: panelID, columns: 70 )
button(id: "button$panelID", text: panelID, actionPerformed : {
panelID.background = java.awt.Color.RED
panelID.repaint()
})
}
}
boxLayout(axis: BXL.Y_AXIS)
panel(id:'secondPanel' , alignmentX: 0f){
button('Quit', actionPerformed:{
dispose()
})
}
}
}
frame.pack()
frame.show()
To get the element based on it's ID, you need to access the ID as a parameter of the SwingBuilder, like so:
import groovy.swing.SwingBuilder
import javax.swing.WindowConstants as WC
import javax.swing.JOptionPane
import javax.swing.BoxLayout as BXL
swing = new SwingBuilder()
frame = swing.frame(title:'test', pack:true, visible:true, defaultCloseOperation:WC.DISPOSE_ON_CLOSE) {
panel(id:'mainPanel'){
(1..6).each { num ->
def panelID = "panel$num"
def pane = panel( alignmentX:0f, id:panelID, background:java.awt.Color.GREEN ) {
label('description')
textField(id: "description$num", text:panelID, columns: 70 )
button(id: "buttonpanel$num", text:panelID, actionPerformed : {
swing."$panelID".background = java.awt.Color.RED
})
}
}
boxLayout(axis: BXL.Y_AXIS)
panel(id:'secondPanel' , alignmentX: 0f){
button('Quit', actionPerformed:{
frame.visible = false
})
}
}
}