diff --git a/.gitignore b/.gitignore index d7b2cce..e0836f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ result twfinder +twfinder.exe config.json .out diff --git a/Ubuntu_Users/config.json b/Ubuntu_Users/config.json index 4b81a0f..9b8aaa9 100644 --- a/Ubuntu_Users/config.json +++ b/Ubuntu_Users/config.json @@ -4,24 +4,34 @@ "ACCESS_TOKEN": "", "ACCESS_TOKEN_SECRET": "", "SEARCH_USER": "", + "TWITTER_LIST": { + "SAVE_LIST": true, + "LIST_NAME": "New-list", + "IS_PUBLIC": false, + "LIST_DESCRIPTION": "my new list" + }, "SEARCH_CRITERIA": { "SEARCH_HANDLE_CONTEXT": [ "adam", - "erik" + "erik", + "-ali" ], "SEARCH_NAME_CONTEXT": [ "dr", - "mr" + "mr", + "-eng" ], "SEARCH_BIO_CONTEXT": [ "Develper", "CEO", "Linux", - "technology" + "technology", + "-doctor" ], "SEARCH_LOCATION_CONTEXT": [ "Berlin", - "Gothenburg" + "Gothenburg", + "-cairo" ], "FOLLOWERS_COUNT_BETWEEN": { "FROM": 0, @@ -47,10 +57,10 @@ "FROM": "2000-09-22T12:42:31Z", "TO": "2018-09-22T12:42:31Z" }, - "VERIFIED": true + "VERIFIED": false }, "FOLLOWING": true, - "FOLLOWERS": false, + "FOLLOWERS": true, "RECURSIVE": true, "CONTINUE_SUCCESS_USERS_ONLY": true } \ No newline at end of file diff --git a/Windows_Users/config.json b/Windows_Users/config.json index 4b81a0f..9b8aaa9 100644 --- a/Windows_Users/config.json +++ b/Windows_Users/config.json @@ -4,24 +4,34 @@ "ACCESS_TOKEN": "", "ACCESS_TOKEN_SECRET": "", "SEARCH_USER": "", + "TWITTER_LIST": { + "SAVE_LIST": true, + "LIST_NAME": "New-list", + "IS_PUBLIC": false, + "LIST_DESCRIPTION": "my new list" + }, "SEARCH_CRITERIA": { "SEARCH_HANDLE_CONTEXT": [ "adam", - "erik" + "erik", + "-ali" ], "SEARCH_NAME_CONTEXT": [ "dr", - "mr" + "mr", + "-eng" ], "SEARCH_BIO_CONTEXT": [ "Develper", "CEO", "Linux", - "technology" + "technology", + "-doctor" ], "SEARCH_LOCATION_CONTEXT": [ "Berlin", - "Gothenburg" + "Gothenburg", + "-cairo" ], "FOLLOWERS_COUNT_BETWEEN": { "FROM": 0, @@ -47,10 +57,10 @@ "FROM": "2000-09-22T12:42:31Z", "TO": "2018-09-22T12:42:31Z" }, - "VERIFIED": true + "VERIFIED": false }, "FOLLOWING": true, - "FOLLOWERS": false, + "FOLLOWERS": true, "RECURSIVE": true, "CONTINUE_SUCCESS_USERS_ONLY": true } \ No newline at end of file diff --git a/Windows_Users/twfinder.exe b/Windows_Users/twfinder.exe index 3fc5c12..c63ea64 100755 Binary files a/Windows_Users/twfinder.exe and b/Windows_Users/twfinder.exe differ diff --git a/config.tmp b/config.tmp index 7e0bf3c..fe8ab5d 100644 --- a/config.tmp +++ b/config.tmp @@ -4,24 +4,28 @@ "ACCESS_TOKEN": "", "ACCESS_TOKEN_SECRET": "", "SEARCH_USER": "", + "TWITTER_LIST": { + "SAVE_LIST": true, + "LIST_NAME": "", + "IS_PUBLIC": false, + "LIST_DESCRIPTION": "" + }, "SEARCH_CRITERIA": { "SEARCH_HANDLE_CONTEXT": [ - "adam", - "erik" + "a", + "-a" ], "SEARCH_NAME_CONTEXT": [ - "dr", - "mr" + "a", + "-a" ], "SEARCH_BIO_CONTEXT": [ - "Developer", - "CEO", - "Linux", - "technology" + "a", + "-a" ], "SEARCH_LOCATION_CONTEXT": [ - "Berlin", - "Gothenburg" + "a", + "-a" ], "FOLLOWERS_COUNT_BETWEEN": { "FROM": 0, @@ -50,7 +54,7 @@ "VERIFIED": true }, "FOLLOWING": true, - "FOLLOWERS": false, + "FOLLOWERS": true, "RECURSIVE": true, "RECURSIVE_SUCCESS_USERS_ONLY": true } \ No newline at end of file diff --git a/config/configuration.go b/config/configuration.go index 708f0f9..47994aa 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -1,19 +1,15 @@ package config import ( + "encoding/json" "fmt" - "sync" + "os" "time" "github.com/kelseyhightower/envconfig" "github.com/tarekbadrshalaan/goStuff/configuration" ) -var ( - readConfigOnce sync.Once - internalConfig Config -) - // Config : application configuration type Config struct { ConsumerKey string `json:"CONSUMER_KEY" envconfig:"CONSUMER_KEY"` @@ -21,6 +17,7 @@ type Config struct { AccessToken string `json:"ACCESS_TOKEN" envconfig:"ACCESS_TOKEN"` AccessTokenSecret string `json:"ACCESS_TOKEN_SECRET" envconfig:"ACCESS_TOKEN_SECRET"` SearchUser string `json:"SEARCH_USER" envconfig:"SEARCH_USER"` + TwitterList TwitterList `json:"TWITTER_LIST" envconfig:"TWITTER_LIST"` SearchCriteria SearchCriteria `json:"SEARCH_CRITERIA" envconfig:"SEARCH_CRITERIA"` Following bool `json:"FOLLOWING" envconfig:"FOLLOWING"` Followers bool `json:"FOLLOWERS" envconfig:"FOLLOWERS"` @@ -43,6 +40,14 @@ type SearchCriteria struct { Verified bool `json:"VERIFIED" envconfig:"VERIFIED"` } +// TwitterList : twitter list to store the result +type TwitterList struct { + SaveList bool `json:"SAVE_LIST" envconfig:"SAVE_LIST"` + Name string `json:"LIST_NAME" envconfig:"LIST_NAME"` + IsPublic bool `json:"IS_PUBLIC" envconfig:"IS_PUBLIC"` + Description string `json:"LIST_DESCRIPTION" envconfig:"LIST_DESCRIPTION"` +} + // FromToNumber : From-To-Number type FromToNumber struct { From int64 `json:"FROM" envconfig:"FROM"` @@ -55,20 +60,95 @@ type FromToDate struct { To time.Time `json:"TO" envconfig:"TO"` } -// Configuration : get configuration based on json file or environment variables -func Configuration() Config { - readConfigOnce.Do(func() { - err := configuration.JSON("config.json", &internalConfig) - if err == nil { - return - } - fmt.Println(err) +var ( + configPath = "" + internalConfig = Config{} + + defaultConfigPath = "config.json" + defaultConfiguration = Config{ + ConsumerKey: "", + ConsumerSecret: "", + AccessToken: "", + AccessTokenSecret: "", + SearchUser: "", + TwitterList: TwitterList{ + SaveList: true, + Name: fmt.Sprintf("Twfinder-%v", time.Now()), + Description: "Twitter finder list (default name)", + IsPublic: false, + }, + SearchCriteria: SearchCriteria{ + SearchHandleContext: []string{"a", "-a"}, + SearchNameContext: []string{"a", "-a"}, + SearchBioContext: []string{"a", "-a"}, + SearchLocationContext: []string{"a", "-a"}, + FollowersCountBetween: FromToNumber{From: 0, To: 100000}, + FollowingCountBetween: FromToNumber{From: 0, To: 100000}, + LikesCountBetween: FromToNumber{From: 0, To: 100000}, + TweetsCountBetween: FromToNumber{From: 0, To: 100000}, + ListsCountBetween: FromToNumber{From: 0, To: 100000}, + JoinedBetween: FromToDate{From: time.Time{}, To: time.Now()}, + Verified: false, + }, + Following: true, + Followers: true, + Recursive: true, + RecursiveSuccessUsersOnly: true, + } +) - err = envconfig.Process("", &internalConfig) - if err != nil { - err = fmt.Errorf("Error while initiating app configuration : %v", err) - panic(err) - } - }) +// BuildConfiguration : cp Configuration path +func BuildConfiguration(cp string) { + if cp != "" { + configPath = cp + } else { + configPath = defaultConfigPath + } + + // get configuration from json file + if err := configuration.JSON(configPath, &internalConfig); err == nil { + return + } + // get configuration from environment variables + if err := envconfig.Process("", internalConfig); err == nil { + return + } + fmt.Printf( + "Error occurred during build the configuration from '%v' & environment variables"+ + "\n <<>>\n", configPath) + internalConfig = defaultConfiguration +} + +// Configuration : get the current available configuration +func Configuration() Config { + // todo check if internalConfig is nil, and rebuild return internalConfig } + +// SetConfiguration : set the configuration +func SetConfiguration(c Config) { + internalConfig = c +} + +// SaveConfiguration : the current available configuration +// cp Configuration file path to save +func SaveConfiguration(cp string) error { + if cp == "" { + // use current configPath + cp = configPath + } + f, err := os.OpenFile(cp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + fmt.Printf("Error occurred during open file %v %v\n", cp, err) + return err + } + defer f.Close() + + jsonToSave, err := json.MarshalIndent(internalConfig, "", " ") + if err != nil { + fmt.Printf("Error occurred during Marshal json %v\n", err) + return err + } + f.Write(jsonToSave) + return nil +} diff --git a/finder/finder.go b/finder/finder.go index dc5d4c9..7153f2f 100644 --- a/finder/finder.go +++ b/finder/finder.go @@ -4,7 +4,7 @@ import ( "strings" "sync" "twfinder/config" - helper "twfinder/helpers" + "twfinder/helper" "github.com/tarekbadrshalaan/anaconda" ) @@ -44,8 +44,12 @@ func CheckUserCriteria(user *anaconda.User) bool { } // BuildSearchCriteria : build interanl search criteria -func BuildSearchCriteria(c config.Config) { +func BuildSearchCriteria() { + c := config.Configuration() buildFinderOnce.Do(func() { + // all accounts in the system should not be protected + intFinder.filters = append(intFinder.filters, protectedFilter) + intFinder.searchHandleContext = c.SearchCriteria.SearchHandleContext if len(intFinder.searchHandleContext) > 1 { intFinder.filters = append(intFinder.filters, handleFilter) @@ -226,7 +230,7 @@ func listsFilter(u *anaconda.User) (string, bool) { func joinedFilter(u *anaconda.User) (string, bool) { match := true - joinedUnx := helper.StringtoDate(u.CreatedAt).Unix() + joinedUnx := helper.StringtoDate(u.CreatedAt, "").Unix() if !intFinder.joinedBetween.From.IsZero() { if joinedUnx <= intFinder.joinedBetween.From.Unix() { match = false @@ -247,3 +251,11 @@ func verifiedFilter(u *anaconda.User) (string, bool) { } return "VERIFIED", match } + +func protectedFilter(u *anaconda.User) (string, bool) { + match := true + if u.Protected { + match = false + } + return "PROTECTED", match +} diff --git a/go.mod b/go.mod index 1793ed2..5a34ded 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect github.com/kelseyhightower/envconfig v1.4.0 github.com/natefinch/lumberjack v2.0.0+incompatible - github.com/tarekbadrshalaan/anaconda v2.0.0+incompatible + github.com/tarekbadrshalaan/anaconda v2.0.2+incompatible github.com/tarekbadrshalaan/goStuff v1.1.1 go.uber.org/zap v1.9.1 golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect diff --git a/go.sum b/go.sum index 8e5a04f..fb4b52d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,16 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 h1:r+EmXjfPosKO4wfiMLe1XQictsIlhErTufbWUsjOTZs= github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7/go.mod h1:b2EuEMLSG9q3bZ95ql1+8oVqzzrTNSiOQqSXWFBzxeI= github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78= github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI= github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw= @@ -13,30 +18,50 @@ github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKB github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/zerolog v1.13.0 h1:hSNcYHyxDWycfePW7pUI8swuFkcSMPKh3E63Pokg1Hk= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/tarekbadrshalaan/anaconda v1.0.0 h1:tGolugTQpiE0+A2sjuJ7gRuC+jkZT2CkQ7wVuprbGdk= github.com/tarekbadrshalaan/anaconda v2.0.0+incompatible h1:Jq8xsYn864R1aSEkehVOz3it/S5wPFiLYmbIczKy5JI= github.com/tarekbadrshalaan/anaconda v2.0.0+incompatible/go.mod h1:JREKfP/TiKETGgsPxR5Ww+9Uk5HjxioOrDnindI/5c4= +github.com/tarekbadrshalaan/anaconda v2.0.1+incompatible h1:HXNODUqeBHwLRLf0akNP4ynPUy0Ez0Zaz8QgTLilpgI= +github.com/tarekbadrshalaan/anaconda v2.0.1+incompatible/go.mod h1:JREKfP/TiKETGgsPxR5Ww+9Uk5HjxioOrDnindI/5c4= +github.com/tarekbadrshalaan/anaconda v2.0.2+incompatible h1:sjiarNyrwtjdEzpyw8XNaRMYlfCN9HvN7KK3ymKxqIM= +github.com/tarekbadrshalaan/anaconda v2.0.2+incompatible/go.mod h1:JREKfP/TiKETGgsPxR5Ww+9Uk5HjxioOrDnindI/5c4= github.com/tarekbadrshalaan/goStuff v1.1.1 h1:KJVsMNTHfSe4/5oQx3WckFtTDqS6Y86dN1EmtiGI96o= github.com/tarekbadrshalaan/goStuff v1.1.1/go.mod h1:O+cIODhYRvpEPh4JjgOB2YggxC8rbolYEMkpIj6ilvY= +github.com/uber-go/zap v1.9.1 h1:CZN7Pmty0PLtqZEi3N8VSI0Us8b0RL5ah6l1jOH3ZJ0= github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -44,14 +69,20 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20181022074355-8b8824e799c8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/gui/frontend/configuration.go b/gui/frontend/configuration.go new file mode 100644 index 0000000..146219d --- /dev/null +++ b/gui/frontend/configuration.go @@ -0,0 +1,384 @@ +package frontend + +import ( + "math/rand" + "strconv" + "time" + "twfinder/config" + "twfinder/gui/server" + "twfinder/logger" +) + +// newStrTxtLblPanel : create new TextBox with lable in Horizontal mode +// strInput : text input for TextBox +func newStrTxtLblPanel(lbltxt string, input *string, isPassword bool) server.Panel { + pan := server.NewHorizontalPanel() + lbl := server.NewLabel(lbltxt) + pan.Add(lbl) + + txtbox := server.NewTextBox(*input) + if isPassword { + txtbox = server.NewPasswBox(*input) + } + txtbox.AddEHandlerFunc(func(e server.Event) { + *input = txtbox.Text() + }, server.ETypeChange) + pan.Add(txtbox) + + return pan +} + +// newIntTxtLblPanel : create new TextBox with lable in Horizontal mode +// intInput : int input for TextBox +func newIntTxtLblPanel(lbltxt string, intInput *int64) server.Panel { + pan := server.NewHorizontalPanel() + + lbl := server.NewLabel(lbltxt) + pan.Add(lbl) + + inputStr := strconv.FormatInt(*intInput, 10) + txtbox := server.NewTextBox(inputStr) + txtbox.AddEHandlerFunc(func(e server.Event) { + eventText := txtbox.Text() + i, err := strconv.ParseInt(eventText, 10, 64) + if err != nil { + logger.Errorf("error occurred during conver string to number input:%v error:%v", eventText, err) + return + } + *intInput = i + }, server.ETypeChange) + pan.Add(txtbox) + + return pan +} + +// newDatepickerLblPanel : create new datepicker with lable in Horizontal mode +// dateInput : date input for datepicker +func newDatepickerLblPanel(lbltxt string, dateInput *time.Time) server.Panel { + pan := server.NewHorizontalPanel() + lbl := server.NewLabel(lbltxt) + pan.Add(lbl) + mydatepicker := server.NewDatepicker(*dateInput) + mydatepicker.AddEHandlerFunc(func(e server.Event) { + *dateInput = mydatepicker.Date() + }, server.ETypeChange) + pan.Add(mydatepicker) + return pan +} + +// newIntTextBoxFromTo : return new two integar only text box +// to handle from/to input +func newIntTextBoxFromTo(panalTitle string, from *int64, to *int64) server.Panel { + // main + mainPanal := server.NewVerticalPanel() + + // header + headerPanel := server.NewHorizontalPanel() + headerPanellbl := server.NewLabel(panalTitle) + headerPanel.Add(headerPanellbl) + + // body + bodyPanelfunc := func(from *int64, to *int64) server.Panel { + pan := server.NewVerticalPanel() + fromPan := newIntTxtLblPanel("From", from) + pan.Add(fromPan) + toPan := newIntTxtLblPanel("To", to) + pan.Add(toPan) + return pan + } + bodyPanel := bodyPanelfunc(from, to) + + // btns + headerPanelAddBtn := server.NewButton("+") + headerPanelRemoveBtn := server.NewButton("-") + + // addbtn + headerPanelAddBtn.AddSyncOnETypes(server.ETypeClick) + headerPanelAddBtn.AddEHandlerFunc(func(e server.Event) { + bodyPanel = bodyPanelfunc(from, to) + mainPanal.Add(bodyPanel) + headerPanel.Insert(headerPanelRemoveBtn, headerPanel.CompsCount()) + headerPanel.Remove(headerPanelAddBtn) + e.MarkDirty(mainPanal, headerPanel) + }, server.ETypeClick) + + // removebtn + headerPanelRemoveBtn.AddSyncOnETypes(server.ETypeClick) + headerPanelRemoveBtn.AddEHandlerFunc(func(e server.Event) { + // update values + *from = 0 + *to = 0 + mainPanal.Remove(bodyPanel) + headerPanel.Insert(headerPanelAddBtn, headerPanel.CompsCount()) + headerPanel.Remove(headerPanelRemoveBtn) + e.MarkDirty(mainPanal, headerPanel) + }, server.ETypeClick) + + mainPanal.Add(headerPanel) + + if *from > 0 || *to > 0 { + headerPanel.Add(headerPanelRemoveBtn) + mainPanal.Add(bodyPanel) + } else { + headerPanel.Add(headerPanelAddBtn) + } + + return mainPanal +} + +// newDatepickerFromTo : return new two Datepicker +// to handle from/to input +func newDatepickerFromTo(panalTitle string, from *time.Time, to *time.Time) server.Panel { + // main + mainPanal := server.NewVerticalPanel() + + // header + headerPanel := server.NewHorizontalPanel() + headerPanellbl := server.NewLabel(panalTitle) + headerPanel.Add(headerPanellbl) + + // body + bodyPanelfunc := func(from *time.Time, to *time.Time) server.Panel { + pan := server.NewVerticalPanel() + fromPan := newDatepickerLblPanel("From", from) + pan.Add(fromPan) + toPan := newDatepickerLblPanel("To", to) + pan.Add(toPan) + return pan + } + bodyPanel := bodyPanelfunc(from, to) + + // btns + headerPanelAddBtn := server.NewButton("+") + headerPanelRemoveBtn := server.NewButton("-") + + // addbtn + headerPanelAddBtn.AddSyncOnETypes(server.ETypeClick) + headerPanelAddBtn.AddEHandlerFunc(func(e server.Event) { + bodyPanel = bodyPanelfunc(from, to) + mainPanal.Add(bodyPanel) + headerPanel.Insert(headerPanelRemoveBtn, headerPanel.CompsCount()) + headerPanel.Remove(headerPanelAddBtn) + e.MarkDirty(mainPanal, headerPanel) + }, server.ETypeClick) + + // removebtn + headerPanelRemoveBtn.AddSyncOnETypes(server.ETypeClick) + headerPanelRemoveBtn.AddEHandlerFunc(func(e server.Event) { + // update values + *from = time.Time{} + *to = time.Time{} + mainPanal.Remove(bodyPanel) + headerPanel.Insert(headerPanelAddBtn, headerPanel.CompsCount()) + headerPanel.Remove(headerPanelRemoveBtn) + e.MarkDirty(mainPanal, headerPanel) + }, server.ETypeClick) + + mainPanal.Add(headerPanel) + + if !from.IsZero() || !to.IsZero() { + headerPanel.Add(headerPanelRemoveBtn) + mainPanal.Add(bodyPanel) + } else { + headerPanel.Add(headerPanelAddBtn) + } + + return mainPanal +} + +// newArrTextBoxPanal : return new text box with two way binding +func newArrTextBoxPanal(panalTitle string, inputArr []string) (server.Panel, map[int]string) { + + // Initialize main items + mainMap := make(map[int]string) + mainPanal := server.NewVerticalPanel() + + // header + headerPanel := server.NewHorizontalPanel() + headerPanellbl := server.NewLabel(panalTitle) + headerPanel.Add(headerPanellbl) + headerPanelAddbtn := server.NewButton("+") + headerPanelAddbtn.AddSyncOnETypes(server.ETypeClick) + headerPanel.Add(headerPanelAddbtn) + mainPanal.Insert(headerPanel, mainPanal.CompsCount()) + + bodyPanel := func(txt string, e server.Event) server.Panel { + bodyPanel := server.NewHorizontalPanel() + elementID := rand.Intn(1000000000000) + mainMap[elementID] = txt + elementTxtbox := server.NewTextBox(txt) + elementTxtbox.AddEHandlerFunc(func(e server.Event) { + mainMap[elementID] = elementTxtbox.Text() + }, server.ETypeChange) + rmvBtn := server.NewButton("-") + rmvBtn.AddSyncOnETypes(server.ETypeClick) + rmvBtn.AddEHandlerFunc(func(e server.Event) { + mainPanal.Remove(bodyPanel) + e.MarkDirty(mainPanal) + delete(mainMap, elementID) + }, server.ETypeClick) + bodyPanel.Insert(rmvBtn, 0) + bodyPanel.Insert(elementTxtbox, 0) + if e != nil { + e.MarkDirty(bodyPanel) + } + return bodyPanel + } + + // handle existing values + for _, v := range inputArr { + bodyPanel := bodyPanel(v, nil) + mainPanal.Insert(bodyPanel, mainPanal.CompsCount()) + } + + // handel add new panel + headerPanelAddbtn.AddEHandlerFunc(func(e server.Event) { + bodyPanel := bodyPanel("", e) + mainPanal.Insert(bodyPanel, mainPanal.CompsCount()) + e.MarkDirty(mainPanal) + }, server.ETypeClick) + + return mainPanal, mainMap +} + +// newCheckPanel : create new checkbos with lable in Horizontal mode +// state : State of the checkbox +func newCheckPanel(lbltxt string, state *bool) server.Panel { + pan := server.NewHorizontalPanel() + lbl := server.NewLabel(lbltxt) + pan.Add(lbl) + checkBox := server.NewCheckBox("") + checkBox.SetState(*state) + checkBox.AddEHandlerFunc(func(e server.Event) { + *state = checkBox.State() + }, server.ETypeChange, server.ETypeKeyUp) + pan.Add(checkBox) + return pan +} + +// ConfigWin : build configuration window with all required elements +func ConfigWin() server.Window { + twitterConfig := config.Configuration() + // Create and build the configuration window + win := server.NewWindow("configuration", "Configuration - Twitter Finder App") + win.Style().SetFullWidth() + win.SetHAlign(server.HACenter) + win.SetCellPadding(2) + + win.Add(server.NewLabel("Configuration builder")) + + consumerKeyPan := newStrTxtLblPanel("Consumer Key", &twitterConfig.ConsumerKey, true) + win.Add(consumerKeyPan) + + consumerSecretPan := newStrTxtLblPanel("Consumer Secret", &twitterConfig.ConsumerSecret, true) + win.Add(consumerSecretPan) + + accessTokenPan := newStrTxtLblPanel("Access Token", &twitterConfig.AccessToken, true) + win.Add(accessTokenPan) + + accessTokenSecretPan := newStrTxtLblPanel("Access Token Secret", &twitterConfig.AccessTokenSecret, true) + win.Add(accessTokenSecretPan) + + searchUserPan := newStrTxtLblPanel("Search User", &twitterConfig.SearchUser, false) + win.Add(searchUserPan) + // + // --- + // + win.Add(server.NewLabel("Twitter list")) + twSaveListCb := newCheckPanel("Save the result to list", &twitterConfig.TwitterList.SaveList) + win.Add(twSaveListCb) + twitterListNamePan := newStrTxtLblPanel("Name", &twitterConfig.TwitterList.Name, false) + win.Add(twitterListNamePan) + twitterListDescPan := newStrTxtLblPanel("Description", &twitterConfig.TwitterList.Description, false) + win.Add(twitterListDescPan) + twPublicListCb := newCheckPanel("public list", &twitterConfig.TwitterList.IsPublic) + win.Add(twPublicListCb) + // + // --- + // + win.Add(server.NewLabel("Search Criteria")) + // searchHandlePanal + searchHandlePanal, handleMainMap := newArrTextBoxPanal("Search Handle Context", twitterConfig.SearchCriteria.SearchHandleContext) + win.Add(searchHandlePanal) + // searchNamePanal + searchNamePanal, nameMainMap := newArrTextBoxPanal("Search Name Context", twitterConfig.SearchCriteria.SearchNameContext) + win.Add(searchNamePanal) + // searchBioPanal + searchBioPanal, bioMainMap := newArrTextBoxPanal("Search Bio Context", twitterConfig.SearchCriteria.SearchBioContext) + win.Add(searchBioPanal) + // searchLocationPanal + searchLocationPanal, locationMainMap := newArrTextBoxPanal("Search Location Context", twitterConfig.SearchCriteria.SearchLocationContext) + win.Add(searchLocationPanal) + // followersPanal + followersPanal := newIntTextBoxFromTo("Followers Count Between", &twitterConfig.SearchCriteria.FollowersCountBetween.From, &twitterConfig.SearchCriteria.FollowersCountBetween.To) + win.Add(followersPanal) + // followingPanal + followingPanal := newIntTextBoxFromTo("Following Count Between", &twitterConfig.SearchCriteria.FollowingCountBetween.From, &twitterConfig.SearchCriteria.FollowingCountBetween.To) + win.Add(followingPanal) + // likesPanal + likesPanal := newIntTextBoxFromTo("Likes Count Between", &twitterConfig.SearchCriteria.LikesCountBetween.From, &twitterConfig.SearchCriteria.LikesCountBetween.To) + win.Add(likesPanal) + // tweetsPanal + tweetsPanal := newIntTextBoxFromTo("Tweets Count Between", &twitterConfig.SearchCriteria.TweetsCountBetween.From, &twitterConfig.SearchCriteria.TweetsCountBetween.To) + win.Add(tweetsPanal) + // listsPanal + listsPanal := newIntTextBoxFromTo("Lists Count Between", &twitterConfig.SearchCriteria.ListsCountBetween.From, &twitterConfig.SearchCriteria.ListsCountBetween.To) + win.Add(listsPanal) + + // JoinDatePanal + JoinDatePanal := newDatepickerFromTo("Joined Date Between", &twitterConfig.SearchCriteria.JoinedBetween.From, &twitterConfig.SearchCriteria.JoinedBetween.To) + win.Add(JoinDatePanal) + + // verifiedCb + verifiedCb := newCheckPanel("Verified", &twitterConfig.SearchCriteria.Verified) + win.Add(verifiedCb) + // + // --- + // + // followingCb + followingCb := newCheckPanel("Following", &twitterConfig.Following) + win.Add(followingCb) + // followersCb + followersCb := newCheckPanel("Followers", &twitterConfig.Followers) + win.Add(followersCb) + // recursiveCb + recursiveCb := newCheckPanel("Recursive", &twitterConfig.Recursive) + win.Add(recursiveCb) + // recursiveSuccessUsersOnlyCb + recursiveSuccessUsersOnlyCb := newCheckPanel("Recursive Success Users Only", &twitterConfig.RecursiveSuccessUsersOnly) + win.Add(recursiveSuccessUsersOnlyCb) + // + // --- + // + saveConfigBtn := server.NewButton("Save & Exit") + saveConfigBtn.AddEHandlerFunc(func(e server.Event) { + twitterConfig.SearchCriteria.SearchHandleContext = nil + for _, v := range handleMainMap { + twitterConfig.SearchCriteria.SearchHandleContext = append(twitterConfig.SearchCriteria.SearchHandleContext, v) + } + twitterConfig.SearchCriteria.SearchNameContext = nil + for _, v := range nameMainMap { + twitterConfig.SearchCriteria.SearchNameContext = append(twitterConfig.SearchCriteria.SearchNameContext, v) + } + twitterConfig.SearchCriteria.SearchBioContext = nil + for _, v := range bioMainMap { + twitterConfig.SearchCriteria.SearchBioContext = append(twitterConfig.SearchCriteria.SearchBioContext, v) + } + twitterConfig.SearchCriteria.SearchLocationContext = nil + for _, v := range locationMainMap { + twitterConfig.SearchCriteria.SearchLocationContext = append(twitterConfig.SearchCriteria.SearchLocationContext, v) + } + + config.SetConfiguration(twitterConfig) + err := config.SaveConfiguration("") + if err != nil { + logger.Error(err) + return + } + e.ReloadWin("home") + logger.Info("Configuration has been successfully updated") + }, server.ETypeClick) + + win.Add(saveConfigBtn) + return win +} diff --git a/gui/frontend/finder.go b/gui/frontend/finder.go new file mode 100644 index 0000000..eee7b30 --- /dev/null +++ b/gui/frontend/finder.go @@ -0,0 +1,24 @@ +package frontend + +import ( + "twfinder/gui/server" +) + +// FinderWin : +func FinderWin() server.Window { + // Create and build a window + win := server.NewWindow("finder", "Finder - Twitter Finder App") + win.Style().SetFullWidth() + win.SetHAlign(server.HACenter) + win.SetCellPadding(2) + lbl := server.NewLabel("Collecting Data") + win.Add(lbl) + lodImg := server.NewHTML(`

via GIPHY

`) + win.Add(lodImg) + bckhomBtn := server.NewButton("back to home") + bckhomBtn.AddEHandlerFunc(func(e server.Event) { + e.ReloadWin("home") + }, server.ETypeClick) + win.Add(bckhomBtn) + return win +} diff --git a/gui/frontend/gui.go b/gui/frontend/gui.go new file mode 100644 index 0000000..b249216 --- /dev/null +++ b/gui/frontend/gui.go @@ -0,0 +1,104 @@ +package frontend + +import ( + "fmt" + "twfinder/gui/server" +) + +type myButtonHandler struct { + counter int + text string +} + +func (h *myButtonHandler) HandleEvent(e server.Event) { + if b, isButton := e.Src().(server.Button); isButton { + b.SetText(b.Text() + h.text) + h.counter++ + b.SetToolTip(fmt.Sprintf("You've clicked %d times!", h.counter)) + e.MarkDirty(b) + } +} + +// Gui : +func Gui(name string) server.Window { + + // Create and build a window + // win := server.NewWindow("main", "Test GUI Window") + win := server.NewWindow(name, "Test GUI Window") + win.Style().SetFullWidth() + win.SetHAlign(server.HACenter) + win.SetCellPadding(2) + + // Button which changes window content + // win.Add(server.NewLabel("I'm a label! Try clicking on the button=>")) + // btn := server.NewButton("Click me") + // btn.AddEHandler(&myButtonHandler{text: ":-)"}, server.ETypeClick) + // win.Add(btn) + // btnsPanel := server.NewNaturalPanel() + // btn.AddEHandlerFunc(func(e server.Event) { + // // Create and add a new button... + // newbtn := server.NewButton(fmt.Sprintf("Extra #%d", btnsPanel.CompsCount())) + // newbtn.AddEHandlerFunc(func(e server.Event) { + // btnsPanel.Remove(newbtn) // ...which removes itself when clicked + // e.MarkDirty(btnsPanel) + // }, server.ETypeClick) + // btnsPanel.Insert(newbtn, 0) + // e.MarkDirty(btnsPanel) + // }, server.ETypeClick) + // win.Add(btnsPanel) + + // ListBox examples + p := server.NewHorizontalPanel() + p.Style().SetBorder2(1, server.BrdStyleSolid, server.ClrBlack) + p.SetCellPadding(2) + p.Add(server.NewLabel("A drop-down list being")) + widelb := server.NewListBox([]string{"50", "100", "150", "200", "250"}) + widelb.Style().SetWidth("50") + widelb.AddEHandlerFunc(func(e server.Event) { + widelb.Style().SetWidth(widelb.SelectedValue() + "px") + e.MarkDirty(widelb) + }, server.ETypeChange) + p.Add(widelb) + p.Add(server.NewLabel("pixel wide. And a multi-select list:")) + listBox := server.NewListBox([]string{"First", "Second", "Third", "Forth", "Fifth", "Sixth"}) + listBox.SetMulti(true) + listBox.SetRows(4) + p.Add(listBox) + countLabel := server.NewLabel("Selected count: 0") + listBox.AddEHandlerFunc(func(e server.Event) { + countLabel.SetText(fmt.Sprintf("Selected count: %d", len(listBox.SelectedIndices()))) + e.MarkDirty(countLabel) + }, server.ETypeChange) + p.Add(countLabel) + win.Add(p) + + // Self-color changer check box + greencb := server.NewCheckBox("I'm a check box. When checked, I'm green!") + greencb.AddEHandlerFunc(func(e server.Event) { + if greencb.State() { + greencb.Style().SetBackground(server.ClrGreen) + } else { + greencb.Style().SetBackground("") + } + e.MarkDirty(greencb) + }, server.ETypeClick) + win.Add(greencb) + + // TextBox with echo + p = server.NewHorizontalPanel() + p.Add(server.NewLabel("Enter your name:")) + tb := server.NewTextBox("") + tb.AddSyncOnETypes(server.ETypeKeyUp) + p.Add(tb) + p.Add(server.NewLabel("You entered:")) + nameLabel := server.NewLabel("") + nameLabel.Style().SetColor(server.ClrRed) + tb.AddEHandlerFunc(func(e server.Event) { + nameLabel.SetText(tb.Text()) + e.MarkDirty(nameLabel) + }, server.ETypeChange, server.ETypeKeyUp) + p.Add(nameLabel) + win.Add(p) + + return win +} diff --git a/gui/frontend/home.go b/gui/frontend/home.go new file mode 100644 index 0000000..03b2514 --- /dev/null +++ b/gui/frontend/home.go @@ -0,0 +1,63 @@ +package frontend + +import ( + "twfinder/finder" + "twfinder/gui/server" + "twfinder/pipeline" + "twfinder/request" +) + +func buildPipeline() { + /* finder build start */ + finder.BuildSearchCriteria() + /* finder build end */ + + /* build TwitterAPI start */ + request.TwitterAPI() + /* build TwitterAPI end */ +} + +// HomeWin : +func HomeWin() server.Window { + /* start Pipline */ + pip := pipeline.NewPipeline() + /* start Pipline */ + + // Create and build a window + win := server.NewWindow("home", "Home - Twitter Finder App") + win.Style().SetFullWidth() + win.SetHAlign(server.HACenter) + win.SetCellPadding(2) + configBtn := server.NewButton("Configuration") + configBtn.AddEHandlerFunc(func(e server.Event) { + e.ReloadWin("configuration") + }, server.ETypeClick) + win.Add(configBtn) + lodImg := server.NewHTML(`

via GIPHY

`) + lblTitle := server.NewLabel("") + + startBtn := server.NewButton("Start") + startBtn.AddEHandlerFunc(func(e server.Event) { + lblTitle.SetText("Collecting Data ... ") + win.Add(lblTitle) + win.Add(lodImg) + // + buildPipeline() + pip.Start() + // + e.MarkDirty(win) + }, server.ETypeClick) + win.Add(startBtn) + + stopBtn := server.NewButton("stop") + stopBtn.AddEHandlerFunc(func(e server.Event) { + lblTitle.SetText("Stop collection data !") + win.Remove(lodImg) + // + pip.Close() + // + e.MarkDirty(win) + }, server.ETypeClick) + win.Add(stopBtn) + return win +} diff --git a/gui/frontend/images/394.gif b/gui/frontend/images/394.gif new file mode 100644 index 0000000..0204fae Binary files /dev/null and b/gui/frontend/images/394.gif differ diff --git a/gui/server/button.go b/gui/server/button.go new file mode 100644 index 0000000..42e2d44 --- /dev/null +++ b/gui/server/button.go @@ -0,0 +1,55 @@ +// Button component interface and implementation. + +package server + +// Button interface defines a clickable button. +// +// Suggested event type to handle actions: ETypeClick +// +// Default style class: "gui-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(nil, text) + c.Style().AddClass("gui-Button") + return &c +} + +// newButtonImpl creates a new buttonImpl. +func newButtonImpl(valueProviderJs []byte, text string) buttonImpl { + return buttonImpl{newCompImpl(valueProviderJs), newHasTextImpl(text), newHasEnabledImpl()} +} + +var ( + strButtonOp = []byte(`" +) + +func (c *buttonImpl) Render(w Writer) { + w.Write(strButtonOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + c.renderEnabled(w) + w.Write(strGT) + + c.renderText(w) + + w.Write(strButtonCl) +} diff --git a/gui/server/comp.go b/gui/server/comp.go new file mode 100644 index 0000000..d984320 --- /dev/null +++ b/gui/server/comp.go @@ -0,0 +1,299 @@ +// Comp component interface and implementation. + +package server + +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 event type. Lazily initialized. + valueProviderJs []byte // If the HTML representation of the component has a value, this JavaScript code code must provide it. It will be automatically sent as the paramCompId 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 []byte) 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 { + 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(EmptyEHandler, etype) + } + } +} + +var ( + strSePrefix = []byte(`="se(event,`) // `="se(event,` + strSeSuffix = []byte(`)"`) // `)"` +) + +// rendrenderEventHandlers renders the event handlers as attributes. +func (c *compImpl) renderEHandlers(w Writer) { + for etype := range c.handlers { + etypeAttr := etypeAttrs[etype] + if len(etypeAttr) == 0 { // Only general events are added to the etypeAttrs map + continue + } + + // To render : ` ="se(event,etype,compId,value)"` + // Example (checkbox onclick): ` onclick="se(event,0,4327,this.checked)"` + w.Write(strSpace) + w.Write(etypeAttr) + w.Write(strSePrefix) + w.Writev(int(etype)) + w.Write(strComma) + w.Writev(int(c.id)) + if len(c.valueProviderJs) > 0 && c.syncOnETypes != nil && c.syncOnETypes[etype] { + w.Write(strComma) + w.Write(c.valueProviderJs) + } + w.Write(strSeSuffix) + } +} + +// THIS IS AN EMPTY IMPLEMENTATION AS NOT ALL COMPONENTS NEED THIS. +// THOSE WHO DO SHOULD DEFINE THEIR OWN. +func (c *compImpl) preprocessEvent(event Event, r *http.Request) { +} + +func (c *compImpl) dispatchEvent(e Event) { + 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/gui/server/comp_addons.go b/gui/server/comp_addons.go new file mode 100644 index 0000000..b5991d3 --- /dev/null +++ b/gui/server/comp_addons.go @@ -0,0 +1,435 @@ +// 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 server + +import ( + "strconv" + "time" +) + +// 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 strDisabled = []byte(` disabled="disabled"`) // ` disabled="disabled"` + +// renderEnabled renders the enabled attribute. +func (c *hasEnabledImpl) renderEnabled(w Writer) { + if !c.enabled { + w.Write(strDisabled) + } +} + +// 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) +} + +// HAlign is the horizontal alignment type. +type HAlign string + +// Horizontal alignment constants. +const ( + HALeft HAlign = "left" // Horizontal left alignment + HACenter = "center" // Horizontal center alignment + HARight = "right" // Horizontal right alignment + + HADefault = "" // Browser default (or inherited) horizontal alignment +) + +// VAlign is the vertical alignment type. +type VAlign string + +// Vertical alignment constants. +const ( + VATop VAlign = "top" // Vertical top alignment + VAMiddle = "middle" // Vertical center alignment + VABottom = "bottom" // Vertical bottom alignment + + VADefault = "" // 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 +} + +// HasDate interface defines a modifiable date property. +type HasDate interface { + // Date returns the date. + Date() time.Time + + // SetDate sets the date. + SetDate(date string) +} + +// newHasDateImpl creates a new hasDateImpl +func newHasDateImpl(date time.Time) hasDateImpl { + return hasDateImpl{date} +} + +// HasDate implementation. +type hasDateImpl struct { + date time.Time // The date +} + +func (d *hasDateImpl) Date() time.Time { + return d.date +} + +func (d *hasDateImpl) SetDate(date time.Time) { + d.date = date +} + +// renderText renders the text. +func (d *hasDateImpl) renderText(w Writer) { + if d.date.IsZero() { + return + } + w.Writees(d.date.Format("2006-01-02")) +} + +// 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 initialized. +} + +// newCellFmtImpl creates a new cellFmtImpl. +// Default horizontal alignment is HADefult, +// default vertical alignment is VADefault. +func newCellFmtImpl() *cellFmtImpl { + // Initialize hasHVAlignImpl with HADefault and VADefault + // so if aligns are not changed, they will not be rendered => + // they will be inherited (from TR). + return &cellFmtImpl{hasHVAlignImpl: newHasHVAlignImpl(HADefault, VADefault)} +} + +func (c *cellFmtImpl) Style() Style { + if c.styleImpl == nil { + c.styleImpl = newStyleImpl() + } + return c.styleImpl +} + +func (c *cellFmtImpl) attr(name string) string { + 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(nil), hasHVAlignImpl: newHasHVAlignImpl(HADefault, VADefault)} + 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 strStVAlign = []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(strTROp) + if c.halign != HADefault { + w.Write(strAlign) + w.Writes(string(c.halign)) + w.Write(strQuote) + } + if c.valign != VADefault { + w.Write(strStVAlign) + w.Writes(string(c.valign)) + w.Write(strQuote) + } + w.Write(strGT) +} diff --git a/gui/server/css.go b/gui/server/css.go new file mode 100644 index 0000000..6f3afbc --- /dev/null +++ b/gui/server/css.go @@ -0,0 +1,92 @@ +// Built-in static CSS themes of Server. + +package server + +// Built-in CSS themes. +const ( + ThemeDefault = "default" // Default CSS theme + ThemeDebug = "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. "server-default-0.8.0.css" + return "server-" + theme + "-" + ServerVersion + ".css" +} + +var staticCSS = make(map[string][]byte) + +func init() { + staticCSS[resNameStaticCSS(ThemeDefault)] = []byte("" + + ` +.guiimg-collapsed {background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATUlEQVQ4y83RsQkAMAhEURNc+iZw7KQNgnjGRlv5D0SRMQPgADjVbr3AuzCz1QJYKAUyiAYiqAx4aHe/p9XAn6C/IQ1kb9TfMATYcM5cL5cg3qDaS5UAAAAASUVORK5CYII=)} +.guiimg-expanded {background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAATElEQVQ4y2NgGGjACGNUVlb+J0Vje3s7IwMDAwMT1VxAiitgtlPfBcS4Atl22rgAnyvQbaedC7C5ApvtVHEBXlBZWfmfUKwwMQx5AADNQhjmAryM3wAAAABJRU5ErkJggg==)} + +.guiimg-collapsed, .guiimg-expanded {background-position:0px 0px; background-repeat:no-repeat} + +body {font-family:Arial} + +.gui-Window {} + +.gui-Panel {} + +.gui-Table {} + +.gui-Label { + color: white; +} + +.gui-Link {} + +.gui-Image {} + +.gui-Button {} + +.gui-CheckBox {} +.gui-CheckBox-Disabled {color:#888} + +.gui-RadioButton {} +.gui-RadioButton-Disabled {color:#888} + +.gui-ListBox {} + +.gui-TextBox {} + +.gui-Datepicker {} + +.gui-PasswBox {} + +.gui-HTML {} + +.gui-SwitchButton {} +.gui-SwitchButton-On-Active {background:#00a000; color:#d0ffd0} +.gui-SwitchButton-Off-Active {background:#d03030; color:#ffd0d0} +.gui-SwitchButton-On-Inactive, .gui-SwitchButton-Off-Inactive {background:#606060; color:#909090} +.gui-SwitchButton-On-Inactive:enabled, .gui-SwitchButton-Off-Inactive:enabled {cursor:pointer} +.gui-SwitchButton-On-Active, .gui-SwitchButton-Off-Active, .gui-SwitchButton-On-Inactive, .gui-SwitchButton-Off-Inactive {margin:0px;border: 0px; width:100%} +.gui-SwitchButton-On-Active:disabled, .gui-SwitchButton-Off-Active:disabled, .gui-SwitchButton-On-Inactive:disabled, .gui-SwitchButton-Off-Inactive:disabled {color:black} + +.gui-Expander {} +.gui-Expander-Header, .gui-Expander-Header-Expanded {cursor:pointer} +.gui-Expander-Header, .gui-Expander-Header-Expanded, .gui-Expander-Content {padding-left:19px} + +.gui-TabBar {} +.gui-TabBar-Top {padding:0px 5px 0px 5px; border-bottom:5px solid #8080f8} +.gui-TabBar-Bottom {padding:0px 5px 0px 5px; border-top:5px solid #8080f8} +.gui-TabBar-Left {padding:5px 0px 5px 0px; border-right:5px solid #8080f8} +.gui-TabBar-Right {padding:5px 0px 5px 0px; border-left:5px solid #8080f8} +.gui-TabBar-NotSelected {padding-left:5px; padding-right:5px; border:1px solid white ; background:#c0c0ff; cursor:default} +.gui-TabBar-Selected {padding-left:5px; padding-right:5px; border:1px solid #8080f8; background:#8080f8; cursor:default} +.gui-TabPanel {} +.gui-TabPanel-Content {border:1px solid #8080f8; width:100%; height:100%} + +.gui-SessMonitor {} +.gui-SessMonitor-Expired, .gui-SessMonitor-Error {color:red} +`) + + staticCSS[resNameStaticCSS(ThemeDebug)] = []byte(string(staticCSS[resNameStaticCSS(ThemeDefault)]) + + ` +.gui-Window td, .gui-Table td, .gui-Panel td, .gui-TabPanel td {border:1px solid black} +`) +} diff --git a/gui/server/date_pwbox.go b/gui/server/date_pwbox.go new file mode 100644 index 0000000..4fed0c2 --- /dev/null +++ b/gui/server/date_pwbox.go @@ -0,0 +1,123 @@ +// Defines the Datepicker component. + +package server + +import ( + "net/http" + "time" + "twfinder/helper" +) + +// Datepicker interface defines a component for date input purpose. +// +// Suggested event type to handle actions: ETypeChange +// +// By default the value of the Datepicker is synchronized with the server +// on ETypeChange event which is when the Datepicker loses focus +// or when the ENTER key is pressed. +// If you want a Datepicker to synchronize values during editing +// add the ETypeKeyUp event type +// to the events on which synchronization happens by calling: +// AddSyncOnETypes(ETypeKeyUp) +// +// Default style class: "gui-Datepicker" +type Datepicker interface { + // Datepicker is a component. + Comp + + // Date returns the datetime. + Date() time.Time + + // SetDate sets the date. + SetDate(date time.Time) + + // Datepicker can be enabled/disabled. + HasEnabled + + // ReadOnly returns if the Datepicker is read-only. + ReadOnly() bool + + // SetReadOnly sets if the Datepicker is read-only. + SetReadOnly(readOnly bool) +} + +// Datepicker implementation. +type datepickerImpl struct { + compImpl // Component implementation + hasDateImpl // Has date implementation + hasEnabledImpl // Has enabled implementation +} + +// NewDatepicker creates a new Datepicker. +func NewDatepicker(data time.Time) Datepicker { + c := newDatepickerImpl(strEncURIThisV, data) + c.Style().AddClass("gui-Datepicker") + return &c +} + +// newDatepickerImpl creates a new DatepickerImpl. +func newDatepickerImpl(valueProviderJs []byte, data time.Time) datepickerImpl { + c := datepickerImpl{newCompImpl(valueProviderJs), newHasDateImpl(data), newHasEnabledImpl()} + c.AddSyncOnETypes(ETypeChange) + return c +} + +func (d *datepickerImpl) Date() time.Time { + dd := d.date + return dd +} + +func (d *datepickerImpl) SetText(date time.Time) { + d.date = date +} + +func (d *datepickerImpl) ReadOnly() bool { + ro := d.Attr("readonly") + return len(ro) > 0 +} + +func (d *datepickerImpl) SetReadOnly(readOnly bool) { + readOnlyTxt := "" + if readOnly { + readOnlyTxt = "readonly" + } + d.SetAttr("readonly", readOnlyTxt) + +} + +func (d *datepickerImpl) preprocessEvent(event Event, r *http.Request) { + // Empty string for datepicker is a valid value. + // So we have to check whether it is supplied, not just whether its len() > 0 + value := r.FormValue(paramCompValue) + if len(value) > 0 { + d.date = helper.StringtoDate(value, "2006-01-02") + } else { + // Empty string might be a valid value, if the component value param is present: + values, present := r.Form[paramCompValue] // Form is surely parsed (we called FormValue()) + if present && len(values) > 0 { + d.date = helper.StringtoDate(values[0], "2006-01-02") + } + } +} + +func (d *datepickerImpl) Render(w Writer) { + d.renderInput(w) +} + +var ( + strDate = []byte("date") // "date" +) + +// renderInput renders the component as an input HTML tag. +func (d *datepickerImpl) renderInput(w Writer) { + w.Write(strInputOp) + w.Write(strDate) + w.Write(strSize) + w.Write(strQuote) + d.renderAttrsAndStyle(w) + d.renderEnabled(w) + d.renderEHandlers(w) + w.Write(strValue) + d.renderText(w) + w.Write(strInputCl) +} diff --git a/gui/server/doc.go b/gui/server/doc.go new file mode 100644 index 0000000..a9bd7aa --- /dev/null +++ b/gui/server/doc.go @@ -0,0 +1,12 @@ +// Package server : +// Base code from : https://github.com/icza/gowut +// For documentation please visit the Gowut Wiki: https://github.com/icza/gowut/wiki +// +package server + +// Server version information. +const ( + ServerVersion = "v1.4.0" // server version: "v"major.minor.maintenance[-dev] + ServerReleaseDate = "2018-10-02 CET" // server release date + ServerRelDateLayout = "2006-01-02 MST" // server release date layout (for time.Parse()) +) diff --git a/gui/server/event.go b/gui/server/event.go new file mode 100644 index 0000000..c982d1d --- /dev/null +++ b/gui/server/event.go @@ -0,0 +1,477 @@ +// Defines the Event type and event handling. + +package server + +import ( + "net/http" + "strconv" +) + +// EventType is the 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 ( + // General events for all components + ETypeClick EventType = iota // Mouse click event + ETypeDblClick // Mouse double click event + ETypeMousedown // Mouse down event + ETypeMouseMove // Mouse move event + ETypeMouseOver // Mouse over event + ETypeMouseOut // Mouse out event + ETypeMouseUp // Mouse up event + ETypeKeyDown // Key down event + ETypeKeyPress // Key press event + ETypeKeyUp // Key up event + ETypeBlur // Blur event (component loses focus) + ETypeChange // Change event (value change) + ETypeFocus // Focus event (component gains focus) + + // Window events (for Window only) + ETypeWinLoad // Window load event + ETypeWinUnload // Window unload event + + // Internal events, generated and dispatched internally while processing another event + ETypeStateChange // State change +) + +const ( + // ETypeMouseDown is identical to ETypeMousedown + ETypeMouseDown = ETypeMousedown +) + +// EventCategory is the event type category. +type EventCategory int + +// Event type categories. +const ( + ECatGeneral EventCategory = iota // General event type for all components + ECatWindow // Window event type for Window only + ECatInternal // Internal event generated and dispatched internally while processing another event + + ECatUnknown EventCategory = -1 // Unknown event category +) + +// Category returns the event type category. +func (etype EventType) Category() EventCategory { + switch { + case etype >= ETypeClick && etype <= ETypeFocus: + return ECatGeneral + case etype >= ETypeWinLoad && etype <= ETypeWinUnload: + return ECatWindow + case etype >= ETypeStateChange && etype <= ETypeStateChange: + return ECatInternal + } + + return ECatUnknown +} + +// Attribute names for the general event types; only for the general event types. +var etypeAttrs = map[EventType][]byte{ + ETypeClick: []byte("onclick"), + ETypeDblClick: []byte("ondblclick"), + ETypeMousedown: []byte("onmousedown"), + ETypeMouseMove: []byte("onmousemove"), + ETypeMouseOver: []byte("onmouseover"), + ETypeMouseOut: []byte("onmouseout"), + ETypeMouseUp: []byte("onmouseup"), + ETypeKeyDown: []byte("onkeydown"), + ETypeKeyPress: []byte("onkeypress"), + ETypeKeyUp: []byte("onkeyup"), + ETypeBlur: []byte("onblur"), + ETypeChange: []byte("onchange"), + ETypeFocus: []byte("onfocus")} + +// Function names for window event types. +var etypeFuncs = map[EventType][]byte{ + ETypeWinLoad: []byte("onload"), + ETypeWinUnload: []byte("onbeforeunload")} // Bind it to onbeforeunload (instead of onunload) for several reasons (onunload might cause trouble for AJAX; onunload is not called in IE if page is just refreshed...) + +// MouseBtn is the mouse button type. +type MouseBtn int + +// Mouse buttons +const ( + MouseBtnUnknown MouseBtn = -1 // Unknown mouse button (info not available) + MouseBtnLeft = 0 // Left mouse button + MouseBtnMiddle = 1 // Middle mouse button + MouseBtnRight = 2 // Right mouse button +) + +// ModKey is the modifier key type. +type ModKey int + +// Modifier key masks. +const ( + ModKeyAlt ModKey = 1 << iota // Alt key + ModKeyCtrl // Control key + ModKeyMeta // Meta key + ModKeyShift // Shift key +) + +// Key (keyboard key) type. +type Key int + +// Some key codes. +const ( + KeyBackspace Key = 8 + KeyEnter = 13 + KeyShift = 16 + KeyCtrl = 17 + KeyAlt = 18 + KeyCapsLock = 20 + KeyEscape = 27 + KeySpace = 32 + KeyPgUp = 33 + KeyPgDown = 34 + KeyEnd = 35 + KeyHome = 36 + KeyLeft = 37 + KeyUp = 38 + KeyRight = 39 + KeyDown = 40 + KeyPrintScrn = 44 + KeyInsert = 45 + KeyDel = 46 + + Key0 = 48 + Key9 = 57 + + KeyA = 65 + KeyZ = 90 + + KeyWin = 91 + + KeyNumpad0 = 96 + KeyNumpad9 = 105 + KeyNumpadMul = 106 + KeyNumpadPlus = 107 + KeyNumpadMinus = 109 + KeyNumpadDot = 110 + KeyNumpadDiv = 111 + + KeyF1 = 112 + KeyF2 = 113 + KeyF3 = 114 + KeyF4 = 115 + KeyF5 = 116 + KeyF6 = 117 + KeyF7 = 118 + KeyF8 = 119 + KeyF9 = 120 + KeyF10 = 121 + KeyF11 = 122 + KeyF12 = 123 + + KeyNumLock = 144 + KeyScrollLock = 145 +) + +// EmptyEHandler is the empty event handler which does nothing. +const EmptyEHandler 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 Container 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 + + // Parent returns the parent event if there's one. + // Usually internal events have parent event for which the internal + // event was created and dispatched. + // The parent event can be used to identify the original source and event type. + Parent() Event + + // 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, MouseBtnUnknown 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 descendants 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() + + // 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 works as if they would be done on this event. + forkEvent(etype EventType, src Comp) Event +} + +// HasRequestResponse defines methods to acquire / access +// http.ResponseWriter and http.Request from something that supports this. +// +// The concrete type that implements Event does implement this too, +// but this is not added to the Event interface intentionally to not urge the use of this. +// Users should not rely on this as in a future implementation there might not be +// a response and request associated with an event. +// But this may be useful in certain scenarios, such as you need to know the client IP address, +// or you want to use custom authentication that needs the request/response. +// +// To get access to these methods, simply use a type assertion, asserting that the event value +// implements this interface. For example: +// +// someButton.AddEHandlerFunc(func(e server.Event) { +// if hrr, ok := e.(server.HasRequestResponse); ok { +// req := hrr.Request() +// log.Println("Client addr:", req.RemoteAddr) +// } +// }, server.ETypeClick) +type HasRequestResponse interface { + // ResponseWriter returns the associated HTTP response writer. + ResponseWriter() http.ResponseWriter + + // Request returns the associated HTTP request. + Request() *http.Request +} + +// Event implementation. +type eventImpl struct { + etype EventType // Event type + src Comp // Source of the event, the component the event is originating from + parent *eventImpl // Optional parent event + + x, y int // Mouse coordinates (relative to component); not part of shared data because they component-relative + + shared *sharedEvtData // Shared event data +} + +// Event data shared between an event and its child events (forks). +type sharedEvtData struct { + server *serverImpl // Server implementation + + 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 + + rw http.ResponseWriter // ResponseWriter of the HTTP request the event was created from + req *http.Request // Request of the HTTP request the event was created from +} + +// newEventImpl creates a new eventImpl +func newEventImpl(etype EventType, src Comp, server *serverImpl, session Session, + rw http.ResponseWriter, req *http.Request) *eventImpl { + e := eventImpl{etype: etype, src: src, + shared: &sharedEvtData{server: server, dirtyComps: make(map[ID]Comp, 2), session: session, rw: rw, req: req}} + return &e +} + +func (e *eventImpl) Type() EventType { + return e.etype +} + +func (e *eventImpl) Src() Comp { + return e.src +} + +func (e *eventImpl) Parent() Event { + return e.parent +} + +func (e *eventImpl) Mouse() (x, y int) { + return e.x, e.y +} + +func (e *eventImpl) MouseWin() (x, y int) { + return e.shared.wx, e.shared.wy +} + +func (e *eventImpl) MouseBtn() MouseBtn { + return e.shared.mbtn +} + +func (e *eventImpl) ModKeys() int { + return e.shared.modKeys +} + +func (e *eventImpl) ModKey(modKey ModKey) bool { + return e.shared.modKeys&int(modKey) != 0 +} + +func (e *eventImpl) KeyCode() Key { + return e.shared.keyCode +} + +func (e *eventImpl) ReloadWin(name string) { + e.shared.reload = true + e.shared.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 Container + // 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. + + shared := e.shared + + for _, comp := range comps { + if !shared.dirty(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 shared.dirtyComps { + if c.DescendantOf(comp) { + delete(shared.dirtyComps, id) + } + } + + shared.dirtyComps[comp.ID()] = comp + } + } +} + +// dirty 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 (s *sharedEvtData) dirty(c2 Comp) bool { + // First-class being dirty: + if _, found := s.dirtyComps[c2.ID()]; found { + return true + } + + // Second-class being dirty: + for _, c := range s.dirtyComps { + if c2.DescendantOf(c) { + return true + } + } + + return false +} + +func (e *eventImpl) SetFocusedComp(comp Comp) { + e.shared.focusedComp = comp +} + +func (e *eventImpl) Session() Session { + return e.shared.session +} + +func (e *eventImpl) NewSession() Session { + return e.shared.server.newSession(e) +} + +func (e *eventImpl) RemoveSess() { + e.shared.server.removeSess(e) +} + +func (e *eventImpl) forkEvent(etype EventType, src Comp) Event { + return &eventImpl{etype: etype, src: src, parent: e, + x: -1, y: -1, // Mouse coordinates are unknown in the new source component... + shared: e.shared} +} + +func (e *eventImpl) ResponseWriter() http.ResponseWriter { + return e.shared.rw +} + +func (e *eventImpl) Request() *http.Request { + return e.shared.req +} + +// 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/gui/server/examples_test.go b/gui/server/examples_test.go new file mode 100644 index 0000000..869d674 --- /dev/null +++ b/gui/server/examples_test.go @@ -0,0 +1,36 @@ +package server_test + +import ( + "twfinder/gui/server" +) + +// Example code determining which button was clicked. +func ExampleButton() { + b := server.NewButton("Click me") + b.AddEHandlerFunc(func(e server.Event) { + if e.MouseBtn() == server.MouseBtnMiddle { + // Middle click + } + }, server.ETypeClick) +} + +// Example code determining what kind of key is involved. +func ExampleTextBox() { + b := server.NewTextBox("") + b.AddSyncOnETypes(server.ETypeKeyUp) // This is here so we will see up-to-date value in the event handler + b.AddEHandlerFunc(func(e server.Event) { + if e.ModKey(server.ModKeyShift) { + // SHIFT is pressed + } + + c := e.KeyCode() + switch { + case c == server.KeyEnter: // Enter + case c >= server.Key0 && c <= server.Key9: + fallthrough + case c >= server.KeyNumpad0 && c <= server.KeyNumpad9: // Number + case c >= server.KeyA && c <= server.KeyZ: // Letter + case c >= server.KeyF1 && c <= server.KeyF12: // Function key + } + }, server.ETypeKeyUp) +} diff --git a/gui/server/expander.go b/gui/server/expander.go new file mode 100644 index 0000000..bed55c4 --- /dev/null +++ b/gui/server/expander.go @@ -0,0 +1,209 @@ +// Expander component interface and implementation. + +package server + +// Expander interface defines a component which can show and hide +// another component when clicked on the header. +// +// You can register ETypeStateChange event handlers which will be called when the user +// expands or collapses the expander by clicking on the header. The event source will be +// the expander. The event will have a parent event whose source will be the clicked +// header component and will contain the mouse coordinates. +// +// Default style classes: "gui-Expander", "gui-Expander-Header", +// "guiimg-collapsed", "gui-Expander-Header-Expanded", "guiimg-expanded", +// "gui-Expander-Content" +type Expander interface { + // Expander is a TableView. + TableView + + // Header returns the header component of the expander. + Header() Comp + + // SetHeader sets the header component of the expander. + SetHeader(h Comp) + + // Content returns the content component of the expander. + Content() Comp + + // SetContent sets the content component of the expander. + SetContent(c Comp) + + // Expanded returns whether the expander is expanded. + Expanded() bool + + // SetExpanded sets whether the expander is expanded. + SetExpanded(expanded bool) + + // HeaderFmt returns the cell formatter of the header. + HeaderFmt() CellFmt + + // ContentFmt returns the cell formatter of the content. + ContentFmt() CellFmt +} + +// Expander implementation. +type expanderImpl struct { + tableViewImpl // TableView implementation + + header Comp // Header component + content Comp // Content component + expanded bool // Tells whether the expander is expanded + + headerFmt *cellFmtImpl // Header cell formatter + contentFmt *cellFmtImpl // Content cell formatter +} + +// NewExpander creates a new Expander. +// By default expanders are collapsed. +func NewExpander() Expander { + c := &expanderImpl{tableViewImpl: newTableViewImpl(), expanded: true, headerFmt: newCellFmtImpl(), contentFmt: newCellFmtImpl()} + c.headerFmt.SetAlign(HALeft, VAMiddle) + c.contentFmt.SetAlign(HALeft, VATop) + c.Style().AddClass("gui-Expander") + // Init styles by changing expanded state, to the default value. + c.SetExpanded(false) + return c +} + +func (c *expanderImpl) Remove(c2 Comp) bool { + if c.content.Equals(c2) { + c2.setParent(nil) + c.content = nil + return true + } + + if c.header.Equals(c2) { + c2.setParent(nil) + c.header = nil + return true + } + + return false +} + +func (c *expanderImpl) ByID(id ID) Comp { + if c.id == id { + return c + } + + if c.header != nil { + if c.header.ID() == id { + return c.header + } + if c2, isContainer := c.header.(Container); isContainer { + if c3 := c2.ByID(id); c3 != nil { + return c3 + } + } + } + + if c.content != nil { + if c.content.ID() == id { + return c.content + } + if c2, isContainer := c.content.(Container); isContainer { + if c3 := c2.ByID(id); c3 != nil { + return c3 + } + } + } + + return nil +} + +func (c *expanderImpl) Clear() { + if c.header != nil { + c.header.setParent(nil) + c.header = nil + } + if c.content != nil { + c.content.setParent(nil) + c.content = nil + } +} + +func (c *expanderImpl) Header() Comp { + return c.header +} + +func (c *expanderImpl) SetHeader(header Comp) { + header.makeOrphan() + c.header = header + header.setParent(c) + + // TODO would be nice to remove this internal handler func when the header is removed! + header.AddEHandlerFunc(func(e Event) { + c.SetExpanded(!c.expanded) + e.MarkDirty(c) + if c.handlers[ETypeStateChange] != nil { + c.dispatchEvent(e.forkEvent(ETypeStateChange, c)) + } + }, ETypeClick) +} + +func (c *expanderImpl) Content() Comp { + return c.content +} + +func (c *expanderImpl) SetContent(content Comp) { + content.makeOrphan() + c.content = content + content.setParent(c) + + c.contentFmt.Style().AddClass("gui-Expander-Content").SetFullSize() +} + +func (c *expanderImpl) Expanded() bool { + return c.expanded +} + +func (c *expanderImpl) SetExpanded(expanded bool) { + if c.expanded == expanded { + return + } + + style := c.headerFmt.Style() + if c.expanded { + style.RemoveClass("gui-Expander-Header-Expanded") + style.RemoveClass("guiimg-expanded") + style.AddClass("gui-Expander-Header") + style.AddClass("guiimg-collapsed") + } else { + style.RemoveClass("gui-Expander-Header") + style.RemoveClass("guiimg-collapsed") + style.AddClass("gui-Expander-Header-Expanded") + style.AddClass("guiimg-expanded") + } + + c.expanded = expanded +} + +func (c *expanderImpl) HeaderFmt() CellFmt { + return c.headerFmt +} + +func (c *expanderImpl) ContentFmt() CellFmt { + return c.contentFmt +} + +func (c *expanderImpl) Render(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + if c.header != nil { + c.renderTr(w) + c.headerFmt.render(strTDOp, w) + c.header.Render(w) + } + + if c.expanded && c.content != nil { + c.renderTr(w) + c.contentFmt.render(strTDOp, w) + c.content.Render(w) + } + + w.Write(strTableCl) +} diff --git a/gui/server/html.go b/gui/server/html.go new file mode 100644 index 0000000..f9aea2e --- /dev/null +++ b/gui/server/html.go @@ -0,0 +1,50 @@ +// Defines the Html component. + +package server + +// HTML interface defines a component which wraps an HTML text into a component. +// +// Default style class: "gui-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(nil), html} + c.Style().AddClass("gui-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(strSpanOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + w.Writes(c.html) + + w.Write(strSpanCl) +} diff --git a/gui/server/id.go b/gui/server/id.go new file mode 100644 index 0000000..d86aa5b --- /dev/null +++ b/gui/server/id.go @@ -0,0 +1,41 @@ +// ID type definition, and unique ID generation. + +package server + +import ( + "strconv" + "sync/atomic" +) + +// ID is the type of the ids of the components. +type ID int + +// Note: it is intentional that base 10 is used (and not e.q. 16), +// because it is handled as a number at the client side (in JavaScript). +// It has some benefit like no need to quote IDs, simpler code generation. + +// Converts an ID to a string. +func (id ID) String() string { + return strconv.Itoa(int(id)) +} + +// AtoID converts a string to ID. +func AtoID(s string) (ID, error) { + id, err := strconv.Atoi(s) + + if err != nil { + return ID(0), err + } + return ID(id), nil +} + +// Component ID generation and provider + +// Last used value for ID +var lastID int64 + +// nextCompID returns a unique component ID +// First ID given is 1. +func nextCompID() ID { + return ID(atomic.AddInt64(&lastID, 1)) +} diff --git a/gui/server/image.go b/gui/server/image.go new file mode 100644 index 0000000..649cbe1 --- /dev/null +++ b/gui/server/image.go @@ -0,0 +1,48 @@ +// Image component interface and implementation. + +package server + +// Image interface defines an image. +// +// Default style class: "gui-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(nil), newHasTextImpl(text), newHasURLImpl(url)} + c.Style().AddClass("gui-Image") + return c +} + +var ( + strImgOp = []byte("`) // `">` +) + +func (c *imageImpl) Render(w Writer) { + w.Write(strImgOp) + c.renderURL("src", w) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strAlt) + c.renderText(w) + w.Write(strImgCl) +} diff --git a/gui/server/js.go b/gui/server/js.go new file mode 100644 index 0000000..3aeb6b8 --- /dev/null +++ b/gui/server/js.go @@ -0,0 +1,317 @@ +// Built-in static JavaScript codes of Server. + +package server + +import ( + "strconv" +) + +// Static JavaScript resource name +const resNameStaticJs = "server-" + ServerVersion + ".js" + +// Static javascript code +var staticJs []byte + +func init() { + // Init staticJs + staticJs = []byte("" + + // Param consts + "var _pEventType='" + paramEventType + + "',_pCompId='" + paramCompID + + "',_pCompValue='" + paramCompValue + + "',_pFocCompId='" + paramFocusedCompID + + "',_pMouseWX='" + paramMouseWX + + "',_pMouseWY='" + paramMouseWY + + "',_pMouseX='" + paramMouseX + + "',_pMouseY='" + paramMouseY + + "',_pMouseBtn='" + paramMouseBtn + + "',_pModKeys='" + paramModKeys + + "',_pKeyCode='" + paramKeyCode + + "';\n" + + // Modifier key masks + "var _modKeyAlt=" + strconv.Itoa(int(ModKeyAlt)) + + ",_modKeyCtlr=" + strconv.Itoa(int(ModKeyCtrl)) + + ",_modKeyMeta=" + strconv.Itoa(int(ModKeyMeta)) + + ",_modKeyShift=" + strconv.Itoa(int(ModKeyShift)) + + ";\n" + + // Event response action consts + "var _eraNoAction=" + strconv.Itoa(eraNoAction) + + ",_eraReloadWin=" + strconv.Itoa(eraReloadWin) + + ",_eraDirtyComps=" + strconv.Itoa(eraDirtyComps) + + ",_eraFocusComp=" + strconv.Itoa(eraFocusComp) + + ";" + + ` + +function createXmlHttp() { + if (window.XMLHttpRequest) // IE7+, Firefox, Chrome, Opera, Safari + return new XMLHttpRequest(); + else // IE6, IE5 + return new ActiveXObject("Microsoft.XMLHTTP"); +} + +// Send event +function se(event, etype, compId, compValue) { + var xhr = createXmlHttp(); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) + procEresp(xhr); + } + + xhr.open("POST", _pathEvent, true); // asynch call + xhr.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 && document.activeElement.id !== "") + data += "&" + _pFocCompId + "=" + document.activeElement.id; + + if (event != null) { + if (event.clientX != null) { + // Mouse data + var x = event.clientX, y = event.clientY; + // Account for the amount body is scrolled: + eventDoc = (event.target && event.target.ownerDocument) || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + x += (doc && doc.scrollLeft || body && body.scrollLeft || 0) - + (doc && doc.clientLeft || body && body.clientLeft || 0); + y += (doc && doc.scrollTop || body && body.scrollTop || 0) - + (doc && doc.clientTop || body && body.clientTop || 0 ); + 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 uses 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); + } + + xhr.send(data); +} + +function procEresp(xhr) { + var actions = xhr.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 xhr = createXmlHttp(); + + 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 = xhr.responseText; + focusComp(focusedCompId); + + // Inserted JS code is not executed automatically, do it manually: + // Have to "re-get" element by compId! + var scripts = document.getElementById(compId).getElementsByTagName("script"); + for (var i = 0; i < scripts.length; i++) { + eval(scripts[i].innerText); + } + } + } + + 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"); + + xhr.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 = "gui-SwitchButton-On-Active"; + offBtn.className = "gui-SwitchButton-Off-Inactive"; + } else { + onBtn.className = "gui-SwitchButton-On-Inactive"; + offBtn.className = "gui-SwitchButton-Off-Active"; + } + + return value; +} + +function focusComp(compId) { + if (compId != null && compId !== "") { + var e = document.getElementById(compId); + if (e) // Else component removed or not visible (e.g. on inactive tab of TabPanel) + e.focus(); + } +} + +function addonload(func) { + var oldonload = window.onload; + if (typeof window.onload != 'function') { + window.onload = func; + } else { + window.onload = function() { + if (oldonload) + oldonload(); + func(); + } + } +} + +function addonbeforeunload(func) { + var oldonbeforeunload = window.onbeforeunload; + if (typeof window.onbeforeunload != 'function') { + window.onbeforeunload = func; + } else { + window.onbeforeunload = function() { + if (oldonbeforeunload) + oldonbeforeunload(); + func(); + } + } +} + +var timers = new Object(); + +function setupTimer(compId, js, timeout, repeat, active, reset) { + var timer = timers[compId]; + + if (timer != null) { + var changed = timer.js != js || timer.timeout != timeout || timer.repeat != repeat || timer.reset != reset; + if (!active || changed) { + if (timer.repeat) + clearInterval(timer.id); + else + clearTimeout(timer.id); + timers[compId] = null; + } + if (!changed) + return; + } + if (!active) + return; + + // Create new timer + timers[compId] = timer = new Object(); + timer.js = js; + timer.timeout = timeout; + timer.repeat = repeat; + timer.reset = reset; + + // Start the timer + 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("gui-SessMonitor-Expired"); + else + e.classList.remove("gui-SessMonitor-Expired"); + var cnvtr = window[e.getAttribute("guiJsFuncName")]; + 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("gui-SessMonitor-Error"); + } catch (err) { + e.classList.add("gui-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() { + focusComp(_focCompId); +}); +`) +} diff --git a/gui/server/label.go b/gui/server/label.go new file mode 100644 index 0000000..a98c700 --- /dev/null +++ b/gui/server/label.go @@ -0,0 +1,38 @@ +// Label component interface and implementation. + +package server + +// Label interface defines a component which wraps a text into a component. +// +// Default style class: "gui-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(nil), newHasTextImpl(text)} + c.Style().AddClass("gui-Label") + return c +} + +func (c *labelImpl) Render(w Writer) { + w.Write(strSpanOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + c.renderText(w) + + w.Write(strSpanCl) +} diff --git a/gui/server/link.go b/gui/server/link.go new file mode 100644 index 0000000..f5dad3d --- /dev/null +++ b/gui/server/link.go @@ -0,0 +1,133 @@ +// Link component interface and implementation. + +package server + +// 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: "gui-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 + // (this is the default). + 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(nil), newHasTextImpl(text), newHasURLImpl(url), nil} + c.SetTarget("_blank") + c.Style().AddClass("gui-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 +} + +var ( + strAOp = []byte("") // "" +) + +func (c *linkImpl) Render(w Writer) { + w.Write(strAOp) + c.renderURL("href", w) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + c.renderText(w) + + if c.comp != nil { + c.comp.Render(w) + } + + w.Write(strACL) +} diff --git a/gui/server/listbox.go b/gui/server/listbox.go new file mode 100644 index 0000000..b598935 --- /dev/null +++ b/gui/server/listbox.go @@ -0,0 +1,228 @@ +// ListBox component interface and implementation. + +package server + +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: ETypeChange +// +// Default style class: "gui-ListBox" +type ListBox interface { + // ListBox is a component + Comp + + // ListBox can be enabled/disabled. + HasEnabled + + // Values returns the values. + Values() []string + + // SetValues sets the values. Also clears the selection. + SetValues(values []string) + + // 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 +} + +var ( + strSelidx = []byte("selIdxs(this)") // "selIdxs(this)" +) + +// NewListBox creates a new ListBox. +func NewListBox(values []string) ListBox { + c := &listBoxImpl{newCompImpl(strSelidx), newHasEnabledImpl(), values, false, make([]bool, len(values)), 1} + c.AddSyncOnETypes(ETypeChange) + c.Style().AddClass("gui-ListBox") + return c +} + +func (c *listBoxImpl) Values() []string { + return c.values +} + +func (c *listBoxImpl) SetValues(values []string) { + c.values = values + c.selected = make([]bool, len(values)) +} + +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(paramCompValue) + c.ClearSelected() + if len(value) == 0 { + return + } + + // Set selected indices + for _, sidx := range strings.Split(value, ",") { + if idx, err := strconv.Atoi(sidx); err == nil { + c.selected[idx] = true + } + } +} + +var ( + strSelectOp = []byte("`) // `") // "" + strSelectCl = []byte("") // "" +) + +func (c *listBoxImpl) Render(w Writer) { + w.Write(strSelectOp) + if c.multi { + w.Write(strMultiple) + } + w.WriteAttr("size", strconv.Itoa(c.rows)) + c.renderAttrsAndStyle(w) + c.renderEnabled(w) + c.renderEHandlers(w) + w.Write(strGT) + + for i, value := range c.values { + if c.selected[i] { + w.Write(strOptionOpSel) + } else { + w.Write(strOptionOp) + } + w.Writees(value) + w.Write(strOptionCl) + } + + w.Write(strSelectCl) +} diff --git a/gui/server/panel.go b/gui/server/panel.go new file mode 100644 index 0000000..0985706 --- /dev/null +++ b/gui/server/panel.go @@ -0,0 +1,375 @@ +// Panel component interface and implementation. + +package server + +import ( + "bytes" +) + +// Layout strategy type. +type Layout int + +// Layout strategies. +const ( + LayoutNatural Layout = iota // Natural layout: elements are displayed in their natural order. + LayoutVertical // Vertical layout: elements are laid out vertically. + LayoutHorizontal // Horizontal layout: elements are laid 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: "gui-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 LayoutNatural. + 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: "gui-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 LayoutHorizontal. + AddHSpace(width int) Comp + + // AddVSpace adds and returns a fixed-height vertical space consumer. + // Useful when layout is LayoutVertical. + 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 LayoutHorizontal. + // + // Tip: When adding a horizontal space consumer, you may set the + // white space style attribute of other components in the the panel + // to WhiteSpaceNowrap to avoid texts getting wrapped to multiple lines. + AddHConsumer() Comp + + // AddVConsumer adds and returns a vertical (free) space consumer. + // Useful when layout is LayoutVertical. + 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 LayoutVertical, +// default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewPanel() Panel { + c := newPanelImpl() + c.Style().AddClass("gui-Panel") + return &c +} + +// NewNaturalPanel creates a new Panel initialized with +// LayoutNatural layout. +// Default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewNaturalPanel() Panel { + p := NewPanel() + p.SetLayout(LayoutNatural) + return p +} + +// NewHorizontalPanel creates a new Panel initialized with +// LayoutHorizontal layout. +// Default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewHorizontalPanel() Panel { + p := NewPanel() + p.SetLayout(LayoutHorizontal) + return p +} + +// NewVerticalPanel creates a new Panel initialized with +// LayoutVertical layout. +// Default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewVerticalPanel() Panel { + return NewPanel() +} + +// newPanelImpl creates a new panelImpl. +func newPanelImpl() panelImpl { + return panelImpl{tableViewImpl: newTableViewImpl(), layout: LayoutVertical, 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(DisplayBlock).SetWidthPx(width) + c.Add(l) + return l +} + +func (c *panelImpl) AddVSpace(height int) Comp { + l := NewLabel("") + l.Style().SetDisplay(DisplayBlock).SetHeightPx(height) + c.Add(l) + return l +} + +func (c *panelImpl) AddSpace(width, height int) Comp { + l := NewLabel("") + l.Style().SetDisplay(DisplayBlock).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 LayoutNatural: + c.layoutNatural(w) + case LayoutHorizontal: + c.layoutHorizontal(w) + case LayoutVertical: + 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(strSpanOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + for _, c2 := range c.comps { + c2.Render(w) + } + + w.Write(strSpanCl) +} + +// layoutHorizontal renders the panel and the child components +// using the horizontal layout strategy. +func (c *panelImpl) layoutHorizontal(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + c.renderTr(w) + + for _, c2 := range c.comps { + c.renderTd(c2, w) + c2.Render(w) + } + + w.Write(strTableCl) +} + +// layoutVertical renders the panel and the child components +// using the vertical layout strategy. +func (c *panelImpl) layoutVertical(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + // 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(strTableCl) +} + +// renderTd renders the formatted HTML TD tag for the specified child component. +func (c *panelImpl) renderTd(c2 Comp, w Writer) { + if cf := c.cellFmts[c2.ID()]; cf == nil { + w.Write(strTD) + } else { + cf.render(strTDOp, w) + } +} diff --git a/gui/server/server.go b/gui/server/server.go new file mode 100644 index 0000000..c4b7101 --- /dev/null +++ b/gui/server/server.go @@ -0,0 +1,864 @@ +// Implementation of the GUI server which handles sessions, +// renders the windows and handles event dispatching. + +package server + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "sync" + "time" + "twfinder/logger" +) + +// Internal path constants. +const ( + pathStatic = "_gui_static/" // App path-relative path for GUI 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 +) + +// Parameters passed between the browser and the server. +const ( + paramEventType = "et" // Event type parameter name + paramCompID = "cid" // Component id parameter name + paramCompValue = "cval" // Component value parameter name + paramFocusedCompID = "fcid" // Focused component id parameter name + paramMouseWX = "mwx" // Mouse x pixel coordinate (inside window) + paramMouseWY = "mwy" // Mouse y pixel coordinate (inside window) + paramMouseX = "mx" // Mouse x pixel coordinate (relative to source component) + paramMouseY = "my" // Mouse y pixel coordinate (relative to source component) + paramMouseBtn = "mb" // Mouse button + paramModKeys = "mk" // Modifier key states + paramKeyCode = "kc" // Key code +) + +// Event response actions (client actions to take after processing an event). +const ( + eraNoAction = iota // Event processing OK and no action required + eraReloadWin // Window name to be reloaded + eraDirtyComps // There are dirty components which needs to be refreshed + eraFocusComp // Focus a component +) + +// Default GUI session id cookie name +const defaultSessIDCookieName = "gui-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) +} + +// AppRootHandlerFunc is the 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 { + // 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 server.Session) { + // win := server.NewWindow("login", "Login Window") + // // ...add content to the login window... + // s.AddWindow(win) + // } + AddSessCreatorName(name, text string) + + // AddSHandler adds a new session handler. + AddSHandler(handler SessionHandler) + + // SetHeaders sets extra HTTP response headers that are added to all responses. + // Supplied values are copied, so changes to the passed map afterwards have no effect. + // + // For example to add an extra "Server" header whose value is the server version: + // server.SetHeaders(map[string][]string{ + // "Server": {server.ServerVersion}, + // }) + SetHeaders(headers map[string][]string) + + // Headers returns the extra HTTP response headers that are added to all repsonses. + // A copy is returned, so changes to the returned map afterwards have no effect. + Headers() map[string][]string + + // AddStaticDir registers a directory whose content (files) recursively + // will be served by the server when requested. + // path is an app-path relative path to address a file, dir is the root directory + // to search in. + // Note that the app name must be included in absolute request paths, + // and it may be omitted if you want to use relative paths. + // Extra headers set by SetHeaders() will also be included in responses serving the static files. + // + // Example: + // AddStaticDir("img", "/tmp/myimg") + // Then request for absolute path "/appname/img/faces/happy.gif" will serve + // "/tmp/myimg/faces/happy.gif", just as the the request for relative path "img/faces/happy.gif". + AddStaticDir(path, dir string) error + + // 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) + + // 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) + + // SetDefaultRootWindow window to run on '/' paht + SetDefaultRootWindow(win Window) + + // SessIDCookieName returns the cookie name used to store the sever + // session ID. + SessIDCookieName() string + + // session ID. + SetSessIDCookieName(name string) + + // 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. + // Tip: Not passing any window names will start the server silently + // without opening any windows. + 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 + appURLString string // Application URL string + appURL *url.URL // Application URL, parsed + 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 + defaultRootWin Window // Default Root Win + sessIDCookieName string // Session ID cookie name + + sessMux sync.RWMutex // Mutex to protect state related to session handling +} + +// 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 addr == "" { + addr = "localhost:3434" + } + + s := &serverImpl{ + sessionImpl: newSessionImpl(false), + appName: appName, + addr: addr, + sessions: make(map[string]Session), + sessCreatorNames: make(map[string]string), + theme: ThemeDefault, + sessIDCookieName: defaultSessIDCookieName, + } + + if s.appName == "" { + s.appPath = "/" + } else { + s.appPath = "/" + s.appName + "/" + } + + if certFile == "" || keyFile == "" { + s.secure = false + s.appURLString = "http://" + addr + s.appPath + } else { + s.secure = true + s.appURLString = "https://" + addr + s.appPath + s.certFile = certFile + s.keyFile = keyFile + } + var err error + if s.appURL, err = url.Parse(s.appURLString); err != nil { + panic(fmt.Sprintf("Parse %q: %+v", s.appURLString, err)) + } + + s.appRootHandlerFunc = s.renderDefaultWin + + return s +} + +func (s *serverImpl) Secure() bool { + return s.secure +} + +func (s *serverImpl) AppURL() string { + return s.appURLString +} + +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.sessMux.Lock() + s.sessionHandlers = append(s.sessionHandlers, handler) + s.sessMux.Unlock() +} + +// 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.shared.session = sess + } + // Store new session + s.sessMux.Lock() + s.sessions[sess.ID()] = sess + + logger.Infof("SESSION created:", sess.ID()) + + // Notify session handlers + for _, handler := range s.sessionHandlers { + handler.Created(sess) + } + s.sessMux.Unlock() + + 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.shared.session.Private() { + s.sessMux.Lock() + s.removeSess2(e.shared.session) + s.sessMux.Unlock() + e.shared.session = &s.sessionImpl + } +} + +// removeSess2 removes (invalidates) the specified session. +// Only private sessions can be removed, calling this with the +// public session is a no-op. +// serverImpl.mux must be locked when this is called. +func (s *serverImpl) removeSess2(sess Session) { + if sess.Private() { + logger.Infof("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 GUI 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 + // MaxAge: to specify the max age of the cookie in seconds, else it's a session cookie and gets deleted after the browser is closed. + c := http.Cookie{ + Name: s.sessIDCookieName, + Value: sess.ID(), + Path: s.appURL.EscapedPath(), + HttpOnly: true, + Secure: s.secure, + MaxAge: 72 * 60 * 60, // 72 hours max age + } + 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() + + s.sessMux.Lock() + for _, sess := range s.sessions { + if now.Sub(sess.Accessed()) > sess.Timeout() { + s.removeSess2(sess) + } + } + s.sessMux.Unlock() + + time.Sleep(sleep) + } +} + +func (s *serverImpl) SetHeaders(headers map[string][]string) { + s.headers = make(map[string][]string, len(headers)) + for k, v := range headers { + // Also copy value which is a slice + s.headers[k] = append(make([]string, 0, len(v)), v...) + } +} + +func (s *serverImpl) Headers() map[string][]string { + headers := make(map[string][]string, len(s.headers)) + for k, v := range s.headers { + // Also copy value which is a slice + headers[k] = append(make([]string, 0, len(v)), v...) + } + return headers +} + +// addHeaders adds the extra headers to the specified response. +func (s *serverImpl) addHeaders(w http.ResponseWriter) { + header := w.Header() + for k, v := range s.headers { + for _, v2 := range v { + header.Add(k, v2) + } + } +} + +func (s *serverImpl) AddStaticDir(path, dir string) error { + if strings.HasPrefix(path, "/") { + path = path[1:] + } + + if path == "" { + return errors.New("path cannot be empty string") + } + + if !strings.HasSuffix(path, "/") { + path += "/" + } + + origPath := path + path = s.appPath + path + + // 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))) + // To include extra headers in the response of static handler: + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + s.addHeaders(w) + handler.ServeHTTP(w, r) + }) + + return nil +} + +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 +} + +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 +} + +func (s *serverImpl) SetDefaultRootWindow(win Window) { + s.defaultRootWin = win +} + +func (s *serverImpl) SessIDCookieName() string { + return s.sessIDCookieName +} + +func (s *serverImpl) SetSessIDCookieName(name string) { + s.sessIDCookieName = name +} + +// serveStatic handles the static contents of server. +func (s *serverImpl) serveStatic(w http.ResponseWriter, r *http.Request) { + s.addHeaders(w) + + // Parts example: "/appname/_gui_static/gui-0.8.0.js" => {"", "appname", "_gui_static", "gui-0.8.0.js"} + parts := strings.Split(r.URL.Path, "/") + + if s.appName == "" { + // 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 pathStatic + 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 pathStatic + parts = parts[3:] + } + + res := parts[0] + if res == resNameStaticJs { + w.Header().Set("Expires", time.Now().UTC().Add(72*time.Hour).Format(http.TimeFormat)) // Set 72 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().UTC().Add(72*time.Hour).Format(http.TimeFormat)) // Set 72 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) + } + + s.addHeaders(w) + + // Check session + var sess Session + c, err := r.Cookie(s.sessIDCookieName) + if err == nil { + s.sessMux.RLock() + sess = s.sessions[c.Value] + s.sessMux.RUnlock() + } + if sess == nil { + sess = &s.sessionImpl + } + + // Parts example: "/appname/winname/e?et=0&cid=1" => {"", "appname", "winname", "e"} + parts := strings.Split(r.URL.Path, "/") + + 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 ("/"). + 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 && parts[0] == pathSessCheck { + // Session check. Must not call sess.access() + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + remaining := sess.Timeout() - time.Now().Sub(sess.Accessed()) + fmt.Fprintf(w, "%f", remaining.Seconds()) + return + } + + if len(parts) < 1 || parts[0] == "" { + // Missing window name, render window list + s.appRootHandlerFunc(w, r, sess) + 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 { + // 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() { + 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. + 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 + } + + sess.access() + + var path string + if len(parts) >= 2 { + path = parts[1] + } + + rwMutex := sess.rwMutex() + switch path { + case pathEvent: + rwMutex.Lock() + defer rwMutex.Unlock() + + s.handleEvent(sess, win, w, r) + case pathRenderComp: + rwMutex.RLock() + defer rwMutex.RUnlock() + + // Render just a component + s.renderComp(win, w, r) + default: + rwMutex.RLock() + defer rwMutex.RUnlock() + + // Render the whole window + win.RenderWin(NewWriter(w), s) + } +} + +// renderWinList builds a temporary Window, adds links to the windows of +// a session, and renders the Window. +func (s *serverImpl) renderWinList(wr http.ResponseWriter, r *http.Request, sess Session) { + if s.logger != nil { + s.logger.Println("\tRendering windows list.") + } + win := NewWindow("windowList", s.text+" - Window List") + + titleLabel := NewLabel(s.text + " - Window List") + titleLabel.Style().SetFontWeight(FontWeightBold).SetFontSize("1.3em") + win.Add(titleLabel) + + addLinks := func(title string, nameTexts [][2]string) { + if len(nameTexts) == 0 { + return + } + win.AddVSpace(10) + win.Add(NewLabel(title)) + for _, nameText := range nameTexts { + link := NewLink(nameText[1], path.Join(s.appPath, nameText[0])) + link.Style().SetPaddingLeftPx(20) + win.Add(link) + } + } + + // Render both private and public session windows + sessions := make([]Session, 1, 2) + sessions[0] = sess + nameTexts := make([][2]string, 0, len(s.sessCreatorNames)+1) + if sess.Private() { + sessions = append(sessions, &s.sessionImpl) + } else if len(s.sessCreatorNames) > 0 { + // No private session yet, render session creators: + nameTexts = nameTexts[:0] + for name, text := range s.sessCreatorNames { + nameTexts = append(nameTexts, [2]string{name, text}) + } + addLinks("Session creators:", nameTexts) + } + + for _, session := range sessions { + text := "Public windows:" + if session.Private() { + text = "Authenticated windows:" + } + nameTexts = nameTexts[:0] + for _, win := range session.SortedWins() { + nameTexts = append(nameTexts, [2]string{win.Name(), win.Text()}) + } + addLinks(text, nameTexts) + } + + win.RenderWin(NewWriter(wr), s) +} + +// renderDefaultWin builds the default Window +func (s *serverImpl) renderDefaultWin(wr http.ResponseWriter, r *http.Request, sess Session) { + if s.defaultRootWin == nil { + s.renderWinList(wr, r, s) + return + } + s.defaultRootWin.RenderWin(NewWriter(wr), s) +} + +// renderComp renders just a component. +func (s *serverImpl) renderComp(win Window, w http.ResponseWriter, r *http.Request) { + id, err := AtoID(r.FormValue(paramCompID)) + 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(paramFocusedCompID)) + if err == nil { + win.SetFocusedCompID(focCompID) + } + + id, err := AtoID(r.FormValue(paramCompID)) + 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(r, paramEventType) + 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(EventType(etype), comp, s, sess, wr, r) + shared := event.shared + + event.x = parseIntParam(r, paramMouseX) + if event.x >= 0 { + event.y = parseIntParam(r, paramMouseY) + shared.wx = parseIntParam(r, paramMouseWX) + shared.wy = parseIntParam(r, paramMouseWY) + shared.mbtn = MouseBtn(parseIntParam(r, paramMouseBtn)) + } else { + event.y, shared.wx, shared.wy, shared.mbtn = -1, -1, -1, -1 + } + + shared.modKeys = parseIntParam(r, paramModKeys) + shared.keyCode = Key(parseIntParam(r, paramKeyCode)) + + comp.preprocessEvent(event, r) + + // Dispatch event... + comp.dispatchEvent(event) + + // Check if a new session was created during event dispatching + if shared.session.New() { + s.addSessCookie(shared.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 shared.reload { + hasAction = true + w.Writevs(eraReloadWin, strComma, shared.reloadWin) + } else { + if len(shared.dirtyComps) > 0 { + hasAction = true + w.Writev(eraDirtyComps) + for id := range shared.dirtyComps { + w.Write(strComma) + w.Writev(int(id)) + } + } + if shared.focusedComp != nil { + if hasAction { + w.Write(strSemicol) + } else { + hasAction = true + } + w.Writevs(eraFocusComp, strComma, int(shared.focusedComp.ID())) + // Also register focusable comp at window + win.SetFocusedCompID(shared.focusedComp.ID()) + } + } + if !hasAction { + w.Writev(eraNoAction) + } +} + +// parseIntParam parses an int param. +// If error occurs, -1 will be returned. +func parseIntParam(r *http.Request, paramName string) int { + if num, err := strconv.Atoi(r.FormValue(paramName)); err == nil { + return num + } + return -1 +} diff --git a/gui/server/server_start.go b/gui/server/server_start.go new file mode 100644 index 0000000..b511de9 --- /dev/null +++ b/gui/server/server_start.go @@ -0,0 +1,65 @@ +// +build !appengine + +// Implementation of the GUI server Start in standalone apps (non-GAE). + +package server + +import ( + "net/http" + "os/exec" + "runtime" + "twfinder/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 = "xdg-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+pathStatic, func(w http.ResponseWriter, r *http.Request) { + s.serveStatic(w, r) + }) + + appURL := s.AppURL() + logger.Infof("Starting GUI server on:%v", appURL) + + for _, winName := range openWins { + if err := open(appURL + winName); err != nil { + if s.logger != nil { + s.logger.Printf("Opening window '%s' err: %v\n", appURL+winName, err) + } + } + } + + 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 +} diff --git a/gui/server/server_start_gae.go b/gui/server/server_start_gae.go new file mode 100644 index 0000000..0af428f --- /dev/null +++ b/gui/server/server_start_gae.go @@ -0,0 +1,26 @@ +// +build appengine + +// Implementation of the GUI server Start on Google App Engine. + +package server + +import ( + "net/http" + "twfinder/logger" +) + +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+pathStatic, func(w http.ResponseWriter, r *http.Request) { + s.serveStatic(w, r) + }) + + logger.Infof("Starting GUI server on path:", s.appPath) + + go s.sessCleaner() + + return nil +} diff --git a/gui/server/sess_monitor.go b/gui/server/sess_monitor.go new file mode 100644 index 0000000..d0e873e --- /dev/null +++ b/gui/server/sess_monitor.go @@ -0,0 +1,83 @@ +// Session monitor component interface and implementation. + +package server + +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: "gui-SessMonitor", "gui-SessMonitor-Expired", +// "gui-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("gui-SessMonitor") + c.SetJsConverter("convertSessTimeout") + return c +} + +func (c *sessMonitorImpl) SetJsConverter(jsFuncName string) { + c.SetAttr("guiJsFuncName", jsFuncName) +} + +func (c *sessMonitorImpl) JsConverter() string { + return c.Attr("guiJsFuncName") +} + +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/gui/server/session.go b/gui/server/session.go new file mode 100644 index 0000000..3b72198 --- /dev/null +++ b/gui/server/session.go @@ -0,0 +1,227 @@ +// Implementation of the GUI session. + +package server + +import ( + "crypto/rand" + "errors" + "fmt" + "sort" + "sync" + "time" + "twfinder/logger" +) + +// Session interface defines the session to the GUI 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. + // Implementation locks or the sessions RW mutex. + access() + + // ClearNew clears the new flag. + // After this New() will return false. + clearNew() + + // rwMutex returns the RW mutex of the session. + rwMutex() *sync.RWMutex +} + +// 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 + + rwMutexF *sync.RWMutex // RW mutex to synchronize session (and related Window and component) access +} + +// 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, rwMutexF: &sync.RWMutex{}} +} + +// Valid characters (bytes) to be used in session IDs +// Its length must be a power of 2. +const idChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" + +func init() { + // Is len(idChars) a power of 2? + if i := byte(len(idChars)); i&(i-1) != 0 { + panic(fmt.Sprint("len(idChars) must be power of 2: ", i)) + } +} + +// Length of the session IDs +const idLength = 22 + +// genID generates a new session ID. +func genID() string { + id := make([]byte, idLength) + if _, err := rand.Read(id); err != nil { + logger.Errorf("Failed to read from secure random: %v", err) + } + + for i, v := range id { + id[i] = idChars[v&byte(len(idChars)-1)] + } + 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 { + s.rwMutexF.RLock() + defer s.rwMutexF.RUnlock() + 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.rwMutexF.Lock() + s.accessed = time.Now() + s.rwMutexF.Unlock() +} + +func (s *sessionImpl) clearNew() { + s.isNew = false +} + +func (s *sessionImpl) rwMutex() *sync.RWMutex { + return s.rwMutexF +} diff --git a/gui/server/state_buttons.go b/gui/server/state_buttons.go new file mode 100644 index 0000000..567504b --- /dev/null +++ b/gui/server/state_buttons.go @@ -0,0 +1,401 @@ +// State button interfaces and implementations +// (CheckBox, RadioButton, SwitchButton). + +package server + +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: ETypeClick +// +// Default style classes: "gui-CheckBox", "gui-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: ETypeClick +// +// Default style classes: "gui-SwitchButton", "gui-SwitchButton-On-Active" +// "gui-SwitchButton-On-Inactive", "gui-SwitchButton-Off-Active", +// "gui-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: ETypeClick +// +// Default style classes: "gui-RadioButton", "gui-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 []byte // 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} +} + +var ( + strCheckbox = []byte("checkbox") // "checkbox" + strRadio = []byte("radio") // "radio" + strThisChecked = []byte("this.checked") // "this.checked" +) + +// NewCheckBox creates a new CheckBox. +// The initial state is false. +func NewCheckBox(text string) CheckBox { + c := newStateButtonImpl(text, strCheckbox, nil, "gui-CheckBox-Disabled") + c.Style().AddClass("gui-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(nil, "ON") + offButton := newButtonImpl(nil, "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 := []byte("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(ETypeClick) + c.SetAttr("cellspacing", "0") + c.SetAttr("cellpadding", "0") + c.Style().AddClass("gui-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, strRadio, group, "gui-RadioButton-Disabled") + c.Style().AddClass("gui-RadioButton") + return c +} + +// newStateButtonImpl creates a new stateButtonImpl. +func newStateButtonImpl(text string, inputType []byte, group RadioGroup, disabledClass string) *stateButtonImpl { + c := &stateButtonImpl{newButtonImpl(strThisChecked, text), false, inputType, group, nextCompID(), disabledClass} + // Use ETypeClick because IE fires onchange only when focus is lost... + c.AddSyncOnETypes(ETypeClick) + 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(paramCompValue) + 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) + } +} + +var ( + strInput = []byte(`" +) + +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(strSpanOp) + c.renderAttrsAndStyle(w) + w.Write(strGT) + + w.Write(strInput) + w.Write(c.inputType) + w.Write(strID) + w.Writev(int(c.inputID)) + w.Write(strQuote) + if c.group != nil { + w.Write(strName) + w.Writes(c.group.Name()) + w.Write(strQuote) + } + if c.state { + w.Write(strChecked) + } + c.renderEnabled(w) + c.renderEHandlers(w) + + w.Write(strLabelFor) + w.Writev(int(c.inputID)) + w.Write(strQuote) + // TODO readding click handler here causes double event sending... + // But we might add mouseover and other handlers still... + //c.renderEHandlers(w) + w.Write(strGT) + c.renderText(w) + w.Write(strLabelCl) + w.Write(strSpanCl) +} + +func (c *switchButtonImpl) Enabled() bool { + return c.onButton.Enabled() +} + +func (c *switchButtonImpl) SetEnabled(enabled bool) { + c.onButton.SetEnabled(enabled) + c.offButton.SetEnabled(enabled) +} + +func (c *switchButtonImpl) State() bool { + return c.state +} + +func (c *switchButtonImpl) SetState(state bool) { + // Only continue if state changes: + if c.state == state { + return + } + + c.state = state + + if c.state { + c.onButton.Style().SetClass("gui-SwitchButton-On-Active") + c.offButton.Style().SetClass("gui-SwitchButton-Off-Inactive") + } else { + c.onButton.Style().SetClass("gui-SwitchButton-On-Inactive") + c.offButton.Style().SetClass("gui-SwitchButton-Off-Active") + } +} + +func (c *switchButtonImpl) On() string { + return c.onButton.Text() +} +func (c *switchButtonImpl) Off() string { + return c.offButton.Text() +} + +func (c *switchButtonImpl) SetOnOff(on, off string) { + c.onButton.SetText(on) + c.offButton.SetText(off) +} + +func (c *switchButtonImpl) preprocessEvent(event Event, r *http.Request) { + value := r.FormValue(paramCompValue) + 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 changes style classes. + c.SetState(v) + // SwitchButtons' client code properly updates internal buttons' style, + // so we're good not to mark the switch button dirty if state changes. + } +} + +var ( + strClTr = []byte(">") // ">" + strTD50 = []byte(``) // `` +) + +func (c *switchButtonImpl) Render(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + // For Internet Explorer only: + // Since state synchronization is done on ETypeClick, which will add a click handler + // to the wrapper tag and not to the on/off buttons, the wrapper tag itself must be + // disabled (must have a 'disabled' attribute) if the switch button is disabled in order + // for clicks really be disabled. + c.onButton.renderEnabled(w) + w.Write(strClTr) + + w.Write(strTD50) + c.onButton.Render(w) + + w.Write(strTD50) + c.offButton.Render(w) + + w.Write(strTableCl) +} diff --git a/gui/server/style.go b/gui/server/style.go new file mode 100644 index 0000000..1a6f7ab --- /dev/null +++ b/gui/server/style.go @@ -0,0 +1,801 @@ +// Defines the style builder and style attribute constants. + +package server + +import ( + "strconv" +) + +// Style attribute constants. +const ( + StBackground = "background" // Background (color) + StBorder = "border" // Border + StBorderLeft = "border-left" // Left border + StBorderRight = "border-right" // Right border + StBorderTop = "border-top" // Top border + StBorderBottom = "border-bottom" // Bottom border + StColor = "color" // (Foreground) color + StCursor = "cursor" // Cursor + StDisplay = "display" // Display + StFontSize = "font-size" // Font size + StFontStyle = "font-style" // Font style + StFontWeight = "font-weight" // Font weight + StHeight = "height" // Height + StMargin = "margin" // Margin + StMarginLeft = "margin-left" // Left margin + StMarginRight = "margin-right" // Right margin + StMarginTop = "margin-top" // Top margin + StMarginBottom = "margin-bottom" // Bottom margin + StPadding = "padding" // Padding + StPaddingLeft = "padding-left" // Left padding + StPaddingRight = "padding-right" // Right padding + StPaddingTop = "padding-top" // Top padding + StPaddingBottom = "padding-bottom" // Bottom padding + StWhiteSpace = "white-space" // White-space + StWidth = "width" // Width +) + +// The 17 standard color constants. +const ( + ClrAqua = "Aqua" // Aqua (#00FFFF) + ClrBlack = "Black" // Black (#000000) + ClrBlue = "Blue" // Blue (#0000FF) + ClrFuchsia = "Fuchsia" // Fuchsia (#FF00FF) + ClrGray = "Gray" // Gray (#808080) + ClrGrey = "Grey" // Grey (#808080) + ClrGreen = "Green" // Green (#008000) + ClrLime = "Lime" // Lime (#00FF00) + ClrMaroon = "Maroon" // Maroon (#800000) + ClrNavy = "Navy" // Navy (#000080) + ClrOlive = "Olive" // Olive (#808000) + ClrPurple = "Purple" // Purple (#800080) + ClrRed = "Red" // Red (#FF0000) + ClrSilver = "Silver" // Silver (#C0C0C0) + ClrTeal = "Teal" // Teal (#008080) + ClrWhite = "White" // White (#FFFFFF) + ClrYellow = "Yellow" // Yellow (#FFFF00) +) + +// Border style constants. +const ( + BrdStyleSolid = "solid" // Solid + BrdStyleDashed = "dashed" // Dashed + BrdStyleDotted = "dotted" // Dotted + BrdStyleDouble = "double" // Double + BrdStyleGroove = "groove" // 3D grooved border + BrdStyleRidge = "ridge" // 3D ridged border + BrdStyleInset = "inset" // 3D inset border + BrdStyleOutset = "outset" // 3D outset border +) + +// Font weight constants. +const ( + FontWeightNormal = "normal" // Normal + FontWeightBold = "bold" // Bold + FontWeightBolder = "bolder" // Bolder + FontWeightLighter = "lighter" // Lighter +) + +// Font style constants. +const ( + FontStyleNormal = "normal" // Normal + FontStyleItalic = "italic" // Italic +) + +// Mouse cursor constants. +const ( + CursorAuto = "auto" // Default. Web browser sets the cursor. + CursorCrosshair = "crosshair" // Crosshair + CursorDefault = "default" // The default cursor. + CursorHelp = "help" // Help + CursorMove = "move" // Move + CursorPointer = "pointer" // Pointer + CursorProgress = "progress" // Progress + CursorText = "text" // Text + CursorWait = "wait" // Wait + CursorInherit = "inherit" // The cursor should be inherited from the parent element. +) + +// Display mode constants. +const ( + DisplayNone = "none" // The element will not be displayed. + DisplayBlock = "block" // The element is displayed as a block. + DisplayInline = "inline" // The element is displayed as an in-line element. This is the default. + DisplayInherit = "inherit" // The display property value will be inherited from the parent element. +) + +// White space constants. +const ( + WhiteSpaceNormal = "normal" // Sequences of white spaces are collapsed into a single whitespace. Text will wrap when necessary. This is the default. + WhiteSpaceNowrap = "nowrap" // Sequences of whitespace will collapse into a single whitespace. Text will never wrap to the next line (the text is in one line). + WhiteSpacePre = "pre" // Whitespace is preserved. Text will only wrap on line breaks. + WhiteSpacePreLine = "pre-line" // Sequences of whitespace will collapse into a single whitespace. Text will wrap when necessary and on line breaks. + WhiteSpacePreWrap = "pre-wrap" // Whitespace is preserved. Text will wrap when necessary, and on line breaks. + WhiteSpaceInherit = "inherit" // Whitespace property will be inherited from the parent element. +) + +// Style interface contains utility methods for manipulating +// the style of a component. +// You can think of it as the Style Builder. +// Set methods return the style reference so setting the values +// of multiple style attributes can be chained. +type Style interface { + // AddClass adds a style class name to the class name list. + AddClass(class string) Style + + // SetClass sets a style class name, removing all previously + // added style class names. + // Tip: set an empty string class name to remove all class names. + SetClass(class string) Style + + // RemoveClass removes a style class name. + // If the specified class is not found, this is a no-op. + RemoveClass(class string) Style + + // Get returns the explicitly set value of the specified style attribute. + // Explicitly set style attributes will be concatenated and rendered + // as the "style" HTML attribute of the component. + Get(name string) string + + // Set sets the value of the specified style attribute. + // Pass an empty string value to delete the specified style attribute. + Set(name, value string) Style + + // Size returns the size. + Size() (width, height string) + + // SetSize sets the width and height. + SetSize(width, height string) Style + + // SetSizePx sets the width and height, in pixels. + SetSizePx(width, height int) Style + + // SetFullSize sets full width (100%) and height (100%). + SetFullSize() Style + + // Padding returns the padding. + // (The "padding" style attribute only.) + Padding() string + + // SetPadding sets the padding. + // (The "padding" style attribute only.) + SetPadding(value string) Style + + // SetPadding2 sets the padding specified by parts. + // (The "padding" style attribute only.) + SetPadding2(top, right, bottom, left string) Style + + // SetPaddingPx sets the padding specified by parts, in pixels. + // (The "padding" style attribute only.) + SetPaddingPx(top, right, bottom, left int) Style + + // PaddingLeft returns the left padding. + // (The "padding-left" style attribute only.) + PaddingLeft() string + + // SetPaddingLeft sets the left padding. + // (The "padding-left" style attribute only.) + SetPaddingLeft(value string) Style + + // SetPaddingLeftPx sets the left padding, in pixels. + // (The "padding-left" style attribute only.) + SetPaddingLeftPx(width int) Style + + // PaddingRight returns the right padding. + // (The "padding-right" style attribute only.) + PaddingRight() string + + // SetPaddingRight sets the right padding. + // (The "padding-right" style attribute only.) + SetPaddingRight(value string) Style + + // SetPaddingRightPx sets the right padding, in pixels. + // (The "padding-right" style attribute only.) + SetPaddingRightPx(width int) Style + + // PaddingTop returns the top padding. + // (The "padding-top" style attribute only.) + PaddingTop() string + + // SetPaddingTop sets the top padding. + // (The "padding-top" style attribute only.) + SetPaddingTop(value string) Style + + // SetPaddingTopPx sets the top padding, in pixels. + // (The "padding-top" style attribute only.) + SetPaddingTopPx(height int) Style + + // PaddingBottom returns the bottom padding. + // (The "padding-bottom" style attribute only.) + PaddingBottom() string + + // SetPaddingBottom sets the bottom padding. + // (The "padding-bottom" style attribute only.) + SetPaddingBottom(value string) Style + + // SetPaddingBottomPx sets the bottom padding, in pixels. + // (The "padding-bottom" style attribute only.) + SetPaddingBottomPx(height int) Style + + // Margin returns the margin. + // (The "margin" style attribute only.) + Margin() string + + // SetMargin sets the margin. + // (The "margin" style attribute only.) + SetMargin(value string) Style + + // SetMargin2 sets the margin specified by parts. + // (The "margin" style attribute only.) + SetMargin2(top, right, bottom, left string) Style + + // SetMarginPx sets the margin specified by parts, in pixels. + // (The "margin" style attribute only.) + SetMarginPx(top, right, bottom, left int) Style + + // MarginLeft returns the left margin. + // (The "margin-left" style attribute only.) + MarginLeft() string + + // SetMarginLeft sets the left margin. + // (The "margin-left" style attribute only.) + SetMarginLeft(value string) Style + + // SetMarginLeftPx sets the left margin, in pixels. + // (The "margin-left" style attribute only.) + SetMarginLeftPx(width int) Style + + // MarginRight returns the right margin. + // (The "margin-right" style attribute only.) + MarginRight() string + + // SetMarginRight sets the right margin. + // (The "margin-right" style attribute only.) + SetMarginRight(value string) Style + + // SetMarginRightPx sets the right margin, in pixels. + // (The "margin-right" style attribute only.) + SetMarginRightPx(width int) Style + + // MarginTop returns the top margin. + // (The "margin-top" style attribute only.) + MarginTop() string + + // SetMarginTop sets the top margin. + // (The "margin-top" style attribute only.) + SetMarginTop(value string) Style + + // SetMarginTopPx sets the top margin, in pixels. + // (The "margin-top" style attribute only.) + SetMarginTopPx(height int) Style + + // MarginBottom returns the bottom margin. + // (The "margin-bottom" style attribute only.) + MarginBottom() string + + // SetMarginBottom sets the bottom margin. + // (The "margin-bottom" style attribute only.) + SetMarginBottom(value string) Style + + // SetMarginBottomPx sets the bottom margin, in pixels. + // (The "margin-bottom" style attribute only.) + SetMarginBottomPx(height int) Style + + // Background returns the background (color). + Background() string + + // SetBackground sets the background (color). + SetBackground(value string) Style + + // Border returns the border. + Border() string + + // SetBorder sets the border. + SetBorder(value string) Style + + // SetBorder2 sets the border specified by parts. + // (The "border" style attribute only.) + SetBorder2(width int, style, color string) Style + + // BorderLeft returns the left border. + BorderLeft() string + + // SetBorderLeft sets the left border. + SetBorderLeft(value string) Style + + // SetBorderLeft2 sets the left border specified by parts. + // (The "border-left" style attribute only.) + SetBorderLeft2(width int, style, color string) Style + + // BorderRight returns the right border. + BorderRight() string + + // SetBorderRight sets the right border. + SetBorderRight(value string) Style + + // SetBorderRight2 sets the right border specified by parts. + // (The "border-right" style attribute only.) + SetBorderRight2(width int, style, color string) Style + + // BorderTop returns the top border. + BorderTop() string + + // SetBorderTop sets the top border. + SetBorderTop(value string) Style + + // SetBorderTop2 sets the top border specified by parts. + // (The "border-top" style attribute only.) + SetBorderTop2(width int, style, color string) Style + + // BorderBottom returns the bottom border. + BorderBottom() string + + // SetBorderBottom sets the bottom border. + SetBorderBottom(value string) Style + + // SetBorderBottom2 sets the bottom border specified by parts. + // (The "border-bottom" style attribute only.) + SetBorderBottom2(width int, style, color string) Style + + // Color returns the (foreground) color. + Color() string + + // SetColor sets the (foreground) color. + SetColor(value string) Style + + // Cursor returns the (mouse) cursor. + Cursor() string + + // SetCursor sets the (mouse) cursor. + SetCursor(value string) Style + + // Display returns the display mode. + Display() string + + // SetDisplay sets the display mode + SetDisplay(value string) Style + + // FontSize returns the font size. + FontSize() string + + // SetFontSize sets the font size. + SetFontSize(value string) Style + + // FontStyle returns the font style. + FontStyle() string + + // SetFontStyle sets the font style. + SetFontStyle(value string) Style + + // FontWeight returns the font weight. + FontWeight() string + + // SetFontWeight sets the font weight. + SetFontWeight(value string) Style + + // Width returns the width. + Width() string + + // SetWidth sets the width. + SetWidth(value string) Style + + // SetWidthPx sets the width, in pixels. + SetWidthPx(width int) Style + + // SetFullWidth sets full width (100%). + SetFullWidth() Style + + // Height returns the height. + Height() string + + // SetHeight sets the height. + SetHeight(value string) Style + + // SetHeightPx sets the height. + SetHeightPx(height int) Style + + // SetFullHeight sets full height (100%). + SetFullHeight() Style + + // WhiteSpace returns the white space attribute value. + WhiteSpace() string + + // SetWhiteSpace sets the white space attribute value. + SetWhiteSpace(value string) Style + + // render renders all style information (style class names + // and style attributes). + render(w Writer) + + // renderClasses renders the style class names. + renderClasses(w Writer) + + // renderAttrs renders the style attributes. + renderAttrs(w Writer) +} + +type styleImpl struct { + classes []string // Style classes. + attrs map[string]string // Explicitly set style attributes. Lazily initialized. +} + +// newStyleImpl creates a new styleImpl. +func newStyleImpl() *styleImpl { + return &styleImpl{} +} + +func (s *styleImpl) AddClass(class string) Style { + s.classes = append(s.classes, class) + return s +} + +func (s *styleImpl) SetClass(class string) Style { + s.classes = s.classes[0:0] + if len(class) > 0 { + s.classes = append(s.classes, class) + } + return s +} + +func (s *styleImpl) RemoveClass(class string) Style { + for i, cl := range s.classes { + if cl == class { + oldClasses := s.classes + s.classes = append(oldClasses[0:i], oldClasses[i+1:]...) + oldClasses[len(oldClasses)-1] = "" + break + } + } + + return s +} + +func (s *styleImpl) Get(name string) string { + return s.attrs[name] +} + +func (s *styleImpl) Set(name, value string) Style { + if s.attrs == nil { + s.attrs = make(map[string]string) + } + + if len(value) > 0 { + s.attrs[name] = value + } else { + delete(s.attrs, name) + } + return s +} + +func (s *styleImpl) Size() (width, height string) { + return s.Get(StWidth), s.Get(StHeight) +} + +func (s *styleImpl) SetSize(width, height string) Style { + s.Set(StWidth, width) + s.Set(StHeight, height) + return s +} + +func (s *styleImpl) SetSizePx(width, height int) Style { + return s.SetSize(strconv.Itoa(width)+"px", strconv.Itoa(height)+"px") +} + +func (s *styleImpl) SetFullSize() Style { + return s.SetSize("100%", "100%") +} + +func (s *styleImpl) Padding() string { + return s.Get(StPadding) +} + +func (s *styleImpl) SetPadding(value string) Style { + return s.Set(StPadding, value) +} + +func (s *styleImpl) SetPadding2(top, right, bottom, left string) Style { + return s.SetPadding(top + " " + right + " " + bottom + " " + left) +} + +func (s *styleImpl) SetPaddingPx(top, right, bottom, left int) Style { + return s.SetPadding(strconv.Itoa(top) + "px " + strconv.Itoa(right) + "px " + strconv.Itoa(bottom) + "px " + strconv.Itoa(left) + "px") +} + +func (s *styleImpl) PaddingLeft() string { + return s.Get(StPaddingLeft) +} + +func (s *styleImpl) SetPaddingLeft(value string) Style { + return s.Set(StPaddingLeft, value) +} + +func (s *styleImpl) SetPaddingLeftPx(width int) Style { + return s.SetPaddingLeft(strconv.Itoa(width) + "px") +} + +func (s *styleImpl) PaddingRight() string { + return s.Get(StPaddingRight) +} + +func (s *styleImpl) SetPaddingRight(value string) Style { + return s.Set(StPaddingRight, value) +} + +func (s *styleImpl) SetPaddingRightPx(width int) Style { + return s.SetPaddingRight(strconv.Itoa(width) + "px") +} + +func (s *styleImpl) PaddingTop() string { + return s.Get(StPaddingTop) +} + +func (s *styleImpl) SetPaddingTop(value string) Style { + return s.Set(StPaddingTop, value) +} + +func (s *styleImpl) SetPaddingTopPx(height int) Style { + return s.SetPaddingTop(strconv.Itoa(height) + "px") +} + +func (s *styleImpl) PaddingBottom() string { + return s.Get(StPaddingBottom) +} + +func (s *styleImpl) SetPaddingBottom(value string) Style { + return s.Set(StPaddingBottom, value) +} + +func (s *styleImpl) SetPaddingBottomPx(height int) Style { + return s.SetPaddingBottom(strconv.Itoa(height) + "px") +} + +func (s *styleImpl) Margin() string { + return s.Get(StMargin) +} + +func (s *styleImpl) SetMargin(value string) Style { + return s.Set(StMargin, value) +} + +func (s *styleImpl) SetMargin2(top, right, bottom, left string) Style { + return s.SetMargin(top + " " + right + " " + bottom + " " + left) +} + +func (s *styleImpl) SetMarginPx(top, right, bottom, left int) Style { + return s.SetMargin(strconv.Itoa(top) + "px " + strconv.Itoa(right) + "px " + strconv.Itoa(bottom) + "px " + strconv.Itoa(left) + "px ") +} + +func (s *styleImpl) MarginLeft() string { + return s.Get(StMarginLeft) +} + +func (s *styleImpl) SetMarginLeft(value string) Style { + return s.Set(StMarginLeft, value) +} + +func (s *styleImpl) SetMarginLeftPx(width int) Style { + return s.SetMarginLeft(strconv.Itoa(width) + "px") +} + +func (s *styleImpl) MarginRight() string { + return s.Get(StMarginRight) +} + +func (s *styleImpl) SetMarginRight(value string) Style { + return s.Set(StMarginRight, value) +} + +func (s *styleImpl) SetMarginRightPx(width int) Style { + return s.SetMarginRight(strconv.Itoa(width) + "px") +} + +func (s *styleImpl) MarginTop() string { + return s.Get(StMarginTop) +} + +func (s *styleImpl) SetMarginTop(value string) Style { + return s.Set(StMarginTop, value) +} + +func (s *styleImpl) SetMarginTopPx(height int) Style { + return s.SetMarginTop(strconv.Itoa(height) + "px") +} + +func (s *styleImpl) MarginBottom() string { + return s.Get(StMarginBottom) +} + +func (s *styleImpl) SetMarginBottom(value string) Style { + return s.Set(StMarginBottom, value) +} + +func (s *styleImpl) SetMarginBottomPx(height int) Style { + return s.SetMarginBottom(strconv.Itoa(height) + "px") +} + +func (s *styleImpl) Background() string { + return s.Get(StBackground) +} + +func (s *styleImpl) SetBackground(value string) Style { + return s.Set(StBackground, value) +} + +func (s *styleImpl) Border() string { + return s.Get(StBorder) +} + +func (s *styleImpl) SetBorder(value string) Style { + return s.Set(StBorder, value) +} + +func (s *styleImpl) SetBorder2(width int, style, color string) Style { + return s.SetBorder(strconv.Itoa(width) + "px " + style + " " + color) +} + +func (s *styleImpl) BorderLeft() string { + return s.Get(StBorderLeft) +} + +func (s *styleImpl) SetBorderLeft(value string) Style { + return s.Set(StBorderLeft, value) +} + +func (s *styleImpl) SetBorderLeft2(width int, style, color string) Style { + return s.SetBorderLeft(strconv.Itoa(width) + "px " + style + " " + color) +} + +func (s *styleImpl) BorderRight() string { + return s.Get(StBorderRight) +} + +func (s *styleImpl) SetBorderRight(value string) Style { + return s.Set(StBorderRight, value) +} + +func (s *styleImpl) SetBorderRight2(width int, style, color string) Style { + return s.SetBorderRight(strconv.Itoa(width) + "px " + style + " " + color) +} + +func (s *styleImpl) BorderTop() string { + return s.Get(StBorderTop) +} + +func (s *styleImpl) SetBorderTop(value string) Style { + return s.Set(StBorderTop, value) +} + +func (s *styleImpl) SetBorderTop2(width int, style, color string) Style { + return s.SetBorderTop(strconv.Itoa(width) + "px " + style + " " + color) +} + +func (s *styleImpl) BorderBottom() string { + return s.Get(StBorderBottom) +} + +func (s *styleImpl) SetBorderBottom(value string) Style { + return s.Set(StBorderBottom, value) +} + +func (s *styleImpl) SetBorderBottom2(width int, style, color string) Style { + return s.SetBorderBottom(strconv.Itoa(width) + "px " + style + " " + color) +} + +func (s *styleImpl) Color() string { + return s.Get(StColor) +} + +func (s *styleImpl) SetColor(value string) Style { + return s.Set(StColor, value) +} + +func (s *styleImpl) Cursor() string { + return s.Get(StCursor) +} + +func (s *styleImpl) SetCursor(value string) Style { + return s.Set(StCursor, value) +} + +func (s *styleImpl) Display() string { + return s.Get(StDisplay) +} + +func (s *styleImpl) SetDisplay(value string) Style { + return s.Set(StDisplay, value) +} + +func (s *styleImpl) FontSize() string { + return s.Get(StFontSize) +} + +func (s *styleImpl) SetFontSize(value string) Style { + return s.Set(StFontSize, value) +} + +func (s *styleImpl) FontStyle() string { + return s.Get(StFontStyle) +} + +func (s *styleImpl) SetFontStyle(value string) Style { + return s.Set(StFontStyle, value) +} + +func (s *styleImpl) FontWeight() string { + return s.Get(StFontWeight) +} + +func (s *styleImpl) SetFontWeight(value string) Style { + return s.Set(StFontWeight, value) +} + +func (s *styleImpl) Height() string { + return s.Get(StHeight) +} + +func (s *styleImpl) SetHeight(value string) Style { + return s.Set(StHeight, value) +} +func (s *styleImpl) SetHeightPx(height int) Style { + return s.SetHeight(strconv.Itoa(height) + "px") +} + +func (s *styleImpl) SetFullHeight() Style { + return s.SetHeight("100%") +} + +func (s *styleImpl) Width() string { + return s.Get(StWidth) +} + +func (s *styleImpl) SetWidth(value string) Style { + return s.Set(StWidth, value) +} + +func (s *styleImpl) SetWidthPx(width int) Style { + return s.SetWidth(strconv.Itoa(width) + "px") +} + +func (s *styleImpl) SetFullWidth() Style { + return s.SetWidth("100%") +} + +func (s *styleImpl) WhiteSpace() string { + return s.Get(StWhiteSpace) +} + +func (s *styleImpl) SetWhiteSpace(value string) Style { + return s.Set(StWhiteSpace, value) +} + +func (s *styleImpl) render(w Writer) { + s.renderClasses(w) + + if s.attrs != nil { + w.Write(strStyle) + s.renderAttrs(w) + w.Write(strQuote) + } +} + +func (s *styleImpl) renderClasses(w Writer) { + if len(s.classes) > 0 { + w.Write(strClass) + for i, class := range s.classes { + if i > 0 { + w.Write(strSpace) + } + w.Writes(class) + } + w.Write(strQuote) + } +} + +func (s *styleImpl) renderAttrs(w Writer) { + for name, value := range s.attrs { + w.Writes(name) + w.Write(strColon) + w.Writes(value) + w.Write(strSemicol) + } +} diff --git a/gui/server/table.go b/gui/server/table.go new file mode 100644 index 0000000..d6ed51e --- /dev/null +++ b/gui/server/table.go @@ -0,0 +1,409 @@ +// Table component interface and implementation. + +package server + +// Table interface defines a container which lays out its children +// using a configurable, flexible table. +// The size of the table grows dynamically, on demand. However, +// if table size is known or can be guessed before/during building it, +// it is recommended to call EnsureSize to minimize reallocations +// in the background. +// +// Default style class: "gui-Table" +type Table interface { + // Table is a TableView. + TableView + + // EnsureSize ensures that the table will have at least the specified + // rows, and at least the specified columns in rows whose index is < rows. + EnsureSize(rows, cols int) + + // EnsureCols ensures that the table will have at least the specified + // cols at the specified row. + // This implicitly includes that the table must have at least (row+1) rows. + // If the table have less than (row+1) rows, empty rows will be added first. + EnsureCols(row, cols int) + + // CompsCount returns the number of components added to the table. + CompsCount() int + + // CompAt returns the component at the specified row and column. + // Returns nil if row or column are invalid. + CompAt(row, col int) Comp + + // CompIdx returns the row and column of the specified component in the table. + // (-1, -1) is returned if the component is not added to the table. + CompIdx(c Comp) (row, col int) + + // RowFmt returns the row formatter of the specified table row. + // If the table does not have a row specified by row, nil is returned. + RowFmt(row int) CellFmt + + // CellFmt returns the cell formatter of the specified table cell. + // If the table does not have a cell specified by row and col, + // nil is returned. + CellFmt(row, col int) CellFmt + + // Add adds a component to the table. + // Return value indicates if the component was added successfully. + // Returns false if row or col is negative. + Add(c Comp, row, col int) bool + + // RowSpan returns the row span of the specified table cell. + // -1 is returned if the table does not have a cell specified by row and col. + RowSpan(row, col int) int + + // SetRowSpan sets the row span of the specified table cell. + // If the table does not have a cell specified by row and col, + // this is a no-op. + SetRowSpan(row, col, rowSpan int) + + // ColSpan returns the col span of the specified table cell. + // -1 is returned if the table does not have a cell specified by row and col. + ColSpan(row, col int) int + + // SetColSpan sets the col span of the specified table cell. + // If the table does not have a cell specified by row and col, + // this is a no-op. + SetColSpan(row, col, colSpan int) + + // Trim trims all the rows: removes trailing cells that has nil component + // by making the rows shorter. + // This comes handy for example if the table contains cells where colspan > 1 is set; + // by calling this method we can ensure no empty cells will be rendered at the end of such rows. + Trim() + + // TrimRow trims the specified row: removes trailing cells that has nil value + // by making the row shorter. + TrimRow(row int) +} + +// cellIdx type specifies a cell by its row and col indices. +type cellIdx struct { + row, col int // Row and col indices of the cell. +} + +// Table implementation. +type tableImpl struct { + tableViewImpl // TableView implementation + + comps [][]Comp // Components added to the table. Structure: comps[rowIdx][colIdx] + rowFmts map[int]*cellFmtImpl // Lazily initialized row formatters of the rows + cellFmts map[cellIdx]*cellFmtImpl // Lazily initialized cell formatters of the cells +} + +// NewTable creates a new Table. +// Default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewTable() Table { + c := &tableImpl{tableViewImpl: newTableViewImpl()} + c.Style().AddClass("gui-Table") + c.SetCellSpacing(0) + c.SetCellPadding(0) + return c +} + +func (c *tableImpl) Remove(c2 Comp) bool { + row, col := c.CompIdx(c2) + if row < 0 { + return false + } + + c2.setParent(nil) + c.comps[row][col] = nil + + return true +} + +func (c *tableImpl) ByID(id ID) Comp { + if c.id == id { + return c + } + + for _, rowComps := range c.comps { + for _, c2 := range rowComps { + if c2 == nil { + continue + } + 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 *tableImpl) Clear() { + // Clear row formatters + if c.rowFmts != nil { + c.rowFmts = nil + } + // Clear cell formatters + if c.cellFmts != nil { + c.cellFmts = nil + } + + for _, rowComps := range c.comps { + for _, c2 := range rowComps { + if c2 != nil { + c2.setParent(nil) + } + } + } + c.comps = nil +} + +func (c *tableImpl) EnsureSize(rows, cols int) { + c.ensureRows(rows) + + // Ensure column count in each row + for i := 0; i < rows; i++ { + c.EnsureCols(i, cols) + } +} + +func (c *tableImpl) EnsureCols(row, cols int) { + c.ensureRows(row + 1) + + rowComps := c.comps[row] + if cols > len(rowComps) { + c.comps[row] = append(rowComps, make([]Comp, cols-len(rowComps))...) + } +} + +// EnsureRows ensures that the table will have at least the specified rows. +func (c *tableImpl) ensureRows(rows int) { + if rows > len(c.comps) { + c.comps = append(c.comps, make([][]Comp, rows-len(c.comps))...) + } +} + +func (c *tableImpl) CompsCount() (count int) { + for _, rowComps := range c.comps { + for _, c2 := range rowComps { + if c2 != nil { + count++ + } + } + } + return +} + +func (c *tableImpl) CompAt(row, col int) Comp { + if row < 0 || col < 0 || row >= len(c.comps) { + return nil + } + + rowComps := c.comps[row] + if col >= len(rowComps) { + return nil + } + + return rowComps[col] +} + +func (c *tableImpl) CompIdx(c2 Comp) (int, int) { + for row, rowComps := range c.comps { + for col, c3 := range rowComps { + if c3 == nil { + continue + } + if c2.Equals(c3) { + return row, col + } + } + } + + return -1, -1 +} + +func (c *tableImpl) RowFmt(row int) CellFmt { + if row < 0 || row >= len(c.comps) { + return nil + } + + if c.rowFmts == nil { + c.rowFmts = make(map[int]*cellFmtImpl) + } + + rf := c.rowFmts[row] + if rf == nil { + rf = newCellFmtImpl() + c.rowFmts[row] = rf + } + + return rf +} + +func (c *tableImpl) CellFmt(row, col int) CellFmt { + if row < 0 || col < 0 || row >= len(c.comps) || col >= len(c.comps[row]) { + return nil + } + + if c.cellFmts == nil { + c.cellFmts = make(map[cellIdx]*cellFmtImpl) + } + + ci := cellIdx{row, col} + + cf := c.cellFmts[ci] + if cf == nil { + cf = newCellFmtImpl() + c.cellFmts[ci] = cf + } + + return cf +} + +func (c *tableImpl) Add(c2 Comp, row, col int) bool { + c2.makeOrphan() + + // Quick check of row and col + if row < 0 || col < 0 { + return false + } + if row >= len(c.comps) { + c.ensureRows(row + 1) + } + if col >= len(c.comps[row]) { + c.EnsureCols(row, col+1) + } + + rowComps := c.comps[row] + + // Remove component if there is already one at the specified row and column: + if rowComps[col] != nil { + rowComps[col].setParent(nil) + } + + rowComps[col] = c2 + c2.setParent(c) + + return true +} + +func (c *tableImpl) RowSpan(row, col int) int { + cf := c.CellFmt(row, col) + if cf == nil { + return -1 + } + + return cf.iAttr("rowspan") +} + +func (c *tableImpl) SetRowSpan(row, col, rowSpan int) { + cf := c.CellFmt(row, col) + if cf == nil { + return + } + + if rowSpan < 2 { + cf.setAttr("rowspan", "") // Delete attribute + } else { + cf.setIAttr("rowspan", rowSpan) + } +} + +func (c *tableImpl) ColSpan(row, col int) int { + cf := c.CellFmt(row, col) + if cf == nil { + return -1 + } + + return cf.iAttr("colspan") +} + +func (c *tableImpl) SetColSpan(row, col, colSpan int) { + cf := c.CellFmt(row, col) + if cf == nil { + return + } + + if colSpan < 2 { + cf.setAttr("colspan", "") // Delete attribute + } else { + cf.setIAttr("colspan", colSpan) + } +} + +func (c *tableImpl) Trim() { + for row := range c.comps { + c.TrimRow(row) + } +} + +func (c *tableImpl) TrimRow(row int) { + if row < 0 || row >= len(c.comps) { + return + } + + rowComps := c.comps[row] + ci := cellIdx{row: row, col: len(rowComps) - 1} // Create a reusable cell index + for ; ci.col >= 0 && rowComps[ci.col] == nil; ci.col-- { + // Cell is about to "disappear", remove its formatter (if any) + delete(c.cellFmts, ci) + } + + // ci.col now points to a non-nil component (or is -1) + c.comps[row] = rowComps[:ci.col+1] +} + +func (c *tableImpl) Render(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + // Create a reusable cell index + ci := cellIdx{} + + for row, rowComps := range c.comps { + c.renderRowTr(row, w) + for col, c2 := range rowComps { + ci.row, ci.col = row, col + c.renderTd(ci, w) + if c2 != nil { + c2.Render(w) + } + } + } + + w.Write(strTableCl) +} + +// renderRowTr renders the formatted HTML TR tag for the specified row. +func (c *tableImpl) renderRowTr(row int, w Writer) { + var defha = c.halign // default halign of the table + var defva = c.valign // default valign of the table + + if rf := c.rowFmts[row]; rf == nil { + c.renderTr(w) + } else { + // If rf does not specify alignments, it means alignments must not be overridden, + // default alignments of the table must be used! + ha, va := rf.halign, rf.valign + if ha == HADefault { + ha = defha + } + if va == VADefault { + va = defva + } + + rf.renderWithAligns(strTROp, ha, va, w) + } +} + +// renderTd renders the formatted HTML TD tag for the specified cell. +func (c *tableImpl) renderTd(ci cellIdx, w Writer) { + if cf := c.cellFmts[ci]; cf == nil { + w.Write(strTD) + } else { + cf.render(strTDOp, w) + } +} diff --git a/gui/server/tabpanel.go b/gui/server/tabpanel.go new file mode 100644 index 0000000..a1e3b32 --- /dev/null +++ b/gui/server/tabpanel.go @@ -0,0 +1,368 @@ +// TabPanel component interface and implementation. + +package server + +// TabBar interface defines the tab bar for selecting the visible +// component of a TabPanel. +// +// Note: Removing a tab component through the tab bar also +// removes the content component from the tab panel of the tab bar. +// +// Default style classes: "gui-TabBar", "gui-TabBar-Top", "gui-TabBar-Bottom", +// "gui-TabBar-Left", "gui-TabBar-Right", "gui-TabBar-NotSelected", +// "gui-TabBar-Selected" +type TabBar interface { + // TabBar is a PanelView. + PanelView +} + +// TabBar implementation. +type tabBarImpl struct { + panelImpl // panel implementation +} + +// newTabBarImpl creates a new tabBarImpl. +func newTabBarImpl() *tabBarImpl { + c := &tabBarImpl{newPanelImpl()} + return c +} + +func (c *tabBarImpl) Remove(c2 Comp) bool { + i := c.CompIdx(c2) + if i < 0 { + return false + } + + // Removing a tab component also needs removing the + // associated content component. Call parent's (TabPanel) Remove() + // method which takes care of everything: + if parent := c.parent; parent != nil { + if tabPanel, isTabPanel := parent.(TabPanel); isTabPanel { + return tabPanel.Remove(tabPanel.CompAt(i)) + } + } + + return c.panelImpl.Remove(c2) +} + +// TabBarPlacement is the Tab bar placement type. +type TabBarPlacement int + +// Tab bar placements. +const ( + TbPlacementTop TabBarPlacement = iota // Tab bar placement to Top + TbPlacementBottom // Tab bar placement to Bottom + TbPlacementLeft // Tab bar placement to Left + TbPlacementRight // Tab bar placement to Right +) + +// TabPanel interface defines a PanelView which has multiple child components +// but only one is visible at a time. The visible child can be visually selected +// using an internal TabBar component. +// +// Both the tab panel and its internal tab bar component are PanelViews. +// This gives high layout configuration possibilities. +// Usually you only need to set the tab bar placement with the SetTabBarPlacement() +// method which also sets other reasonable internal layout defaults. +// But you have many other options to override the layout settings. +// If the content component is bigger than the tab bar, you can set the tab bar +// horizontal and the vertical alignment, e.g. with the TabBar().SetAlignment() method. +// To apply cell formatting to individual content components, you can simply use the +// CellFmt() method. If the tab bar is bigger than the content component, you can set +// the content alignment, e.g. with the SetAlignment() method. You can also set different +// alignments for individual tab components using TabBar().CellFmt(). You can also set +// other cell formatting applied to the tab bar using TabBarFmt() method. +// +// You can register ETypeStateChange event handlers which will be called when the user +// changes tab selection by clicking on a tab. The event source will be the tab panel. +// The event will have a parent event whose source will be the clicked tab and will +// contain the mouse coordinates. +// +// Default style classes: "gui-TabPanel", "gui-TabPanel-Content" +type TabPanel interface { + // TabPanel is a Container. + PanelView + + // TabBar returns the tab bar. + TabBar() TabBar + + // TabBarPlacement returns the tab bar placement. + TabBarPlacement() TabBarPlacement + + // SetTabBarPlacement sets tab bar placement. + // Also sets the alignment of the tab bar according + // to the tab bar placement. + SetTabBarPlacement(tabBarPlacement TabBarPlacement) + + // TabBarFmt returns the cell formatter of the tab bar. + TabBarFmt() CellFmt + + // Add adds a new tab (component) and an associated (content) component + // to the tab panel. + Add(tab, content Comp) + + // Add adds a new tab (string) and an associated (content) component + // to the tab panel. + // This is a shorthand for + // Add(NewLabel(tab), content) + AddString(tab string, content Comp) + + // Selected returns the selected tab idx. + // Returns -1 if no tab is selected. + Selected() int + + // PrevSelected returns the previous selected tab idx. + // Returns -1 if no tab was previously selected. + PrevSelected() int + + // SetSelected sets the selected tab idx. + // If idx < 0, no tabs will be selected. + // If idx > CompsCount(), this is a no-op. + SetSelected(idx int) +} + +// TabPanel implementation. +type tabPanelImpl struct { + panelImpl // panel implementation: TabPanel is a Panel, but only PanelView's methods are exported. + + tabBarImpl *tabBarImpl // Tab bar implementation + tabBarPlacement TabBarPlacement // Tab bar placement + tabBarFmt *cellFmtImpl // Tab bar cell formatter + + selected int // The selected tab idx + prevSelected int // Previous selected tab idx +} + +// NewTabPanel creates a new TabPanel. +// Default tab bar placement is TbPlacementTop, +// default horizontal alignment is HADefault, +// default vertical alignment is VADefault. +func NewTabPanel() TabPanel { + c := &tabPanelImpl{panelImpl: newPanelImpl(), tabBarImpl: newTabBarImpl(), tabBarFmt: newCellFmtImpl(), selected: -1, prevSelected: -1} + c.tabBarFmt.Style().AddClass("gui-TabBar") + c.tabBarImpl.setParent(c) + c.SetTabBarPlacement(TbPlacementTop) + c.tabBarFmt.SetAlign(HALeft, VATop) + c.Style().AddClass("gui-TabPanel") + return c +} + +func (c *tabPanelImpl) Remove(c2 Comp) bool { + i := c.CompIdx(c2) + if i < 0 { + // Try the tab bar: + i = c.tabBarImpl.CompIdx(c2) + if i < 0 { + return false + } + + // It's a tab component + return c.Remove(c.panelImpl.CompAt(i)) + } + + // It's a content component + c.tabBarImpl.panelImpl.Remove(c.tabBarImpl.CompAt(i)) + c.panelImpl.Remove(c2) + + // Update the previous selected + if c.prevSelected >= 0 { + if i < c.prevSelected { + c.prevSelected-- // Keep the same previous selected by decreasing its index by 1 + } else if i == c.prevSelected { // Previous selected tab was removed... + c.prevSelected = -1 + } + } + + // Update the current selected + if i < c.selected { + c.selected-- // Keep the same tab selected by decreasing its index by 1 + } else if i == c.selected { // Selected tab was removed... + // Store previous selected as it will be implicitly changed here + prevSelected := c.prevSelected + if i < c.CompsCount() { + c.SetSelected(i) // There is next tab, select it + } else if i > 0 { // Last was selected and removed but there are previous tabs... + c.SetSelected(i - 1) // ...select the "new" last one + } else { // Last was selected and removed and no previous tabs... + c.SetSelected(-1) // No tabs remained. + } + // Restore previous selected + c.prevSelected = prevSelected + } + + return true +} + +func (c *tabPanelImpl) ByID(id ID) Comp { + // panelImpl.ById() also checks our own id first + c2 := c.panelImpl.ByID(id) + if c2 != nil { + return c2 + } + + c2 = c.tabBarImpl.ByID(id) + if c2 != nil { + return c2 + } + + return nil +} + +func (c *tabPanelImpl) Clear() { + c.tabBarImpl.Clear() + c.panelImpl.Clear() + + c.SetSelected(-1) +} + +func (c *tabPanelImpl) TabBar() TabBar { + return c.tabBarImpl +} + +func (c *tabPanelImpl) TabBarPlacement() TabBarPlacement { + return c.tabBarPlacement +} + +func (c *tabPanelImpl) SetTabBarPlacement(tabBarPlacement TabBarPlacement) { + style := c.tabBarFmt.Style() + + // Remove old style class + switch c.tabBarPlacement { + case TbPlacementTop: + style.RemoveClass("gui-TabBar-Top") + case TbPlacementBottom: + style.RemoveClass("gui-TabBar-Bottom") + case TbPlacementLeft: + style.RemoveClass("gui-TabBar-Left") + case TbPlacementRight: + style.RemoveClass("gui-TabBar-Right") + } + + c.tabBarPlacement = tabBarPlacement + + switch tabBarPlacement { + case TbPlacementTop: + c.tabBarImpl.SetLayout(LayoutHorizontal) + c.tabBarImpl.SetAlign(HALeft, VABottom) + style.AddClass("gui-TabBar-Top") + case TbPlacementBottom: + c.tabBarImpl.SetLayout(LayoutHorizontal) + c.tabBarImpl.SetAlign(HALeft, VATop) + style.AddClass("gui-TabBar-Bottom") + case TbPlacementLeft: + c.tabBarImpl.SetLayout(LayoutVertical) + c.tabBarImpl.SetAlign(HARight, VATop) + style.AddClass("gui-TabBar-Left") + case TbPlacementRight: + c.tabBarImpl.SetLayout(LayoutVertical) + c.tabBarImpl.SetAlign(HALeft, VATop) + style.AddClass("gui-TabBar-Right") + } +} + +func (c *tabPanelImpl) TabBarFmt() CellFmt { + return c.tabBarFmt +} + +func (c *tabPanelImpl) Add(tab, content Comp) { + c.tabBarImpl.Add(tab) + c.panelImpl.Add(content) + c.tabBarImpl.CellFmt(tab).Style().AddClass("gui-TabBar-NotSelected") + c.CellFmt(content).Style().AddClass("gui-TabPanel-Content") + + if c.CompsCount() == 1 { + c.SetSelected(0) + } + + // TODO would be nice to remove this internal handler func when the tab is removed! + tab.AddEHandlerFunc(func(e Event) { + c.SetSelected(c.CompIdx(content)) + e.MarkDirty(c) + if c.handlers[ETypeStateChange] != nil { + c.dispatchEvent(e.forkEvent(ETypeStateChange, c)) + } + }, ETypeClick) +} + +func (c *tabPanelImpl) AddString(tab string, content Comp) { + tabc := NewLabel(tab) + tabc.Style().SetDisplay(DisplayBlock) // Display: block - so the whole cell of the tab is clickable + c.Add(tabc, content) +} + +func (c *tabPanelImpl) Selected() int { + return c.selected +} + +func (c *tabPanelImpl) PrevSelected() int { + return c.prevSelected +} + +func (c *tabPanelImpl) SetSelected(idx int) { + if idx >= c.CompsCount() { + return + } + + if c.selected >= 0 { + // Deselect current selected + style := c.tabBarImpl.CellFmt(c.tabBarImpl.CompAt(c.selected)).Style() + style.RemoveClass("gui-TabBar-Selected") + style.AddClass("gui-TabBar-NotSelected") + } + + c.prevSelected = c.selected + c.selected = idx + + if c.selected >= 0 { + // Select new selected + style := c.tabBarImpl.CellFmt(c.tabBarImpl.CompAt(c.selected)).Style() + style.RemoveClass("gui-TabBar-NotSelected") + style.AddClass("gui-TabBar-Selected") + } +} + +func (c *tabPanelImpl) Render(w Writer) { + w.Write(strTableOp) + c.renderAttrsAndStyle(w) + c.renderEHandlers(w) + w.Write(strGT) + + switch c.tabBarPlacement { + case TbPlacementTop: + w.Write(strTR) + c.tabBarFmt.render(strTDOp, w) + c.tabBarImpl.Render(w) + c.renderTr(w) + c.renderContent(w) + case TbPlacementBottom: + c.renderTr(w) + c.renderContent(w) + w.Write(strTR) + c.tabBarFmt.render(strTDOp, w) + c.tabBarImpl.Render(w) + case TbPlacementLeft: + c.renderTr(w) + c.tabBarFmt.render(strTDOp, w) + c.tabBarImpl.Render(w) + c.renderContent(w) + case TbPlacementRight: + c.renderTr(w) + c.renderContent(w) + c.tabBarFmt.render(strTDOp, w) + c.tabBarImpl.Render(w) + } + + w.Write(strTableCl) +} + +// renderContent renders the selected content component. +func (c *tabPanelImpl) renderContent(w Writer) { + // Render only the selected content component + if c.selected >= 0 { + c2 := c.comps[c.selected] + c.renderTd(c2, w) + c2.Render(w) + } else { + w.Write(strTD) + } +} diff --git a/gui/server/textbox_pwbox.go b/gui/server/textbox_pwbox.go new file mode 100644 index 0000000..b9b216a --- /dev/null +++ b/gui/server/textbox_pwbox.go @@ -0,0 +1,241 @@ +// Defines the TextBox component. + +package server + +import ( + "net/http" + "strconv" +) + +// TextBox interface defines a component for text input purpose. +// +// Suggested event type to handle actions: ETypeChange +// +// By default the value of the TextBox is synchronized with the server +// on ETypeChange event which is when the TextBox loses focus +// or when the ENTER key is pressed. +// If you want a TextBox to synchronize values during editing +// (while you type in characters), add the ETypeKeyUp event type +// to the events on which synchronization happens by calling: +// AddSyncOnETypes(ETypeKeyUp) +// +// Default style class: "gui-TextBox" +type TextBox interface { + // TextBox is a component. + Comp + + // TextBox has text. + HasText + + // TextBox can be enabled/disabled. + HasEnabled + + // ReadOnly returns if the text box is read-only. + ReadOnly() bool + + // SetReadOnly sets if the text box is read-only. + SetReadOnly(readOnly bool) + + // Rows returns the number of displayed rows. + Rows() int + + // SetRows sets the number of displayed rows. + // rows=1 will make this a simple, one-line input text box, + // rows>1 will make this a text area. + SetRows(rows int) + + // Cols returns the number of displayed columns. + Cols() int + + // SetCols sets the number of displayed columns. + SetCols(cols int) + + // MaxLength returns the maximum number of characters + // allowed in the text box. + // -1 is returned if there is no maximum length set. + MaxLength() int + + // SetMaxLength sets the maximum number of characters + // allowed in the text box. + // Pass -1 to not limit the maximum length. + SetMaxLength(maxLength int) +} + +// PasswBox interface defines a text box for password input purpose. +// +// Suggested event type to handle actions: ETypeChange +// +// By default the value of the PasswBox is synchronized with the server +// on ETypeChange event which is when the PasswBox loses focus +// or when the ENTER key is pressed. +// If you want a PasswBox to synchronize values during editing +// (while you type in characters), add the ETypeKeyUp event type +// to the events on which synchronization happens by calling: +// AddSyncOnETypes(ETypeKeyUp) +// +// Default style class: "gui-PasswBox" +type PasswBox interface { + // PasswBox is a TextBox. + TextBox +} + +// TextBox implementation. +type textBoxImpl struct { + compImpl // Component implementation + hasTextImpl // Has text implementation + hasEnabledImpl // Has enabled implementation + + isPassw bool // Tells if the text box is a password box + rows, cols int // Number of displayed rows and columns. +} + +var ( + strEncURIThisV = []byte("encodeURIComponent(this.value)") // "encodeURIComponent(this.value)" +) + +// NewTextBox creates a new TextBox. +func NewTextBox(text string) TextBox { + c := newTextBoxImpl(strEncURIThisV, text, false) + c.Style().AddClass("gui-TextBox") + return &c +} + +// NewPasswBox creates a new PasswBox. +func NewPasswBox(text string) TextBox { + c := newTextBoxImpl(strEncURIThisV, text, true) + c.Style().AddClass("gui-PasswBox") + return &c +} + +// newTextBoxImpl creates a new textBoxImpl. +func newTextBoxImpl(valueProviderJs []byte, text string, isPassw bool) textBoxImpl { + c := textBoxImpl{newCompImpl(valueProviderJs), newHasTextImpl(text), newHasEnabledImpl(), isPassw, 1, 20} + c.AddSyncOnETypes(ETypeChange) + return c +} + +func (c *textBoxImpl) ReadOnly() bool { + ro := c.Attr("readonly") + return len(ro) > 0 +} + +func (c *textBoxImpl) SetReadOnly(readOnly bool) { + if readOnly { + c.SetAttr("readonly", "readonly") + } else { + c.SetAttr("readonly", "") + } +} + +func (c *textBoxImpl) Rows() int { + return c.rows +} + +func (c *textBoxImpl) SetRows(rows int) { + c.rows = rows +} + +func (c *textBoxImpl) Cols() int { + return c.cols +} + +func (c *textBoxImpl) SetCols(cols int) { + c.cols = cols +} + +func (c *textBoxImpl) MaxLength() int { + if ml := c.Attr("maxlength"); len(ml) > 0 { + if i, err := strconv.Atoi(ml); err == nil { + return i + } + } + return -1 +} + +func (c *textBoxImpl) SetMaxLength(maxLength int) { + if maxLength < 0 { + c.SetAttr("maxlength", "") + } else { + c.SetAttr("maxlength", strconv.Itoa(maxLength)) + } +} + +func (c *textBoxImpl) preprocessEvent(event Event, r *http.Request) { + // Empty string for text box is a valid value. + // So we have to check whether it is supplied, not just whether its len() > 0 + value := r.FormValue(paramCompValue) + if len(value) > 0 { + c.text = value + } else { + // Empty string might be a valid value, if the component value param is present: + values, present := r.Form[paramCompValue] // Form is surely parsed (we called FormValue()) + if present && len(values) > 0 { + c.text = values[0] + } + } +} + +func (c *textBoxImpl) Render(w Writer) { + if c.rows <= 1 || c.isPassw { + c.renderInput(w) + } else { + c.renderTextArea(w) + } +} + +var ( + strInputOp = []byte(``) // `"/>` +) + +// renderInput renders the component as an input HTML tag. +func (c *textBoxImpl) renderInput(w Writer) { + w.Write(strInputOp) + if c.isPassw { + w.Write(strPassword) + } else { + w.Write(strText) + } + w.Write(strSize) + w.Writev(c.cols) + w.Write(strQuote) + c.renderAttrsAndStyle(w) + c.renderEnabled(w) + c.renderEHandlers(w) + + w.Write(strValue) + c.renderText(w) + w.Write(strInputCl) +} + +var ( + strTextareaOp = []byte("\n") // "\">\n" + strTextAreaCl = []byte("") // "" +) + +// renderTextArea renders the component as an textarea HTML tag. +func (c *textBoxImpl) renderTextArea(w Writer) { + w.Write(strTextareaOp) + c.renderAttrsAndStyle(w) + c.renderEnabled(w) + c.renderEHandlers(w) + + // New line char after the