From c5b7f6c77d4ab8f9f378a9f7028884f00037dbc2 Mon Sep 17 00:00:00 2001 From: icza Date: Mon, 14 Mar 2016 23:01:16 +0100 Subject: [PATCH] Release 1.1.0. --- .../showcase/showcasecore/showcasecore.go | 35 +++-- gwu/css.go | 3 + gwu/doc.go | 9 +- gwu/event.go | 4 +- gwu/examples_test.go | 6 +- gwu/js.go | 80 +++++++++--- gwu/server.go | 120 ++++++++++++++---- gwu/sess_monitor.go | 98 ++++++++++++++ gwu/session.go | 4 + gwu/style.go | 2 +- gwu/timer.go | 44 ++++--- gwu/window.go | 28 +++- gwu/writer.go | 38 +++--- 13 files changed, 366 insertions(+), 105 deletions(-) create mode 100644 gwu/sess_monitor.go diff --git a/examples/showcase/showcasecore/showcasecore.go b/examples/showcase/showcasecore/showcasecore.go index a8c9f82..4309da9 100644 --- a/examples/showcase/showcasecore/showcasecore.go +++ b/examples/showcase/showcasecore/showcasecore.go @@ -289,7 +289,7 @@ func buildWindowDemo(event gwu.Event) gwu.Comp { p := gwu.NewPanel() p.Add(gwu.NewLabel("The Window represents the whole window, the page inside the browser.")) - p.AddVSpace(5) + p.AddVSpace(20) p.Add(gwu.NewLabel("The Window is the top of the component hierarchy. It is an extension of the Panel.")) return p @@ -586,6 +586,16 @@ func buildLinkDemo(event gwu.Event) gwu.Comp { return p } +func buildSessMonitorDemo(event gwu.Event) gwu.Comp { + p := gwu.NewPanel() + + p.Add(gwu.NewLabel("The SessMonitor component monitors and displays the session timeout and network connectivity without interacting with the session.")) + p.AddVSpace(20) + p.Add(gwu.NewLabel("An example SessMonitor can be viewed on the right side of the header.")) + + return p +} + func buildTimerDemo(event gwu.Event) gwu.Comp { p := gwu.NewPanel() p.SetCellPadding(3) @@ -689,11 +699,15 @@ func buildShowcaseWin(sess gwu.Session) { sess.SetAttr("hiddenPan", hiddenPan) header := gwu.NewHorizontalPanel() - header.Style().SetFullWidth().SetBorderBottom2(2, gwu.BrdStyleSolid, "#777777") - l := gwu.NewLabel("Gowut - Showcase of Features") - l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("120%") - header.Add(l) + header.Style().SetFullWidth().SetBorderBottom2(2, gwu.BrdStyleSolid, "#cccccc") + title := gwu.NewLink("Gowut - Showcase of Features", win.Name()) + title.SetTarget("") + title.Style().SetColor(gwu.ClrBlue).SetFontWeight(gwu.FontWeightBold).SetFontSize("120%").Set("text-decoration", "none") + header.Add(title) header.AddHConsumer() + header.Add(gwu.NewLabel("Session timeout:")) + header.Add(gwu.NewSessMonitor()) + header.AddHSpace(10) header.Add(gwu.NewLabel("Theme:")) themes := gwu.NewListBox([]string{"default", "debug"}) themes.AddEHandlerFunc(func(e gwu.Event) { @@ -703,6 +717,7 @@ func buildShowcaseWin(sess gwu.Session) { header.Add(themes) header.AddHSpace(10) reset := gwu.NewLink("Reset", "#") + reset.Style().SetColor(gwu.ClrBlue) reset.SetTarget("") reset.AddEHandlerFunc(func(e gwu.Event) { e.RemoveSess() @@ -764,12 +779,12 @@ func buildShowcaseWin(sess gwu.Session) { return demo } - links.Style().SetFullHeight().SetBorderRight2(2, gwu.BrdStyleSolid, "#777777") + links.Style().SetFullHeight().SetBorderRight2(2, gwu.BrdStyleSolid, "#cccccc") links.AddVSpace(5) homeDemo := createDemo("Home", buildHomeDemo) selectDemo(homeDemo, nil) links.AddVSpace(5) - l = gwu.NewLabel("Component Palette") + l := gwu.NewLabel("Component Palette") l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("110%") links.Add(l) links.AddVSpace(5) @@ -801,6 +816,7 @@ func buildShowcaseWin(sess gwu.Session) { createDemo("Image", buildImageDemo) createDemo("Label", buildLabelDemo) createDemo("Link", buildLinkDemo) + createDemo("SessMonitor", buildSessMonitorDemo) createDemo("Timer", buildTimerDemo) links.AddVConsumer() setNoWrap(links) @@ -812,7 +828,7 @@ func buildShowcaseWin(sess gwu.Session) { win.CellFmt(content).Style().SetFullSize() footer := gwu.NewHorizontalPanel() - footer.Style().SetFullWidth().SetBorderTop2(2, gwu.BrdStyleSolid, "#777777") + footer.Style().SetFullWidth().SetBorderTop2(2, gwu.BrdStyleSolid, "#cccccc") footer.Add(hiddenPan) footer.AddHConsumer() l = gwu.NewLabel("Copyright © 2013-2016 András Belicza. All rights reserved.") @@ -849,6 +865,9 @@ func (h SessHandler) Removed(s gwu.Session) {} func StartServer(appName string) { // Create GUI server server := gwu.NewServer(appName, "") + for _, headHtml := range extraHeadHtmls { + server.AddRootHeadHtml(headHtml) + } server.AddStaticDir("/asdf", "w:/") server.SetText("Gowut - Showcase of Features") diff --git a/gwu/css.go b/gwu/css.go index 20369d4..15bc446 100644 --- a/gwu/css.go +++ b/gwu/css.go @@ -91,6 +91,9 @@ body {font-family:Arial} .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%} + +.gwu-SessMonitor {} +.gwu-SessMonitor-Expired, .gwu-SessMonitor-Error {color:red} `) staticCss[resNameStaticCss(ThemeDebug)] = []byte(string(staticCss[resNameStaticCss(ThemeDefault)]) + diff --git a/gwu/doc.go b/gwu/doc.go index bfff859..a488373 100644 --- a/gwu/doc.go +++ b/gwu/doc.go @@ -218,8 +218,8 @@ Containers to group and lay out components: 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) + 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 @@ -230,6 +230,7 @@ Other components: Image Label Link + SessMonitor Timer @@ -397,7 +398,7 @@ package gwu // Gowut version information. const ( - GowutVersion = "1.0.0" // Gowut version (major.minor.maintenance[dev]) - GowutReleaseDate = "2016-03-08 CET" // Gowut release date + GowutVersion = "1.1.0" // Gowut version: major.minor.maintenance[-dev] + GowutReleaseDate = "2016-03-14 CET" // Gowut release date GowutRelDateLayout = "2006-01-02 MST" // Gowut release date layout (for time.Parse()) ) diff --git a/gwu/event.go b/gwu/event.go index be89009..8e9853f 100644 --- a/gwu/event.go +++ b/gwu/event.go @@ -157,7 +157,7 @@ const ( KeyWin = 91 KeyNumpad0 = 96 - KeyNumPad9 = 105 + KeyNumpad9 = 105 KeyNumpadMul = 106 KeyNumpadPlus = 107 KeyNumpadMinus = 109 @@ -279,7 +279,7 @@ type Event interface { // forkEvent forks a new Event from this one. // The new event will have a parent pointing to us. // Accessing/changing the session and defining post-event actions in the forked - // event work like if they would be done on this event. + // event works as if they would be done on this event. forkEvent(etype EventType, src Comp) Event } diff --git a/gwu/examples_test.go b/gwu/examples_test.go index d474c70..1b9c001 100644 --- a/gwu/examples_test.go +++ b/gwu/examples_test.go @@ -13,10 +13,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package gwu +package gwu_test import ( - "code/google/com/p/gowut/gwu" + "github.com/icza/gowut/gwu" ) // Example code determining which button was clicked. @@ -32,7 +32,7 @@ func ExampleButton() { // Example code determining what kind of key is involved. func ExampleTextBox() { b := gwu.NewTextBox("") - tb.AddSyncOnETypes(gwu.ETypeKeyUp) // This is here so we will see up-to-date value in the event handler + b.AddSyncOnETypes(gwu.ETypeKeyUp) // 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.ModKeyShift) { // SHIFT is pressed diff --git a/gwu/js.go b/gwu/js.go index 8762d57..87d2161 100644 --- a/gwu/js.go +++ b/gwu/js.go @@ -59,22 +59,22 @@ func init() { function createXmlHttp() { if (window.XMLHttpRequest) // IE7+, Firefox, Chrome, Opera, Safari - return xmlhttp=new XMLHttpRequest(); + return new XMLHttpRequest(); else // IE6, IE5 - return xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); + return new ActiveXObject("Microsoft.XMLHTTP"); } // Send event function se(event, etype, compId, compValue) { - var xmlhttp = createXmlHttp(); + var xhr = createXmlHttp(); - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState == 4 && xmlhttp.status == 200) - procEresp(xmlhttp); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) + procEresp(xhr); } - xmlhttp.open("POST", _pathEvent, true); // asynch call - xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.open("POST", _pathEvent, true); // asynch call + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); var data=""; @@ -112,11 +112,11 @@ function se(event, etype, compId, compValue) { data += "&" + _pKeyCode + "=" + (event.which ? event.which : event.keyCode); } - xmlhttp.send(data); + xhr.send(data); } -function procEresp(xmlhttp) { - var actions = xmlhttp.responseText.split(";"); +function procEresp(xhr) { + var actions = xhr.responseText.split(";"); if (actions.length == 0) { window.alert("No response received!"); @@ -154,13 +154,13 @@ function rerenderComp(compId) { if (!e) // Component removed or not visible (e.g. on inactive tab of TabPanel) return; - var xmlhttp = createXmlHttp(); + var xhr = createXmlHttp(); - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) { // Remember focused comp which might be replaced here: var focusedCompId = document.activeElement.id; - e.outerHTML = xmlhttp.responseText; + e.outerHTML = xhr.responseText; focusComp(focusedCompId); // Inserted JS code is not executed automatically, do it manually: @@ -172,10 +172,10 @@ function rerenderComp(compId) { } } - xmlhttp.open("POST", _pathRenderComp, false); // synch call (if async, browser specific DOM rendering errors may arise) - xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.open("POST", _pathRenderComp, false); // synch call (if async, browser specific DOM rendering errors may arise) + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xmlhttp.send(_pCompId + "=" + compId); + xhr.send(_pCompId + "=" + compId); } // Get selected indices (of an HTML select) @@ -245,11 +245,11 @@ function addonbeforeunload(func) { var timers = new Object(); -function setupTimer(compId, etype, timeout, repeat, active, reset) { +function setupTimer(compId, js, timeout, repeat, active, reset) { var timer = timers[compId]; if (timer != null) { - var changed = timer.timeout != timeout || timer.repeat != repeat || timer.reset != reset; + var changed = timer.js != js || timer.timeout != timeout || timer.repeat != repeat || timer.reset != reset; if (!active || changed) { if (timer.repeat) clearInterval(timer.id); @@ -265,18 +265,56 @@ function setupTimer(compId, etype, timeout, repeat, active, reset) { // Create new timer timers[compId] = timer = new Object(); + timer.js = js; timer.timeout = timeout; timer.repeat = repeat; timer.reset = reset; // Start the timer - var js = "se(null," + etype + "," + compId + ");"; if (timer.repeat) timer.id = setInterval(js, timeout); else timer.id = setTimeout(js, timeout); } +function checkSession(compId) { + var e = document.getElementById(compId); + if (!e) // Component removed or not visible (e.g. on inactive tab of TabPanel) + return; + + var xhr = createXmlHttp(); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) { + var timeoutSec = parseFloat(xhr.responseText); + if (timeoutSec < 60) + e.classList.add("gwu-SessMonitor-Expired"); + else + e.classList.remove("gwu-SessMonitor-Expired"); + var cnvtr = window[e.getAttribute("gwuJsFuncName")]; + e.children[0].innerText = typeof cnvtr === 'function' ? cnvtr(timeoutSec) : convertSessTimeout(timeoutSec); + } + } + + xhr.open("GET", _pathSessCheck, false); // synch call (else we can't catch connection error) + try { + xhr.send(); + e.classList.remove("gwu-SessMonitor-Error"); + } catch (err) { + e.classList.add("gwu-SessMonitor-Error"); + e.children[0].innerText = "CONN ERR"; + } +} + +function convertSessTimeout(sec) { + if (sec <= 0) + return "Expired!"; + else if (sec < 60) + return "<1 min"; + else + return "~" + Math.round(sec / 60) + " min"; +} + // INITIALIZATION addonload(function() { diff --git a/gwu/server.go b/gwu/server.go index 8f072f9..bda7d72 100644 --- a/gwu/server.go +++ b/gwu/server.go @@ -31,6 +31,7 @@ import ( // Internal path constants. const ( pathStatic = "_gwu_static/" // App path-relative path for GWU static contents. + pathSessCheck = "_sess_ch" // App path-relative path for checking session (without registering access) pathEvent = "e" // Window-relative path for sending events pathRenderComp = "rc" // Window-relative path for rendering a component ) @@ -74,6 +75,10 @@ type SessionHandler interface { Removed(sess Session) } +// Function type that handles the application root (when no window name is specified). +// sess is the shared, public session if no private session is created. +type AppRootHandlerFunc func(w http.ResponseWriter, r *http.Request, sess Session) + // Server interface defines the GUI server which handles sessions, // renders the windows, components and handles event dispatching. type Server interface { @@ -165,6 +170,25 @@ type Server interface { // Pass nil to disable logging. This is the default. SetLogger(logger *log.Logger) + // Logger returns the logger that is used to log incoming requests. + Logger() *log.Logger + + // AddRootHeadHtml adds an HTML text which will be included + // in the HTML 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

") + w.Writes(" - Window list") + w.Writess(s.rootHeads...) + w.Writes("

") w.Writees(s.text) w.Writes(" - 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("") // "" - strTD = []byte("") // "" - strTR = []byte("") // "" - strTDOp = []byte("") // ">" (greater than string) + strParenCl = []byte(")") // ")" (closing parenthesis) + strJsFuncCl = []byte(");") // ");" (closing parenthesis and a semicolon) + + strSpanOp = []byte("") // "" + strTableOp = []byte("") // "" + strTD = []byte("") // "" + strTR = []byte("") // "" + strTDOp = []byte("") // "") // "" strStyle = []byte(` style="`) // ` style="` strClass = []byte(` class="`) // ` class="`