diff --git a/examples/doc.go b/examples/doc.go new file mode 100644 index 0000000..871f270 --- /dev/null +++ b/examples/doc.go @@ -0,0 +1,19 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* +Package examples contains example applications using/showcasing Gowut. +*/ +package examples diff --git a/examples/login_demo.go b/examples/login_demo.go new file mode 100644 index 0000000..efe6957 --- /dev/null +++ b/examples/login_demo.go @@ -0,0 +1,352 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// A GWU example application with login window and session management. + +package main + +import ( + "code.google.com/p/gowut/gwu" + "fmt" + "log" + "math/rand" + "os" + "strconv" +) + +type MyButtonHandler struct { + counter int + text string +} + +func (h *MyButtonHandler) HandleEvent(e gwu.Event) { + // Check if event source is a Button, just to be sure... + // We add this handler to a button only, so this'll be always false. + if b, isButton := e.Src().(gwu.Button); isButton { + b.SetText(b.Text() + h.text) + h.counter++ + b.SetToolTip("You've clicked " + strconv.Itoa(h.counter) + " times!") + e.MarkDirty(b) + } +} + +type GreenHandler int + +func (h *GreenHandler) HandleEvent(e gwu.Event) { + var state bool + src := e.Src() + + switch c := src.(type) { + case gwu.CheckBox: + state = c.State() + case gwu.RadioButton: + state = c.State() + } + + if state { + src.Style().SetBackground(gwu.CLR_GREEN) + } else { + src.Style().SetBackground("") + } + e.MarkDirty(src) +} + +var greenHandler_ = GreenHandler(0) +var greenHandler = &greenHandler_ + +func buildPrivateWins(s gwu.Session) { + // Create and build a window + win := gwu.NewWindow("main", "Main Window") + win.Style().SetFullWidth() + win.SetCellPadding(2) + + p := gwu.NewPanel() + p.SetLayout(gwu.LAYOUT_HORIZONTAL) + p.SetCellPadding(2) + p.Add(gwu.NewLabel("I'm a label! Try clicking on the button=>")) + p.Add(gwu.NewLink("Google Home", "https://google.com")) + img := gwu.NewImage("", "https://www.google.com/images/srpr/logo3w.png") + img.Style().SetSize("25%", "25%") + p.Add(img) + win.Add(p) + button := gwu.NewButton("Click me") + button.AddEHandler(&MyButtonHandler{text: ":-)"}, gwu.ETYPE_CLICK) + win.Add(button) + extraBtns := gwu.NewPanel() + extraBtns.SetLayout(gwu.LAYOUT_NATURAL) + button.AddEHandlerFunc(func(e gwu.Event) { + extraBtn := gwu.NewButton("Extra #" + strconv.Itoa(extraBtns.CompsCount())) + extraBtn.AddEHandlerFunc(func(e gwu.Event) { + extraBtn.Parent().Remove(extraBtn) + e.MarkDirty(extraBtns) + }, gwu.ETYPE_CLICK) + extraBtns.Insert(extraBtn, 0) + e.MarkDirty(extraBtns) + }, gwu.ETYPE_CLICK) + win.Add(extraBtns) + + p = gwu.NewPanel() + p.SetLayout(gwu.LAYOUT_HORIZONTAL) + p.SetCellPadding(2) + p.Style().SetBorder2(1, gwu.BRD_STYLE_SOLID, gwu.CLR_BLACK) + p.Add(gwu.NewLabel("A drop-down list being")) + wideListBox := gwu.NewListBox([]string{"50", "100", "150", "200", "250"}) + wideListBox.Style().SetWidth("50") + wideListBox.AddEHandlerFunc(func(e gwu.Event) { + wideListBox.Style().SetWidth(wideListBox.SelectedValue() + "px") + e.MarkDirty(wideListBox) + }, gwu.ETYPE_CHANGE) + p.Add(wideListBox) + p.Add(gwu.NewLabel("pixel wide. And a multi-select list:")) + listBox := gwu.NewListBox([]string{"First", "Second", "Third", "Forth", "Fifth", "Sixth"}) + listBox.SetMulti(true) + listBox.SetRows(4) + p.Add(listBox) + countLabel := gwu.NewLabel("Selected count: 0") + listBox.AddEHandlerFunc(func(e gwu.Event) { + selCount := len(listBox.SelectedIndices()) + countLabel.SetText("Selected count: " + strconv.Itoa(selCount)) + e.MarkDirty(countLabel) + }, gwu.ETYPE_CHANGE) + p.Add(countLabel) + win.Add(p) + + greenCheckBox := gwu.NewCheckBox("I'm a check box. When checked, I'm green!") + greenCheckBox.AddEHandlerFunc(func(e gwu.Event) { + if greenCheckBox.State() { + greenCheckBox.Style().SetBackground(gwu.CLR_GREEN) + } else { + greenCheckBox.Style().SetBackground("") + } + e.MarkDirty(greenCheckBox) + }, gwu.ETYPE_CLICK) + greenCheckBox.AddEHandler(greenHandler, gwu.ETYPE_CLICK) + win.Add(greenCheckBox) + + table := gwu.NewTable() + table.SetCellPadding(2) + table.Style().SetBorder2(1, gwu.BRD_STYLE_SOLID, gwu.CLR_BLACK) + table.EnsureSize(2, 4) + table.Add(gwu.NewLabel("TAB-"), 0, 0) + table.Add(gwu.NewLabel("LE"), 0, 1) + table.Add(gwu.NewLabel("DE-"), 0, 2) + table.Add(gwu.NewLabel("MO"), 0, 3) + table.Add(gwu.NewLabel("Enter your name:"), 1, 0) + tb := gwu.NewTextBox("") + tb.AddSyncOnETypes(gwu.ETYPE_KEY_UP) + table.Add(tb, 1, 1) + table.Add(gwu.NewLabel("You entered:"), 1, 2) + nameLabel := gwu.NewLabel("") + nameLabel.Style().SetColor(gwu.CLR_RED) + tb.AddEHandlerFunc(func(e gwu.Event) { + nameLabel.SetText(tb.Text()) + e.MarkDirty(nameLabel) + }, gwu.ETYPE_CHANGE, gwu.ETYPE_KEY_UP) + table.Add(nameLabel, 1, 3) + win.Add(table) + + table = gwu.NewTable() + table.Style().SetBorder2(1, gwu.BRD_STYLE_SOLID, gwu.CLR_BLACK) + table.SetAlign(gwu.HA_RIGHT, gwu.VA_TOP) + table.EnsureSize(5, 5) + for row := 0; row < 5; row++ { + group := gwu.NewRadioGroup(strconv.Itoa(row)) + for col := 0; col < 5; col++ { + radio := gwu.NewRadioButton("= "+strconv.Itoa(col)+" =", group) + radio.AddEHandlerFunc(func(e gwu.Event) { + radios := []gwu.RadioButton{radio, radio.Group().PrevSelected()} + for _, radio := range radios { + if radio != nil { + if radio.State() { + radio.Style().SetBackground(gwu.CLR_GREEN) + } else { + radio.Style().SetBackground("") + } + e.MarkDirty(radio) + } + } + }, gwu.ETYPE_CLICK) + table.Add(radio, row, col) + } + } + table.SetColSpan(2, 1, 2) + table.SetRowSpan(3, 1, 2) + table.CellFmt(2, 2).Style().SetSizePx(150, 80) + table.CellFmt(2, 2).SetAlign(gwu.HA_RIGHT, gwu.VA_BOTTOM) + table.RowFmt(2).Style().SetBackground("#808080") + table.RowFmt(2).SetAlign(gwu.HA_DEFAULT, gwu.VA_MIDDLE) + table.RowFmt(3).Style().SetBackground("#d0d0d0") + table.RowFmt(4).Style().SetBackground("#b0b0b0") + win.Add(table) + + tabPanel := gwu.NewTabPanel() + tabPanel.SetTabBarPlacement(gwu.TB_PLACEMENT_TOP) + for i := 0; i < 6; i++ { + if i == 3 { + img := gwu.NewImage("", "https://www.google.com/images/srpr/logo3w.png") + img.Style().SetWidthPx(100) + tabPanel.Add(img, gwu.NewLabel("This is some long content, random="+strconv.Itoa(rand.Int()))) + continue + } + tabPanel.AddString(strconv.Itoa(i)+". tab", gwu.NewLabel("This is some long content, random="+strconv.Itoa(rand.Int()))) + } + win.Add(tabPanel) + tabPanel = gwu.NewTabPanel() + tabPanel.SetTabBarPlacement(gwu.TB_PLACEMENT_LEFT) + tabPanel.TabBarFmt().SetVAlign(gwu.VA_BOTTOM) + for i := 7; i < 11; i++ { + l := gwu.NewLabel("This is some long content, random=" + strconv.Itoa(rand.Int())) + if i == 9 { + img := gwu.NewImage("", "https://www.google.com/images/srpr/logo3w.png") + img.Style().SetWidthPx(100) + tabPanel.Add(img, l) + tabPanel.CellFmt(l).Style().SetSizePx(400, 400) + continue + } + tabPanel.AddString(strconv.Itoa(i)+". tab", l) + tabPanel.CellFmt(l).Style().SetSizePx(400, 400) + } + win.Add(tabPanel) + s.AddWin(win) + + win2 := gwu.NewWindow("main2", "Main2 Window") + win2.Add(gwu.NewLabel("This is just a test 2nd window.")) + back := gwu.NewButton("Back") + back.AddEHandlerFunc(func(e gwu.Event) { + e.ReloadWin(win.Name()) + }, gwu.ETYPE_CLICK) + win2.Add(back) + s.AddWin(win2) +} + +func buildLoginWin(s gwu.Session) { + win := gwu.NewWindow("login", "Login Window") + win.Style().SetFullSize() + win.SetAlign(gwu.HA_CENTER, gwu.VA_MIDDLE) + + p := gwu.NewPanel() + p.SetHAlign(gwu.HA_CENTER) + p.SetCellPadding(2) + + l := gwu.NewLabel("Test GUI Login Window") + l.Style().SetFontWeight(gwu.FONT_WEIGHT_BOLD).SetFontSize("150%") + p.Add(l) + l = gwu.NewLabel("Login") + l.Style().SetFontWeight(gwu.FONT_WEIGHT_BOLD).SetFontSize("130%") + p.Add(l) + p.CellFmt(l).Style().SetBorder2(1, gwu.BRD_STYLE_DASHED, gwu.CLR_NAVY) + l = gwu.NewLabel("user/pass: admin/a") + l.Style().SetFontSize("80%").SetFontStyle(gwu.FONT_STYLE_ITALIC) + p.Add(l) + + errL := gwu.NewLabel("") + errL.Style().SetColor(gwu.CLR_RED) + p.Add(errL) + + table := gwu.NewTable() + table.SetCellPadding(2) + table.EnsureSize(2, 2) + table.Add(gwu.NewLabel("User name:"), 0, 0) + tb := gwu.NewTextBox("") + tb.Style().SetWidthPx(160) + table.Add(tb, 0, 1) + table.Add(gwu.NewLabel("Password:"), 1, 0) + pb := gwu.NewPasswBox("") + pb.Style().SetWidthPx(160) + table.Add(pb, 1, 1) + p.Add(table) + b := gwu.NewButton("OK") + b.AddEHandlerFunc(func(e gwu.Event) { + if tb.Text() == "admin" && pb.Text() == "a" { + e.Session().RemoveWin(win) // Login win is removed, password will not be retrievable from the browser + buildPrivateWins(e.Session()) + e.ReloadWin("main") + } else { + e.SetFocusedComp(tb) + errL.SetText("Invalid user name or password!") + e.MarkDirty(errL) + } + }, gwu.ETYPE_CLICK) + p.Add(b) + l = gwu.NewLabel("") + p.Add(l) + p.CellFmt(l).Style().SetHeightPx(200) + + win.Add(p) + win.SetFocusedCompId(tb.Id()) + + p = gwu.NewPanel() + p.SetLayout(gwu.LAYOUT_HORIZONTAL) + p.SetCellPadding(2) + p.Add(gwu.NewLabel("Here's an ON/OFF switch which enables/disables the other one:")) + sw := gwu.NewSwitchButton() + sw.SetOnOff("ENB", "DISB") + sw.SetState(true) + p.Add(sw) + p.Add(gwu.NewLabel("And the other one:")) + sw2 := gwu.NewSwitchButton() + sw2.SetEnabled(true) + sw2.Style().SetWidthPx(100) + p.Add(sw2) + sw.AddEHandlerFunc(func(e gwu.Event) { + sw2.SetEnabled(sw.State()) + e.MarkDirty(sw2) + }, gwu.ETYPE_CLICK) + win.Add(p) + + s.AddWin(win) +} + +type SessHandler struct{} + +func (h SessHandler) Created(s gwu.Session) { + fmt.Println("SESSION created:", s.Id()) + buildLoginWin(s) +} + +func (h SessHandler) Removed(s gwu.Session) { + fmt.Println("SESSION removed:", s.Id()) +} + +func main() { + // Create GUI server + server := gwu.NewServer("guitest", "") + //server := gwu.NewServerTLS("guitest", "", "test_tls/cert.pem", "test_tls/key.pem") + server.SetText("Test GUI Application") + + server.AddSessCreatorName("login", "Login Window") + server.AddSHandler(SessHandler{}) + + win := gwu.NewWindow("home", "Home Window") + l := gwu.NewLabel("Home, sweet home of " + server.Text()) + l.Style().SetFontWeight(gwu.FONT_WEIGHT_BOLD).SetFontSize("130%") + win.Add(l) + win.Add(gwu.NewLabel("Click on the button to login:")) + b := gwu.NewButton("Login") + b.AddEHandlerFunc(func(e gwu.Event) { + e.ReloadWin("login") + }, gwu.ETYPE_CLICK) + win.Add(b) + + server.AddWin(win) + + server.SetLogger(log.New(os.Stdout, "", log.LstdFlags)) + + // Start GUI server + if err := server.Start(); err != nil { + fmt.Println("Error: Cound not start GUI server:", err) + return + } +} diff --git a/examples/simple_demo.go b/examples/simple_demo.go new file mode 100644 index 0000000..ca8d6dd --- /dev/null +++ b/examples/simple_demo.go @@ -0,0 +1,122 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// A GWU example application with a single public window (no sessions). + +package main + +import ( + "code.google.com/p/gowut/gwu" + "strconv" +) + +type MyButtonHandler struct { + counter int + text string +} + +func (h *MyButtonHandler) HandleEvent(e gwu.Event) { + if b, isButton := e.Src().(gwu.Button); isButton { + b.SetText(b.Text() + h.text) + h.counter++ + b.SetToolTip("You've clicked " + strconv.Itoa(h.counter) + " times!") + e.MarkDirty(b) + } +} + +func main() { + // Create and build a window + win := gwu.NewWindow("main", "Test GUI Window") + win.Style().SetFullWidth() + win.SetHAlign(gwu.HA_CENTER) + win.SetCellPadding(2) + + // Button which changes window content + win.Add(gwu.NewLabel("I'm a label! Try clicking on the button=>")) + btn := gwu.NewButton("Click me") + btn.AddEHandler(&MyButtonHandler{text: ":-)"}, gwu.ETYPE_CLICK) + win.Add(btn) + btnsPanel := gwu.NewNaturalPanel() + btn.AddEHandlerFunc(func(e gwu.Event) { + // Create and add a new button... + newbtn := gwu.NewButton("Extra #" + strconv.Itoa(btnsPanel.CompsCount())) + newbtn.AddEHandlerFunc(func(e gwu.Event) { + btnsPanel.Remove(newbtn) // ...which removes itself when clicked + e.MarkDirty(btnsPanel) + }, gwu.ETYPE_CLICK) + btnsPanel.Insert(newbtn, 0) + e.MarkDirty(btnsPanel) + }, gwu.ETYPE_CLICK) + win.Add(btnsPanel) + + // ListBox examples + p := gwu.NewHorizontalPanel() + p.Style().SetBorder2(1, gwu.BRD_STYLE_SOLID, gwu.CLR_BLACK) + p.SetCellPadding(2) + p.Add(gwu.NewLabel("A drop-down list being")) + widelb := gwu.NewListBox([]string{"50", "100", "150", "200", "250"}) + widelb.Style().SetWidth("50") + widelb.AddEHandlerFunc(func(e gwu.Event) { + widelb.Style().SetWidth(widelb.SelectedValue() + "px") + e.MarkDirty(widelb) + }, gwu.ETYPE_CHANGE) + p.Add(widelb) + p.Add(gwu.NewLabel("pixel wide. And a multi-select list:")) + listBox := gwu.NewListBox([]string{"First", "Second", "Third", "Forth", "Fifth", "Sixth"}) + listBox.SetMulti(true) + listBox.SetRows(4) + p.Add(listBox) + countLabel := gwu.NewLabel("Selected count: 0") + listBox.AddEHandlerFunc(func(e gwu.Event) { + countLabel.SetText("Selected count: " + strconv.Itoa(len(listBox.SelectedIndices()))) + e.MarkDirty(countLabel) + }, gwu.ETYPE_CHANGE) + p.Add(countLabel) + win.Add(p) + + // Self-color changer check box + greencb := gwu.NewCheckBox("I'm a check box. When checked, I'm green!") + greencb.AddEHandlerFunc(func(e gwu.Event) { + if greencb.State() { + greencb.Style().SetBackground(gwu.CLR_GREEN) + } else { + greencb.Style().SetBackground("") + } + e.MarkDirty(greencb) + }, gwu.ETYPE_CLICK) + win.Add(greencb) + + // TextBox with echo + p = gwu.NewHorizontalPanel() + p.Add(gwu.NewLabel("Enter your name:")) + tb := gwu.NewTextBox("") + tb.AddSyncOnETypes(gwu.ETYPE_KEY_UP) + p.Add(tb) + p.Add(gwu.NewLabel("You entered:")) + nameLabel := gwu.NewLabel("") + nameLabel.Style().SetColor(gwu.CLR_RED) + tb.AddEHandlerFunc(func(e gwu.Event) { + nameLabel.SetText(tb.Text()) + e.MarkDirty(nameLabel) + }, gwu.ETYPE_CHANGE, gwu.ETYPE_KEY_UP) + p.Add(nameLabel) + win.Add(p) + + // Create and start a GUI server (omitting error check) + server := gwu.NewServer("guitest", "localhost:8081") + server.SetText("Test GUI App") + server.AddWin(win) + server.Start("") // Also opens windows list in browser +} diff --git a/gwu/button.go b/gwu/button.go new file mode 100644 index 0000000..497ac4c --- /dev/null +++ b/gwu/button.go @@ -0,0 +1,70 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Button component interface and implementation. + +package gwu + +// Button interface defines a clickable button. +// +// Suggested event type to handle actions: ETYPE_CLICK +// +// Default style class: "gwu-Button" +type Button interface { + // Button is a component. + Comp + + // Button has text. + HasText + + // Button can be enabled/disabled. + HasEnabled +} + +// Button implementation. +type buttonImpl struct { + compImpl // Component implementation + hasTextImpl // Has text implementation + hasEnabledImpl // Has enabled implementation +} + +// NewButton creates a new Button. +func NewButton(text string) Button { + c := newButtonImpl("", text) + c.Style().AddClass("gwu-Button") + return &c +} + +// newButtonImpl creates a new buttonImpl. +func newButtonImpl(valueProviderJs string, text string) buttonImpl { + return buttonImpl{newCompImpl(valueProviderJs), newHasTextImpl(text), newHasEnabledImpl()} +} + +var ( + _STR_BUTTON_OP = []byte("" +) + +func (c *buttonImpl) Render(w writer) { + w.Write(_STR_BUTTON_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + c.renderEnabled(w) + w.Write(_STR_GT) + + c.renderText(w) + + w.Write(_STR_BUTTON_CL) +} diff --git a/gwu/comp.go b/gwu/comp.go new file mode 100644 index 0000000..edfbc46 --- /dev/null +++ b/gwu/comp.go @@ -0,0 +1,315 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Comp component interface and implementation. + +package gwu + +import ( + "html" + "net/http" + "strconv" +) + +// Container interface defines a component that can contain other components. +// Since a Container is a component itself, it can be added to +// other containers as well. The contained components are called +// the child components. +type Container interface { + // Container is a component. + Comp + + // Remove removes a component from this container. + // Return value indicates if the specified component was a child + // and was removed successfully. + // After a successful Remove the specified component's + // Parent() method will return nil. + Remove(c Comp) bool + + // ById finds a component (recursively) by its ID and returns it. + // nil is returned if no child component is found (recursively) + // with the specified ID. + ById(id ID) Comp + + // Clear clears the container, removes all child components. + Clear() +} + +// Comp interface: the base of all UI components. +type Comp interface { + // Id returns the unique id of the component + Id() ID + + // Equals tells if this component is equal to the specified another component. + Equals(c2 Comp) bool + + // Parent returns the component's parent container. + Parent() Container + + // setParent sets the component's parent container. + setParent(parent Container) + + // makeOrphan makes this component orphan: if the component + // has a parent, the component will be removed from the parent. + // Return value indicates if the component was a child + // and was removed successfully. + makeOrphan() bool + + // Attr returns the explicitly set value of the specified HTML attribute. + Attr(name string) string + + // SetAttr sets the value of the specified HTML attribute. + // Pass an empty string value to delete the attribute. + SetAttr(name, value string) + + // IAttr returns the explicitly set value of the specified HTML attribute + // as an int. + // -1 is returned if the value is not set explicitly or is not an int. + IAttr(name string) int + + // SetAttr sets the value of the specified HTML attribute as an int. + SetIAttr(name string, value int) + + // ToolTip returns the tool tip of the component. + ToolTip() string + + // SetToolTip sets the tool tip of the component. + SetToolTip(toolTip string) + + // Style returns the Style builder of the component. + Style() Style + + // DescendantOf tells if this component is a descendant of the specified another component. + DescendantOf(c2 Comp) bool + + // AddEHandler adds a new event handler. + AddEHandler(handler EventHandler, etypes ...EventType) + + // AddEHandlerFunc adds a new event handler generated from a handler function. + AddEHandlerFunc(hf func(e Event), etypes ...EventType) + + // HandlersCount returns the number of added handlers. + HandlersCount(etype EventType) int + + // SyncOnETypes returns the event types on which to synchronize component value + // from browser to the server. + SyncOnETypes() []EventType + + // AddSyncOnETypes adds additional event types on which to synchronize + // component value from browser to the server. + AddSyncOnETypes(etypes ...EventType) + + // PreprocessEvent preprocesses an incoming event before it is dispatched. + // This gives the opportunity for components to update their new value + // before event handlers are called for example. + preprocessEvent(event Event, r *http.Request) + + // DispatchEvent dispatches the event to all registered event handlers. + dispatchEvent(e Event) + + // Render renders the component (as HTML code). + Render(w writer) +} + +// Comp implementation. +type compImpl struct { + id ID // The component id + parent Container // Parent container + + attrs map[string]string // Explicitly set HTML attributes for the component's wrapper tag. + styleImpl *styleImpl // Style builder. + + handlers map[EventType][]EventHandler // Event handlers mapped from even type. Lazily initialized. + valueProviderJs string // If the HTML representation of the component has a value, this JavaScript code code must provide it. It will be automatically sent as the PARAM_COMP_ID parameter. + syncOnETypes map[EventType]bool // Tells on which event types should comp value sync happen. +} + +// newCompImpl creates a new compImpl. +// If the component has a value, the valueProviderJs must be a +// JavaScript code which when evaluated provides the component's +// value. Pass an empty string if the component does not have a value. +func newCompImpl(valueProviderJs string) compImpl { + id := nextCompId() + return compImpl{id: id, attrs: map[string]string{"id": id.String()}, styleImpl: newStyleImpl(), valueProviderJs: valueProviderJs} +} + +func (c *compImpl) Id() ID { + return c.id +} + +func (c *compImpl) Equals(c2 Comp) bool { + return c.id == c2.Id() +} + +func (c *compImpl) Parent() Container { + return c.parent +} + +func (c *compImpl) setParent(parent Container) { + c.parent = parent +} + +func (c *compImpl) makeOrphan() bool { + if c.parent == nil { + return false + } + + return c.parent.Remove(c) +} + +func (c *compImpl) Attr(name string) string { + return c.attrs[name] +} + +func (c *compImpl) SetAttr(name, value string) { + if len(value) > 0 { + c.attrs[name] = value + } else { + delete(c.attrs, name) + } +} + +func (c *compImpl) IAttr(name string) int { + if value, err := strconv.Atoi(c.Attr(name)); err == nil { + return value + } + return -1 +} + +func (c *compImpl) SetIAttr(name string, value int) { + c.SetAttr(name, strconv.Itoa(value)) +} + +func (c *compImpl) ToolTip() string { + return html.UnescapeString(c.Attr("title")) +} + +func (c *compImpl) SetToolTip(toolTip string) { + c.SetAttr("title", html.EscapeString(toolTip)) +} + +func (c *compImpl) Style() Style { + return c.styleImpl +} + +func (c *compImpl) DescendantOf(c2 Comp) bool { + for parent := c.parent; parent != nil; parent = parent.Parent() { + // Always compare components by id, because Comp.Parent() + // only returns Parent and not the components real type (e.g. windowImpl)! + if parent.Equals(c2) { + return true + } + } + + return false +} + +// renderAttrs renders the explicitly set attributes and styles. +func (c *compImpl) renderAttrsAndStyle(w writer) { + for name, value := range c.attrs { + w.WriteAttr(name, value) + } + + c.styleImpl.render(w) +} + +func (c *compImpl) AddEHandler(handler EventHandler, etypes ...EventType) { + if c.handlers == nil { + c.handlers = make(map[EventType][]EventHandler) + } + for _, etype := range etypes { + c.handlers[etype] = append(c.handlers[etype], handler) + } +} + +func (c *compImpl) AddEHandlerFunc(hf func(e Event), etypes ...EventType) { + c.AddEHandler(handlerFuncWrapper{hf}, etypes...) +} + +func (c *compImpl) HandlersCount(etype EventType) int { + if c.handlers == nil { + return 0 + } + return len(c.handlers[etype]) +} + +func (c *compImpl) SyncOnETypes() []EventType { + if c.syncOnETypes == nil { + return nil + } + + etypes := make([]EventType, len(c.syncOnETypes)) + i := 0 + for etype := range c.syncOnETypes { + etypes[i] = etype + i++ + } + return etypes +} + +func (c *compImpl) AddSyncOnETypes(etypes ...EventType) { + if c.syncOnETypes == nil { + c.syncOnETypes = make(map[EventType]bool, len(etypes)) + } + for _, etype := range etypes { + if !c.syncOnETypes[etype] { // If not yet synced... + c.syncOnETypes[etype] = true + c.AddEHandler(EMPTY_EHANDLER, etype) + } + } +} + +var ( + _STR_SE_PREFIX = []byte("=\"se(event,") // "=\"se(event," + _STR_SE_SUFFIX = []byte(")\"") // ")\"" +) + +// rendrenderEventHandlers renders the event handlers as attributes. +func (c *compImpl) renderEHandlers(w writer) { + for etype, _ := range c.handlers { + // To render : se(event,etype,compId,value) + // Example (checkbox): se(event,0,14,this.checked) + w.Write(_STR_SPACE) + w.Write(etypeAttrs[etype]) + w.Write(_STR_SE_PREFIX) + w.Writev(int(etype)) + w.Write(_STR_COMMA) + w.Writev(int(c.id)) + if len(c.valueProviderJs) > 0 && c.syncOnETypes != nil && c.syncOnETypes[etype] { + w.Write(_STR_COMMA) + w.Writes(c.valueProviderJs) + } + w.Write(_STR_SE_SUFFIX) + } +} + +// THIS IS AN EMPTY IMPLEMENTATION AS NOT ALL COMPONENTS NEED THIS. +// THOSE WHO DO SHOULD DEFINE THEIR OWN. +func (b *compImpl) preprocessEvent(event Event, r *http.Request) { +} + +func (c *compImpl) dispatchEvent(e Event) { + if c.handlers == nil { + return + } + for _, handler := range c.handlers[e.Type()] { + handler.HandleEvent(e) + } +} + +// THIS IS AN EMPTY IMPLEMENTATION. +// ALL COMPONENTS SHOULD DEFINE THEIR OWN +func (c *compImpl) Render(w writer) { +} diff --git a/gwu/comp_addons.go b/gwu/comp_addons.go new file mode 100644 index 0000000..57aea83 --- /dev/null +++ b/gwu/comp_addons.go @@ -0,0 +1,419 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Defines optional, additional features components might have. +// These include features only some component has, so it cannot be +// defined in Comp, and not worth making an own component type for these... +// ...not to mention these can be combined arbitrary. + +package gwu + +import ( + "strconv" +) + +// HasText interface defines a modifiable text property. +type HasText interface { + // Text returns the text. + Text() string + + // SetText sets the text. + SetText(text string) +} + +// newHasTextImpl creates a new hasTextImpl +func newHasTextImpl(text string) hasTextImpl { + return hasTextImpl{text} +} + +// HasText implementation. +type hasTextImpl struct { + text string // The text +} + +func (c *hasTextImpl) Text() string { + return c.text +} + +func (c *hasTextImpl) SetText(text string) { + c.text = text +} + +// renderText renders the text. +func (c *hasTextImpl) renderText(w writer) { + w.Writees(c.text) +} + +// HasEnabled interface defines an enabled property. +type HasEnabled interface { + // Enabled returns the enabled property. + Enabled() bool + + // SetEnabled sets the enabled property. + SetEnabled(enabled bool) +} + +// newHasEnabledImpl returns a new hasEnabledImpl. +func newHasEnabledImpl() hasEnabledImpl { + return hasEnabledImpl{true} // Enabled by default +} + +// HasEnabled implementation. +type hasEnabledImpl struct { + enabled bool // The enabled property +} + +func (c *hasEnabledImpl) Enabled() bool { + return c.enabled +} + +func (c *hasEnabledImpl) SetEnabled(enabled bool) { + c.enabled = enabled +} + +var _STR_DISABLED = []byte(" disabled=\"disabled\"") // " disabled=\"disabled\"" + +// renderEnabled renders the enabled attribute. +func (c *hasEnabledImpl) renderEnabled(w writer) { + if !c.enabled { + w.Write(_STR_DISABLED) + } +} + +// HasUrl interface defines a URL string property. +type HasUrl interface { + // URL returns the URL string. + Url() string + + // SetUrl sets the URL string. + SetUrl(url string) +} + +// newHasUrlImpl creates a new hasUrlImpl +func newHasUrlImpl(url string) hasUrlImpl { + return hasUrlImpl{url} +} + +// HasUrl implementation. +type hasUrlImpl struct { + url string // The URL string +} + +func (c *hasUrlImpl) Url() string { + return c.url +} + +func (c *hasUrlImpl) SetUrl(url string) { + c.url = url +} + +// renderUrl renders the URL string. +func (c *hasUrlImpl) renderUrl(attr string, w writer) { + w.WriteAttr(attr, c.url) +} + +// Horizontal alignment type. +type HAlign string + +// Horizontal alignment constants. +const ( + HA_LEFT HAlign = "left" // Horizontal left alignment + HA_CENTER HAlign = "center" // Horizontal center alignment + HA_RIGHT HAlign = "right" // Horizontal right alignment + + HA_DEFAULT HAlign = "" // Browser default (or inherited) horizontal alignment +) + +// Vertical alignment type. +type VAlign string + +// Vertical alignment constants. +const ( + VA_TOP VAlign = "top" // Vertical top alignment + VA_MIDDLE VAlign = "middle" // Vertical center alignment + VA_BOTTOM VAlign = "bottom" // Vertical bottom alignment + + VA_DEFAULT VAlign = "" // Browser default (or inherited) vertical alignment +) + +// HasHVAlign interfaces defines a horizontal and a vertical +// alignment property. +type HasHVAlign interface { + // HAlign returns the horizontal alignment. + HAlign() HAlign + + // SetHAlign sets the horizontal alignment. + SetHAlign(halign HAlign) + + // VAlign returns the vertical alignment. + VAlign() VAlign + + // SetVAlign sets the vertical alignment. + SetVAlign(valign VAlign) + + // SetAlign sets both the horizontal and vertical alignments. + SetAlign(halign HAlign, valign VAlign) +} + +// HasHVAlign implementation. +type hasHVAlignImpl struct { + halign HAlign // Horizontal alignment + valign VAlign // Vertical alignment +} + +// newHasHVAlignImpl creates a new hasHVAlignImpl +func newHasHVAlignImpl(halign HAlign, valign VAlign) hasHVAlignImpl { + return hasHVAlignImpl{halign, valign} +} + +func (c *hasHVAlignImpl) HAlign() HAlign { + return c.halign +} + +func (c *hasHVAlignImpl) SetHAlign(halign HAlign) { + c.halign = halign +} + +func (c *hasHVAlignImpl) VAlign() VAlign { + return c.valign +} + +func (c *hasHVAlignImpl) SetVAlign(valign VAlign) { + c.valign = valign +} + +func (c *hasHVAlignImpl) SetAlign(halign HAlign, valign VAlign) { + c.halign = halign + c.valign = valign +} + +// CellFmt interface defines a cell formatter which can be used to +// format and style the wrapper cells of individual components such as +// child components of a PanelView or a Table. +type CellFmt interface { + // CellFmt allows overriding horizontal and vertical alignment. + HasHVAlign + + // Style returns the Style builder of the wrapper cell. + Style() Style + + // Attr returns the explicitly set value of the specified HTML attribute. + attr(name string) string + + // SetAttr sets the value of the specified HTML attribute. + // Pass an empty string value to delete the attribute. + setAttr(name, value string) + + // iAttr returns the explicitly set value of the specified HTML attribute + // as an int. + // -1 is returned if the value is not set explicitly or is not an int. + iAttr(name string) int + + // setIAttr sets the value of the specified HTML attribute as an int. + setIAttr(name string, value int) +} + +// CellFmt implementation +type cellFmtImpl struct { + hasHVAlignImpl // Has horizontal and vertical alignment implementation + + styleImpl *styleImpl // Style builder. Lazily initialized. + attrs map[string]string // Explicitly set HTML attributes for the cell. Lazily initalized. +} + +// newCellFmtImpl creates a new cellFmtImpl. +// Default horizontal alignment is HA_DEFAULT, +// default vertical alignment is VA_DEFAULT. +func newCellFmtImpl() *cellFmtImpl { + // Initialize hasHVAlignImpl with HA_DEFAULT and VA_DEFAULT + // so if aligns are not changed, they will not be rendered => + // they will be inherited (from TR). + return &cellFmtImpl{hasHVAlignImpl: newHasHVAlignImpl(HA_DEFAULT, VA_DEFAULT)} +} + +func (c *cellFmtImpl) Style() Style { + if c.styleImpl == nil { + c.styleImpl = newStyleImpl() + } + return c.styleImpl +} + +func (c *cellFmtImpl) attr(name string) string { + if c.attrs == nil { + return "" + } + return c.attrs[name] +} + +func (c *cellFmtImpl) setAttr(name, value string) { + if c.attrs == nil { + c.attrs = make(map[string]string, 2) + } + if len(value) > 0 { + c.attrs[name] = value + } else { + delete(c.attrs, name) + } +} + +func (c *cellFmtImpl) iAttr(name string) int { + if value, err := strconv.Atoi(c.attr(name)); err == nil { + return value + } + return -1 +} + +func (c *cellFmtImpl) setIAttr(name string, value int) { + c.setAttr(name, strconv.Itoa(value)) +} + +// render renders the formatted HTML tag for the specified tag name. +// tag must start with a less than sign, e.g. " + // they will be inherited (from TR). + c := tableViewImpl{compImpl: newCompImpl(""), hasHVAlignImpl: newHasHVAlignImpl(HA_DEFAULT, VA_DEFAULT)} + c.SetCellSpacing(0) + c.SetCellPadding(0) + return c +} + +func (c *tableViewImpl) Border() int { + return c.IAttr("border") +} + +func (c *tableViewImpl) SetBorder(width int) { + c.SetIAttr("border", width) +} + +func (c *tableViewImpl) CellSpacing() int { + return c.IAttr("cellspacing") +} + +func (c *tableViewImpl) SetCellSpacing(spacing int) { + c.SetIAttr("cellspacing", spacing) +} + +func (c *tableViewImpl) CellPadding() int { + return c.IAttr("cellpadding") +} + +func (c *tableViewImpl) SetCellPadding(padding int) { + c.SetIAttr("cellpadding", padding) +} + +var _STR_ST_VALIGN = []byte(" style=\"vertical-align:") // " style=\"vertical-align:" + +// renderTr renders an HTML TR tag with horizontal and vertical +// alignment info included. +func (c *tableViewImpl) renderTr(w writer) { + w.Write(_STR_TR_OP) + if c.halign != HA_DEFAULT { + w.Write(_STR_ALIGN) + w.Writes(string(c.halign)) + w.Write(_STR_QUOTE) + } + if c.valign != VA_DEFAULT { + w.Write(_STR_ST_VALIGN) + w.Writes(string(c.valign)) + w.Write(_STR_QUOTE) + } + w.Write(_STR_GT) +} diff --git a/gwu/css.go b/gwu/css.go new file mode 100644 index 0000000..bda12e7 --- /dev/null +++ b/gwu/css.go @@ -0,0 +1,100 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Built-in static CSS themes of GWU. + +package gwu + +// Built-in CSS themes. +const ( + THEME_DEFAULT = "default" // Default CSS theme + THEME_DEBUG = "debug" // Debug CSS theme, useful for developing/debugging purposes. +) + +// resNameStaticCss returns the CSS resource name +// for the specified CSS theme. +func resNameStaticCss(theme string) string { + // E.g. "gowut-default-0.8.0.css" + return "gowut-" + theme + "-" + GOWUT_VERSION + ".css" +} + +var staticCss map[string][]byte = make(map[string][]byte) + +func init() { + staticCss[resNameStaticCss(THEME_DEFAULT)] = []byte("" + + ` +.gwuimg-collapsed {background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATUlEQVQ4y83RsQkAMAhEURNc+iZw7KQNgnjGRlv5D0SRMQPgADjVbr3AuzCz1QJYKAUyiAYiqAx4aHe/p9XAn6C/IQ1kb9TfMATYcM5cL5cg3qDaS5UAAAAASUVORK5CYII=)} +.gwuimg-expanded {background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATElEQVQ4y2NgGGjACGNUVlb+J0Vje3s7IwMDAwMT1VxAiitgtlPfBcS4Atl22rgAnyvQbaedC7C5ApvtVHEBXlBZWfmfUKwwMQx5AADNQhjmAryM3wAAAABJRU5ErkJggg==)} + +.gwuimg-collapsed, .gwuimg-expanded {background-position:0px 0px; background-repeat:no-repeat} + +body {font-family:Arial} + +.gwu-Window {} + +.gwu-Panel {} + +.gwu-Table {} + +.gwu-Label {} + +.gwu-Link {} + +.gwu-Image {} + +.gwu-Button {} + +.gwu-CheckBox {} +.gwu-CheckBox-Disabled {color:#888} + +.gwu-RadioButton {} +.gwu-RadioButton-Disabled {color:#888} + +.gwu-ListBox {} + +.gwu-TextBox {} + +.gwu-PasswBox {} + +.gwu-Html {} + +.gwu-SwitchButton {} +.gwu-SwitchButton-On-Active {background:#00a000; color:#d0ffd0} +.gwu-SwitchButton-Off-Active {background:#d03030; color:#ffd0d0} +.gwu-SwitchButton-On-Inactive, .gwu-SwitchButton-Off-Inactive {background:#606060; color:#909090} +.gwu-SwitchButton-On-Inactive:enabled, .gwu-SwitchButton-Off-Inactive:enabled {cursor:pointer} +.gwu-SwitchButton-On-Active, .gwu-SwitchButton-Off-Active, .gwu-SwitchButton-On-Inactive, .gwu-SwitchButton-Off-Inactive {margin:0px;border: 0px; width:100%} +.gwu-SwitchButton-On-Active:disabled, .gwu-SwitchButton-Off-Active:disabled, .gwu-SwitchButton-On-Inactive:disabled, .gwu-SwitchButton-Off-Inactive:disabled {color:black} + +.gwu-Expander {} +.gwu-Expander-Header, .gwu-Expander-Header-Expanded {padding-left:19px; cursor:pointer} +.gwu-Expander-Content {padding-left:19px} + +.gwu-TabBar {} +.gwu-TabBar-Top {padding:0px 5px 0px 5px; border-bottom:5px solid #8080f8} +.gwu-TabBar-Bottom {padding:0px 5px 0px 5px; border-top:5px solid #8080f8} +.gwu-TabBar-Left {padding:5px 0px 5px 0px; border-right:5px solid #8080f8} +.gwu-TabBar-Right {padding:5px 0px 5px 0px; border-left:5px solid #8080f8} +.gwu-TabBar-NotSelected {padding-left:5px; padding-right:5px; border:1px solid white ; background:#c0c0ff; cursor:default} +.gwu-TabBar-Selected {padding-left:5px; padding-right:5px; border:1px solid #8080f8; background:#8080f8; cursor:default} +.gwu-TabPanel {} +.gwu-TabPanel-Content {border:1px solid #8080f8; width:100%; height:100%} +`) + + staticCss[resNameStaticCss(THEME_DEBUG)] = []byte(string(staticCss[resNameStaticCss(THEME_DEFAULT)]) + + ` +.gwu-Window td, .gwu-Table td, .gwu-Panel td, .gwu-TabPanel td {border:1px solid black} +`) +} diff --git a/gwu/doc.go b/gwu/doc.go new file mode 100644 index 0000000..638b9be --- /dev/null +++ b/gwu/doc.go @@ -0,0 +1,394 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* +Package gwu implements an easy to use, platform independent Web UI Toolkit +in pure Go. + + +For additional documentation, News and more please visit the home page: +https://sites.google.com/site/gowebuitoolkit/ + + +Introduction + +Gowut (Go Web UI Toolkit) is a full-featured, easy to use, platform independent +Web UI Toolkit written in pure Go, no platform dependent native code is linked +or called. + +The usage of the Gowut is similar to Google's GWT and the Java Swing toolkit. +If you are familiar with those, you will get started very easily. The main +difference compared to GWT is that this solution does not compile into JavaScript +but remains and runs as Go code (on the server side). Remaining on the server +side means you don't have to hassle with asynchronous event handlers like in GWT, +you can write real synchronous event handlers (like in Java Swing). + +You can use this toolkit and build user interfaces with writing +Go code only: you can assemble the client interface in Go, and write +event handlers in Go. +You may optionally spice it and make it more customized with some HTML +and CSS (also added from Go code), but that is not required. + +The UI can be simply assembled hierarchically from containers +and components. Components can generate events which are dispatched +to event handlers - also written in pure Go. +If there is no component for an HTML tag you wish to use, you can +use the Html component to wrap your custom HTML code. Components also allow +you to specify custom HTML attributes that will be added for their +(wrapper) HTML tags. + +Creating user interfaces using Gowut does not require you to think like that +the clients will view it and interact with it through a browser. +The "browser" layer is hidden by Gowut. +While styling the components is done through CSS (either by calling +the style builder's methods or passing direct CSS codes), think of it +like a way similar to formatting HTML tags with CSS. + +The state of the components are stored on server side, in the memory. +This means that if a browser is closed and reopened, or you navigate +away and back, the same state will be rendered again. AJAX technology +is used to automatically synchronize component's state from browser +to server, and to dispatch events. +AJAX technology is used also to refresh some parts (components) that +change (during event handling) without having to reload the whole page +to see the changes. + + +Features of Gowut + +-A component library to assemble your user interfaces with + +-A GUI server which serves browser clients + +-Session management + +-Automatic event handling and dispatching + +-(CSS) Style builder to easily manipulate the style of components + + +Server and Events and Sessions + +The package contains a GUI server which is responsible to serve GUI +clients which are standard browsers. The user interface can be viewed +from any browsers (including smart phones) which makes this a cross +platform solution. +Starting the GUI server with a non-local address gives you +the possibility to view the GUI from a remote computer. +The server can be configured to run in normal mode (HTTP) or in secure +mode (HTTPS). + +The GUI server also has Session management. By default windows added to the +server are public windows, and shared between all users (clients). This +means if a user changes the content (e.g. enters a text into a text box), +that text will be visible to all other users. This is suitable for most +desktop applications. + +Sessions can be created during event handling (by calling the +Event.NewSession() method), and windows added to the session will only be +visible to the client associated with the session. If other users request +the same window, a new instance of the window is to be created and added +to their sessions. + +Event handling is possible via event handlers. An event handler is +an implementation of the EventHandler interface. Event handlers have to be +attached to the components which will be the source of the event. Event +handlers are registered to event types or kinds (EventType) such as click +event (ETYPE_CLICK), value change event (ETYPE_CHANGE), key up event +(ETYPE_KEY_UP) etc. + +The HandleEvent method of an event handler gets an Event value which has +multiple purposes and functions. 1) The event contains the parameters +of the event (such as the event type, the event source component, mouse +position in case of a mouse event etc.). 2) The Event is an accessor to the +Session associated with the client the event is originating from. Through +the event an event handler may access the current Session, create a new +Session or may remove it (invalidate it). 3) The event is also used +to define actions to be executed (automatically by Gowut) after the event +handling. For example if the event handler changes a component, the handler +has to mark it dirty causing it to be re-rendered in the client browser, +or an event handler can change the focused component, or reload another window. + +Creating a session from an event handler during event dispatching requires +a public window and an event source component (e.g. a Button). +There is another handy way to create sessions. Sessions can also be created +automatically by requesting pre-registered paths, paths of not-yet existing +windows. When such a window is requested and no private session associated +with the client exists, a new session will be created. A registered +SessionHandler can be used then to create the window prior to it being served. +Here's an example how to do it: + // A SessionHandler implementation: + type MySessHandler struct {} + func (h SessHandler) Created(sess gwu.Session) { + win := gwu.NewWindow("login", "Login Window") + // ...add content to the login window... + s.AddWindow(win) + } + func (h SessHandler) Removed(s gwu.Session) {} + + // And to auto-create sessions for the login window: + server := gwu.NewServer("guitest","") + server.AddSessCreatorName("login", "Login Window") + server.AddSHandler(MySessHandler{}) + +Despite the use of sessions if you access the application remotely (e.g. not +from localhost), security is only guaranteed if you configure the server to run +in secure (HTTPS) mode. + + +Under the hood + +User interfaces are generated HTML documents which communicate with the server +with AJAX calls. The GUI server is based on the web server integrated in Go. + +When a Window is requested by its URL, the Window will render a complete HTML +document. The Window will recursively include its child components. +Components render themselves into HTML codes. +When a component generates an event, the page in the browser will make an +AJAX call sending the event to the server. The event will be passed to all the +appropriate event handlers. Event handlers can mark components dirty, +specifying that they may have changed and they must be re-rendered. +When all the even handlers are done, the ids of the dirty components are sent +back, and the browser will request only to render the dirty components, +with AJAX calls, and the results will replace the old component nodes in the +HTML DOM. + +Since the clients are HTTP browsers, the GWU sessions are implemented and +function as HTTP sessions. Cookies are used to maintain the browser sessions. + + +Styling + +Styling the components is done through CSS. You can do this from Go code by +calling the style builder's methods, or you can create external CSS files. + +The Comp interface contains a Style() method which returns the style builder +of the component. The builder can be used to set/manipulate the style class names +of the component (e.g. SetClass(), AddClass(), RemoveClass() methods). +The builder also has get and set methods for the common CSS attributes, and the +GWU package contains many CSS constants for CSS attribute values. Many styling +can be achieved using the builder's built-in methods and constants resulting in +the Go code containing no direct CSS at all. You can use the general Get() and +Set() methods of the style builder to manipulate any style attributes which it +does not have predefined methods for. + +Each Gowut component has its own CSS class derived from its name using the "gwu-" +prefix, for example the Button component has the default CSS class "gwu-Button". +Many components use multiple CSS classes for their internal structure. These +classes are listed in the documentation of the components. +Gowut has multiple built-in CSS themes. A CSS theme is basically the collection +of the style definitions of the style classes used by the components. You can +set the default theme with the Server.SetTheme() method. This will be used for +all windows. You can set themes individually for windows too, using the +Window.SetTheme() method. + +You can create your own external CSS files where you can extend/override the +definitions of the built-in style classes. For example you can define the +"gwu-Button" style class to have red background, and the result will be that all +Buttons will have red background without having to change their style individually. + + +Component palette + +Containers to group and lay out components: + Expander - shows and hides a content comp when clicking on the header comp + (Link) - allows only one optional child + Panel - it has configurable layout + Table - it is dynamic and flexible + TabPanel - for tabbed displaying components (only 1 is visible at a time) + Window - top of component hierarchy, it is an extension of the Panel + +Input components to get data from users: + CheckBox + ListBox (it's either a drop-down list or a multi-line/multi-select list box) + TextBox (it's either a one-line text box or a multi-line text area) + PasswBox + RadioButton + SwitchButton + +Other components: + Button + Html + Image + Label + Link + + +Full application example + +Let a full example follow here which is a complete application. +It builds a simple window, adds components to it, registers event handlers which +modifies the content and starts the GUI server. +Component modifications (including both individual components and component +structure) will be seen without page reload. +All written in Go. + +Source of this application is available here: +http://code.google.com/p/gowut/source/browse/examples/simple_demo.go + + type MyButtonHandler struct { + counter int + text string + } + + func (h *MyButtonHandler) HandleEvent(e gwu.Event) { + if b, isButton := e.Src().(gwu.Button); isButton { + b.SetText(b.Text() + h.text) + h.counter++ + b.SetToolTip("You've clicked " + strconv.Itoa(h.counter) + " times!") + e.MarkDirty(b) + } + } + + func main() { + // Create and build a window + win := gwu.NewWindow("main", "Test GUI Window") + win.Style().SetFullWidth() + win.SetHAlign(gwu.HA_CENTER) + win.SetCellPadding(2) + + // Button which changes window content + win.Add(gwu.NewLabel("I'm a label! Try clicking on the button=>")) + btn := gwu.NewButton("Click me") + btn.AddEHandler(&MyButtonHandler{text: ":-)"}, gwu.ETYPE_CLICK) + win.Add(btn) + btnsPanel := gwu.NewNaturalPanel() + btn.AddEHandlerFunc(func(e gwu.Event) { + // Create and add a new button... + newbtn := gwu.NewButton("Extra #" + strconv.Itoa(btnsPanel.CompsCount())) + newbtn.AddEHandlerFunc(func(e gwu.Event) { + btnsPanel.Remove(newbtn) // ...which removes itself when clicked + e.MarkDirty(btnsPanel) + }, gwu.ETYPE_CLICK) + btnsPanel.Insert(newbtn, 0) + e.MarkDirty(btnsPanel) + }, gwu.ETYPE_CLICK) + win.Add(btnsPanel) + + // ListBox examples + p := gwu.NewHorizontalPanel() + p.Style().SetBorder2(1, gwu.BRD_STYLE_SOLID, gwu.CLR_BLACK) + p.SetCellPadding(2) + p.Add(gwu.NewLabel("A drop-down list being")) + widelb := gwu.NewListBox([]string{"50", "100", "150", "200", "250"}) + widelb.Style().SetWidth("50") + widelb.AddEHandlerFunc(func(e gwu.Event) { + widelb.Style().SetWidth(widelb.SelectedValue() + "px") + e.MarkDirty(widelb) + }, gwu.ETYPE_CHANGE) + p.Add(widelb) + p.Add(gwu.NewLabel("pixel wide. And a multi-select list:")) + listBox := gwu.NewListBox([]string{"First", "Second", "Third", "Forth", "Fifth", "Sixth"}) + listBox.SetMulti(true) + listBox.SetRows(4) + p.Add(listBox) + countLabel := gwu.NewLabel("Selected count: 0") + listBox.AddEHandlerFunc(func(e gwu.Event) { + countLabel.SetText("Selected count: " + strconv.Itoa(len(listBox.SelectedIndices()))) + e.MarkDirty(countLabel) + }, gwu.ETYPE_CHANGE) + p.Add(countLabel) + win.Add(p) + + // Self-color changer check box + greencb := gwu.NewCheckBox("I'm a check box. When checked, I'm green!") + greencb.AddEHandlerFunc(func(e gwu.Event) { + if greencb.State() { + greencb.Style().SetBackground(gwu.CLR_GREEN) + } else { + greencb.Style().SetBackground("") + } + e.MarkDirty(greencb) + }, gwu.ETYPE_CLICK) + win.Add(greencb) + + // TextBox with echo + p = gwu.NewHorizontalPanel() + p.Add(gwu.NewLabel("Enter your name:")) + tb := gwu.NewTextBox("") + tb.AddSyncOnETypes(gwu.ETYPE_KEY_UP) + p.Add(tb) + p.Add(gwu.NewLabel("You entered:")) + nameLabel := gwu.NewLabel("") + nameLabel.Style().SetColor(gwu.CLR_RED) + tb.AddEHandlerFunc(func(e gwu.Event) { + nameLabel.SetText(tb.Text()) + e.MarkDirty(nameLabel) + }, gwu.ETYPE_CHANGE, gwu.ETYPE_KEY_UP) + p.Add(nameLabel) + win.Add(p) + + // Create and start a GUI server (omitting error check) + server := gwu.NewServer("guitest", "localhost:8081") + server.SetText("Test GUI App") + server.AddWin(win) + server.Start("") // Also opens windows list in browser + } + +Now start the application and open the http://localhost:8081/guitest/main URL in your +browser to see the window. You can also try visiting http://localhost:8081/guitest/ +which will render the available window list. +Test the components. Now close the browser and reopen the page. Gowut remembers +everything. + + +Limitations + +1) Attaching onmouseover and onmouseout event handlers to a component and +changing (re-rendering) the same component causes some trouble (the browsers +generate multiple mouseover and mouseout events because the same HTML node is replaced +under the mouse cursor). + +2) Attaching onmousedown and onmouseup event handlers to a check box and re-rendering it +prevents ETYPE_CHANGE handlers being called when clicking on it. + + +Closing + +From the MVC point of view looking at a Go application using Gowut, the Go +components are the Model, the generated (and manipulated) HTML document in the +browser is the View and the Controller is integrated in both. + +Gowut is ideal to create (cross platform) user interfaces for desktop +applications written in Go. It is also easy and handy to write the admin +and also client interfaces of your Go web application using Gowut. + +Happy UI coding in Go :-) + + +Links + +Author: AndrĂ¡s Belicza + +Author email: gmail.com, user name: iczaaa + +Home page: https://sites.google.com/site/gowebuitoolkit/ + +Source code: http://code.google.com/p/gowut/ + +Discussion forum: https://groups.google.com/d/forum/gowebuitoolkit + +Live demo: Coming soon... + + +*/ +package gwu + +// Gowut version information. +const ( + GOWUT_VERSION = "0.7.0" // Gowut version (major.minor.maintenance) + GOWUT_RELEASE_DATE = "2013-02-12 CET" // Gowut release date + GOWUT_REL_DATE_LAYOUT = "2006-01-02 MST" // Gowut release date layout (for time.Parse()) +) diff --git a/gwu/event.go b/gwu/event.go new file mode 100644 index 0000000..5e1b386 --- /dev/null +++ b/gwu/event.go @@ -0,0 +1,375 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Defines the Event type and event handling. + +package gwu + +import ( + "strconv" +) + +// Event type (kind) type. +type EventType int + +// Converts an Event type to a string. +func (etype EventType) String() string { + return strconv.Itoa(int(etype)) +} + +// Event types. +const ( + ETYPE_CLICK EventType = iota // Mouse click event + ETYPE_DBL_CLICK // Mouse double click event + ETYPE_MOUSE_DOWN // Mouse down event + ETYPE_MOUSE_MOVE // Mouse move event + ETYPE_MOUSE_OVER // Mouse over event + ETYPE_MOUSE_OUT // Mouse out event + ETYPE_MOUSE_UP // Mouse up event + ETYPE_KEY_DOWN // Key down event + ETYPE_KEY_PRESS // Key press event + ETYPE_KEY_UP // Key up event + ETYPE_BLUR // Blur event (component loses focus) + ETYPE_CHANGE // Change event + ETYPE_FOCUS // Focus event (component gains focus) +) + +// Attribute names for the event types. +var etypeAttrs map[EventType][]byte = map[EventType][]byte{ + ETYPE_CLICK: []byte("onclick"), + ETYPE_DBL_CLICK: []byte("ondblclick"), + ETYPE_MOUSE_DOWN: []byte("onmousedown"), + ETYPE_MOUSE_MOVE: []byte("onmousemove"), + ETYPE_MOUSE_OVER: []byte("onmouseover"), + ETYPE_MOUSE_OUT: []byte("onmouseout"), + ETYPE_MOUSE_UP: []byte("onmouseup"), + ETYPE_KEY_DOWN: []byte("onkeydown"), + ETYPE_KEY_PRESS: []byte("onkeypress"), + ETYPE_KEY_UP: []byte("onkeyup"), + ETYPE_BLUR: []byte("onblur"), + ETYPE_CHANGE: []byte("onchange"), + ETYPE_FOCUS: []byte("onfocus")} + +// Mouse button type. +type MouseBtn int + +// Mouse buttons +const ( + MOUSE_BTN_UNKNOWN MouseBtn = -1 // Unknown mouse button (info not available) + MOUSE_BTN_LEFT = 0 // Left mouse button + MOUSE_BTN_MIDDLE = 1 // Middle mouse button + MOUSE_BTN_RIGHT = 2 // Right mouse button +) + +// Modifier key type. +type ModKey int + +// Modifier key masks. +const ( + MOD_KEY_ALT ModKey = 1 << iota // Alt key + MOD_KEY_CTRL // Control key + MOD_KEY_META // Meta key + MOD_KEY_SHIFT // Shift key +) + +// Key (keyboard key) type. +type Key int + +// Some key codes. +const ( + KEY_BACKSPACE Key = 8 + KEY_ENTER Key = 13 + KEY_SHIFT Key = 16 + KEY_CTRL Key = 17 + KEY_ALT Key = 18 + KEY_CAPS_LOCK Key = 20 + KEY_ESCAPE Key = 27 + KEY_SPACE Key = 32 + KEY_PG_UP Key = 33 + KEY_PG_DOWN Key = 34 + KEY_END Key = 35 + KEY_HOME Key = 36 + KEY_LEFT Key = 37 + KEY_UP Key = 38 + KEY_RIGHT Key = 39 + KEY_DOWN Key = 40 + KEY_PRINT_SCRN Key = 44 + KEY_INSERT Key = 45 + KEY_DEL Key = 46 + + KEY_0 Key = 48 + KEY_9 Key = 57 + + KEY_A Key = 65 + KEY_Z Key = 90 + + KEY_WIN Key = 91 + + KEY_NUMPAD_0 Key = 96 + KEY_NUMPAD_9 Key = 105 + KEY_NUMPAD_MUL Key = 106 + KEY_NUMPAD_PLUS Key = 107 + KEY_NUMPAD_MINUS Key = 109 + KEY_NUMPAD_DOT Key = 110 + KEY_NUMPAD_DIV Key = 111 + + KEY_F1 Key = 112 + KEY_F2 Key = 113 + KEY_F3 Key = 114 + KEY_F4 Key = 115 + KEY_F5 Key = 116 + KEY_F6 Key = 117 + KEY_F7 Key = 118 + KEY_F8 Key = 119 + KEY_F9 Key = 120 + KEY_F10 Key = 121 + KEY_F11 Key = 122 + KEY_F12 Key = 123 + + KEY_NUM_LOCK Key = 144 + KEY_SCROLL_LOCK Key = 145 +) + +// Empty event handler which does nothing. +const EMPTY_EHANDLER emptyEventHandler = emptyEventHandler(0) + +// EventHandler interface defines a handler capable of handling events. +type EventHandler interface { + // Handles the event. + // + // If components are modified in a way that their view changes, + // these components must be marked dirty in the event object + // (so the client will see up-to-date state). + // + // If the component tree is modified (new component added + // or removed for example), then the Panel whose structure was modified + // has to be marked dirty. + HandleEvent(e Event) +} + +// Event interface defines the event originating from components. +type Event interface { + // Type returns the type of the event. + Type() EventType + + // Src returns the source of the event, + // the component the event is originating from + Src() Comp + + // Mouse returns the mouse x and y coordinates relative to the component. + // If no mouse coordinate info is available, (-1, -1) is returned. + Mouse() (x, y int) + + // MouseWin returns the mouse x and y coordinates inside the window. + // If no mouse coordinate info is available, (-1, -1) is returned. + MouseWin() (x, y int) + + // MouseBtn returns the mouse button. + // If no mouse button info is available, MOUSE_BTN_UNKNOWN is returned. + MouseBtn() MouseBtn + + // ModKeys returns the states of the modifier keys. + // The returned value contains the states of all modifier keys, + // constants of type ModKey can be used to test a specific modifier key, + // or use the ModKey method. + ModKeys() int + + // ModKey returns the state of the specified modifier key. + ModKey(modKey ModKey) bool + + // Key code returns the key code. + KeyCode() Key + + // Requests the specified window to be reloaded + // after processing the current event. + // Tip: pass an empty string to reload the current window. + ReloadWin(name string) + + // MarkDirty marks components dirty, + // causing them to be re-rendered after processing the current event. + // Component re-rendering happens without page reload in the browser. + // + // Note: the Window itself (which is a Comp) can also be marked dirty + // causing the whole window content to be re-rendered without page reload! + // + // Marking a component dirty also marks all of its decendants dirty, recursively. + // + // Also note that components will not be re-rendered multiple times. + // For example if a child component and its parent component are both + // marked dirty, the child component will only be re-rendered once. + MarkDirty(comps ...Comp) + + // SetFocusedComp sets the component to be focused after processing + // the current event. + SetFocusedComp(comp Comp) + + // Session returns the current session. + // The Private() method of the session can be used to tell if the session + // is a private session or the public shared session. + Session() Session + + // NewSession creates a new (private) session. + // If the current session (as returned by Session()) is private, + // it will be removed first. + NewSession() Session + + // RemoveSess removes (invalidates) the current session. + // Only private sessions can be removed, calling this + // when the current session (as returned by Session()) is public is a no-op. + // After this method Session() will return the shared public session. + RemoveSess() +} + +// Event implementation. +type eventImpl struct { + server *serverImpl // Server implementation + + etype EventType // Event type + src Comp // Source of the event, the component the event is originating from + + x, y int // Mouse coordinates (relative to component) + wx, wy int // Mouse coordinates (inside the window) + mbtn MouseBtn // Mouse button + modKeys int // State of the modifier keys + keyCode Key // Key code + reload bool // Tells if the window has to be reloaded + reloadWin string // The name of the window to be reloaded + dirtyComps map[ID]Comp // The dirty components + focusedComp Comp // Component to be focused after the event processing + session Session // Session +} + +// newEventImpl creates a new eventImpl +func newEventImpl(server *serverImpl, etype EventType, src Comp, session Session) *eventImpl { + e := eventImpl{server: server, etype: etype, src: src, dirtyComps: make(map[ID]Comp, 2), session: session} + return &e +} + +func (e *eventImpl) Type() EventType { + return e.etype +} + +func (e *eventImpl) Src() Comp { + return e.src +} + +func (e *eventImpl) Mouse() (x, y int) { + return e.x, e.y +} + +func (e *eventImpl) MouseWin() (x, y int) { + return e.wx, e.wy +} + +func (e *eventImpl) MouseBtn() MouseBtn { + return e.mbtn +} + +func (e *eventImpl) ModKeys() int { + return e.modKeys +} + +func (e *eventImpl) ModKey(modKey ModKey) bool { + return e.modKeys&int(modKey) != 0 +} + +func (e *eventImpl) KeyCode() Key { + return e.keyCode +} + +func (e *eventImpl) ReloadWin(name string) { + e.reload = true + e.reloadWin = name +} + +func (e *eventImpl) MarkDirty(comps ...Comp) { + // We can optimize "on the run" (during dispatching) because we rely on the fact + // that if the component tree is modified later by a handler, the Panel + // whose structure was modified will also be marked dirty. + // + // So for example if a Panel (P) is already dirty, marking dirty one of its child (A) can be omitted + // even if later the panel (P) is removed completely, and its child (A) is added to another Panel (P2). + // In this case P2 will be (must be) marked dirty, and the child (A) will be re-rendered properly + // along with P2. + + for _, comp := range comps { + if !e.isDirty(comp) { // If not yet dirty + // Before adding it, remove all components that are + // descendants of comp, they will inherit the dirty mark from comp. + for id, c := range e.dirtyComps { + if c.DescendantOf(comp) { + delete(e.dirtyComps, id) + } + } + + e.dirtyComps[comp.Id()] = comp + } + } +} + +// IsDirty returns true if the specified component is already marked dirty. +// Note that a component being dirty makes all of its descendants dirty, recursively. +// +// Also note that the "dirty" flag might change during the event dispatching +// because if a "clean" component is moved from a dirty parent to a clean parent, +// its inherited dirty flag changes from true to false. +func (e *eventImpl) isDirty(c2 Comp) bool { + // First-class being dirty: + if _, found := e.dirtyComps[c2.Id()]; found { + return true + } + + // Second-class being dirty: + for _, c := range e.dirtyComps { + if c2.DescendantOf(c) { + return true + } + } + + return false +} + +func (e *eventImpl) SetFocusedComp(comp Comp) { + e.focusedComp = comp +} + +func (e *eventImpl) Session() Session { + return e.session +} + +func (e *eventImpl) NewSession() Session { + return e.server.newSession(e) +} + +func (e *eventImpl) RemoveSess() { + e.server.removeSess(e) +} + +// Handler function wrapper +type handlerFuncWrapper struct { + hf func(e Event) // The handler function to be called as part of implementing the EventHandler interface +} + +// HandleEvent forwards the call to the handler function. +func (hfw handlerFuncWrapper) HandleEvent(e Event) { + hfw.hf(e) +} + +// Empty Event Handler type. +type emptyEventHandler int + +// HandleEvent does nothing as to this is an empty event handler. +func (ee emptyEventHandler) HandleEvent(e Event) { +} diff --git a/gwu/examples_test.go b/gwu/examples_test.go new file mode 100644 index 0000000..1f0240c --- /dev/null +++ b/gwu/examples_test.go @@ -0,0 +1,51 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package gwu_test + +import ( + "code/google/com/p/gowut/gwu" +) + +// Example code determining which button was clicked. +func ExampleButton() { + b := gwu.NewButton("Click me") + b.AddEHandlerFunc(func(e gwu.Event) { + if e.MouseBtn() == gwu.MOUSE_BTN_MIDDLE { + // Middle click + } + }, gwu.ETYPE_CLICK) +} + +// Example code determining what kind of key is involved. +func ExampleTextBox() { + b := gwu.NewTextBox("") + tb.AddSyncOnETypes(gwu.ETYPE_KEY_UP) // This is here so we will see up-to-date value in the event handler + b.AddEHandlerFunc(func(e gwu.Event) { + if e.ModKey(gwu.MOD_KEY_SHIFT) { + // SHIFT is pressed + } + + c := e.KeyCode() + switch { + case c == gwu.KEY_ENTER: // Enter + case c >= gwu.KEY_0 && c <= gwu.KEY_9: + fallthrough + case c >= gwu.KEY_NUMPAD_0 && c <= gwuKEY_NUMPAD_9: // Number + case c >= gwu.KEY_A && c <= gwu.KEY_Z: // Letter + case c >= gwu.KEY_F1 && c <= gwu.KEY_F12: // Function key + } + }, gwu.ETYPE_KEY_UP) +} diff --git a/gwu/html.go b/gwu/html.go new file mode 100644 index 0000000..656af33 --- /dev/null +++ b/gwu/html.go @@ -0,0 +1,65 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Defines the Html component. + +package gwu + +// Html interface defines a component which wraps an HTML text into a component. +// +// Default style class: "gwu-Html" +type Html interface { + // Html is a component. + Comp + + // Html returns the HTML text. + Html() string + + // SetHtml sets the HTML text. + SetHtml(html string) +} + +// Html implementation +type htmlImpl struct { + compImpl // Component implementation + + html string // HTML text +} + +// NewHtml creates a new Html. +func NewHtml(html string) Html { + c := &htmlImpl{newCompImpl(""), html} + c.Style().AddClass("gwu-Html") + return c +} + +func (c *htmlImpl) Html() string { + return c.html +} + +func (c *htmlImpl) SetHtml(html string) { + c.html = html +} + +func (c *htmlImpl) Render(w writer) { + w.Write(_STR_SPAN_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + w.Writes(c.html) + + w.Write(_STR_SPAN_CL) +} diff --git a/gwu/id.go b/gwu/id.go new file mode 100644 index 0000000..6a098cd --- /dev/null +++ b/gwu/id.go @@ -0,0 +1,59 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ID type definition, and unique ID generation. + +package gwu + +import ( + "strconv" +) + +// The type of the ids of the components. +type ID int + +// Converts an ID to a string. +func (id ID) String() string { + return strconv.Itoa(int(id)) +} + +// Converts a string to ID +func AtoID(s string) (ID, error) { + id, err := strconv.Atoi(s) + + if err != nil { + return ID(-1), err + } + return ID(id), nil +} + +// Component id generation and provider + +// A channel used to generate unique ids +var idChan chan ID = make(chan ID) + +// init stats a new go routine to generate unique ids +func init() { + go func() { + for i := 0; ; i++ { + idChan <- ID(i) + } + }() +} + +// nextCompId returns a unique component id +func nextCompId() ID { + return <-idChan +} diff --git a/gwu/image.go b/gwu/image.go new file mode 100644 index 0000000..d596d7c --- /dev/null +++ b/gwu/image.go @@ -0,0 +1,57 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Image component interface and implementation. + +package gwu + +// Image interface defines an image. +// +// Default style class: "gwu-Image" +type Image interface { + // Image is a component. + Comp + + // Image has text which is its description (alternate text). + HasText + + // Image has URL string. + HasUrl +} + +// Image implementation +type imageImpl struct { + compImpl // Component implementation + hasTextImpl // Has text implementation + hasUrlImpl // Has text implementation +} + +// NewImage creates a new Image. +// The text is used as the alternate text for the image. +func NewImage(text, url string) Image { + c := &imageImpl{newCompImpl(""), newHasTextImpl(text), newHasUrlImpl(url)} + c.Style().AddClass("gwu-Image") + return c +} + +func (c *imageImpl) Render(w writer) { + w.Writes("") +} diff --git a/gwu/js.go b/gwu/js.go new file mode 100644 index 0000000..2551cbf --- /dev/null +++ b/gwu/js.go @@ -0,0 +1,214 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Built-in static JavaScript codes of GWU. + +package gwu + +import ( + "strconv" +) + +// Static JavaScript resource name +const _RES_NAME_STATIC_JS = "gowut-" + GOWUT_VERSION + ".js" + +// Static javascript code +var staticJs []byte + +func init() { + // Init staticJs + staticJs = []byte("" + + // Param consts + "var _pEventType='" + _PARAM_EVENT_TYPE + + "',_pCompId='" + _PARAM_COMP_ID + + "',_pCompValue='" + _PARAM_COMP_VALUE + + "',_pFocCompId='" + _PARAM_FOCUSED_COMP_ID + + "',_pMouseWX='" + _PARAM_MOUSE_WX + + "',_pMouseWY='" + _PARAM_MOUSE_WY + + "',_pMouseX='" + _PARAM_MOUSE_X + + "',_pMouseY='" + _PARAM_MOUSE_Y + + "',_pMouseBtn='" + _PARAM_MOUSE_BTN + + "',_pModKeys='" + _PARAM_MOD_KEYS + + "',_pKeyCode='" + _PARAM_KEY_CODE + + "';\n" + + // Modifier key masks + "var _modKeyAlt=" + strconv.Itoa(int(MOD_KEY_ALT)) + + ",_modKeyCtlr=" + strconv.Itoa(int(MOD_KEY_CTRL)) + + ",_modKeyMeta=" + strconv.Itoa(int(MOD_KEY_META)) + + ",_modKeyShift=" + strconv.Itoa(int(MOD_KEY_SHIFT)) + + ";\n" + + // Event response action consts + "var _eraNoAction=" + strconv.Itoa(_ERA_NO_ACTION) + + ",_eraReloadWin=" + strconv.Itoa(_ERA_RELOAD_WIN) + + ",_eraDirtyComps=" + strconv.Itoa(_ERA_DIRTY_COMPS) + + ",_eraFocusComp=" + strconv.Itoa(_ERA_FOCUS_COMP) + + ";" + + ` + +function createXmlHttp() { + if (window.XMLHttpRequest) // IE7+, Firefox, Chrome, Opera, Safari + return xmlhttp=new XMLHttpRequest(); + else // IE6, IE5 + return xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); +} + +// Send event +function se(event, etype, compId, compValue) { + var xmlhttp = createXmlHttp(); + + xmlhttp.onreadystatechange=function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) + procEresp(xmlhttp); + } + + xmlhttp.open("POST", _pathEvent, false); // synch call + xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + + var data=""; + + if (etype != null) + data += "&" + _pEventType + "=" + etype; + if (compId != null) + data += "&" + _pCompId + "=" + compId; + if (compValue != null) + data += "&" + _pCompValue + "=" + compValue; + if (document.activeElement.id != null) + data += "&" + _pFocCompId + "=" + document.activeElement.id; + if (event.clientX != null) { + // Mouse data + var x = event.clientX, y = event.clientY; + data += "&" + _pMouseWX + "=" + x; + data += "&" + _pMouseWY + "=" + y; + var parent = document.getElementById(compId); + do { + x -= parent.offsetLeft; + y -= parent.offsetTop; + } while (parent = parent.offsetParent); + data += "&" + _pMouseX + "=" + x; + data += "&" + _pMouseY + "=" + y; + data += "&" + _pMouseBtn + "=" + (event.button < 4 ? event.button : 1); // IE8 and below use 4 for middle btn + } + + var modKeys; + modKeys += event.altKey ? _modKeyAlt : 0; + modKeys += event.ctlrKey ? _modKeyCtlr : 0; + modKeys += event.metaKey ? _modKeyMeta : 0; + modKeys += event.shiftKey ? _modKeyShift : 0; + data += "&" + _pModKeys + "=" + modKeys; + data += "&" + _pKeyCode + "=" + (event.which ? event.which : event.keyCode); + + xmlhttp.send(data); +} + +function procEresp(xmlhttp) { + var actions = xmlhttp.responseText.split(";"); + + if (actions.length == 0) { + window.alert("No response received!"); + return; + } + for (var i = 0; i < actions.length; i++) { + var n = actions[i].split(","); + + switch (parseInt(n[0])) { + case _eraDirtyComps: + for (var j = 1; j < n.length; j++) + rerenderComp(n[j]); + break; + case _eraFocusComp: + if (n.length > 1) + focusComp(parseInt(n[1])) + break; + case _eraNoAction: + break; + case _eraReloadWin: + if (n.length > 1 && n[1].length > 0) + window.location.href = _pathApp + n[1]; + else + window.location.reload(true); // force reload + break; + default: + window.alert("Unknown response code:" + n[0]); + break; + } + } +} + +function rerenderComp(compId) { + var e = document.getElementById(compId); + if (!e) // Component removed or not visible (e.g. on inactive tab of TabPanel) + return; + + var xmlhttp=createXmlHttp(); + + xmlhttp.onreadystatechange=function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + // Remember focused comp which might be replaced here: + var focusedCompId = document.activeElement.id; + e.outerHTML = xmlhttp.responseText; + focusComp(focusedCompId); + } + } + + xmlhttp.open("POST", _pathRenderComp, false); // synch call + xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + + xmlhttp.send(_pCompId + "=" + compId); +} + +// Get selected indices (of an HTML select) +function selIdxs(select) { + var selected = ""; + + for (var i = 0; i < select.options.length; i++) + if(select.options[i].selected) + selected += i + ","; + + return selected; +} + +// Get and update switch button value +function sbtnVal(event, onBtnId, offBtnId) { + var onBtn = document.getElementById(onBtnId); + var offBtn = document.getElementById(offBtnId); + + if (onBtn == null) + return false; + + var value = onBtn == document.elementFromPoint(event.clientX, event.clientY); + if (value) { + onBtn.className = "gwu-SwitchButton-On-Active"; + offBtn.className = "gwu-SwitchButton-Off-Inactive"; + } else { + onBtn.className = "gwu-SwitchButton-On-Inactive"; + offBtn.className = "gwu-SwitchButton-Off-Active"; + } + + return value; +} + +function focusComp(compId) { + if (compId != null) { + var e = document.getElementById(compId); + if (e) // Else component removed or not visible (e.g. on inactive tab of TabPanel) + e.focus(); + } +} + +window.onload = function() { + focusComp(_focCompId); +} +`) +} diff --git a/gwu/label.go b/gwu/label.go new file mode 100644 index 0000000..5073c28 --- /dev/null +++ b/gwu/label.go @@ -0,0 +1,53 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Label component interface and implementation. + +package gwu + +// Label interface defines a component which wraps a text into a component. +// +// Default style class: "gwu-Label" +type Label interface { + // Label is a component. + Comp + + // Label has text. + HasText +} + +// Label implementation +type labelImpl struct { + compImpl // Component implementation + hasTextImpl // Has text implementation +} + +// NewLabel creates a new Label. +func NewLabel(text string) Label { + c := &labelImpl{newCompImpl(""), newHasTextImpl(text)} + c.Style().AddClass("gwu-Label") + return c +} + +func (c *labelImpl) Render(w writer) { + w.Write(_STR_SPAN_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + c.renderText(w) + + w.Write(_STR_SPAN_CL) +} diff --git a/gwu/link.go b/gwu/link.go new file mode 100644 index 0000000..894e600 --- /dev/null +++ b/gwu/link.go @@ -0,0 +1,142 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Link component interface and implementation. + +package gwu + +// Link interface defines a clickable link pointing to a URL. +// Links are usually used with a text, although Link is a +// container, and allows to set a child component +// which if set will also be a part of the clickable link. +// +// Default style class: "gwu-Link" +type Link interface { + // Link is a Container. + Container + + // Link has text. + HasText + + // Link has URL string. + HasUrl + + // Target returns the target of the link. + Target() string + + // SetTarget sets the target of the link. + // Tip: pass "_blank" if you want the URL to open in a new window. + SetTarget(target string) + + // Comp returns the optional child component, if set. + Comp() Comp + + // SetComp sets the only child component + // (which can be a Container of course). + SetComp(c Comp) +} + +// Link implementation. +type linkImpl struct { + compImpl // Component implementation + hasTextImpl // Has text implementation + hasUrlImpl // Has text implementation + + comp Comp // Optional child component +} + +// NewLink creates a new Link. +// By default links open in a new window (tab) +// because their target is set to "_blank". +func NewLink(text, url string) Link { + c := &linkImpl{newCompImpl(""), newHasTextImpl(text), newHasUrlImpl(url), nil} + c.SetTarget("_blank") + c.Style().AddClass("gwu-Link") + return c +} + +func (c *linkImpl) Remove(c2 Comp) bool { + if c.comp == nil || !c.comp.Equals(c2) { + return false + } + + c2.setParent(nil) + c.comp = nil + + return true +} + +func (c *linkImpl) ById(id ID) Comp { + if c.id == id { + return c + } + + if c.comp != nil { + if c.comp.Id() == id { + return c.comp + } + if c2, isContainer := c.comp.(Container); isContainer { + if c3 := c2.ById(id); c3 != nil { + return c3 + } + } + + } + + return nil +} + +func (c *linkImpl) Clear() { + if c.comp != nil { + c.comp.setParent(nil) + c.comp = nil + } +} + +func (c *linkImpl) Target() string { + return c.attrs["target"] +} + +func (c *linkImpl) SetTarget(target string) { + if len(target) == 0 { + delete(c.attrs, "target") + } else { + c.attrs["target"] = target + } +} + +func (c *linkImpl) Comp() Comp { + return c.comp +} + +func (c *linkImpl) SetComp(c2 Comp) { + c.comp = c2 +} + +func (c *linkImpl) Render(w writer) { + w.Writes("") +} diff --git a/gwu/listbox.go b/gwu/listbox.go new file mode 100644 index 0000000..c16a8e8 --- /dev/null +++ b/gwu/listbox.go @@ -0,0 +1,224 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ListBox component interface and implementation. + +package gwu + +import ( + "net/http" + "strconv" + "strings" +) + +// ListBox interface defines a component which allows selecting one or multiple values +// from a predefined list. +// +// Suggested event type to handle changes: ETYPE_CHANGE +// +// Default style class: "gwu-ListBox" +type ListBox interface { + // ListBox is a component + Comp + + // ListBox can be enabled/disabled. + HasEnabled + + // Multi tells if multiple selections are allowed. + Multi() bool + + // SetMulti sets whether multiple selections are allowed. + SetMulti(multi bool) + + // Rows returns the number of displayed rows. + Rows() int + + // SetRows sets the number of displayed rows. + // rows=1 will make this ListBox a dropdown list (if multi is false!). + // Note that if rows is greater than 1, most browsers enforce a visual minimum size + // (about 4 rows) even if rows is less than that. + SetRows(rows int) + + // SelectedValue retruns the first selected value. + // Empty string is returned if nothing is selected. + SelectedValue() string + + // SelectedValues retruns all the selected values. + SelectedValues() []string + + // Selected tells if the value at index i is selected. + Selected(i int) bool + + // SelectedIdx returns the first selected index. + // Returns -1 if nothing is selected. + SelectedIdx() int + + // SelectedIndices returns a slice of the indices of the selected values. + SelectedIndices() []int + + // SetSelected sets the selection state of the value at index i. + SetSelected(i int, selected bool) + + // SetSelectedIndices sets the (only) selected values. + // Only values will be selected that are contained in the specified indices slice. + SetSelectedIndices(indices []int) + + // ClearSelected deselects all values. + ClearSelected() +} + +// ListBox implementation. +type listBoxImpl struct { + compImpl // Component implementation + hasEnabledImpl // Has enabled implementation + + values []string // Values to choose from + multi bool // Allow multiple selection + selected []bool // Array of selection state of the values + rows int // Number of displayed rows +} + +// NewListBox creates a new ListBox. +func NewListBox(values []string) ListBox { + c := &listBoxImpl{newCompImpl("selIdxs(this)"), newHasEnabledImpl(), values, false, make([]bool, len(values)), 1} + c.AddSyncOnETypes(ETYPE_CHANGE) + c.Style().AddClass("gwu-ListBox") + return c +} + +func (c *listBoxImpl) Multi() bool { + return c.multi +} + +func (c *listBoxImpl) SetMulti(multi bool) { + c.multi = multi +} + +func (c *listBoxImpl) Rows() int { + return c.rows +} + +func (c *listBoxImpl) SetRows(rows int) { + c.rows = rows +} + +func (c *listBoxImpl) SelectedValue() string { + if i := c.SelectedIdx(); i >= 0 { + return c.values[i] + } + + return "" +} + +func (c *listBoxImpl) SelectedValues() (sv []string) { + for i, s := range c.selected { + if s { + sv = append(sv, c.values[i]) + } + } + return +} + +func (c *listBoxImpl) Selected(i int) bool { + return c.selected[i] +} + +func (c *listBoxImpl) SelectedIdx() int { + for i, s := range c.selected { + if s { + return i + } + } + return -1 +} + +func (c *listBoxImpl) SelectedIndices() (si []int) { + for i, s := range c.selected { + if s { + si = append(si, i) + } + } + return +} + +func (c *listBoxImpl) SetSelected(i int, selected bool) { + c.selected[i] = selected +} + +func (c *listBoxImpl) SetSelectedIndices(indices []int) { + // First clear selected slice + for i, _ := range c.selected { + c.selected[i] = false + } + + // And now select that needs to be selected + for _, idx := range indices { + c.selected[idx] = true + } +} + +func (c *listBoxImpl) ClearSelected() { + for i, _ := range c.selected { + c.selected[i] = false + } +} + +func (c *listBoxImpl) preprocessEvent(event Event, r *http.Request) { + value := r.FormValue(_PARAM_COMP_VALUE) + if len(value) == 0 { + return + } + + // Set selected indices + c.ClearSelected() + for _, sidx := range strings.Split(value, ",") { + if idx, err := strconv.Atoi(sidx); err == nil { + c.selected[idx] = true + } + } +} + +var ( + _STR_SELECT_OP = []byte("") // "") // "" + _STR_SELECT_CL = []byte("") // "" +) + +func (c *listBoxImpl) Render(w writer) { + w.Write(_STR_SELECT_OP) + if c.multi { + w.Write(_STR_MULTIPLE) + } + w.WriteAttr("size", strconv.Itoa(c.rows)) + c.renderAttrsAndStyle(w) + c.renderEnabled(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + for i, value := range c.values { + if c.selected[i] { + w.Write(_STR_OPTION_OP_SEL) + } else { + w.Write(_STR_OPTION_OP) + } + w.Writees(value) + w.Write(_STR_OPTION_CL) + } + + w.Write(_STR_SELECT_CL) +} diff --git a/gwu/panel.go b/gwu/panel.go new file mode 100644 index 0000000..5623d6d --- /dev/null +++ b/gwu/panel.go @@ -0,0 +1,395 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Panel component interface and implementation. + +package gwu + +import ( + "bytes" +) + +// Layout strategy type. +type Layout int + +// Layout strategies. +const ( + LAYOUT_NATURAL Layout = iota // Natural layout: elements are displayed in their natural order. + LAYOUT_VERTICAL // Vertical layout: elements are layed out vertically. + LAYOUT_HORIZONTAL // Horizontal layout: elements are layed out horizontally. +) + +// PanelView interface defines a container which stores child components +// sequentially (one dimensional, associated with an index), and lays out +// its children in a row or column using TableView based on a layout strategy, +// but does not define the way how child components can be added. +// +// Default style class: "gwu-Panel" +type PanelView interface { + // PanelView is a TableView. + TableView + + // Layout returns the layout strategy used to lay out components when rendering. + Layout() Layout + + // SetLayout sets the layout strategy used to lay out components when rendering. + SetLayout(layout Layout) + + // CompsCount returns the number of components added to the panel. + CompsCount() int + + // CompAt returns the component at the specified index. + // Returns nil if idx<0 or idx>=CompsCount(). + CompAt(idx int) Comp + + // CompIdx returns the index of the specified component in the panel. + // -1 is returned if the component is not added to the panel. + CompIdx(c Comp) int + + // CellFmt returns the cell formatter of the specified child component. + // If the specified component is not a child, nil is returned. + // Cell formatting has no effect if layout is LAYOUT_NATURAL. + CellFmt(c Comp) CellFmt +} + +// Panel interface defines a container which stores child components +// associated with an index, and lays out its children based on a layout +// strategy. +// Default style class: "gwu-Panel" +type Panel interface { + // Panel is a PanelView. + PanelView + + // Add adds a component to the panel. + Add(c Comp) + + // Insert inserts a component at the specified index. + // Returns true if the index was valid and the component is inserted + // successfully, false otherwise. idx=CompsCount() is also allowed + // in which case comp will be the last component. + Insert(c Comp, idx int) bool + + // AddHSpace adds and returns a fixed-width horizontal space consumer. + // Useful when layout is LAYOUT_HORIZONTAL. + AddHSpace(width int) Comp + + // AddVSpace adds and returns a fixed-height vertical space consumer. + // Useful when layout is LAYOUT_VERTICAL. + AddVSpace(height int) Comp + + // AddSpace adds and returns a fixed-size space consumer. + AddSpace(width, height int) Comp + + // AddHConsumer adds and returns a horizontal (free) space consumer. + // Useful when layout is LAYOUT_HORIZONTAL. + // + // Tip: When adding a horizontal space consumer, you may set the + // white space style attribute of other components in the the panel + // to WHITE_SPACE_NOWRAP to avoid texts getting wrapped to multiple lines. + AddHConsumer() Comp + + // AddVConsumer adds and returns a vertical (free) space consumer. + // Useful when layout is LAYOUT_VERTICAL. + AddVConsumer() Comp +} + +// Panel implementation. +type panelImpl struct { + tableViewImpl // TableView implementation + + layout Layout // Layout strategy + comps []Comp // Components added to this panel + cellFmts map[ID]*cellFmtImpl // Lazily initialized cell formatters of the child components +} + +// NewPanel creates a new Panel. +// Default layout strategy is LAYOUT_VERTICAL, +// default horizontal alignment is HA_DEFAULT, +// default vertical alignment is VA_DEFAULT. +func NewPanel() Panel { + c := newPanelImpl() + c.Style().AddClass("gwu-Panel") + return &c +} + +// NewNaturalPanel creates a new Panel initialized with +// LAYOUT_NATURAL layout. +// Default horizontal alignment is HA_DEFAULT, +// default vertical alignment is VA_DEFAULT. +func NewNaturalPanel() Panel { + p := NewPanel() + p.SetLayout(LAYOUT_NATURAL) + return p +} + +// NewHorizontalPanel creates a new Panel initialized with +// LAYOUT_HORIZONTAL layout. +// Default horizontal alignment is HA_DEFAULT, +// default vertical alignment is VA_DEFAULT. +func NewHorizontalPanel() Panel { + p := NewPanel() + p.SetLayout(LAYOUT_HORIZONTAL) + return p +} + +// NewVerticalPanel creates a new Panel initialized with +// LAYOUT_VERTICAL layout. +// Default horizontal alignment is HA_DEFAULT, +// default vertical alignment is VA_DEFAULT. +func NewVerticalPanel() Panel { + return NewPanel() +} + +// newPanelImpl creates a new panelImpl. +func newPanelImpl() panelImpl { + return panelImpl{tableViewImpl: newTableViewImpl(), layout: LAYOUT_VERTICAL, comps: make([]Comp, 0, 2)} +} + +func (c *panelImpl) Remove(c2 Comp) bool { + i := c.CompIdx(c2) + if i < 0 { + return false + } + + // Remove associated cell formatter + if c.cellFmts != nil { + delete(c.cellFmts, c2.Id()) + } + + c2.setParent(nil) + // When removing, also reference must be cleared to allow the comp being gc'ed, also to prevent memory leak. + oldComps := c.comps + // Copy the part after the removable comp, backward by 1: + c.comps = append(oldComps[:i], oldComps[i+1:]...) + // Clear the reference that becomes unused: + oldComps[len(oldComps)-1] = nil + + return true +} + +func (c *panelImpl) ById(id ID) Comp { + if c.id == id { + return c + } + + for _, c2 := range c.comps { + if c2.Id() == id { + return c2 + } + + if c3, isContainer := c2.(Container); isContainer { + if c4 := c3.ById(id); c4 != nil { + return c4 + } + } + } + return nil +} + +func (c *panelImpl) Clear() { + // Clear cell formatters + if c.cellFmts != nil { + c.cellFmts = nil + } + + for _, c2 := range c.comps { + c2.setParent(nil) + } + c.comps = nil +} + +func (c *panelImpl) Layout() Layout { + return c.layout +} + +func (c *panelImpl) SetLayout(layout Layout) { + c.layout = layout +} + +func (c *panelImpl) CompsCount() int { + return len(c.comps) +} + +func (c *panelImpl) CompAt(idx int) Comp { + if idx < 0 || idx >= len(c.comps) { + return nil + } + return c.comps[idx] +} + +func (c *panelImpl) CompIdx(c2 Comp) int { + for i, c3 := range c.comps { + if c2.Equals(c3) { + return i + } + } + return -1 +} + +func (c *panelImpl) CellFmt(c2 Comp) CellFmt { + if c.CompIdx(c2) < 0 { + return nil + } + + if c.cellFmts == nil { + c.cellFmts = make(map[ID]*cellFmtImpl) + } + + cf := c.cellFmts[c2.Id()] + if cf == nil { + cf = newCellFmtImpl() + c.cellFmts[c2.Id()] = cf + } + return cf +} + +func (c *panelImpl) Add(c2 Comp) { + c2.makeOrphan() + c.comps = append(c.comps, c2) + c2.setParent(c) +} + +func (c *panelImpl) Insert(c2 Comp, idx int) bool { + if idx < 0 || idx > len(c.comps) { + return false + } + + c2.makeOrphan() + + // Make sure we have room for the extra component: + c.comps = append(c.comps, nil) + copy(c.comps[idx+1:], c.comps[idx:len(c.comps)-1]) + c.comps[idx] = c2 + + c2.setParent(c) + + return true +} + +func (c *panelImpl) AddHSpace(width int) Comp { + l := NewLabel("") + l.Style().SetDisplay(DISPLAY_BLOCK).SetWidthPx(width) + c.Add(l) + return l +} + +func (c *panelImpl) AddVSpace(height int) Comp { + l := NewLabel("") + l.Style().SetDisplay(DISPLAY_BLOCK).SetHeightPx(height) + c.Add(l) + return l +} + +func (c *panelImpl) AddSpace(width, height int) Comp { + l := NewLabel("") + l.Style().SetDisplay(DISPLAY_BLOCK).SetSizePx(width, height) + c.Add(l) + return l +} + +func (c *panelImpl) AddHConsumer() Comp { + l := NewLabel("") + c.Add(l) + c.CellFmt(l).Style().SetFullWidth() + return l +} + +func (c *panelImpl) AddVConsumer() Comp { + l := NewLabel("") + c.Add(l) + c.CellFmt(l).Style().SetFullHeight() + return l +} + +func (c *panelImpl) Render(w writer) { + switch c.layout { + case LAYOUT_NATURAL: + c.layoutNatural(w) + case LAYOUT_HORIZONTAL: + c.layoutHorizontal(w) + case LAYOUT_VERTICAL: + c.layoutVertical(w) + } +} + +// layoutNatural renders the panel and the child components +// using the natural layout strategy. +func (c *panelImpl) layoutNatural(w writer) { + // No wrapper table but we still need a wrapper tag for attributes... + w.Write(_STR_SPAN_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + for _, c2 := range c.comps { + c2.Render(w) + } + + w.Write(_STR_SPAN_CL) +} + +// layoutHorizontal renders the panel and the child components +// using the horizontal layout strategy. +func (c *panelImpl) layoutHorizontal(w writer) { + w.Write(_STR_TABLE_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + c.renderTr(w) + + for _, c2 := range c.comps { + c.renderTd(c2, w) + c2.Render(w) + } + + w.Write(_STR_TABLE_CL) +} + +// layoutVertical renders the panel and the child components +// using the vertical layout strategy. +func (c *panelImpl) layoutVertical(w writer) { + w.Write(_STR_TABLE_OP) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(_STR_GT) + + // There is the same TR tag for each cell: + trWriter := bytes.NewBuffer(nil) + c.renderTr(NewWriter(trWriter)) + tr := trWriter.Bytes() + + for _, c2 := range c.comps { + w.Write(tr) + c.renderTd(c2, w) + c2.Render(w) + } + + w.Write(_STR_TABLE_CL) +} + +// renderTd renders the formatted HTML TD tag for the specified child component. +func (c *panelImpl) renderTd(c2 Comp, w writer) { + var cf *cellFmtImpl + if c.cellFmts != nil { + cf = c.cellFmts[c2.Id()] + } + + if cf == nil { + w.Write(_STR_TD) + } else { + cf.render(_STR_TD_OP, w) + } +} diff --git a/gwu/server.go b/gwu/server.go new file mode 100644 index 0000000..eaa3d87 --- /dev/null +++ b/gwu/server.go @@ -0,0 +1,694 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Implementation of the GUI server which handles sessions, +// renders the windows and handles event dispatching. + +package gwu + +import ( + "fmt" + "log" + "net/http" + "os/exec" + "runtime" + "strconv" + "strings" + "time" +) + +// Internal path constants. +const ( + _PATH_STATIC = "_gwu_static/" // App path-relative path for GWU static contents. + _PATH_EVENT = "e" // Window-relative path for sending events + _PATH_RENDER_COMP = "rc" // Window-relative path for rendering a component +) + +// Parameters passed between the browser and the server. +const ( + _PARAM_EVENT_TYPE = "et" // Event type parameter name + _PARAM_COMP_ID = "cid" // Component id parameter name + _PARAM_COMP_VALUE = "cval" // Component value parameter name + _PARAM_FOCUSED_COMP_ID = "fcid" // Focused component id parameter name + _PARAM_MOUSE_WX = "mwx" // Mouse x pixel coordinate (inside window) + _PARAM_MOUSE_WY = "mwy" // Mouse y pixel coordinate (inside window) + _PARAM_MOUSE_X = "mx" // Mouse x pixel coordinate (relative to source component) + _PARAM_MOUSE_Y = "my" // Mouse y pixel coordinate (relative to source component) + _PARAM_MOUSE_BTN = "mb" // Mouse button + _PARAM_MOD_KEYS = "mk" // Modifier key states + _PARAM_KEY_CODE = "kc" // Key code +) + +// Event response actions (client actions to take after processing an event). +const ( + _ERA_NO_ACTION = iota // Event processing OK and no action required + _ERA_RELOAD_WIN // Window name to be reloaded + _ERA_DIRTY_COMPS // There are dirty components which needs to be refreshed + _ERA_FOCUS_COMP // Focus a compnent +) + +// GWU session id cookie name +const _GWU_SESSID_COOKIE = "gwu-sessid" + +// SessionHandler interface defines a callback to get notified +// for certain events related to session life-cycles. +type SessionHandler interface { + // Created is called when a new session is created. + // At this time the client does not yet know about the session. + Created(sess Session) + + // Removed is called when a session is being removed + // from the server. After removal, the session id will become + // an invalid session id. + Removed(sess Session) +} + +// Server interface defines the GUI server which handles sessions, +// renders the windows, components and handles event dispatching. +type Server interface { + // The Server implements the Session interface: + // there is one public session which is shared between + // the "sessionless" requests. + // This is to maintain windows without a session. + Session + + // A server has text which will be used as the title + // of the server. + HasText + + // Secure returns if the server is configured to run + // in secure (HTTPS) mode or in HTTP mode. + Secure() bool + + // AppUrl returns the application URL string. + AppUrl() string + + // AppPath returns the application path string. + AppPath() string + + // AddSessCreatorName registers a nonexistent window name + // whose path auto-creates a new session. + // + // Normally sessions are created from event handlers during + // event dispatching by calling Event.NewSession(). This + // requires a public window and an event source component + // (e.g. a Button) to create a session. + // With AddSessCreatorName you can register nonexistent (meaning + // not-yet added) window names whose path will trigger an automatic + // session creation (if the current session is not private), and + // with a registered SessionHandler you can build the window and + // add it to the auto-created new session prior to it being served. + // + // The text linking to the name will be included in the window list + // if text is a non-empty string. + // + // Tip: You can use this to pre-register a login window for example. + // You can call + // AddSessCreatorName("login", "Login Window") + // and in the Created() method of a registered SessionHandler: + // func (h MySessHanlder) Created(s gwu.Session) { + // win := gwu.NewWindow("login", "Login Window") + // // ...add content to the login window... + // session.AddWindow(win) + // } + AddSessCreatorName(name, text string) + + // AddSHandler adds a new session handler. + AddSHandler(handler SessionHandler) + + // Theme returns the default CSS theme of the server. + Theme() string + + // SetTheme sets the default CSS theme of the server. + SetTheme(theme string) + + // SetLogger sets the logger to be used + // to log incoming requests. + // Pass nil to disable logging. This is the default. + SetLogger(logger *log.Logger) + + // Start starts the GUI server and waits for incoming connections. + // + // Sessionless window names may be specified as optional parameters + // that will be opened in the default browser. + // Tip: Pass an empty string to open the window list. + Start(openWins ...string) error +} + +// Server implementation. +type serverImpl struct { + sessionImpl // Single public session implementation + hasTextImpl // Has text implementation + + appName string // Application name (part of the application path) + addr string // Server address + secure bool // Tells if the server is configured to run in secure (HTTPS) mode + appPath string // Application path + appUrl string // Application URL + sessions map[string]Session // Sessions + certFile, keyFile string // Certificate and key files for secure (HTTPS) mode + sessCreatorNames map[string]string // Session creator names + sessionHandlers []SessionHandler // Registered session handlers + theme string // Default CSS theme of the server + logger *log.Logger // Logger. +} + +// NewServer creates a new GUI server in HTTP mode. +// The specified app name will be part of the application path (the first part). +// If addr is empty string, "localhost:3434" will be used. +// +// Tip: Pass an empty string as appName to place the GUI server to the root path ("/"). +func NewServer(appName, addr string) Server { + return newServerImpl(appName, addr, "", "") +} + +// NewServerTLS creates a new GUI server in secure (HTTPS) mode. +// The specified app name will be part of the application path (the first part). +// If addr is empty string, "localhost:3434" will be used. +// +// Tip: Pass an empty string as appName to place the GUI server to the root path ("/"). +// Tip: You can use generate_cert.go in crypto/tls to generate +// a test certificate and key file (cert.pem andkey.pem). +func NewServerTLS(appName, addr, certFile, keyFile string) Server { + return newServerImpl(appName, addr, certFile, keyFile) +} + +// newServerImpl creates a new serverImpl. +func newServerImpl(appName, addr, certFile, keyFile string) *serverImpl { + if len(addr) == 0 { + addr = "localhost:3434" + } + + s := &serverImpl{sessionImpl: newSessionImpl(false), appName: appName, addr: addr, sessions: make(map[string]Session), + sessCreatorNames: make(map[string]string), theme: THEME_DEFAULT} + + if len(s.appName) == 0 { + s.appPath = "/" + } else { + s.appPath = "/" + s.appName + "/" + } + + if len(certFile) == 0 || len(keyFile) == 0 { + s.secure = false + s.appUrl = "http://" + addr + s.appPath + } else { + s.secure = true + s.appUrl = "https://" + addr + s.appPath + s.certFile = certFile + s.keyFile = keyFile + } + + return s +} + +func (s *serverImpl) Secure() bool { + return s.secure +} + +func (s *serverImpl) AppUrl() string { + return s.appUrl +} + +func (s *serverImpl) AppPath() string { + return s.appPath +} + +func (s *serverImpl) AddSessCreatorName(name, text string) { + if len(name) > 0 { + s.sessCreatorNames[name] = text + } +} + +func (s *serverImpl) AddSHandler(handler SessionHandler) { + s.sessionHandlers = append(s.sessionHandlers, handler) +} + +// newSession creates a new (private) Session. +// The event is optional. If specified and the current session +// (as returned by Event.Session()) is private, it will be removed first. +// The new session is set to the event, and also returned. +func (s *serverImpl) newSession(e *eventImpl) Session { + if e != nil { + // First remove old session + s.removeSess(e) + } + + sessImpl := newSessionImpl(true) + sess := &sessImpl + if e != nil { + e.session = sess + } + // Store new session + s.sessions[sess.Id()] = sess + + if s.logger != nil { + s.logger.Println("SESSION created:", sess.Id()) + } + + // Notify session handlers + for _, handler := range s.sessionHandlers { + handler.Created(sess) + } + + return sess +} + +// removeSess removes (invalidates) the current session of the specified event. +// Only private sessions can be removed, calling this +// when the current session (as returned by Event.Session()) is public is a no-op. +// After this method Event.Session() will return the shared public session. +func (s *serverImpl) removeSess(e *eventImpl) { + if e.session.Private() { + s.removeSess2(e.session) + e.session = &s.sessionImpl + } +} + +// removeSess2 removes (invalidates) the specified session. +// Only private sessions can be removed, calling this +// the public session is a no-op. +func (s *serverImpl) removeSess2(sess Session) { + if sess.Private() { + if s.logger != nil { + s.logger.Println("SESSION removed:", sess.Id()) + } + + // Notify session handlers + for _, handler := range s.sessionHandlers { + handler.Removed(sess) + } + delete(s.sessions, sess.Id()) + } +} + +// addSessCookie lets the client know about the specified (new) session +// by setting the GWU session id cookie. +// Also clears the new flag of the session. +func (s *serverImpl) addSessCookie(sess Session, w http.ResponseWriter) { + // HttpOnly: do not allow non-HTTP access to it (like javascript) to prevent stealing it... + // Secure: only send it over HTTPS + c := http.Cookie{Name: _GWU_SESSID_COOKIE, Value: sess.Id(), Path: s.appPath, HttpOnly: true, Secure: s.secure} + http.SetCookie(w, &c) + + sess.clearNew() +} + +// sessCleaner periodically checks whether private sessions has timed out +// in an endless loop. If a session has timed out, removes it. +// This method is to start as a new go routine. +func (s *serverImpl) sessCleaner() { + sleep := 10 * time.Second + for { + now := time.Now() + + // TODO synchronization? + for _, sess := range s.sessions { + if now.Sub(sess.Accessed()) > sess.Timeout() { + s.removeSess2(sess) + } + } + + time.Sleep(sleep) + } +} + +func (s *serverImpl) Theme() string { + return s.theme +} + +func (s *serverImpl) SetTheme(theme string) { + s.theme = theme +} + +func (s *serverImpl) SetLogger(logger *log.Logger) { + s.logger = logger +} + +// open opens the specified URL in the default browser of the user. +func open(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xgd-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} + +func (s *serverImpl) Start(openWins ...string) error { + http.HandleFunc(s.appPath, func(w http.ResponseWriter, r *http.Request) { + s.serveHTTP(w, r) + }) + + http.HandleFunc(s.appPath+_PATH_STATIC, func(w http.ResponseWriter, r *http.Request) { + s.serveStatic(w, r) + }) + + fmt.Println("Starting GUI server on:", s.appUrl) + if s.logger != nil { + s.logger.Println("Starting GUI server on:", s.appUrl) + } + + for _, winName := range openWins { + open(s.appUrl + winName) + } + + go s.sessCleaner() + + var err error + if s.secure { + err = http.ListenAndServeTLS(s.addr, s.certFile, s.keyFile, nil) + } else { + err = http.ListenAndServe(s.addr, nil) + } + + if err != nil { + return err + } + return nil +} + +// serveStatic handles the static contents of GWU. +func (s *serverImpl) serveStatic(w http.ResponseWriter, r *http.Request) { + // Parts example: "/appname/_gwu_static/gwu-0.8.0.js" => {"", "appname", "_gwu_static", "gwu-0.8.0.js"} + parts := strings.Split(r.URL.Path, "/") + + if len(s.appName) == 0 { + // No app name, gui server resides in root + if len(parts) < 2 { + // This should never happen. Path is always at least a slash ("/"). + http.NotFound(w, r) + return + } + // Omit the first empty string and _PATH_STATIC + parts = parts[2:] + } else { + // We have app name + if len(parts) < 3 { + // Missing app name from path + http.NotFound(w, r) + return + } + // Omit the first empty string, app name and _PATH_STATIC + parts = parts[3:] + } + + res := parts[0] + if res == _RES_NAME_STATIC_JS { + w.Header().Set("Expires", time.Now().Add(24*time.Hour).Format(http.TimeFormat)) // Set 24 hours caching + w.Header().Set("Content-Type", "application/x-javascript; charset=utf-8") + w.Write(staticJs) + return + } + if strings.HasSuffix(res, ".css") { + cssCode := staticCss[res] + if cssCode != nil { + w.Header().Set("Expires", time.Now().Add(24*time.Hour).Format(http.TimeFormat)) // Set 24 hours caching + w.Header().Set("Content-Type", "text/css; charset=utf-8") + w.Write(cssCode) + return + } + } + + http.NotFound(w, r) +} + +// serveHTTP handles the incoming requests. +// Renders of the URL-selected window, +// and also handles event dispatching. +func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) { + if s.logger != nil { + s.logger.Println("Incoming: ", r.URL.Path) + } + + // Check session + var sess Session + c, err := r.Cookie(_GWU_SESSID_COOKIE) + if err == nil { + sess = s.sessions[c.Value] + } + if sess == nil { + sess = &s.sessionImpl + } + sess.access() + + // Parts example: "/appname/winname/event?et=0&cid=1" => {"", "appname", "winname", "event"} + parts := strings.Split(r.URL.Path, "/") + + if len(s.appName) == 0 { + // No app name, gui server resides in root + if len(parts) < 1 { + // This should never happen. Path is always at least a slash ("/"). + http.NotFound(w, r) + return + } + // Omit the first empty string + parts = parts[1:] + } else { + // We have app name + if len(parts) < 2 { + // Missing app name from path + http.NotFound(w, r) + return + } + // Omit the first empty string and the app name + parts = parts[2:] + } + + if len(parts) < 1 || len(parts[0]) == 0 { + // Missing window name, render window list + s.renderWinList(sess, w, r) + return + } + + winName := parts[0] + + win := sess.WinByName(winName) + // If not found and we're on an authenticated session, try the public window list + if win == nil && sess.Private() { + win = s.WinByName(winName) // Server is a Session, the public session + if win != nil { + s.access() + } + } + // If still not found and no private session, try the session creator names + if win == nil && !sess.Private() { + _, found := s.sessCreatorNames[winName] + if found { + sess = s.newSession(nil) + s.addSessCookie(sess, w) + // Search again in the new session as SessionHandlers may have added windows. + win = sess.WinByName(winName) + } + } + + if win == nil { + // Invalid window name, render an error message with a link to the window list + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + NewWriter(w).Writess("Window for name '", winName, "' not found. See the Window list.") + return + } + + var path string + if len(parts) >= 2 { + path = parts[1] + } + + switch path { + case _PATH_EVENT: + s.handleEvent(sess, win, w, r) + case _PATH_RENDER_COMP: + // Render just a component + s.renderComp(win, w, r) + default: + // Render the whole window + win.RenderWin(NewWriter(w), s) + } +} + +// renderWinList renders the window list of a session as HTML document with clickable links. +func (s *serverImpl) renderWinList(sess Session, wr http.ResponseWriter, r *http.Request) { + if s.logger != nil { + s.logger.Println("\tRending windows list.") + } + wr.Header().Set("Content-Type", "text/html; charset=utf-8") + + w := NewWriter(wr) + + w.Writes("") + w.Writees(s.text) + w.Writess(" - Window list

") + w.Writees(s.text) + w.Writes(" - Window list

") + + // Render both private and public session windows + sessions := make([]Session, 1, 2) + sessions[0] = sess + if sess.Private() { + sessions = append(sessions, &s.sessionImpl) + } else { + // No private session yet, render session creators: + if len(s.sessCreatorNames) > 0 { + w.Writes("Session creators:
    ") // TODO needs a better name + for name, text := range s.sessCreatorNames { + w.Writess("
  • ", text, "") + } + w.Writes("
") + } + } + + for _, session := range sessions { + if session.Private() { + w.Writes("Authenticated windows:") + } else { + w.Writes("Public windows:") + } + w.Writes("
    ") + for _, win := range session.SortedWins() { + w.Writess("
  • ", win.Text(), "") + } + w.Writes("
") + } + + w.Writes("") +} + +// renderComp renders just a component. +func (s *serverImpl) renderComp(win Window, w http.ResponseWriter, r *http.Request) { + id, err := AtoID(r.FormValue(_PARAM_COMP_ID)) + if err != nil { + http.Error(w, "Invalid component id!", http.StatusBadRequest) + return + } + + if s.logger != nil { + s.logger.Println("\tRendering comp:", id) + } + + comp := win.ById(id) + if comp == nil { + http.Error(w, fmt.Sprint("Component not found: ", id), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") // We send it as text! + comp.Render(NewWriter(w)) +} + +// handleEvent handles the event dispatching. +func (s *serverImpl) handleEvent(sess Session, win Window, wr http.ResponseWriter, r *http.Request) { + focCompId, err := AtoID(r.FormValue(_PARAM_FOCUSED_COMP_ID)) + if err == nil { + win.SetFocusedCompId(focCompId) + } + + id, err := AtoID(r.FormValue(_PARAM_COMP_ID)) + if err != nil { + http.Error(wr, "Invalid component id!", http.StatusBadRequest) + return + } + + comp := win.ById(id) + if comp == nil { + if s.logger != nil { + s.logger.Println("\tComp not found:", id) + } + http.Error(wr, fmt.Sprint("Component not found: ", id), http.StatusBadRequest) + return + } + + etype := parseIntParam(_PARAM_EVENT_TYPE, r) + if etype < 0 { + http.Error(wr, "Invalid event type!", http.StatusBadRequest) + return + } + if s.logger != nil { + s.logger.Println("\tEvent from comp:", id, " event:", etype) + } + + event := newEventImpl(s, EventType(etype), comp, sess) + + event.x = parseIntParam(_PARAM_MOUSE_X, r) + if event.x >= 0 { + event.y = parseIntParam(_PARAM_MOUSE_Y, r) + event.wx = parseIntParam(_PARAM_MOUSE_WX, r) + event.wy = parseIntParam(_PARAM_MOUSE_WY, r) + event.mbtn = MouseBtn(parseIntParam(_PARAM_MOUSE_BTN, r)) + } else { + event.y, event.wx, event.wy, event.mbtn = -1, -1, -1, -1 + } + + event.modKeys = parseIntParam(_PARAM_MOD_KEYS, r) + event.keyCode = Key(parseIntParam(_PARAM_KEY_CODE, r)) + + comp.preprocessEvent(event, r) + + // Dispatch event... + comp.dispatchEvent(event) + + // Check if a new session was created during event dispatching + if event.session.New() { + s.addSessCookie(event.session, wr) + } + + // ...and send back the result + wr.Header().Set("Content-Type", "text/plain; charset=utf-8") // We send it as text + w := NewWriter(wr) + hasAction := false + // If we reload, nothing else matters + if event.reload { + hasAction = true + w.Writevs(_ERA_RELOAD_WIN, _STR_COMMA, event.reloadWin) + } else { + if len(event.dirtyComps) > 0 { + hasAction = true + w.Writev(_ERA_DIRTY_COMPS) + for id, _ := range event.dirtyComps { + w.Write(_STR_COMMA) + w.Writev(int(id)) + } + } + if event.focusedComp != nil { + if hasAction { + w.Write(_STR_SEMICOL) + } else { + hasAction = true + } + w.Writevs(_ERA_FOCUS_COMP, _STR_COMMA, int(event.focusedComp.Id())) + // Also register focusable comp at window + win.SetFocusedCompId(event.focusedComp.Id()) + } + } + if !hasAction { + w.Writev(_ERA_NO_ACTION) + } +} + +// parseNumParam parses an int param. +// If error occurs, -1 will be returned. +func parseIntParam(paramName string, r *http.Request) int { + if num, err := strconv.Atoi(r.FormValue(paramName)); err == nil { + return num + } + return -1 +} diff --git a/gwu/session.go b/gwu/session.go new file mode 100644 index 0000000..ed978cf --- /dev/null +++ b/gwu/session.go @@ -0,0 +1,237 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Implementation of the GWU session. + +package gwu + +import ( + "crypto/rand" + "errors" + "fmt" + "io" + "sort" + "time" +) + +// Session interface defines the session to the GWU users (clients). +type Session interface { + // Id returns the id of the session. + Id() string + + // New tells if the session is new meaning the client + // does not (yet) know about it. + New() bool + + // Private tells if the session is a private session. + // There is only one public session, and it is shared + // between the "sessionless" users. + Private() bool + + // AddWin adds a window to the session. + // Returns an error if window name is empty or + // a window with the same name has already been added. + AddWin(w Window) error + + // RemoveWin removes a window from the session. + // Returns if the window was removed from the session. + RemoveWin(w Window) bool + + // SortedWins returns a sorted slice of windows. + // The slice is sorted by window text (title). + SortedWins() []Window + + // WinByName returns a window specified by its name. + WinByName(name string) Window + + // Attr returns the value of an attribute stored in the session. + // TODO use an interface type something like "serializable". + Attr(name string) interface{} + + // SetAttr sets the value of an attribute stored in the session. + // Pass the nil value to delete the attribute. + SetAttr(name string, value interface{}) + + // Created returns the time when the session was created. + Created() time.Time + + // Accessed returns the time when the session was last accessed. + Accessed() time.Time + + // Timeout returns the session timeout. + Timeout() time.Duration + + // SetTimeout sets the session timeout. + SetTimeout(timeout time.Duration) + + // access registers an access to the session. + access() + + // ClearNew clears the new flag. + // After this New() will return false. + clearNew() +} + +// Session implementation. +type sessionImpl struct { + id string // Id of the session + isNew bool // Tells if the session is new + created time.Time // Creation time + accessed time.Time // Last accessed time + windows map[string]Window // Windows of the session + attrs map[string]interface{} // Attributes stored in the session + timeout time.Duration // Session timeout +} + +// newSessionImpl creates a new sessionImpl. +// The default timeout is 30 minutes. +func newSessionImpl(private bool) sessionImpl { + var id string + // The public session has an empty string id + if private { + id = genId() + } + + now := time.Now() + + // Initialzie private sessions as new, but not the public session + return sessionImpl{id: id, isNew: private, created: now, accessed: now, windows: make(map[string]Window), + attrs: make(map[string]interface{}), timeout: 30 * time.Minute} +} + +// Number of valid id runes. +// Must be a power of 2! +const _ID_RUNES_COUNT = 64 + +// Mask to get an id rune idx from a random byte. +const _ID_RUNES_IDX_MASK = _ID_RUNES_COUNT - 1 + +// Valid runes to be used for session ids +// Its length must be _ID_RUNES_COUNT. +var _ID_RUNES = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_") + +func init() { + // Is _ID_RUNES_COUNT a power of 2? + if _ID_RUNES_COUNT&(_ID_RUNES_COUNT-1) != 0 { + panic(fmt.Sprint("_ID_RUNES_COUNT is not a power of 2: ", _ID_RUNES_COUNT)) + } + if len(_ID_RUNES) != _ID_RUNES_COUNT { + panic(fmt.Sprint("len(_ID_RUNES) != ", _ID_RUNES_COUNT)) + } +} + +// Length of the session ids +const _ID_LENGTH = 22 + +// genId generates a session id. +func genId() string { + r := make([]byte, _ID_LENGTH) + io.ReadFull(rand.Reader, r) + + id := make([]rune, _ID_LENGTH) + for i := 0; i < _ID_LENGTH; i++ { + id[i] = _ID_RUNES[r[i]&_ID_RUNES_IDX_MASK] + } + + return string(id) +} + +func (s *sessionImpl) Id() string { + return s.id +} + +func (s *sessionImpl) New() bool { + return s.isNew +} + +func (s *sessionImpl) Private() bool { + return len(s.id) > 0 +} + +func (s *sessionImpl) AddWin(w Window) error { + if len(w.Name()) == 0 { + return errors.New("Window name cannot be empty string!") + } + if _, exists := s.windows[w.Name()]; exists { + return errors.New("A window with the same name has already been added: " + w.Name()) + } + + s.windows[w.Name()] = w + + return nil +} + +func (s *sessionImpl) RemoveWin(w Window) bool { + win := s.windows[w.Name()] + if win != nil && win.Id() == w.Id() { + delete(s.windows, w.Name()) + return true + } + return false +} + +func (s *sessionImpl) SortedWins() []Window { + wins := make(WinSlice, len(s.windows)) + + i := 0 + for _, win := range s.windows { + wins[i] = win + i++ + } + + sort.Sort(wins) + + return wins +} + +func (s *sessionImpl) WinByName(name string) Window { + return s.windows[name] +} + +func (s *sessionImpl) Attr(name string) interface{} { + return s.attrs[name] +} + +func (s *sessionImpl) SetAttr(name string, value interface{}) { + if value == nil { + delete(s.attrs, name) + } else { + s.attrs[name] = value + } +} + +func (s *sessionImpl) Created() time.Time { + return s.created +} + +func (s *sessionImpl) Accessed() time.Time { + return s.accessed +} + +func (s *sessionImpl) Timeout() time.Duration { + return s.timeout +} + +func (s *sessionImpl) SetTimeout(timeout time.Duration) { + s.timeout = timeout +} + +func (s *sessionImpl) access() { + s.accessed = time.Now() +} + +func (s *sessionImpl) clearNew() { + s.isNew = false +} diff --git a/gwu/state_buttons.go b/gwu/state_buttons.go new file mode 100644 index 0000000..4a78394 --- /dev/null +++ b/gwu/state_buttons.go @@ -0,0 +1,393 @@ +// Copyright (C) 2013 Andras Belicza. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// State button interfaces and implementations +// (CheckBox, RadioButton, SwitchButton). + +package gwu + +import ( + "net/http" + "strconv" +) + +// StateButton interface defines a button which has a boolean state: +// true/false or selected/deselected. +type StateButton interface { + // stateButton is a button + Button + + // State returns the state of the button. + State() bool + + // SetState sets the state of the button. + // In case of RadioButton, the button's RadioGroup is managed + // so that only one can be selected. + SetState(state bool) +} + +// CheckBox interface defines a check box, a button which has +// 2 states: selected/deselected. +// +// Suggested event type to handle changes: ETYPE_CLICK +// +// Default style classes: "gwu-CheckBox", "gwu-CheckBox-Disabled" +type CheckBox interface { + // CheckBox is a StateButton. + StateButton +} + +// SwitchButton interface defines a button which can be switched +// ON and OFF. +// +// Suggested event type to handle changes: ETYPE_CLICK +// +// Default style classes: "gwu-SwitchButton", "gwu-SwitchButton-On-Active" +// "gwu-SwitchButton-On-Inactive", "gwu-SwitchButton-Off-Active", +// "gwu-SwitchButton-Off-Inactive" +type SwitchButton interface { + // SwitchButton is a component. + Comp + + // SwitchButton can be enabled/disabled. + HasEnabled + + // State returns the state of the switch button. + State() bool + + // SetState sets the state of the switch button. + SetState(state bool) + + // On returns the text displayed for the ON side. + On() string + + // Off returns the text displayed for the OFF side. + Off() string + + // SetOnOff sets the texts of the ON and OFF sides. + SetOnOff(on, off string) +} + +// RadioGroup interface defines the group for grouping radio buttons. +type RadioGroup interface { + // Name returns the name of the radio group. + Name() string + + // Selected returns the selected radio button of the group. + Selected() RadioButton + + // PrevSelected returns the radio button that was selected + // before the current selected radio button. + PrevSelected() RadioButton + + // setSelected sets the selected radio button of the group, + // and before that sets the current selected as the prev selected + setSelected(selected RadioButton) +} + +// RadioButton interface defines a radio button, a button which has +// 2 states: selected/deselected. +// In addition to the state, radio buttons belong to a group, +// and in each group only one radio button can be selected. +// Selecting an unselected radio button deselects the selected +// radio button of the group, if there was one. +// +// Suggested event type to handle changes: ETYPE_CLICK +// +// Default style classes: "gwu-RadioButton", "gwu-RadioButton-Disabled" +type RadioButton interface { + // RadioButton is a StateButton. + StateButton + + // Group returns the group of the radio button. + Group() RadioGroup + + // setStateProp sets the state of the button + // without managing the group of the radio button. + setStateProp(state bool) +} + +// RadioGroup implementation. +type radioGroupImpl struct { + name string // Name of the radio group + selected RadioButton // Selected radio button of the group + prevSelected RadioButton // Previous selected radio button of the group +} + +// StateButton implementation. +type stateButtonImpl struct { + buttonImpl // Button implementation + + state bool // State of the button + inputType string // Type of the underlying input tag + group RadioGroup // Group of the button + inputId ID // distinct id for the rendered input tag + disabledClass string // Disabled style class +} + +// SwitchButton implementation. +type switchButtonImpl struct { + compImpl // Component implementation + + onButton, offButton *buttonImpl // ON and OFF button implementations + state bool // State of the switch +} + +// NewRadioGroup creates a new RadioGroup. +func NewRadioGroup(name string) RadioGroup { + return &radioGroupImpl{name: name} +} + +// NewCheckBox creates a new CheckBox. +// The initial state is false. +func NewCheckBox(text string) CheckBox { + c := newStateButtonImpl(text, "checkbox", nil, "gwu-CheckBox-Disabled") + c.Style().AddClass("gwu-CheckBox") + return c +} + +// NewSwitchButton creates a new SwitchButton. +// Default texts for ON and OFF sides are: "ON" and "OFF". +// The initial state is false (OFF). +func NewSwitchButton() SwitchButton { + onButton := newButtonImpl("", "ON") + offButton := newButtonImpl("", "OFF") + + // We only want to switch the state if the opposite button is pressed + // (e.g. OFF is pressed when switch is ON and vice versa; + // if ON is pressed when switch is ON, do not switch to OFF): + valueProviderJs := "sbtnVal(event,'" + onButton.Id().String() + "','" + offButton.Id().String() + "')" + + c := &switchButtonImpl{newCompImpl(valueProviderJs), &onButton, &offButton, true} // Note the "true" state, so the following SetState(false) will be executed (different states)! + c.AddSyncOnETypes(ETYPE_CLICK) + c.SetAttr("cellspacing", "0") + c.SetAttr("cellpadding", "0") + c.Style().AddClass("gwu-SwitchButton") + c.SetState(false) + return c +} + +// NewRadioButton creates a new radio button. +// The initial state is false. +func NewRadioButton(text string, group RadioGroup) RadioButton { + c := newStateButtonImpl(text, "radio", group, "gwu-RadioButton-Disabled") + c.Style().AddClass("gwu-RadioButton") + return c +} + +// newStateButtonImpl creates a new stateButtonImpl. +func newStateButtonImpl(text, inputType string, group RadioGroup, disabledClass string) *stateButtonImpl { + c := &stateButtonImpl{newButtonImpl("this.checked", text), false, inputType, group, nextCompId(), disabledClass} + // Use ETYPE_CLICK because IE fires onchange only when focus is lost... + c.AddSyncOnETypes(ETYPE_CLICK) + return c +} + +func (r *radioGroupImpl) Name() string { + return r.name +} + +func (r *radioGroupImpl) Selected() RadioButton { + return r.selected +} + +func (r *radioGroupImpl) PrevSelected() RadioButton { + return r.prevSelected +} + +func (r *radioGroupImpl) setSelected(selected RadioButton) { + r.prevSelected = r.selected + r.selected = selected +} + +// SetEnabled sets the enabled property. +// We have some extra job to do when changing enabled status: +// we have to manage disabled class style. +func (c *stateButtonImpl) SetEnabled(enabled bool) { + if enabled { + c.Style().RemoveClass(c.disabledClass) + } else { + c.Style().AddClass(c.disabledClass) + } + + c.hasEnabledImpl.SetEnabled(enabled) +} + +func (c *stateButtonImpl) State() bool { + return c.state +} + +func (c *stateButtonImpl) SetState(state bool) { + // Only continue if state changes: + if c.state == state { + return + } + + if c.group != nil { + // Group management: if a new radio button is selected, the old one must be deselected. + sel := c.group.Selected() + + if sel == nil { + // no prev selection + if state { + c.group.setSelected(c) + } + } else { + // There is a prev selection + if state { + if !sel.Equals(c) { + sel.setStateProp(false) + c.group.setSelected(c) + } + } else { + // There is prev selection, and our new state is false + // (and our prev state was true => we are selected) + c.group.setSelected(nil) + } + } + } + + c.state = state +} + +func (c *stateButtonImpl) Group() RadioGroup { + return c.group +} + +func (c *stateButtonImpl) setStateProp(state bool) { + c.state = state +} + +func (c *stateButtonImpl) preprocessEvent(event Event, r *http.Request) { + value := r.FormValue(_PARAM_COMP_VALUE) + if len(value) == 0 { + return + } + + if v, err := strconv.ParseBool(value); err == nil { + // Call SetState instead of assigning to the state property + // because SetState properly manages radio groups. + c.SetState(v) + } +} + +func (c *stateButtonImpl) Render(w writer) { + // Proper state button consists of multiple HTML tags (input and label), so render a wrapper tag for them: + w.Write(_STR_SPAN_OP) + c.renderAttrsAndStyle(w) + w.Write(_STR_GT) + + w.Writess("