section of the window list page (the app root).
+ // Note that these will be ignored if you take over the app root
+ // (by calling SetAppRootHandler).
+ AddRootHeadHtml(html string)
+
+ // RemoveRootHeadHtml removes an HTML head text
+ // that was previously added with AddRootHeadHtml().
+ RemoveRootHeadHtml(html string)
+
+ // SetAppRootHandler sets a function that is called when the app root is requested.
+ // The default function renders the window list, including authenticated windows
+ // and session creators - with clickable links.
+ // By setting your own hander, you will completely take over the app root.
+ SetAppRootHandler(f AppRootHandlerFunc)
+
// Start starts the GUI server and waits for incoming connections.
//
// Sessionless window names may be specified as optional parameters
@@ -180,18 +204,20 @@ 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.
- headers http.Header // Extra headers that will be added to all responses.
+ 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.
+ headers http.Header // Extra headers that will be added to all responses.
+ rootHeads []string // Additional head HTML texts of the window list page (app root)
+ appRootHandlerFunc AppRootHandlerFunc // App root handler function
}
// NewServer creates a new GUI server in HTTP mode.
@@ -239,6 +265,8 @@ func newServerImpl(appName, addr, certFile, keyFile string) *serverImpl {
s.keyFile = keyFile
}
+ s.appRootHandlerFunc = s.renderWinList
+
return s
}
@@ -307,8 +335,8 @@ func (s *serverImpl) removeSess(e *eventImpl) {
}
// removeSess2 removes (invalidates) the specified session.
-// Only private sessions can be removed, calling this
-// the public session is a no-op.
+// Only private sessions can be removed, calling this with the
+// public session is a no-op.
func (s *serverImpl) removeSess2(sess Session) {
if sess.Private() {
log.Println("SESSION removed:", sess.Id())
@@ -397,10 +425,12 @@ func (s *serverImpl) AddStaticDir(path, dir string) error {
path += "/"
}
+ origPath := path
path = s.appPath + path
- if path == s.appPath+pathStatic || path == s.appPath+pathEvent || path == s.appPath+pathRenderComp {
- return errors.New("Path cannot be '" + pathStatic + "' (reserved)!")
+ // pathEvent and pathRenderComp are window-relative so no need to check with those
+ if path == s.appPath+pathStatic || path == s.appPath+pathSessCheck {
+ return errors.New("Path cannot be '" + origPath + "' (reserved)!")
}
handler := http.StripPrefix(path, http.FileServer(http.Dir(dir)))
@@ -425,6 +455,29 @@ func (s *serverImpl) SetLogger(logger *log.Logger) {
s.logger = logger
}
+func (s *serverImpl) Logger() *log.Logger {
+ return s.logger
+}
+
+func (s *serverImpl) AddRootHeadHtml(html string) {
+ s.rootHeads = append(s.rootHeads, html)
+}
+
+func (s *serverImpl) RemoveRootHeadHtml(html string) {
+ for i, v := range s.rootHeads {
+ if v == html {
+ old := s.rootHeads
+ s.rootHeads = append(s.rootHeads[:i], s.rootHeads[i+1:]...)
+ old[len(old)-1] = ""
+ return
+ }
+ }
+}
+
+func (s *serverImpl) SetAppRootHandler(f AppRootHandlerFunc) {
+ s.appRootHandlerFunc = f
+}
+
// serveStatic handles the static contents of GWU.
func (s *serverImpl) serveStatic(w http.ResponseWriter, r *http.Request) {
s.addHeaders(w)
@@ -477,7 +530,7 @@ func (s *serverImpl) serveStatic(w http.ResponseWriter, r *http.Request) {
// 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)
+ s.logger.Println("Incoming:", r.URL.Path)
}
s.addHeaders(w)
@@ -491,12 +544,11 @@ func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) {
if sess == nil {
sess = &s.sessionImpl
}
- sess.access()
// Parts example: "/appname/winname/e?et=0&cid=1" => {"", "appname", "winname", "e"}
parts := strings.Split(r.URL.Path, "/")
- if len(s.appName) == 0 {
+ if s.appName == "" {
// No app name, gui server resides in root
if len(parts) < 1 {
// This should never happen. Path is always at least a slash ("/").
@@ -516,9 +568,19 @@ func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) {
parts = parts[2:]
}
- if len(parts) < 1 || len(parts[0]) == 0 {
+ if len(parts) >= 1 && parts[0] == pathSessCheck {
+ // Session check. Must not call sess.acess()
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ sess.rwMutex().RLock()
+ remaining := sess.Timeout() - time.Now().Sub(sess.Accessed())
+ sess.rwMutex().RUnlock()
+ fmt.Fprintf(w, "%f", remaining.Seconds())
+ return
+ }
+
+ if len(parts) < 1 || parts[0] == "" {
// Missing window name, render window list
- s.renderWinList(sess, w, r)
+ s.appRootHandlerFunc(w, r, sess)
return
}
@@ -529,13 +591,14 @@ func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) {
if win == nil && sess.Private() {
win = s.WinByName(winName) // Server is a Session, the public session
if win != nil {
- s.access()
+ // We're serving a public window, switch to public session here entirely
+ sess = &s.sessionImpl
}
}
+
// If still not found and no private session, try the session creator names
if win == nil && !sess.Private() {
- _, found := s.sessCreatorNames[winName]
- if found {
+ if _, found := s.sessCreatorNames[winName]; found {
sess = s.newSession(nil)
s.addSessCookie(sess, w)
// Search again in the new session as SessionHandlers may have added windows.
@@ -551,13 +614,14 @@ func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) {
return
}
+ sess.access()
+
var path string
if len(parts) >= 2 {
path = parts[1]
}
rwMutex := sess.rwMutex()
-
switch path {
case pathEvent:
rwMutex.Lock()
@@ -580,7 +644,7 @@ func (s *serverImpl) serveHTTP(w http.ResponseWriter, r *http.Request) {
}
// 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) {
+func (s *serverImpl) renderWinList(wr http.ResponseWriter, r *http.Request, sess Session) {
if s.logger != nil {
s.logger.Println("\tRending windows list.")
}
@@ -590,7 +654,9 @@ func (s *serverImpl) renderWinList(sess Session, wr http.ResponseWriter, r *http
w.Writes(``)
w.Writees(s.text)
- w.Writess(" - Window list
")
diff --git a/gwu/sess_monitor.go b/gwu/sess_monitor.go
new file mode 100644
index 0000000..fa28902
--- /dev/null
+++ b/gwu/sess_monitor.go
@@ -0,0 +1,98 @@
+// 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 .
+
+// Session monitor component interface and implementation.
+
+package gwu
+
+import (
+ "time"
+)
+
+// SessMonitor interface defines a component which monitors and displays
+// the session timeout and network connectivity at client side without
+// interacting with the session.
+//
+// Default style classes: "gwu-SessMonitor", "gwu-SessMonitor-Expired",
+// ".gwu-SessMonitor-Error"
+type SessMonitor interface {
+ // SessMonitor is a Timer, but it does not generate Events!
+ Timer
+
+ // SetJsConverter sets the Javascript function name which converts
+ // a float second time value to a displayable string.
+ // The default value is "convertSessTimeout" whose implementation is:
+ // function convertSessTimeout(sec) {
+ // if (sec <= 0)
+ // return "Expired!";
+ // else if (sec < 60)
+ // return "<1 min";
+ // else
+ // return "~" + Math.round(sec / 60) + " min";
+ // }
+ SetJsConverter(jsFuncName string)
+
+ // JsConverter returns the name of the Javascript function which converts
+ // float second time values to displayable strings.
+ JsConverter() string
+}
+
+// SessMonitor implementation
+type sessMonitorImpl struct {
+ timerImpl // Timer implementation
+}
+
+// NewSessMonitor creates a new SessMonitor.
+// By default it is active repeats with 1 minute timeout duration.
+func NewSessMonitor() SessMonitor {
+ c := &sessMonitorImpl{
+ timerImpl{compImpl: newCompImpl(nil), timeout: time.Minute, active: true, repeat: true},
+ }
+ c.Style().AddClass("gwu-SessMonitor")
+ c.SetJsConverter("convertSessTimeout")
+ return c
+}
+
+func (c *sessMonitorImpl) SetJsConverter(jsFuncName string) {
+ c.SetAttr("gwuJsFuncName", jsFuncName)
+}
+
+func (c *sessMonitorImpl) JsConverter() string {
+ return c.Attr("gwuJsFuncName")
+}
+
+var (
+ strEmptySpan = []byte("") // ""
+ strJsCheckSessOp = []byte("checkSession(") // "checkSession("
+)
+
+func (c *sessMonitorImpl) Render(w Writer) {
+ w.Write(strSpanOp)
+ c.renderAttrsAndStyle(w)
+ c.renderEHandlers(w)
+ w.Write(strGT)
+
+ w.Write(strEmptySpan) // Placeholder for session timeout value
+
+ w.Write(strScriptOp)
+ c.renderSetupTimerJs(w, strJsCheckSessOp, int(c.id), strParenCl)
+ // Call sess check right away:
+ w.Write(strJsCheckSessOp)
+ w.Writev(int(c.id))
+ w.Write(strJsFuncCl)
+ w.Write(strScriptCl)
+
+ w.Write(strSpanCl)
+}
diff --git a/gwu/session.go b/gwu/session.go
index c6d4579..d330939 100644
--- a/gwu/session.go
+++ b/gwu/session.go
@@ -78,6 +78,7 @@ type Session interface {
SetTimeout(timeout time.Duration)
// access registers an access to the session.
+ // Implementation locks or the sessions RW mutex.
access()
// ClearNew clears the new flag.
@@ -225,6 +226,9 @@ func (s *sessionImpl) SetTimeout(timeout time.Duration) {
}
func (s *sessionImpl) access() {
+ s.rwMutex_.Lock()
+ defer s.rwMutex_.Unlock()
+
s.accessed = time.Now()
}
diff --git a/gwu/style.go b/gwu/style.go
index 15252dd..f5b1276 100644
--- a/gwu/style.go
+++ b/gwu/style.go
@@ -58,7 +58,7 @@ const (
ClrFuchsia = "Fuchsia" // Fuchsia (#FF00FF)
ClrGray = "Gray" // Gray (#808080)
ClrGrey = "Grey" // Grey (#808080)
- ClrGreen = "Green" // Green (#008000)
+ ClrGreen = "Green" // Green (#008000)
ClrLime = "Lime" // Lime (#00FF00)
ClrMaroon = "Maroon" // Maroon (#800000)
ClrNavy = "Navy" // Navy (#000080)
diff --git a/gwu/timer.go b/gwu/timer.go
index e1a5e8c..025e922 100644
--- a/gwu/timer.go
+++ b/gwu/timer.go
@@ -39,10 +39,10 @@ type Timer interface {
// Timer is a component.
Comp
- // Timeout returns the timeout of the timer.
+ // Timeout returns the timeout duration of the timer.
Timeout() time.Duration
- // SetTimeout sets the timeout of the timer.
+ // SetTimeout sets the timeout duration of the timer.
// Event will be generated after the timeout period. If timer is on repeat,
// events will be generated periodically after each timeout.
//
@@ -68,7 +68,7 @@ type Timer interface {
SetActive(active bool)
// Reset will cause the timer to restart/reschedule.
- // A Timer does not resets the countdown when it is re-rendered,
+ // A Timer does not reset the countdown when it is re-rendered,
// only if the timer config is changed (e.g. timeout or repeat).
// By calling Reset() the countdown will reset when the timer is
// re-rendered.
@@ -97,10 +97,9 @@ func (c *timerImpl) Timeout() time.Duration {
func (c *timerImpl) SetTimeout(timeout time.Duration) {
if timeout < time.Millisecond {
- c.timeout = time.Millisecond
- } else {
- c.timeout = timeout
+ timeout = time.Millisecond
}
+ c.timeout = timeout
}
func (c *timerImpl) Repeat() bool {
@@ -124,20 +123,22 @@ func (c *timerImpl) Reset() {
}
var (
- strScriptOp = []byte("") // ");"
+ strSetupTimerOp = []byte("setupTimer(") // "setupTimer("
+ strJsSendEvtOp = []byte("se(null,") // "se(null,"
)
-func (c *timerImpl) Render(w Writer) {
- w.Write(strSpanOp)
- c.renderAttrsAndStyle(w)
- c.renderEHandlers(w)
- w.Write(strGT)
-
- w.Write(strScriptOp)
+// renderSetupTimerJs renders the Javascript code which sets up the timer.
+// js_vs param holds the values which render Javascript code to be scheduled:
+// setupTimer(compId,"jscode",timeout,repeat,active,reset);
+func (c *timerImpl) renderSetupTimerJs(w Writer, js_vs ...interface{}) {
+ w.Write(strSetupTimerOp)
w.Writev(int(c.id))
w.Write(strComma)
- w.Writev(int(ETypeStateChange))
+ // js param
+ w.Write(strQuote)
+ w.Writevs(js_vs...)
+ w.Write(strQuote)
+ // end of js param
w.Write(strComma)
w.Writev(int(c.timeout / time.Millisecond))
w.Write(strComma)
@@ -146,6 +147,17 @@ func (c *timerImpl) Render(w Writer) {
w.Writev(c.active)
w.Write(strComma)
w.Writev(c.reset)
+ w.Write(strJsFuncCl)
+}
+
+func (c *timerImpl) Render(w Writer) {
+ w.Write(strSpanOp)
+ c.renderAttrsAndStyle(w)
+ c.renderEHandlers(w)
+ w.Write(strGT)
+
+ w.Write(strScriptOp)
+ c.renderSetupTimerJs(w, strJsSendEvtOp, int(ETypeStateChange), strComma, int(c.id), strJsFuncCl)
w.Write(strScriptCl)
w.Write(strSpanCl)
diff --git a/gwu/window.go b/gwu/window.go
index 507cd5e..8ccaee0 100644
--- a/gwu/window.go
+++ b/gwu/window.go
@@ -40,9 +40,13 @@ type Window interface {
SetName(name string)
// AddHeadHtml adds an HTML text which will be included
- // in the HTML head section.
+ // in the HTML section.
AddHeadHtml(html string)
+ // RemoveHeadHtml removes an HTML head text
+ // that was previously added with AddHeadHtml().
+ RemoveHeadHtml(html string)
+
// SetFocusedCompId sets the id of the currently focused component.
SetFocusedCompId(id ID)
@@ -105,6 +109,17 @@ func (w *windowImpl) AddHeadHtml(html string) {
w.heads = append(w.heads, html)
}
+func (w *windowImpl) RemoveHeadHtml(html string) {
+ for i, v := range w.heads {
+ if v == html {
+ old := w.heads
+ w.heads = append(w.heads[:i], w.heads[i+1:]...)
+ old[len(old)-1] = ""
+ return
+ }
+ }
+}
+
func (w *windowImpl) SetFocusedCompId(id ID) {
w.focusedCompId = id
}
@@ -132,14 +147,14 @@ func (c *windowImpl) Render(w Writer) {
if !found {
found = true
- w.Writes("")
+ w.Write(strScriptCl)
}
// And now call panelImpl's Render()
@@ -152,7 +167,7 @@ func (win *windowImpl) RenderWin(w Writer, s Server) {
w.Writes(``)
w.Writees(win.text)
w.Writess(`")
+ w.Write(strScriptOp)
w.Writess("var _pathApp='", s.AppPath(), "';")
+ w.Writess("var _pathSessCheck=_pathApp+'", pathSessCheck, "';")
w.Writess("var _pathWin='", s.AppPath(), win.name, "/';")
w.Writess("var _pathEvent=_pathWin+'", pathEvent, "';")
w.Writess("var _pathRenderComp=_pathWin+'", pathRenderComp, "';")
w.Writess("var _focCompId='", win.focusedCompId.String(), "';")
- w.Writes("")
+ w.Write(strScriptCl)
}
diff --git a/gwu/writer.go b/gwu/writer.go
index 010f610..6eea231 100644
--- a/gwu/writer.go
+++ b/gwu/writer.go
@@ -33,23 +33,27 @@ const cachedInts = 64
// Render methods use these to avoid array allocations
// when converting strings to byte slices in order to write them.
var (
- strSpace = []byte(" ") // " " (space string)
- strQuote = []byte(`"`) // `"` (quotation mark)
- strEqQuote = []byte(`="`) // `="` (equal sign and a quotation mark)
- strComma = []byte(",") // "," (comma string)
- strColon = []byte(":") // ":" (colon string)
- strSemicol = []byte(";") // ";" (semicolon string)
- strLT = []byte("<") // "<" (less than string)
- strGT = []byte(">") // ">" (greater than string)
-
- strSpanOp = []byte("") // ""
- strTableOp = []byte("