// Copyright 2010 The Walk Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build windows package walk import ( "bytes" "log" "strconv" "strings" "unsafe" "github.com/lxn/win" ) const splitterWindowClass = `\o/ Walk_Splitter_Class \o/` var splitterHandleDraggingBrush *SolidColorBrush func init() { AppendToWalkInit(func() { MustRegisterWindowClass(splitterWindowClass) splitterHandleDraggingBrush, _ = NewSolidColorBrush(Color(win.GetSysColor(win.COLOR_BTNSHADOW))) splitterHandleDraggingBrush.wb2info = map[*WindowBase]*windowBrushInfo{nil: nil} }) } type Splitter struct { ContainerBase handleWidth int mouseDownPos Point // in native pixels draggedHandle *splitterHandle persistent bool removing bool } func newSplitter(parent Container, orientation Orientation) (*Splitter, error) { layout := newSplitterLayout(Horizontal) s := &Splitter{ ContainerBase: ContainerBase{ layout: layout, }, handleWidth: 5, } s.children = newWidgetList(s) layout.container = s if err := InitWidget( s, parent, splitterWindowClass, win.WS_VISIBLE, win.WS_EX_CONTROLPARENT); err != nil { return nil, err } var succeeded bool defer func() { if !succeeded { s.Dispose() } }() s.SetBackground(NullBrush()) if err := s.setOrientation(orientation); err != nil { return nil, err } s.SetPersistent(true) succeeded = true return s, nil } func NewHSplitter(parent Container) (*Splitter, error) { return newSplitter(parent, Horizontal) } func NewVSplitter(parent Container) (*Splitter, error) { return newSplitter(parent, Vertical) } func (s *Splitter) SetLayout(value Layout) error { return newError("not supported") } func (s *Splitter) HandleWidth() int { return s.handleWidth } func (s *Splitter) SetHandleWidth(value int) error { if value == s.handleWidth { return nil } if value < 1 { return newError("invalid handle width") } s.handleWidth = value s.RequestLayout() return nil } func (s *Splitter) Orientation() Orientation { layout := s.layout.(*splitterLayout) return layout.Orientation() } func (s *Splitter) setOrientation(value Orientation) error { var cursor Cursor if value == Horizontal { cursor = CursorSizeWE() } else { cursor = CursorSizeNS() } for i, wb := range s.Children().items { if i%2 == 1 { wb.window.SetCursor(cursor) } } layout := s.layout.(*splitterLayout) return layout.SetOrientation(value) } func (s *Splitter) updateMarginsForFocusEffect() { var margins Margins var parentLayout Layout if s.parent != nil { if parentLayout = s.parent.Layout(); parentLayout != nil { if m := parentLayout.Margins(); m.HNear < 9 || m.HFar < 9 || m.VNear < 9 || m.VFar < 9 { parentLayout = nil } } } var affected bool if FocusEffect != nil { for _, wb := range s.children.items { if wb.window.(Widget).GraphicsEffects().Contains(FocusEffect) { affected = true break } } } if affected { var marginsNeeded bool for _, wb := range s.children.items { switch wb.window.(type) { case *splitterHandle, *TabWidget, Container: default: marginsNeeded = true break } } if marginsNeeded { margins = Margins{5, 5, 5, 5} } } if parentLayout != nil { parentLayout.SetMargins(Margins{9 - margins.HNear, 9 - margins.VNear, 9 - margins.HFar, 9 - margins.VFar}) } s.layout.SetMargins(margins) } func (s *Splitter) Persistent() bool { return s.persistent } func (s *Splitter) SetPersistent(value bool) { s.persistent = value } func (s *Splitter) SaveState() error { buf := bytes.NewBuffer(nil) count := s.children.Len() layout := s.Layout().(*splitterLayout) for i := 0; i < count; i += 2 { if i > 0 { buf.WriteString(" ") } item := layout.hwnd2Item[s.children.At(i).Handle()] size := item.oldExplicitSize if size == 0 { size = item.size } buf.WriteString(strconv.FormatInt(int64(size), 10)) } s.WriteState(buf.String()) for _, wb := range s.children.items { if persistable, ok := wb.window.(Persistable); ok { if err := persistable.SaveState(); err != nil { return err } } } return nil } func (s *Splitter) RestoreState() error { childCount := s.children.Len()/2 + 1 if childCount == 0 { return nil } state, err := s.ReadState() if err != nil { return err } if state == "" { return nil } sizeStrs := strings.Split(state, " ") // FIXME: Solve this in a better way. if len(sizeStrs) != childCount { log.Print("*Splitter.RestoreState: failed due to unexpected child count (FIXME!)") return nil } layout := s.layout.(*splitterLayout) s.SetSuspended(true) layout.suspended = true defer func() { layout.suspended = false s.SetSuspended(false) }() var space int size := s.ClientBoundsPixels().Size() if s.Orientation() == Horizontal { space = size.Width } else { space = size.Height } regularSpace := space - layout.spaceUnavailableToRegularWidgets() for i, wb := range s.children.items { widget := wb.window.(Widget) if i%2 == 0 { j := i/2 + i%2 s := sizeStrs[j] size, err := strconv.Atoi(s) if err != nil { // OK, we probably got old style settings which were stored as fractions. fraction, err := strconv.ParseFloat(s, 64) if err != nil { return err } size = int(float64(regularSpace) * fraction) } item := layout.hwnd2Item[widget.Handle()] item.size = size item.oldExplicitSize = size } } for _, wb := range s.children.items { if persistable, ok := wb.window.(Persistable); ok { if err := persistable.RestoreState(); err != nil { return err } } } return nil } func (s *Splitter) Fixed(widget Widget) bool { return s.layout.(*splitterLayout).Fixed(widget) } func (s *Splitter) SetFixed(widget Widget, fixed bool) error { item := s.layout.(*splitterLayout).hwnd2Item[widget.Handle()] if item == nil { return newError("unknown widget") } item.fixed = fixed if b := widget.BoundsPixels(); fixed && item.size == 0 && (b.Width == 0 || b.Height == 0) { b.Width, b.Height = 100, 100 widget.SetBoundsPixels(b) item.size = 100 } return nil } func (s *Splitter) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_WINDOWPOSCHANGED: wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam)) if wp.Flags&win.SWP_NOSIZE != 0 { break } layout := s.layout.(*splitterLayout) layout.resetNeeded = false for _, item := range layout.hwnd2Item { item.oldExplicitSize = 0 } } return s.ContainerBase.WndProc(hwnd, msg, wParam, lParam) } func (s *Splitter) onInsertingWidget(index int, widget Widget) (err error) { return s.ContainerBase.onInsertingWidget(index, widget) } func (s *Splitter) onInsertedWidget(index int, widget Widget) (err error) { defer func() { if err != nil { return } s.updateMarginsForFocusEffect() }() _, isHandle := widget.(*splitterHandle) if isHandle { if s.Orientation() == Horizontal { widget.SetCursor(CursorSizeWE()) } else { widget.SetCursor(CursorSizeNS()) } } else { layout := s.Layout().(*splitterLayout) item := &splitterLayoutItem{stretchFactor: 1, wasVisible: true} layout.hwnd2Item[widget.Handle()] = item layout.resetNeeded = true if !layout.suspended && widget.AsWidgetBase().visible { s.RequestLayout() } item.visibleChangedHandle = widget.VisibleChanged().Attach(func() { if !layout.suspended && widget.AsWidgetBase().visible != item.wasVisible { layout.resetNeeded = true s.RequestLayout() } }) if s.children.Len()%2 == 0 { defer func() { if err != nil { return } var handle *splitterHandle handle, err = newSplitterHandle(s) if err != nil { return } closestVisibleWidget := func(offset, direction int) Widget { index := offset + direction for index >= 0 && index < len(s.children.items) { if wb := s.children.items[index]; wb.visible { return wb.window.(Widget) } index += direction } return nil } handleIndex := index + 1 - index%2 err = s.children.Insert(handleIndex, handle) if err == nil { // FIXME: These handlers will be leaked, if widgets get removed. handle.MouseDown().Attach(func(x, y int, button MouseButton) { if button != LeftButton { return } s.draggedHandle = handle s.mouseDownPos = Point{x, y} handle.SetBackground(splitterHandleDraggingBrush) }) handle.MouseMove().Attach(func(x, y int, button MouseButton) { if s.draggedHandle == nil { return } handleIndex := s.children.Index(s.draggedHandle) bh := s.draggedHandle.BoundsPixels() prev := closestVisibleWidget(handleIndex, -1) bp := prev.BoundsPixels() msep := minSizeEffective(createLayoutItemForWidget(prev)) next := closestVisibleWidget(handleIndex, 1) bn := next.BoundsPixels() msen := minSizeEffective(createLayoutItemForWidget(next)) dpi := s.draggedHandle.DPI() handleWidth := IntFrom96DPI(s.handleWidth, dpi) if s.Orientation() == Horizontal { xh := s.draggedHandle.XPixels() xnew := xh + x - s.mouseDownPos.X if xnew < bp.X+msep.Width { xnew = bp.X + msep.Width } else if xnew >= bn.X+bn.Width-msen.Width-handleWidth { xnew = bn.X + bn.Width - msen.Width - handleWidth } if e := s.draggedHandle.SetXPixels(xnew); e != nil { return } } else { yh := s.draggedHandle.YPixels() ynew := yh + y - s.mouseDownPos.Y if ynew < bp.Y+msep.Height { ynew = bp.Y + msep.Height } else if ynew >= bn.Y+bn.Height-msen.Height-handleWidth { ynew = bn.Y + bn.Height - msen.Height - handleWidth } if e := s.draggedHandle.SetYPixels(ynew); e != nil { return } } rc := bh.toRECT() if s.Orientation() == Horizontal { rc.Left -= int32(bp.X) rc.Right -= int32(bp.X) } else { rc.Top -= int32(bp.Y) rc.Bottom -= int32(bp.Y) } win.InvalidateRect(prev.Handle(), &rc, true) rc = bh.toRECT() if s.Orientation() == Horizontal { rc.Left -= int32(bn.X) rc.Right -= int32(bn.X) } else { rc.Top -= int32(bn.Y) rc.Bottom -= int32(bn.Y) } win.InvalidateRect(next.Handle(), &rc, true) s.draggedHandle.Invalidate() }) handle.MouseUp().Attach(func(x, y int, button MouseButton) { if s.draggedHandle == nil { return } defer s.RequestLayout() dragHandle := s.draggedHandle handleIndex := s.children.Index(dragHandle) prev := closestVisibleWidget(handleIndex, -1) next := closestVisibleWidget(handleIndex, 1) s.draggedHandle = nil dragHandle.SetBackground(NullBrush()) prev.AsWidgetBase().invalidateBorderInParent() next.AsWidgetBase().invalidateBorderInParent() prev.SetSuspended(true) defer prev.Invalidate() defer prev.SetSuspended(false) next.SetSuspended(true) defer next.Invalidate() defer next.SetSuspended(false) bh := dragHandle.BoundsPixels() bp := prev.BoundsPixels() bn := next.BoundsPixels() var sizePrev int var sizeNext int if s.Orientation() == Horizontal { bp.Width = bh.X - bp.X bn.Width -= (bh.X + bh.Width) - bn.X bn.X = bh.X + bh.Width sizePrev = bp.Width sizeNext = bn.Width } else { bp.Height = bh.Y - bp.Y bn.Height -= (bh.Y + bh.Height) - bn.Y bn.Y = bh.Y + bh.Height sizePrev = bp.Height sizeNext = bn.Height } layout := s.Layout().(*splitterLayout) prevItem := layout.hwnd2Item[prev.Handle()] prevItem.size = sizePrev prevItem.oldExplicitSize = sizePrev nextItem := layout.hwnd2Item[next.Handle()] nextItem.size = sizeNext nextItem.oldExplicitSize = sizeNext }) } }() } } return s.ContainerBase.onInsertedWidget(index, widget) } func (s *Splitter) onRemovingWidget(index int, widget Widget) (err error) { return s.ContainerBase.onRemovingWidget(index, widget) } func (s *Splitter) onRemovedWidget(index int, widget Widget) (err error) { defer func() { if err != nil { return } s.updateMarginsForFocusEffect() }() _, isHandle := widget.(*splitterHandle) if !s.removing && isHandle && s.children.Len()%2 == 1 { return newError("cannot remove splitter handle") } if !isHandle { sl := s.layout.(*splitterLayout) widget.AsWidgetBase().Property("Visible").Changed().Detach(sl.hwnd2Item[widget.Handle()].visibleChangedHandle) } if !isHandle && s.children.Len() > 1 { defer func() { if err != nil { return } var handleIndex int if index == 0 { handleIndex = 0 } else { handleIndex = index - 1 } s.removing = true handle := s.children.items[handleIndex].window.(*splitterHandle) if err = handle.SetParent(nil); err == nil { sl := s.layout.(*splitterLayout) for _, item := range sl.hwnd2Item { item.oldExplicitSize = 0 item.keepSize = false } sl.resetNeeded = true s.RequestLayout() handle.Dispose() } s.removing = false }() } err = s.ContainerBase.onRemovedWidget(index, widget) return } func (s *Splitter) onClearingWidgets() (err error) { panic("not implemented") } func (s *Splitter) onClearedWidgets() (err error) { panic("not implemented") } func (s *Splitter) CreateLayoutItem(ctx *LayoutContext) LayoutItem { return s.layout.CreateLayoutItem(ctx) }