545 lines
15 KiB
Go
545 lines
15 KiB
Go
// 1 march 2014
|
|
|
|
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"unsafe"
|
|
"sync"
|
|
)
|
|
|
|
// #cgo LDFLAGS: -lobjc -framework Foundation -framework AppKit
|
|
// #include "objc_darwin.h"
|
|
import "C"
|
|
|
|
type sysData struct {
|
|
cSysData
|
|
|
|
id C.id
|
|
}
|
|
|
|
type classData struct {
|
|
make func(parentWindow C.id, alternate bool) C.id
|
|
getinside func(scrollview C.id) C.id
|
|
show func(what C.id)
|
|
hide func(what C.id)
|
|
settextsel C.SEL
|
|
textsel C.SEL
|
|
alttextsel C.SEL
|
|
append func(id C.id, what string, alternate bool)
|
|
insertBefore func(id C.id, what string, before int, alternate bool)
|
|
selIndex func(id C.id) int
|
|
selIndices func(id C.id) []int
|
|
selTexts func(id C.id) []string
|
|
delete func(id C.id, index int)
|
|
len func(id C.id) int
|
|
}
|
|
|
|
var (
|
|
_NSWindow = objc_getClass("NSWindow")
|
|
_NSButton = objc_getClass("NSButton")
|
|
_NSPopUpButton = objc_getClass("NSPopUpButton")
|
|
_NSComboBox = objc_getClass("NSComboBox")
|
|
_NSTextField = objc_getClass("NSTextField")
|
|
_NSSecureTextField = objc_getClass("NSSecureTextField")
|
|
_NSProgressIndicator = objc_getClass("NSProgressIndicator")
|
|
|
|
_initWithContentRect = sel_getUid("initWithContentRect:styleMask:backing:defer:")
|
|
_initWithFrame = sel_getUid("initWithFrame:")
|
|
_setDelegate = sel_getUid("setDelegate:")
|
|
_setAcceptsMouseMovedEvents = sel_getUid("setAcceptsMouseMovedEvents:")
|
|
_makeKeyAndOrderFront = sel_getUid("makeKeyAndOrderFront:")
|
|
_orderOut = sel_getUid("orderOut:")
|
|
_setHidden = sel_getUid("setHidden:")
|
|
_setTitle = sel_getUid("setTitle:")
|
|
_setStringValue = sel_getUid("setStringValue:")
|
|
_setFrame = sel_getUid("setFrame:")
|
|
_state = sel_getUid("state")
|
|
_title = sel_getUid("title")
|
|
_stringValue = sel_getUid("stringValue")
|
|
_frame = sel_getUid("frame")
|
|
_setFrameDisplay = sel_getUid("setFrame:display:")
|
|
_setBezelStyle = sel_getUid("setBezelStyle:")
|
|
_setTarget = sel_getUid("setTarget:")
|
|
_setAction = sel_getUid("setAction:")
|
|
_contentView = sel_getUid("contentView")
|
|
_addSubview = sel_getUid("addSubview:")
|
|
_setButtonType = sel_getUid("setButtonType:")
|
|
_initWithFramePullsDown = sel_getUid("initWithFrame:pullsDown:")
|
|
_setUsesDataSource = sel_getUid("setUsesDataSource:")
|
|
_addItemWithTitle = sel_getUid("addItemWithTitle:")
|
|
_insertItemWithTitleAtIndex = sel_getUid("insertItemWithTitle:atIndex:")
|
|
_removeItemAtIndex = sel_getUid("removeItemAtIndex:")
|
|
_titleOfSelectedItem = sel_getUid("titleOfSelectedItem")
|
|
_indexOfSelectedItem = sel_getUid("indexOfSelectedItem")
|
|
_addItemWithObjectValue = sel_getUid("addItemWithObjectValue:")
|
|
_insertItemWithObjectValueAtIndex = sel_getUid("insertItemWithObjectValue:atIndex:")
|
|
_setEditable = sel_getUid("setEditable:")
|
|
_setBordered = sel_getUid("setBordered:")
|
|
_setDrawsBackground = sel_getUid("setDrawsBackground:")
|
|
_setStyle = sel_getUid("setStyle:")
|
|
_setControlSize = sel_getUid("setControlSize:")
|
|
_setIndeterminate = sel_getUid("setIndeterminate:")
|
|
_setDoubleValue = sel_getUid("setDoubleValue:")
|
|
_numberOfItems = sel_getUid("numberOfItems")
|
|
)
|
|
|
|
// because the only way to make a new NSControl/NSView is with a frame (it gets overridden later)
|
|
func initWithDummyFrame(self C.id) C.id {
|
|
return C.objc_msgSend_rect(self, _initWithFrame,
|
|
C.int64_t(0), C.int64_t(0), C.int64_t(100), C.int64_t(100))
|
|
}
|
|
|
|
func addControl(parentWindow C.id, control C.id) {
|
|
windowView := C.objc_msgSend_noargs(parentWindow, _contentView)
|
|
C.objc_msgSend_id(windowView, _addSubview, control)
|
|
}
|
|
|
|
func controlShow(what C.id) {
|
|
C.objc_msgSend_bool(what, _setHidden, C.BOOL(C.NO))
|
|
}
|
|
|
|
func controlHide(what C.id) {
|
|
C.objc_msgSend_bool(what, _setHidden, C.BOOL(C.YES))
|
|
}
|
|
|
|
const (
|
|
_NSRegularControlSize = 0
|
|
)
|
|
|
|
// By default some controls do not use the correct font.
|
|
// These functions set the appropriate control font.
|
|
// Which one is used on each control was determined by comparing https://developer.apple.com/library/mac/documentation/UserExperience/Conceptual/AppleHIGuidelines/Characteristics/Characteristics.html#//apple_ref/doc/uid/TP40002721-SW10 to what Interface Builder says for each control.
|
|
// (not applicable to ProgressBar, Area)
|
|
|
|
// Button, Checkbox, Combobox, LineEdit, Label, Listbox
|
|
func applyStandardControlFont(id C.id) {
|
|
C.objc_setFont(id, _NSRegularControlSize)
|
|
}
|
|
|
|
var classTypes = [nctypes]*classData{
|
|
c_window: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
const (
|
|
_NSBorderlessWindowMask = 0
|
|
_NSTitledWindowMask = 1 << 0
|
|
_NSClosableWindowMask = 1 << 1
|
|
_NSMiniaturizableWindowMask = 1 << 2
|
|
_NSResizableWindowMask = 1 << 3
|
|
_NSTexturedBackgroundWindowMask = 1 << 8
|
|
|
|
_NSBackingStoreBuffered = 2 // the only backing store method that Apple says we should use (the others are legacy)
|
|
)
|
|
|
|
// we have to specify a content rect to start; it will be overridden soon though
|
|
win := C.objc_msgSend_noargs(_NSWindow, _alloc)
|
|
win = C.objc_msgSend_rect_uint_uint_bool(win,
|
|
_initWithContentRect,
|
|
C.int64_t(0), C.int64_t(0), C.int64_t(100), C.int64_t(100),
|
|
C.uintptr_t(_NSTitledWindowMask | _NSClosableWindowMask | _NSMiniaturizableWindowMask | _NSResizableWindowMask),
|
|
C.uintptr_t(_NSBackingStoreBuffered),
|
|
C.BOOL(C.YES)) // defer creation of device until we show the window
|
|
C.objc_msgSend_id(win, _setDelegate, appDelegate)
|
|
// this is needed for Areas in the window to receive mouse move events
|
|
// C.objc_msgSend_bool(win, _setAcceptsMouseMovedEvents, C.BOOL(C.YES))
|
|
return win
|
|
},
|
|
show: func(what C.id) {
|
|
C.objc_msgSend_id(what, _makeKeyAndOrderFront, what)
|
|
},
|
|
hide: func(what C.id) {
|
|
C.objc_msgSend_id(what, _orderOut, what)
|
|
},
|
|
settextsel: _setTitle,
|
|
textsel: _title,
|
|
},
|
|
c_button: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
const (
|
|
_NSRoundedBezelStyle = 1
|
|
)
|
|
|
|
button := C.objc_msgSend_noargs(_NSButton, _alloc)
|
|
button = initWithDummyFrame(button)
|
|
C.objc_msgSend_uint(button, _setBezelStyle, C.uintptr_t(_NSRoundedBezelStyle))
|
|
C.objc_msgSend_id(button, _setTarget, appDelegate)
|
|
C.objc_msgSend_sel(button, _setAction, _buttonClicked)
|
|
applyStandardControlFont(button)
|
|
addControl(parentWindow, button)
|
|
return button
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
settextsel: _setTitle,
|
|
textsel: _title,
|
|
},
|
|
c_checkbox: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
const (
|
|
_NSSwitchButton = 3
|
|
)
|
|
|
|
checkbox := C.objc_msgSend_noargs(_NSButton, _alloc)
|
|
checkbox = initWithDummyFrame(checkbox)
|
|
C.objc_msgSend_uint(checkbox, _setButtonType, C.uintptr_t(_NSSwitchButton))
|
|
applyStandardControlFont(checkbox)
|
|
addControl(parentWindow, checkbox)
|
|
return checkbox
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
settextsel: _setTitle,
|
|
textsel: _title,
|
|
},
|
|
c_combobox: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
var combobox C.id
|
|
|
|
if alternate {
|
|
combobox = C.objc_msgSend_noargs(_NSComboBox, _alloc)
|
|
combobox = initWithDummyFrame(combobox)
|
|
C.objc_msgSend_bool(combobox, _setUsesDataSource, C.BOOL(C.NO))
|
|
} else {
|
|
combobox = C.objc_msgSend_noargs(_NSPopUpButton, _alloc)
|
|
combobox = C.objc_msgSend_rect_bool(combobox, _initWithFramePullsDown,
|
|
C.int64_t(0), C.int64_t(0), C.int64_t(100), C.int64_t(100),
|
|
C.BOOL(C.NO))
|
|
}
|
|
applyStandardControlFont(combobox)
|
|
addControl(parentWindow, combobox)
|
|
return combobox
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
textsel: _titleOfSelectedItem,
|
|
alttextsel: _stringValue,
|
|
append: func(id C.id, what string, alternate bool) {
|
|
str := toNSString(what)
|
|
if alternate {
|
|
C.objc_msgSend_id(id, _addItemWithObjectValue, str)
|
|
} else {
|
|
C.objc_msgSend_id(id, _addItemWithTitle, str)
|
|
}
|
|
},
|
|
insertBefore: func(id C.id, what string, before int, alternate bool) {
|
|
str := toNSString(what)
|
|
if alternate {
|
|
C.objc_msgSend_id_int(id, _insertItemWithObjectValueAtIndex, str, C.intptr_t(before))
|
|
} else {
|
|
C.objc_msgSend_id_int(id, _insertItemWithTitleAtIndex, str, C.intptr_t(before))
|
|
}
|
|
},
|
|
selIndex: func(id C.id) int {
|
|
return int(C.objc_msgSend_intret_noargs(id, _indexOfSelectedItem))
|
|
},
|
|
delete: func(id C.id, index int) {
|
|
C.objc_msgSend_int(id, _removeItemAtIndex, C.intptr_t(index))
|
|
},
|
|
len: func(id C.id) int {
|
|
return int(C.objc_msgSend_intret_noargs(id, _numberOfItems))
|
|
},
|
|
},
|
|
c_lineedit: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
var lineedit C.id
|
|
|
|
if alternate {
|
|
lineedit = C.objc_msgSend_noargs(_NSSecureTextField, _alloc)
|
|
} else {
|
|
lineedit = C.objc_msgSend_noargs(_NSTextField, _alloc)
|
|
}
|
|
lineedit = initWithDummyFrame(lineedit)
|
|
applyStandardControlFont(lineedit)
|
|
addControl(parentWindow, lineedit)
|
|
return lineedit
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
settextsel: _setStringValue,
|
|
textsel: _stringValue,
|
|
alttextsel: _stringValue,
|
|
},
|
|
c_label: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
label := C.objc_msgSend_noargs(_NSTextField, _alloc)
|
|
label = initWithDummyFrame(label)
|
|
C.objc_msgSend_bool(label, _setEditable, C.BOOL(C.NO))
|
|
C.objc_msgSend_bool(label, _setBordered, C.BOOL(C.NO))
|
|
C.objc_msgSend_bool(label, _setDrawsBackground, C.BOOL(C.NO))
|
|
applyStandardControlFont(label)
|
|
addControl(parentWindow, label)
|
|
return label
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
settextsel: _setStringValue,
|
|
textsel: _stringValue,
|
|
},
|
|
c_listbox: &classData{
|
|
make: makeListbox,
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
append: appendListbox,
|
|
insertBefore: insertListboxBefore,
|
|
selIndices: selectedListboxIndices,
|
|
selTexts: selectedListboxTexts,
|
|
delete: deleteListbox,
|
|
len: listboxLen,
|
|
},
|
|
c_progressbar: &classData{
|
|
make: func(parentWindow C.id, alternate bool) C.id {
|
|
const (
|
|
_NSProgressIndicatorBarStyle = 0
|
|
)
|
|
|
|
pbar := C.objc_msgSend_noargs(_NSProgressIndicator, _alloc)
|
|
pbar = initWithDummyFrame(pbar)
|
|
// NSProgressIndicatorStyle doesn't have an explicit typedef; just use int for now
|
|
C.objc_msgSend_int(pbar, _setStyle, _NSProgressIndicatorBarStyle)
|
|
C.objc_msgSend_uint(pbar, _setControlSize, C.uintptr_t(_NSRegularControlSize))
|
|
C.objc_msgSend_bool(pbar, _setIndeterminate, C.BOOL(C.NO))
|
|
addControl(parentWindow, pbar)
|
|
return pbar
|
|
},
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
},
|
|
c_area: &classData{
|
|
make: makeArea,
|
|
getinside: areaInScrollView,
|
|
show: controlShow,
|
|
hide: controlHide,
|
|
},
|
|
}
|
|
|
|
// I need to access sysData from appDelegate, but appDelegate doesn't store any data. So, this.
|
|
var (
|
|
sysdatas = make(map[C.id]*sysData)
|
|
sysdatalock sync.Mutex
|
|
)
|
|
|
|
func addSysData(key C.id, value *sysData) {
|
|
sysdatalock.Lock()
|
|
sysdatas[key] = value
|
|
sysdatalock.Unlock()
|
|
}
|
|
|
|
func getSysData(key C.id) *sysData {
|
|
sysdatalock.Lock()
|
|
defer sysdatalock.Unlock()
|
|
|
|
v, ok := sysdatas[key]
|
|
if !ok {
|
|
panic(fmt.Errorf("internal error: getSysData(%v) unknown", key))
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (s *sysData) make(window *sysData) error {
|
|
var parentWindow C.id
|
|
|
|
ct := classTypes[s.ctype]
|
|
if window != nil {
|
|
parentWindow = window.id
|
|
}
|
|
ret := make(chan C.id)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
ret <- ct.make(parentWindow, s.alternate)
|
|
}
|
|
s.id = <-ret
|
|
if ct.getinside != nil {
|
|
uitask <- func() {
|
|
ret <- ct.getinside(s.id)
|
|
}
|
|
addSysData(<-ret, s)
|
|
} else {
|
|
addSysData(s.id, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// used for Windows; nothing special needed elsewhere
|
|
func (s *sysData) firstShow() error {
|
|
s.show()
|
|
return nil
|
|
}
|
|
|
|
func (s *sysData) show() {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
classTypes[s.ctype].show(s.id)
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) hide() {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
classTypes[s.ctype].hide(s.id)
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) setText(text string) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
C.objc_msgSend_id(s.id, classTypes[s.ctype].settextsel, toNSString(text))
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) setRect(x int, y int, width int, height int, winheight int) error {
|
|
// winheight - y because (0,0) is the bottom-left corner of the window and not the top-left corner
|
|
// (winheight - y) - height because (x, y) is the bottom-left corner of the control and not the top-left
|
|
C.objc_msgSend_rect(s.id, _setFrame,
|
|
C.int64_t(x), C.int64_t((winheight - y) - height), C.int64_t(width), C.int64_t(height))
|
|
return nil
|
|
}
|
|
|
|
func (s *sysData) isChecked() bool {
|
|
const (
|
|
NSOnState = 1
|
|
)
|
|
|
|
ret := make(chan bool)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
k := C.objc_msgSend_noargs(s.id, _state)
|
|
ret <- uintptr(unsafe.Pointer(k)) == NSOnState
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) text() string {
|
|
sel := classTypes[s.ctype].textsel
|
|
if s.alternate {
|
|
sel = classTypes[s.ctype].alttextsel
|
|
}
|
|
ret := make(chan string)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
str := C.objc_msgSend_noargs(s.id, sel)
|
|
ret <- fromNSString(str)
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) append(what string) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
classTypes[s.ctype].append(s.id, what, s.alternate)
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) insertBefore(what string, before int) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
classTypes[s.ctype].insertBefore(s.id, what, before, s.alternate)
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) selectedIndex() int {
|
|
ret := make(chan int)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
ret <- classTypes[s.ctype].selIndex(s.id)
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) selectedIndices() []int {
|
|
ret := make(chan []int)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
ret <- classTypes[s.ctype].selIndices(s.id)
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) selectedTexts() []string {
|
|
ret := make(chan []string)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
ret <- classTypes[s.ctype].selTexts(s.id)
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) setWindowSize(width int, height int) error {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
// we need to get the top left point
|
|
r := C.objc_msgSend_stret_rect_noargs(s.id, _frame)
|
|
C.objc_msgSend_rect_bool(s.id, _setFrameDisplay,
|
|
C.int64_t(r.x), C.int64_t(r.y), C.int64_t(width), C.int64_t(height),
|
|
C.BOOL(C.YES)) // TODO set to NO to prevent subviews from being redrawn before they are resized?
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
return nil
|
|
}
|
|
|
|
func (s *sysData) delete(index int) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
classTypes[s.ctype].delete(s.id, index)
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) setProgress(percent int) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
if percent == -1 {
|
|
// At least on Mac OS X 10.8, if the progressbar was already on 0 or 100% when turning on indeterminate mode, the indeterminate animation won't play, leaving just a still progress bar. This is a workaround. Note the selector call order.
|
|
// TODO will the value chosen affect the animation speed?
|
|
C.objc_msgSend_double(s.id, _setDoubleValue, C.double(50))
|
|
C.objc_msgSend_bool(s.id, _setIndeterminate, C.BOOL(C.YES))
|
|
} else {
|
|
C.objc_msgSend_bool(s.id, _setIndeterminate, C.BOOL(C.NO))
|
|
C.objc_msgSend_double(s.id, _setDoubleValue, C.double(percent))
|
|
}
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|
|
|
|
func (s *sysData) len() int {
|
|
ret := make(chan int)
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
ret <- classTypes[s.ctype].len(s.id)
|
|
}
|
|
return <-ret
|
|
}
|
|
|
|
func (s *sysData) setAreaSize(width int, height int) {
|
|
ret := make(chan struct{})
|
|
defer close(ret)
|
|
uitask <- func() {
|
|
area := areaInScrollView(s.id)
|
|
C.objc_msgSend_rect(area, _setFrame,
|
|
C.int64_t(0), C.int64_t(0), C.int64_t(width), C.int64_t(height))
|
|
C.objc_msgSend_noargs(area, _display) // and redraw
|
|
ret <- struct{}{}
|
|
}
|
|
<-ret
|
|
}
|