Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
490 lines
12 KiB
Go
490 lines
12 KiB
Go
package container
|
|
|
|
import (
|
|
"image/color"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// Declare conformity with Widget interface.
|
|
var _ fyne.Widget = (*DocTabs)(nil)
|
|
|
|
// DocTabs container is used to display various pieces of content identified by tabs.
|
|
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
|
|
// Each item is represented by a button at the edge of the container.
|
|
//
|
|
// Since: 2.1
|
|
type DocTabs struct {
|
|
widget.BaseWidget
|
|
|
|
Items []*TabItem
|
|
|
|
CreateTab func() *TabItem `json:"-"`
|
|
CloseIntercept func(*TabItem) `json:"-"`
|
|
OnClosed func(*TabItem) `json:"-"`
|
|
OnSelected func(*TabItem) `json:"-"`
|
|
OnUnselected func(*TabItem) `json:"-"`
|
|
|
|
current int
|
|
location TabLocation
|
|
isTransitioning bool
|
|
|
|
popUpMenu *widget.PopUpMenu
|
|
}
|
|
|
|
// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content.
|
|
//
|
|
// Since: 2.1
|
|
func NewDocTabs(items ...*TabItem) *DocTabs {
|
|
tabs := &DocTabs{Items: items}
|
|
tabs.ExtendBaseWidget(tabs)
|
|
return tabs
|
|
}
|
|
|
|
// Append adds a new TabItem to the end of the tab bar.
|
|
func (t *DocTabs) Append(item *TabItem) {
|
|
t.SetItems(append(t.Items, item))
|
|
}
|
|
|
|
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
|
func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer {
|
|
t.ExtendBaseWidget(t)
|
|
th := t.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
r := &docTabsRenderer{
|
|
baseTabsRenderer: baseTabsRenderer{
|
|
bar: &fyne.Container{},
|
|
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
|
|
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
|
|
},
|
|
docTabs: t,
|
|
scroller: NewScroll(&fyne.Container{}),
|
|
}
|
|
r.action = r.buildAllTabsButton()
|
|
r.create = r.buildCreateTabsButton()
|
|
r.tabs = t
|
|
|
|
r.box = NewHBox(r.create, r.action)
|
|
r.scroller.OnScrolled = func(offset fyne.Position) {
|
|
r.updateIndicator(false)
|
|
}
|
|
r.updateAllTabs()
|
|
r.updateCreateTab()
|
|
r.updateTabs()
|
|
r.updateIndicator(false)
|
|
r.applyTheme(t)
|
|
return r
|
|
}
|
|
|
|
// DisableIndex disables the TabItem at the specified index.
|
|
//
|
|
// Since: 2.3
|
|
func (t *DocTabs) DisableIndex(i int) {
|
|
disableIndex(t, i)
|
|
}
|
|
|
|
// DisableItem disables the specified TabItem.
|
|
//
|
|
// Since: 2.3
|
|
func (t *DocTabs) DisableItem(item *TabItem) {
|
|
disableItem(t, item)
|
|
}
|
|
|
|
// EnableIndex enables the TabItem at the specified index.
|
|
//
|
|
// Since: 2.3
|
|
func (t *DocTabs) EnableIndex(i int) {
|
|
enableIndex(t, i)
|
|
}
|
|
|
|
// EnableItem enables the specified TabItem.
|
|
//
|
|
// Since: 2.3
|
|
func (t *DocTabs) EnableItem(item *TabItem) {
|
|
enableItem(t, item)
|
|
}
|
|
|
|
// Hide hides the widget.
|
|
func (t *DocTabs) Hide() {
|
|
if t.popUpMenu != nil {
|
|
t.popUpMenu.Hide()
|
|
t.popUpMenu = nil
|
|
}
|
|
t.BaseWidget.Hide()
|
|
}
|
|
|
|
// MinSize returns the size that this widget should not shrink below
|
|
func (t *DocTabs) MinSize() fyne.Size {
|
|
t.ExtendBaseWidget(t)
|
|
return t.BaseWidget.MinSize()
|
|
}
|
|
|
|
// Remove tab by value.
|
|
func (t *DocTabs) Remove(item *TabItem) {
|
|
removeItem(t, item)
|
|
t.Refresh()
|
|
}
|
|
|
|
// RemoveIndex removes tab by index.
|
|
func (t *DocTabs) RemoveIndex(index int) {
|
|
removeIndex(t, index)
|
|
t.Refresh()
|
|
}
|
|
|
|
// Select sets the specified TabItem to be selected and its content visible.
|
|
func (t *DocTabs) Select(item *TabItem) {
|
|
selectItem(t, item)
|
|
t.Refresh()
|
|
}
|
|
|
|
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
|
|
func (t *DocTabs) SelectIndex(index int) {
|
|
selectIndex(t, index)
|
|
}
|
|
|
|
// Selected returns the currently selected TabItem.
|
|
func (t *DocTabs) Selected() *TabItem {
|
|
return selected(t)
|
|
}
|
|
|
|
// SelectedIndex returns the index of the currently selected TabItem.
|
|
func (t *DocTabs) SelectedIndex() int {
|
|
return t.selected()
|
|
}
|
|
|
|
// SetItems sets the containers items and refreshes.
|
|
func (t *DocTabs) SetItems(items []*TabItem) {
|
|
setItems(t, items)
|
|
t.Refresh()
|
|
}
|
|
|
|
// SetTabLocation sets the location of the tab bar
|
|
func (t *DocTabs) SetTabLocation(l TabLocation) {
|
|
t.location = tabsAdjustedLocation(l, t)
|
|
t.Refresh()
|
|
}
|
|
|
|
// Show this widget, if it was previously hidden
|
|
func (t *DocTabs) Show() {
|
|
t.BaseWidget.Show()
|
|
t.SelectIndex(t.current)
|
|
}
|
|
|
|
func (t *DocTabs) close(item *TabItem) {
|
|
if f := t.CloseIntercept; f != nil {
|
|
f(item)
|
|
} else {
|
|
t.Remove(item)
|
|
if f := t.OnClosed; f != nil {
|
|
f(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *DocTabs) onUnselected() func(*TabItem) {
|
|
return t.OnUnselected
|
|
}
|
|
|
|
func (t *DocTabs) onSelected() func(*TabItem) {
|
|
return t.OnSelected
|
|
}
|
|
|
|
func (t *DocTabs) items() []*TabItem {
|
|
return t.Items
|
|
}
|
|
|
|
func (t *DocTabs) selected() int {
|
|
if len(t.Items) == 0 {
|
|
return -1
|
|
}
|
|
return t.current
|
|
}
|
|
|
|
func (t *DocTabs) setItems(items []*TabItem) {
|
|
t.Items = items
|
|
}
|
|
|
|
func (t *DocTabs) setSelected(selected int) {
|
|
t.current = selected
|
|
}
|
|
|
|
func (t *DocTabs) setTransitioning(transitioning bool) {
|
|
t.isTransitioning = transitioning
|
|
}
|
|
|
|
func (t *DocTabs) tabLocation() TabLocation {
|
|
return t.location
|
|
}
|
|
|
|
func (t *DocTabs) transitioning() bool {
|
|
return t.isTransitioning
|
|
}
|
|
|
|
// Declare conformity with WidgetRenderer interface.
|
|
var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil)
|
|
|
|
type docTabsRenderer struct {
|
|
baseTabsRenderer
|
|
docTabs *DocTabs
|
|
scroller *Scroll
|
|
box *fyne.Container
|
|
create *widget.Button
|
|
lastSelected int
|
|
}
|
|
|
|
func (r *docTabsRenderer) Layout(size fyne.Size) {
|
|
r.updateAllTabs()
|
|
r.updateCreateTab()
|
|
r.updateTabs()
|
|
r.layout(r.docTabs, size)
|
|
|
|
// lay out buttons before updating indicator, which is relative to their position
|
|
buttons := r.scroller.Content.(*fyne.Container)
|
|
buttons.Layout.Layout(buttons.Objects, buttons.Size())
|
|
r.updateIndicator(r.docTabs.transitioning())
|
|
|
|
if r.docTabs.transitioning() {
|
|
r.docTabs.setTransitioning(false)
|
|
}
|
|
}
|
|
|
|
func (r *docTabsRenderer) MinSize() fyne.Size {
|
|
return r.minSize(r.docTabs)
|
|
}
|
|
|
|
func (r *docTabsRenderer) Objects() []fyne.CanvasObject {
|
|
return r.objects(r.docTabs)
|
|
}
|
|
|
|
func (r *docTabsRenderer) Refresh() {
|
|
r.Layout(r.docTabs.Size())
|
|
|
|
if c := r.docTabs.current; c != r.lastSelected {
|
|
if c >= 0 && c < len(r.docTabs.Items) {
|
|
r.scrollToSelected()
|
|
}
|
|
r.lastSelected = c
|
|
}
|
|
|
|
r.refresh(r.docTabs)
|
|
|
|
canvas.Refresh(r.docTabs)
|
|
}
|
|
|
|
func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) {
|
|
all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() {
|
|
// Show pop up containing all tabs
|
|
|
|
items := make([]*fyne.MenuItem, len(r.docTabs.Items))
|
|
for i := 0; i < len(r.docTabs.Items); i++ {
|
|
index := i // capture
|
|
// FIXME MenuItem doesn't support icons (#1752)
|
|
items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() {
|
|
r.docTabs.SelectIndex(index)
|
|
if r.docTabs.popUpMenu != nil {
|
|
r.docTabs.popUpMenu.Hide()
|
|
r.docTabs.popUpMenu = nil
|
|
}
|
|
})
|
|
items[i].Checked = index == r.docTabs.current
|
|
}
|
|
|
|
r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items)
|
|
}}
|
|
|
|
return all
|
|
}
|
|
|
|
func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button {
|
|
create := widget.NewButton("", func() {
|
|
if f := r.docTabs.CreateTab; f != nil {
|
|
if tab := f(); tab != nil {
|
|
r.docTabs.Append(tab)
|
|
r.docTabs.SelectIndex(len(r.docTabs.Items) - 1)
|
|
}
|
|
}
|
|
})
|
|
create.Importance = widget.LowImportance
|
|
return create
|
|
}
|
|
|
|
func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
|
|
buttons.Objects = nil
|
|
|
|
var iconPos buttonIconPosition
|
|
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
|
buttons.Layout = layout.NewVBoxLayout()
|
|
iconPos = buttonIconTop
|
|
} else {
|
|
buttons.Layout = layout.NewHBoxLayout()
|
|
iconPos = buttonIconInline
|
|
}
|
|
|
|
for i := 0; i < count; i++ {
|
|
item := r.docTabs.Items[i]
|
|
if item.button == nil {
|
|
item.button = &tabButton{
|
|
onTapped: func() { r.docTabs.Select(item) },
|
|
onClosed: func() { r.docTabs.close(item) },
|
|
tabs: r.tabs,
|
|
}
|
|
if item.disabled {
|
|
item.button.Disable()
|
|
}
|
|
}
|
|
button := item.button
|
|
button.icon = item.Icon
|
|
button.iconPosition = iconPos
|
|
if i == r.docTabs.current {
|
|
button.importance = widget.HighImportance
|
|
} else {
|
|
button.importance = widget.MediumImportance
|
|
}
|
|
button.text = item.Text
|
|
button.textAlignment = fyne.TextAlignLeading
|
|
button.Refresh()
|
|
buttons.Objects = append(buttons.Objects, button)
|
|
}
|
|
}
|
|
|
|
func (r *docTabsRenderer) scrollToSelected() {
|
|
buttons := r.scroller.Content.(*fyne.Container)
|
|
|
|
// https://github.com/fyne-io/fyne/issues/3909
|
|
// very dirty temporary fix to this crash!
|
|
if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) {
|
|
return
|
|
}
|
|
|
|
button := buttons.Objects[r.docTabs.current]
|
|
pos := button.Position()
|
|
size := button.Size()
|
|
offset := r.scroller.Offset
|
|
viewport := r.scroller.Size()
|
|
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
|
if pos.Y < offset.Y {
|
|
offset.Y = pos.Y
|
|
} else if pos.Y+size.Height > offset.Y+viewport.Height {
|
|
offset.Y = pos.Y + size.Height - viewport.Height
|
|
}
|
|
} else {
|
|
if pos.X < offset.X {
|
|
offset.X = pos.X
|
|
} else if pos.X+size.Width > offset.X+viewport.Width {
|
|
offset.X = pos.X + size.Width - viewport.Width
|
|
}
|
|
}
|
|
r.scroller.Offset = offset
|
|
r.updateIndicator(false)
|
|
}
|
|
|
|
func (r *docTabsRenderer) updateIndicator(animate bool) {
|
|
th := r.docTabs.Theme()
|
|
if r.docTabs.current < 0 {
|
|
r.indicator.FillColor = color.Transparent
|
|
r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), th, animate)
|
|
return
|
|
}
|
|
|
|
var selectedPos fyne.Position
|
|
var selectedSize fyne.Size
|
|
|
|
buttons := r.scroller.Content.(*fyne.Container).Objects
|
|
|
|
if r.docTabs.current >= len(buttons) {
|
|
if a := r.action; a != nil {
|
|
selectedPos = a.Position()
|
|
selectedSize = a.Size()
|
|
minSize := a.MinSize()
|
|
if minSize.Width > selectedSize.Width {
|
|
selectedSize = minSize
|
|
}
|
|
}
|
|
} else {
|
|
selected := buttons[r.docTabs.current]
|
|
selectedPos = selected.Position()
|
|
selectedSize = selected.Size()
|
|
minSize := selected.MinSize()
|
|
if minSize.Width > selectedSize.Width {
|
|
selectedSize = minSize
|
|
}
|
|
}
|
|
|
|
scrollOffset := r.scroller.Offset
|
|
scrollSize := r.scroller.Size()
|
|
|
|
var indicatorPos fyne.Position
|
|
var indicatorSize fyne.Size
|
|
pad := th.Size(theme.SizeNamePadding)
|
|
|
|
switch r.docTabs.location {
|
|
case TabLocationTop:
|
|
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height)
|
|
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
|
|
case TabLocationLeading:
|
|
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y)
|
|
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
|
case TabLocationBottom:
|
|
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-pad)
|
|
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
|
|
case TabLocationTrailing:
|
|
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y-scrollOffset.Y)
|
|
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
|
}
|
|
|
|
if indicatorPos.X < 0 {
|
|
indicatorSize.Width = indicatorSize.Width + indicatorPos.X
|
|
indicatorPos.X = 0
|
|
}
|
|
if indicatorPos.Y < 0 {
|
|
indicatorSize.Height = indicatorSize.Height + indicatorPos.Y
|
|
indicatorPos.Y = 0
|
|
}
|
|
if indicatorSize.Width < 0 || indicatorSize.Height < 0 {
|
|
r.indicator.FillColor = color.Transparent
|
|
r.indicator.Refresh()
|
|
return
|
|
}
|
|
|
|
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
|
|
}
|
|
|
|
func (r *docTabsRenderer) updateAllTabs() {
|
|
if len(r.docTabs.Items) > 0 {
|
|
r.action.Show()
|
|
} else {
|
|
r.action.Hide()
|
|
}
|
|
}
|
|
|
|
func (r *docTabsRenderer) updateCreateTab() {
|
|
if r.docTabs.CreateTab != nil {
|
|
r.create.SetIcon(theme.ContentAddIcon())
|
|
r.create.Show()
|
|
} else {
|
|
r.create.Hide()
|
|
}
|
|
}
|
|
|
|
func (r *docTabsRenderer) updateTabs() {
|
|
tabCount := len(r.docTabs.Items)
|
|
r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container))
|
|
|
|
// Set layout of tab bar containing tab buttons and overflow action
|
|
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
|
r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil)
|
|
r.scroller.Direction = ScrollVerticalOnly
|
|
} else {
|
|
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box)
|
|
r.scroller.Direction = ScrollHorizontalOnly
|
|
}
|
|
|
|
r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box}
|
|
r.bar.Refresh()
|
|
}
|