14 Commits

14 changed files with 660 additions and 17 deletions

View File

@@ -47,8 +47,7 @@ func NewCalendar (tm time.Time) *Calendar {
calendar.grid = tomo.NewContainerBox()
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid", ""))
calendar.grid.SetLayout(layouts.NewGrid (
[]bool { true, true, true, true, true, true, true },
[]bool { }))
true, true, true, true, true, true, true)())
calendar.Add(NewInnerContainer (
layouts.Row { false, true, false },
prevButton, calendar.monthLabel, nextButton))

View File

@@ -19,7 +19,7 @@ func NewCheckbox (value bool) *Checkbox {
Box: tomo.NewBox(),
}
box.SetRole(tomo.R("objects", "Checkbox", ""))
box.SetValue(false)
box.SetValue(value)
box.OnMouseUp(box.handleMouseUp)
box.OnKeyUp(box.handleKeyUp)

156
colorpicker.go Normal file
View File

@@ -0,0 +1,156 @@
package objects
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/objects/layouts"
import "git.tebibyte.media/tomo/objects/internal"
// ColorPicker allows the user to pick a color by controlling its HSBA
// parameters.
type ColorPicker struct {
tomo.ContainerBox
value internal.HSVA
pickerMap *colorPickerMap
hueSlider *Slider
alphaSlider *Slider
on struct {
valueChange event.FuncBroadcaster
}
}
// NewColorPicker creates a new color picker with the specified color.
func NewColorPicker (value color.Color) *ColorPicker {
picker := &ColorPicker {
ContainerBox: tomo.NewContainerBox(),
}
picker.SetRole(tomo.R("objects", "ColorPicker", ""))
picker.SetLayout(layouts.Row { true, false, false })
picker.pickerMap = newColorPickerMap(picker)
picker.Add(picker.pickerMap)
picker.hueSlider = NewVerticalSlider(0.0)
picker.Add(picker.hueSlider)
picker.hueSlider.OnSlide(func () {
picker.value.H = picker.hueSlider.Value()
picker.on.valueChange.Broadcast()
picker.pickerMap.Invalidate()
})
picker.alphaSlider = NewVerticalSlider(0.0)
picker.Add(picker.alphaSlider)
picker.alphaSlider.OnSlide(func () {
picker.value.A = uint8(picker.alphaSlider.Value() * 255)
picker.on.valueChange.Broadcast()
picker.pickerMap.Invalidate()
})
if value == nil { value = color.Transparent }
picker.SetValue(value)
return picker
}
// SetValue sets the color of the picker.
func (this *ColorPicker) SetValue (value color.Color) {
if value == nil { value = color.Transparent }
this.value = internal.RGBAToHSVA(value.RGBA())
this.hueSlider.SetValue(this.value.H)
this.alphaSlider.SetValue(float64(this.value.A) / 255)
}
// Value returns the color of the picker.
func (this *ColorPicker) Value () color.Color {
return this.value
}
// RGBA satisfies the color.Color interface
func (this *ColorPicker) RGBA () (r, g, b, a uint32) {
return this.value.RGBA()
}
// OnValueChange specifies a function to be called when the swatch's color
// changes.
func (this *ColorPicker) OnValueChange (callback func ()) event.Cookie {
return this.on.valueChange.Connect(callback)
}
type colorPickerMap struct {
tomo.CanvasBox
dragging bool
parent *ColorPicker
}
func newColorPickerMap (parent *ColorPicker) *colorPickerMap {
picker := &colorPickerMap {
CanvasBox: tomo.NewCanvasBox(),
parent: parent,
}
picker.SetDrawer(picker)
picker.SetRole(tomo.R("objects", "ColorPickerMap", ""))
picker.OnMouseUp(picker.handleMouseUp)
picker.OnMouseDown(picker.handleMouseDown)
picker.OnMouseMove(picker.handleMouseMove)
return picker
}
func (this *colorPickerMap) handleMouseDown (button input.Button) {
if button != input.ButtonLeft { return }
this.dragging = true
this.drag()
}
func (this *colorPickerMap) handleMouseUp (button input.Button) {
if button != input.ButtonLeft || !this.dragging { return }
this.dragging = false
}
func (this *colorPickerMap) handleMouseMove () {
if !this.dragging { return }
this.drag()
}
func (this *colorPickerMap) drag () {
pointer := this.MousePosition()
bounds := this.InnerBounds()
this.parent.value.S = float64(pointer.X - bounds.Min.X) / float64(bounds.Dx())
this.parent.value.V = 1 - float64(pointer.Y - bounds.Min.Y) / float64(bounds.Dy())
this.parent.value = this.parent.value.Canon()
this.parent.on.valueChange.Broadcast()
this.Invalidate()
}
func (this *colorPickerMap) Draw (can canvas.Canvas) {
bounds := can.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
xx := x - bounds.Min.X
yy := y - bounds.Min.Y
pixel := internal.HSVA {
H: this.parent.value.H,
S: float64(xx) / float64(bounds.Dx()),
V: 1 - float64(yy) / float64(bounds.Dy()),
A: 255,
}
sPos := int( this.parent.value.S * float64(bounds.Dx()))
vPos := int((1 - this.parent.value.V) * float64(bounds.Dy()))
sDist := sPos - xx
vDist := vPos - yy
crosshair :=
(sDist == 0 || vDist == 0) &&
-8 < sDist && sDist < 8 &&
-8 < vDist && vDist < 8
if crosshair {
pixel.S = 1 - pixel.S
pixel.V = 1 - pixel.V
}
can.Set(x, y, pixel)
}}
}

View File

@@ -66,7 +66,7 @@ func NewDialog (kind DialogKind, parent tomo.Window, title, message string, opti
dialog.controlRow.SetAlign(tomo.AlignEnd, tomo.AlignEnd)
dialog.SetRoot(NewOuterContainer (
layouts.NewGrid([]bool { true }, []bool { true, false }),
layouts.Column { true, false },
NewInnerContainer(layouts.ContractHorizontal, icon, messageText),
dialog.controlRow))
return dialog, nil

104
internal/color.go Normal file
View File

@@ -0,0 +1,104 @@
package internal
type HSVA struct {
H float64
S float64
V float64
A uint8
}
func (hsva HSVA) RGBA () (r, g, b, a uint32) {
// Adapted from:
// https://www.cs.rit.edu/~ncs/color/t_convert.html
component := func (x float64) uint32 {
return uint32(float64(0xFFFF) * x)
}
ca := uint32(hsva.A) << 8
s := clamp01(hsva.S)
v := clamp01(hsva.V)
if s == 0 {
light := component(v)
return light, light, light, ca
}
h := clamp01(hsva.H) * 360
sector := int(h / 60)
offset := (h / 60) - float64(sector)
fac := float64(hsva.A) / 255
p := component(fac * v * (1 - s))
q := component(fac * v * (1 - s * offset))
t := component(fac * v * (1 - s * (1 - offset)))
va := component(v)
switch sector {
case 0: return va, t, p, ca
case 1: return q, va, p, ca
case 2: return p, va, t, ca
case 3: return p, q, va, ca
case 4: return t, p, va, ca
default: return va, p, q, ca
}
}
// Canon returns the color but with the H, S, and V fields are constrained to
// the range 0.0-1.0
func (hsva HSVA) Canon () HSVA {
hsva.H = clamp01(hsva.H)
hsva.S = clamp01(hsva.S)
hsva.V = clamp01(hsva.V)
return hsva
}
func clamp01 (x float64) float64 {
if x > 1.0 { return 1.0 }
if x < 0.0 { return 0.0 }
return x
}
func RGBAToHSVA (r, g, b, a uint32) HSVA {
// Adapted from:
// https://www.cs.rit.edu/~ncs/color/t_convert.html
component := func (x uint32) float64 {
return clamp01(float64(x) / 0xFFFF)
}
cr := component(r)
cg := component(g)
cb := component(b)
var maxComponent float64
if cr > maxComponent { maxComponent = cr }
if cg > maxComponent { maxComponent = cg }
if cb > maxComponent { maxComponent = cb }
var minComponent = 1.0
if cr < minComponent { minComponent = cr }
if cg < minComponent { minComponent = cg }
if cb < minComponent { minComponent = cb }
hsva := HSVA {
V: maxComponent,
A: uint8(a >> 8),
}
delta := maxComponent - minComponent
if delta == 0 {
// hsva.S is undefined, so hue doesn't matter
return hsva
}
hsva.S = delta / maxComponent
switch {
case cr == maxComponent: hsva.H = (cg - cb) / delta
case cg == maxComponent: hsva.H = 2 + (cb - cr) / delta
case cb == maxComponent: hsva.H = 4 + (cr - cg) / delta
}
hsva.H *= 60
if hsva.H < 0 { hsva.H += 360 }
hsva.H /= 360
return hsva
}

View File

@@ -12,5 +12,6 @@ func NewLabel (text string) *Label {
this := &Label { TextBox: tomo.NewTextBox() }
this.SetRole(tomo.R("objects", "Label", ""))
this.SetText(text)
this.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
return this
}

View File

@@ -24,7 +24,7 @@ func NewLabelCheckbox (value bool, text string) *LabelCheckbox {
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.Add(box.checkbox)
box.Add(box.label)
box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { false }))
box.SetLayout(layouts.Row { false, true })
box.OnMouseUp(box.handleMouseUp)
box.label.OnMouseUp(box.handleMouseUp)

69
labelswatch.go Normal file
View File

@@ -0,0 +1,69 @@
package objects
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
// LabelSwatch is a swatch with a label.
type LabelSwatch struct {
tomo.ContainerBox
swatch *Swatch
label *Label
}
// NewLabelSwatch creates a new labeled swatch with the specified color and
// label text.
func NewLabelSwatch (value color.Color, text string) *LabelSwatch {
box := &LabelSwatch {
ContainerBox: tomo.NewContainerBox(),
swatch: NewSwatch(value),
label: NewLabel(text),
}
box.SetRole(tomo.R("objects", "LabelSwatch", ""))
box.swatch.label = text
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.Add(box.swatch)
box.Add(box.label)
box.SetLayout(layouts.Row { false, true })
box.OnMouseUp(box.handleMouseUp)
box.label.OnMouseUp(box.handleMouseUp)
return box
}
// SetValue sets the color of the swatch.
func (this *LabelSwatch) SetValue (value color.Color) {
this.swatch.SetValue(value)
}
// Value returns the color of the swatch.
func (this *LabelSwatch) Value () color.Color {
return this.swatch.Value()
}
// RGBA satisfies the color.Color interface
func (this *LabelSwatch) RGBA () (r, g, b, a uint32) {
return this.swatch.RGBA()
}
// OnValueChange specifies a function to be called when the swatch's color
// is changed by the user.
func (this *LabelSwatch) OnValueChange (callback func ()) event.Cookie {
return this.swatch.OnValueChange(callback)
}
// OnEnter specifies a function to be called when the user selects "OK" in the
// color picker.
func (this *LabelSwatch) OnEnter (callback func ()) event.Cookie {
return this.swatch.OnEnter(callback)
}
func (this *LabelSwatch) handleMouseUp (button input.Button) {
if button != input.ButtonLeft { return }
if this.MousePosition().In(this.Bounds()) {
this.swatch.SetFocused(true)
this.swatch.Choose()
}
}

View File

@@ -18,12 +18,18 @@ type Grid struct {
// will contract. Boxes are laid out left to right, then top to bottom. Boxes
// that go beyond the lengh of rows will be laid out according to columns, but
// they will not expand vertically.
func NewGrid (columns, rows []bool) *Grid {
this := &Grid {
xExpand: columns,
yExpand: rows,
//
// If you aren't sure how to use this constructor, here is an example:
//
// X0 X1 X2 Y0 Y1 Y2
// NewGrid(true, false, true)(false, true, true)
func NewGrid (columns ...bool) func (rows ...bool) *Grid {
return func (rows ...bool) *Grid {
return &Grid {
xExpand: columns,
yExpand: rows,
}
}
return this
}
func (this *Grid) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
@@ -106,15 +112,14 @@ func expand (hints tomo.LayoutHints, sizes []int, space int, expands func (int)
}
func ceilDiv (x, y int) int {
if y == 0 { return 0 }
return int(math.Ceil(float64(x) / float64(y)))
}
func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
// TODO
return 0
return this.MinimumSize(hints, boxes).Y
}
func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
// TODO
return 0
return this.MinimumSize(hints, boxes).X
}

View File

@@ -5,7 +5,7 @@ import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
// MenuItem is a clickable button.
// MenuItem is a selectable memu item.
type MenuItem struct {
tomo.ContainerBox
@@ -27,7 +27,7 @@ func NewMenuItem (text string) *MenuItem {
}
box.SetRole(tomo.R("objects", "MenuItem", ""))
box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle)
box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { true }))
box.SetLayout(layouts.Row { false, true })
box.Add(box.icon)
box.Add(box.label)

View File

@@ -32,7 +32,7 @@ func NewNumberInput (value float64) *NumberInput {
box.Add(box.input)
box.Add(box.decrement)
box.Add(box.increment)
box.SetLayout(layouts.NewGrid([]bool { true, false, false }, []bool { true }))
box.SetLayout(layouts.Row { true, false, false })
box.increment.SetIcon(tomo.IconValueIncrement)
box.decrement.SetIcon(tomo.IconValueDecrement)

View File

@@ -1,5 +1,6 @@
package objects
import "math"
import "image"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
@@ -16,6 +17,7 @@ type Slider struct {
on struct {
slide event.FuncBroadcaster
enter event.FuncBroadcaster
}
}
@@ -33,6 +35,7 @@ func newSlider (orient string, value float64) *Slider {
},
layout: sliderLayout {
vertical: orient == "vertical",
value: math.NaN(),
},
step: 0.05,
}
@@ -87,6 +90,12 @@ func (this *Slider) OnSlide (callback func ()) event.Cookie {
return this.on.slide.Connect(callback)
}
// OnEnter specifies a function to be called when the user stops moving the
// slider.
func (this *Slider) OnEnter (callback func ()) event.Cookie {
return this.on.enter.Connect(callback)
}
func (this *Slider) handleKeyDown (key input.Key, numpad bool) {
var increment float64; if this.layout.vertical {
increment = -0.05
@@ -146,17 +155,21 @@ func (this *Slider) handleMouseDown (button input.Button) {
if above {
this.SetValue(0)
this.on.slide.Broadcast()
this.on.enter.Broadcast()
} else {
this.SetValue(1)
this.on.slide.Broadcast()
this.on.enter.Broadcast()
}
case input.ButtonRight:
if above {
this.SetValue(this.Value() - this.step)
this.on.slide.Broadcast()
this.on.enter.Broadcast()
} else {
this.SetValue(this.Value() + this.step)
this.on.slide.Broadcast()
this.on.enter.Broadcast()
}
}
}
@@ -164,6 +177,7 @@ func (this *Slider) handleMouseDown (button input.Button) {
func (this *Slider) handleMouseUp (button input.Button) {
if button != input.ButtonLeft || !this.dragging { return }
this.dragging = false
this.on.enter.Broadcast()
}
func (this *Slider) handleMouseMove () {
@@ -175,6 +189,7 @@ func (this *Slider) handleScroll (x, y float64) {
delta := (x + y) * 0.005
this.SetValue(this.Value() + delta)
this.on.slide.Broadcast()
this.on.enter.Broadcast()
}
func (this *Slider) drag () {

168
swatch.go Normal file
View File

@@ -0,0 +1,168 @@
package objects
import "log"
import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/objects/layouts"
// Swatch displays a color, allowing the user to edit it by clicking on it.
type Swatch struct {
tomo.CanvasBox
value color.Color
editing bool
label string
on struct {
valueChange event.FuncBroadcaster
enter event.FuncBroadcaster
}
}
// NewSwatch creates a new swatch with the given color.
func NewSwatch (value color.Color) *Swatch {
swatch := &Swatch {
CanvasBox: tomo.NewCanvasBox(),
}
swatch.SetRole(tomo.R("objects", "Swatch", ""))
swatch.SetDrawer(swatch)
swatch.SetValue(value)
swatch.OnMouseUp(swatch.handleMouseUp)
swatch.OnKeyUp(swatch.handleKeyUp)
swatch.SetFocusable(true)
return swatch
}
// SetValue sets the color of the swatch.
func (this *Swatch) SetValue (value color.Color) {
this.value = value
if value == nil { value = color.Transparent }
this.Invalidate()
}
// Value returns the color of the swatch.
func (this *Swatch) Value () color.Color {
return this.value
}
// RGBA satisfies the color.Color interface
func (this *Swatch) RGBA () (r, g, b, a uint32) {
if this.value == nil { return }
return this.value.RGBA()
}
// OnValueChange specifies a function to be called when the swatch's color
// is changed by the user.
func (this *Swatch) OnValueChange (callback func ()) event.Cookie {
return this.on.valueChange.Connect(callback)
}
// OnEnter specifies a function to be called when the user selects "OK" in the
// color picker.
func (this *Swatch) OnEnter (callback func ()) event.Cookie {
return this.on.enter.Connect(callback)
}
// Choose creates a modal that allows the user to edit the color of the swatch.
func (this *Swatch) Choose () {
if this.editing { return }
var err error
var window tomo.Window
if parent := this.Window(); parent != nil {
window, err = parent.NewChild(image.Rectangle { })
} else {
window, err = tomo.NewWindow(image.Rectangle { })
}
if err != nil {
log.Println("objects: could not create swatch modal:", err)
return
}
if this.label == "" {
window.SetTitle("Select Color")
} else {
window.SetTitle(this.label)
}
committed := false
colorPicker := NewColorPicker(this.Value())
colorPicker.OnValueChange(func () {
this.userSetValue(colorPicker.Value())
})
hexInput := NewTextInput("TODO")
colorMemory := this.value
cancelButton := NewButton("Cancel")
cancelButton.SetIcon(tomo.IconDialogCancel)
cancelButton.OnClick(func () {
window.Close()
})
okButton := NewButton("OK")
okButton.SetFocused(true)
okButton.SetIcon(tomo.IconDialogOkay)
okButton.OnClick(func () {
committed = true
window.Close()
})
controlRow := NewInnerContainer (
layouts.ContractHorizontal,
cancelButton,
okButton)
controlRow.SetAlign(tomo.AlignEnd, tomo.AlignMiddle)
window.SetRoot(NewOuterContainer (
layouts.Column { true, false },
colorPicker,
NewInnerContainer(layouts.Row { false, true },
NewLabel("Hex"),
hexInput),
controlRow))
window.OnClose(func () {
if committed {
this.on.enter.Broadcast()
} else {
this.userSetValue(colorMemory)
}
this.editing = false
})
this.editing = true
window.SetVisible(true)
}
func (this *Swatch) Draw (can canvas.Canvas) {
pen := can.Pen()
// transparency slash
pen.Stroke(color.RGBA { R: 255, A: 255 })
pen.StrokeWeight(1)
pen.Path(this.Bounds().Min, this.Bounds().Max)
// color
if this.value != nil {
pen.StrokeWeight(0)
pen.Fill(this.value)
pen.Rectangle(this.Bounds())
}
}
func (this *Swatch) userSetValue (value color.Color) {
this.SetValue(value)
this.on.valueChange.Broadcast()
}
func (this *Swatch) handleKeyUp (key input.Key, numberPad bool) {
if key != input.KeyEnter && key != input.Key(' ') { return }
this.Choose()
}
func (this *Swatch) handleMouseUp (button input.Button) {
if button != input.ButtonLeft { return }
if this.MousePosition().In(this.Bounds()) {
this.Choose()
}
}

126
tabbedcontainer.go Normal file
View File

@@ -0,0 +1,126 @@
package objects
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/objects/layouts"
type TabbedContainer struct {
tomo.ContainerBox
leftSpacer tomo.Box
rightSpacer tomo.Box
tabsRow tomo.ContainerBox
active string
tabs []*tab
}
func NewTabbedContainer () *TabbedContainer {
container := &TabbedContainer {
ContainerBox: tomo.NewContainerBox(),
}
container.SetRole(tomo.R("objects", "TabbedContainer", ""))
container.SetLayout(layouts.Column { false, true } )
container.tabsRow = tomo.NewContainerBox()
container.tabsRow.SetRole(tomo.R("objects", "TabRow", ""))
container.Add(container.tabsRow)
container.leftSpacer = tomo.NewBox()
container.leftSpacer.SetRole(tomo.R("objects", "TabSpacer", "left"))
container.rightSpacer = tomo.NewBox()
container.rightSpacer.SetRole(tomo.R("objects", "TabSpacer", "right"))
container.ClearTabs()
container.setTabRowLayout()
return container
}
func (this *TabbedContainer) Activate (name string) {
if _, tab := this.findTab(this.active); tab != nil {
tab.setActive(false)
this.Remove(tab.root)
}
if _, tab := this.findTab(name); tab != nil {
tab.setActive(true)
this.Add(tab.root)
} else {
name = ""
}
this.active = name
}
func (this *TabbedContainer) AddTab (name string, root tomo.Object) {
tab := &tab {
TextBox: tomo.NewTextBox(),
name: name,
root: root,
}
tab.SetRole(tomo.R("objects", "Tab", ""))
tab.SetText(name)
tab.OnMouseDown(func (button input.Button) {
if button != input.ButtonLeft { return }
this.Activate(name)
})
this.tabs = append(this.tabs, tab)
this.tabsRow.Insert(tab, this.rightSpacer)
this.setTabRowLayout()
// if the row was empty before, activate this tab
if len(this.tabs) == 1 {
this.Activate(name)
}
}
func (this *TabbedContainer) RemoveTab (name string) {
index, tab := this.findTab(name)
if index < 0 { return }
nextIndex := index - 1
this.tabsRow.Remove(tab)
this.tabs = append(this.tabs[:index], this.tabs[index - 1:]...)
this.setTabRowLayout()
if nextIndex < 0 { nextIndex = 0 }
if nextIndex >= len(this.tabs) { nextIndex = len(this.tabs) - 1 }
if nextIndex < 0 {
this.Activate("")
} else {
this.Activate(this.tabs[nextIndex].name)
}
}
func (this *TabbedContainer) ClearTabs () {
this.tabs = nil
this.tabsRow.Clear()
this.tabsRow.Add(this.leftSpacer)
this.tabsRow.Add(this.rightSpacer)
}
func (this *TabbedContainer) setTabRowLayout () {
row := make(layouts.Row, 1 + len(this.tabs) + 1)
row[len(row) - 1] = true
this.tabsRow.SetLayout(row)
}
func (this *TabbedContainer) findTab (name string) (int, *tab) {
for index, tab := range this.tabs {
if tab.name == name { return index, tab }
}
return -1, nil
}
type tab struct {
tomo.TextBox
name string
root tomo.Object
}
func (this *tab) setActive (active bool) {
if active {
this.SetRole(tomo.R("objects", "Tab", "active"))
} else {
this.SetRole(tomo.R("objects", "Tab", ""))
}
}