diff options
Diffstat (limited to 'pim/cmd/cal9/menu.go')
| -rw-r--r-- | pim/cmd/cal9/menu.go | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/pim/cmd/cal9/menu.go b/pim/cmd/cal9/menu.go new file mode 100644 index 0000000..4443b00 --- /dev/null +++ b/pim/cmd/cal9/menu.go @@ -0,0 +1,267 @@ +package main + +import ( + "fmt" + "time" + "os" + "os/exec" + "regexp" + "strings" + + "9front/gui/draw" +) + +// A button-3 menu, the way page and vdir do it: press 3, drag, release on an +// item. acme's plumb-on-3 is the outlier on this system, so plumbing lives +// in here as an item rather than owning the button. +type menuItem struct { + label string + do func(*state) +} + +func (s *state) menu(at draw.Point, mc *draw.Mousectl) { + items := s.menuItems() + if len(items) == 0 { + return + } + + lh := int32(s.f.Height) + 4 + var w int32 + for _, it := range items { + if x := s.f.Width(it.label) + 20; x > w { + w = x + } + } + h := lh*int32(len(items)) + 4 + win := s.win.Rect() + r := draw.Rect(at.X, at.Y, at.X+w, at.Y+h) + // Keep it on screen. + if r.Max.X > win.Max.X { + r = r.Add(draw.Point{X: win.Max.X - r.Max.X}) + } + if r.Max.Y > win.Max.Y { + r = r.Add(draw.Point{Y: win.Max.Y - r.Max.Y}) + } + + sel := -1 + paint := func() { + s.fill(r, "tag") + s.fill(draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+1), "border") + s.fill(draw.Rect(r.Min.X, r.Max.Y-1, r.Max.X, r.Max.Y), "border") + s.fill(draw.Rect(r.Min.X, r.Min.Y, r.Min.X+1, r.Max.Y), "border") + s.fill(draw.Rect(r.Max.X-1, r.Min.Y, r.Max.X, r.Max.Y), "border") + for i, it := range items { + ir := draw.Rect(r.Min.X+1, r.Min.Y+2+int32(i)*lh, r.Max.X-1, r.Min.Y+2+int32(i+1)*lh) + if i == sel { + s.fill(ir, "today") + } + s.text(draw.Point{X: ir.Min.X + 8, Y: ir.Min.Y + 2}, "ink", it.label) + } + s.d.Flush() + } + itemAt := func(p draw.Point) int { + if p.X < r.Min.X || p.X >= r.Max.X { + return -1 + } + i := int((p.Y - r.Min.Y - 2) / lh) + if i < 0 || i >= len(items) || p.Y < r.Min.Y+2 { + return -1 + } + return i + } + paint() + + // Track until button 3 comes back up. + for m := range mc.C { + if n := itemAt(m.Point); n != sel { + sel = n + paint() + } + if m.Buttons&4 == 0 { + if sel >= 0 { + items[sel].do(s) + } else { + s.redraw() + } + return + } + } +} + +func (s *state) menuItems() []menuItem { + items := []menuItem{} + if s.sel != nil { + items = append(items, menuItem{"Plumb", func(st *state) { + if err := st.plumb(st.sel); err != nil { + fmt.Fprintf(os.Stderr, "cal: plumb: %v\n", err) + } + st.redraw() + }}) + items = append(items, menuItem{"Open event", func(st *state) { + st.openEvent(st.sel) + }}) + items = append(items, menuItem{"Close event", func(st *state) { + st.sel = nil + st.redraw() + }}) + } + if s.sel == nil && !s.pick.IsZero() { + items = append(items, menuItem{"Close day", func(st *state) { + st.pick = time.Time{} + st.redraw() + }}) + } + // one toggle per calendar, so a crowded work calendar can be put + // aside without unmounting anything + for _, c := range s.cals { + c := c + mark := "[ ] " + if s.on[c] { + mark = "[x] " + } + items = append(items, menuItem{mark + c, func(st *state) { + st.on[c] = !st.on[c] + st.load() + st.redraw() + }}) + } + // A second view of the calendar is a second cal, in its own window. + items = append(items, menuItem{"Open " + strings.ToLower(viewName[s.view]), + func(st *state) { st.openCal(st.view, st.at) }}) + if !s.pick.IsZero() && s.view != vDay { + d := s.pick + items = append(items, menuItem{"Open " + d.Format("2 Jan"), + func(st *state) { st.openCal(vDay, d) }}) + } + items = append(items, + menuItem{"Today", func(st *state) { + st.at = now() + st.top = 8 * 60 + st.load() + st.redraw() + }}, + // rio has no iconify, so "compact" is a resize request: shrink the + // window and the small-window view takes over by itself. + menuItem{"Compact", func(st *state) { + r := st.win.Rect() + st.wctl(fmt.Sprintf("resize -r %d %d %d %d", + r.Min.X, r.Min.Y, r.Min.X+320, r.Min.Y+72)) + }}, + menuItem{"Restore", func(st *state) { + r := st.win.Rect() + st.wctl(fmt.Sprintf("resize -r %d %d %d %d", + r.Min.X, r.Min.Y, r.Min.X+820, r.Min.Y+620)) + }}, + menuItem{"Move", func(st *state) { st.track(st.mc, false) }}, + menuItem{"Resize", func(st *state) { st.track(st.mc, true) }}, + menuItem{"Hide", func(st *state) { st.wctl("hide") }}, + menuItem{"Exit", func(st *state) { st.quit = true }}, + ) + return items +} + +// wctl asks rio to do something to our window. Errors are worth showing: +// outside rio there is no wctl and the menu items simply do nothing. +func (s *state) wctl(cmd string) { + f, err := os.OpenFile("/dev/wctl", os.O_WRONLY, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "cal: wctl: %v\n", err) + return + } + defer f.Close() + if _, err := f.WriteString(cmd); err != nil { + fmt.Fprintf(os.Stderr, "cal: wctl %q: %v\n", cmd, err) + } +} + +var urlRe = regexp.MustCompile(`https?://[^\s<>"]+`) + +// plumb sends the event's join link to the plumber, falling back to the +// event file itself, which lands in acme. +func (s *state) plumb(e *event) error { + data := e.file + if body, err := os.ReadFile(e.file); err == nil { + if m := urlRe.Find(body); m != nil { + data = strings.TrimRight(string(m), ".,)") + } + } + return exec.Command("/bin/plumb", data).Run() +} + +// track implements Move and Resize ourselves. rio's wctl has no "let the +// user sweep" verb -- only move/resize with an explicit rectangle -- so we +// follow the pointer and write a new rect as it goes, and rio does the +// actual work. Any button press drops the window where it is. +// +// rio hands out a new image whenever the window's screen rect changes, so +// this loop has to service resize events as well as motion, or every write +// would be drawing into a stale image. +func (s *state) track(mc *draw.Mousectl, resize bool) { + r := s.win.Rect() + w, h := r.Dx(), r.Dy() + grab := draw.Point{} // pointer offset within the window, for Move + first := true + last := r + + for { + select { + case m, ok := <-mc.C: + if !ok { + return + } + if first { + grab = draw.Point{X: m.X - r.Min.X, Y: m.Y - r.Min.Y} + first = false + continue + } + if m.Buttons != 0 { // any press drops it + s.redraw() + return + } + var nr draw.Rectangle + if resize { + nr = draw.Rect(r.Min.X, r.Min.Y, m.X, m.Y) + if nr.Dx() < 120 { + nr.Max.X = nr.Min.X + 120 + } + if nr.Dy() < 60 { + nr.Max.Y = nr.Min.Y + 60 + } + } else { + min := draw.Point{X: m.X - grab.X, Y: m.Y - grab.Y} + nr = draw.Rect(min.X, min.Y, min.X+w, min.Y+h) + } + // Only bother rio when it would actually change something. + if abs(nr.Min.X-last.Min.X)+abs(nr.Min.Y-last.Min.Y)+ + abs(nr.Max.X-last.Max.X)+abs(nr.Max.Y-last.Max.Y) < 4 { + continue + } + last = nr + verb := "move" + if resize { + verb = "resize" + } + s.wctl(fmt.Sprintf("%s -r %d %d %d %d", verb, + nr.Min.X, nr.Min.Y, nr.Max.X, nr.Max.Y)) + + case <-mc.Resize: + win, err := s.d.Reattach("/dev", s.win) + if err != nil { + return + } + s.win = win + r = s.win.Rect() + if !resize { + w, h = r.Dx(), r.Dy() + } + s.redraw() + } + } +} + +func abs(n int32) int32 { + if n < 0 { + return -n + } + return n +} |
