feat(gui): enhance application UI with new status segments and improved layout
Tests / Go and GTK tests (push) Failing after 2m14s
Tests / Go and GTK tests (push) Failing after 2m14s
- Refactor status bar to include separate labels for source, speech, and outputs. - Update buildWindow method to adjust layout and styling of status elements. - Introduce new text tags for console and caption buffers to improve text rendering. - Modify settings controls to use CheckButton instead of Switch for better clarity. - Revise CSS styles for improved visual consistency and responsiveness across UI elements.
This commit is contained in:
+38
-19
@@ -61,14 +61,21 @@ type Application struct {
|
|||||||
statusLabel *gtk.Label
|
statusLabel *gtk.Label
|
||||||
statusDot *gtk.Label
|
statusDot *gtk.Label
|
||||||
startStopButton *gtk.Button
|
startStopButton *gtk.Button
|
||||||
statusbarLabel *gtk.Label
|
statusSource *gtk.Label
|
||||||
|
statusSpeech *gtk.Label
|
||||||
|
statusOutputs *gtk.Label
|
||||||
captionBuffer *gtk.TextBuffer
|
captionBuffer *gtk.TextBuffer
|
||||||
captionView *gtk.TextView
|
captionView *gtk.TextView
|
||||||
captionScroll *gtk.ScrolledWindow
|
captionScroll *gtk.ScrolledWindow
|
||||||
|
captionMetaTag *gtk.TextTag
|
||||||
|
captionHintTag *gtk.TextTag
|
||||||
provisionalLabel *gtk.Label
|
provisionalLabel *gtk.Label
|
||||||
consoleBuffer *gtk.TextBuffer
|
consoleBuffer *gtk.TextBuffer
|
||||||
consoleView *gtk.TextView
|
consoleView *gtk.TextView
|
||||||
consoleScroll *gtk.ScrolledWindow
|
consoleScroll *gtk.ScrolledWindow
|
||||||
|
consoleMetaTag *gtk.TextTag
|
||||||
|
consoleWarnTag *gtk.TextTag
|
||||||
|
consoleErrorTag *gtk.TextTag
|
||||||
settings settingsControls
|
settings settingsControls
|
||||||
settingsError *gtk.Label
|
settingsError *gtk.Label
|
||||||
applyButton *gtk.Button
|
applyButton *gtk.Button
|
||||||
@@ -177,7 +184,7 @@ func (a *Application) buildWindow() {
|
|||||||
brand := gtk.NewBox(gtk.OrientationHorizontal, 7)
|
brand := gtk.NewBox(gtk.OrientationHorizontal, 7)
|
||||||
if iconPath := findIconPath(); iconPath != "" {
|
if iconPath := findIconPath(); iconPath != "" {
|
||||||
icon := gtk.NewImageFromFile(iconPath)
|
icon := gtk.NewImageFromFile(iconPath)
|
||||||
icon.SetPixelSize(27)
|
icon.SetPixelSize(20)
|
||||||
brand.Append(icon)
|
brand.Append(icon)
|
||||||
}
|
}
|
||||||
title := gtk.NewLabel("CAPTIONEER")
|
title := gtk.NewLabel("CAPTIONEER")
|
||||||
@@ -187,6 +194,7 @@ func (a *Application) buildWindow() {
|
|||||||
|
|
||||||
statusBox := gtk.NewBox(gtk.OrientationHorizontal, 6)
|
statusBox := gtk.NewBox(gtk.OrientationHorizontal, 6)
|
||||||
statusBox.SetHAlign(gtk.AlignCenter)
|
statusBox.SetHAlign(gtk.AlignCenter)
|
||||||
|
statusBox.AddCSSClass("status-cell")
|
||||||
a.statusDot = gtk.NewLabel("●")
|
a.statusDot = gtk.NewLabel("●")
|
||||||
a.statusDot.AddCSSClass("status-dot")
|
a.statusDot.AddCSSClass("status-dot")
|
||||||
a.statusLabel = gtk.NewLabel("STOPPED · VAD")
|
a.statusLabel = gtk.NewLabel("STOPPED · VAD")
|
||||||
@@ -211,21 +219,28 @@ func (a *Application) buildWindow() {
|
|||||||
|
|
||||||
tabs := gtk.NewStackSwitcher()
|
tabs := gtk.NewStackSwitcher()
|
||||||
tabs.SetStack(a.stack)
|
tabs.SetStack(a.stack)
|
||||||
tabs.AddCSSClass("captioneer-tabs")
|
tabs.SetHAlign(gtk.AlignStart)
|
||||||
tabs.SetHAlign(gtk.AlignFill)
|
tabStrip := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
||||||
root.Append(tabs)
|
tabStrip.AddCSSClass("captioneer-tabs")
|
||||||
|
tabStrip.Append(tabs)
|
||||||
|
root.Append(tabStrip)
|
||||||
|
|
||||||
a.stack.AddTitled(a.buildCaptionsPage(), "captions", "CAPTIONS")
|
a.stack.AddTitled(a.buildCaptionsPage(), "captions", "CAPTIONS")
|
||||||
a.stack.AddTitled(a.buildConsolePage(), "console", "CONSOLE")
|
a.stack.AddTitled(a.buildConsolePage(), "console", "CONSOLE")
|
||||||
a.stack.AddTitled(a.buildSettingsPage(), "settings", "SETTINGS")
|
a.stack.AddTitled(a.buildSettingsPage(), "settings", "SETTINGS")
|
||||||
root.Append(a.stack)
|
root.Append(a.stack)
|
||||||
|
|
||||||
statusbar := gtk.NewBox(gtk.OrientationHorizontal, 8)
|
statusbar := gtk.NewBox(gtk.OrientationHorizontal, 4)
|
||||||
statusbar.AddCSSClass("statusbar")
|
statusbar.AddCSSClass("statusbar")
|
||||||
a.statusbarLabel = gtk.NewLabel("Source: not connected · Speech: quiet · Outputs: preparing")
|
a.statusSource = statusSegment("Source: not connected")
|
||||||
a.statusbarLabel.SetXAlign(0)
|
a.statusSource.SetHExpand(true)
|
||||||
a.statusbarLabel.SetEllipsize(3)
|
a.statusSource.SetXAlign(0)
|
||||||
statusbar.Append(a.statusbarLabel)
|
a.statusSource.SetEllipsize(3)
|
||||||
|
a.statusSpeech = statusSegment("Speech: quiet")
|
||||||
|
a.statusOutputs = statusSegment("Outputs: preparing")
|
||||||
|
statusbar.Append(a.statusSource)
|
||||||
|
statusbar.Append(a.statusSpeech)
|
||||||
|
statusbar.Append(a.statusOutputs)
|
||||||
root.Append(statusbar)
|
root.Append(statusbar)
|
||||||
a.window.SetChild(root)
|
a.window.SetChild(root)
|
||||||
}
|
}
|
||||||
@@ -479,17 +494,15 @@ func (a *Application) renderStatus(status app.Status) {
|
|||||||
if source == "" {
|
if source == "" {
|
||||||
source = "not connected"
|
source = "not connected"
|
||||||
}
|
}
|
||||||
overload := ""
|
|
||||||
if overloaded {
|
if overloaded {
|
||||||
overload = " · OVERLOADED"
|
speech += " · OVERLOADED"
|
||||||
|
a.statusSpeech.AddCSSClass("warning-text")
|
||||||
|
} else {
|
||||||
|
a.statusSpeech.RemoveCSSClass("warning-text")
|
||||||
}
|
}
|
||||||
a.statusbarLabel.SetText(fmt.Sprintf(
|
a.statusSource.SetText("Source: " + source)
|
||||||
"Source: %s · Speech: %s · Outputs: %s%s",
|
a.statusSpeech.SetText("Speech: " + speech)
|
||||||
source,
|
a.statusOutputs.SetText("Outputs: " + a.outputSummary())
|
||||||
speech,
|
|
||||||
a.outputSummary(),
|
|
||||||
overload,
|
|
||||||
))
|
|
||||||
if status.LastError != "" && status.State == app.StateError && status.LastError != a.lastShownError {
|
if status.LastError != "" && status.State == app.StateError && status.LastError != a.lastShownError {
|
||||||
a.log.Add(diagnostics.Error, "engine", status.LastError)
|
a.log.Add(diagnostics.Error, "engine", status.LastError)
|
||||||
a.lastShownError = status.LastError
|
a.lastShownError = status.LastError
|
||||||
@@ -573,6 +586,12 @@ func (a *Application) shutdown() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusSegment(text string) *gtk.Label {
|
||||||
|
label := gtk.NewLabel(text)
|
||||||
|
label.AddCSSClass("status-segment")
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
func joinError(current, next error) error {
|
func joinError(current, next error) error {
|
||||||
if current == nil {
|
if current == nil {
|
||||||
return next
|
return next
|
||||||
|
|||||||
+64
-5
@@ -6,9 +6,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||||
|
"tea.chunkbyte.com/kato/captioneer/src/diagnostics"
|
||||||
"tea.chunkbyte.com/kato/captioneer/src/output/history"
|
"tea.chunkbyte.com/kato/captioneer/src/output/history"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ func (a *Application) buildCaptionsPage() *gtk.Box {
|
|||||||
page.Append(toolbar)
|
page.Append(toolbar)
|
||||||
|
|
||||||
a.captionBuffer = gtk.NewTextBuffer(nil)
|
a.captionBuffer = gtk.NewTextBuffer(nil)
|
||||||
|
a.captionMetaTag = bufferTag(a.captionBuffer, "meta", "#8A9384")
|
||||||
|
a.captionHintTag = bufferTag(a.captionBuffer, "hint", "#6E7763")
|
||||||
a.captionView = gtk.NewTextViewWithBuffer(a.captionBuffer)
|
a.captionView = gtk.NewTextViewWithBuffer(a.captionBuffer)
|
||||||
a.captionView.SetEditable(false)
|
a.captionView.SetEditable(false)
|
||||||
a.captionView.SetCursorVisible(true)
|
a.captionView.SetCursorVisible(true)
|
||||||
@@ -54,8 +58,9 @@ func (a *Application) buildCaptionsPage() *gtk.Box {
|
|||||||
|
|
||||||
provisional := gtk.NewBox(gtk.OrientationHorizontal, 8)
|
provisional := gtk.NewBox(gtk.OrientationHorizontal, 8)
|
||||||
provisional.AddCSSClass("provisional-row")
|
provisional.AddCSSClass("provisional-row")
|
||||||
caption := labelWithClass("LIVE", "section-title")
|
caption := labelWithClass("LIVE", "live-tag")
|
||||||
caption.SetVAlign(gtk.AlignStart)
|
caption.SetVAlign(gtk.AlignStart)
|
||||||
|
caption.SetMarginTop(2)
|
||||||
provisional.Append(caption)
|
provisional.Append(caption)
|
||||||
a.provisionalLabel = gtk.NewLabel("Waiting for speech…")
|
a.provisionalLabel = gtk.NewLabel("Waiting for speech…")
|
||||||
a.provisionalLabel.SetXAlign(0)
|
a.provisionalLabel.SetXAlign(0)
|
||||||
@@ -93,6 +98,9 @@ func (a *Application) buildConsolePage() *gtk.Box {
|
|||||||
page.Append(toolbar)
|
page.Append(toolbar)
|
||||||
|
|
||||||
a.consoleBuffer = gtk.NewTextBuffer(nil)
|
a.consoleBuffer = gtk.NewTextBuffer(nil)
|
||||||
|
a.consoleMetaTag = bufferTag(a.consoleBuffer, "meta", "#79826F")
|
||||||
|
a.consoleWarnTag = bufferTag(a.consoleBuffer, "warn", "#C79C4E")
|
||||||
|
a.consoleErrorTag = bufferTag(a.consoleBuffer, "error", "#CF826D")
|
||||||
a.consoleView = gtk.NewTextViewWithBuffer(a.consoleBuffer)
|
a.consoleView = gtk.NewTextViewWithBuffer(a.consoleBuffer)
|
||||||
a.consoleView.SetEditable(false)
|
a.consoleView.SetEditable(false)
|
||||||
a.consoleView.SetCursorVisible(true)
|
a.consoleView.SetCursorVisible(true)
|
||||||
@@ -113,18 +121,44 @@ func (a *Application) buildConsolePage() *gtk.Box {
|
|||||||
return page
|
return page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tagSpan marks a character range of freshly rendered buffer text so a
|
||||||
|
// TextTag can be applied after SetText replaces the whole buffer.
|
||||||
|
type tagSpan struct {
|
||||||
|
tag *gtk.TextTag
|
||||||
|
start, end int
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTagSpans(buffer *gtk.TextBuffer, spans []tagSpan) {
|
||||||
|
for _, span := range spans {
|
||||||
|
buffer.ApplyTag(span.tag, buffer.IterAtOffset(span.start), buffer.IterAtOffset(span.end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Application) renderHistory(snapshot history.Snapshot) {
|
func (a *Application) renderHistory(snapshot history.Snapshot) {
|
||||||
follow := isAtBottom(a.captionScroll)
|
follow := isAtBottom(a.captionScroll)
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
|
var spans []tagSpan
|
||||||
|
offset := 0
|
||||||
|
if len(snapshot.Finals) == 0 {
|
||||||
|
hint := "No captions yet. Press START to begin listening."
|
||||||
|
text.WriteString(hint)
|
||||||
|
spans = append(spans, tagSpan{a.captionHintTag, 0, utf8.RuneCountInString(hint)})
|
||||||
|
}
|
||||||
for _, entry := range snapshot.Finals {
|
for _, entry := range snapshot.Finals {
|
||||||
fmt.Fprintf(&text, "%s [%s–%s] %s\n",
|
meta := fmt.Sprintf("%s [%s–%s] ",
|
||||||
entry.CreatedAt.Format("15:04:05"),
|
entry.CreatedAt.Format("15:04:05"),
|
||||||
formatCaptionTime(entry.StartedAt),
|
formatCaptionTime(entry.StartedAt),
|
||||||
formatCaptionTime(entry.EndedAt),
|
formatCaptionTime(entry.EndedAt),
|
||||||
entry.Text,
|
|
||||||
)
|
)
|
||||||
|
text.WriteString(meta)
|
||||||
|
spans = append(spans, tagSpan{a.captionMetaTag, offset, offset + utf8.RuneCountInString(meta)})
|
||||||
|
offset += utf8.RuneCountInString(meta)
|
||||||
|
line := entry.Text + "\n"
|
||||||
|
text.WriteString(line)
|
||||||
|
offset += utf8.RuneCountInString(line)
|
||||||
}
|
}
|
||||||
a.captionBuffer.SetText(text.String())
|
a.captionBuffer.SetText(text.String())
|
||||||
|
applyTagSpans(a.captionBuffer, spans)
|
||||||
if snapshot.Provisional == nil || strings.TrimSpace(snapshot.Provisional.Text) == "" {
|
if snapshot.Provisional == nil || strings.TrimSpace(snapshot.Provisional.Text) == "" {
|
||||||
a.provisionalLabel.SetText("Waiting for speech…")
|
a.provisionalLabel.SetText("Waiting for speech…")
|
||||||
} else {
|
} else {
|
||||||
@@ -138,15 +172,33 @@ func (a *Application) renderHistory(snapshot history.Snapshot) {
|
|||||||
func (a *Application) renderConsole() {
|
func (a *Application) renderConsole() {
|
||||||
follow := isAtBottom(a.consoleScroll)
|
follow := isAtBottom(a.consoleScroll)
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
|
var spans []tagSpan
|
||||||
|
offset := 0
|
||||||
for _, entry := range a.log.Entries() {
|
for _, entry := range a.log.Entries() {
|
||||||
fmt.Fprintf(&text, "%s %-7s %-14s %s\n",
|
var severityTag *gtk.TextTag
|
||||||
|
switch entry.Severity {
|
||||||
|
case diagnostics.Warning:
|
||||||
|
severityTag = a.consoleWarnTag
|
||||||
|
case diagnostics.Error:
|
||||||
|
severityTag = a.consoleErrorTag
|
||||||
|
}
|
||||||
|
meta := fmt.Sprintf("%s %-7s %-14s ",
|
||||||
entry.At.Format("15:04:05.000"),
|
entry.At.Format("15:04:05.000"),
|
||||||
strings.ToUpper(string(entry.Severity)),
|
strings.ToUpper(string(entry.Severity)),
|
||||||
"["+entry.Category+"]",
|
"["+entry.Category+"]",
|
||||||
entry.Message,
|
|
||||||
)
|
)
|
||||||
|
text.WriteString(meta)
|
||||||
|
spans = append(spans, tagSpan{a.consoleMetaTag, offset, offset + utf8.RuneCountInString(meta)})
|
||||||
|
offset += utf8.RuneCountInString(meta)
|
||||||
|
line := entry.Message + "\n"
|
||||||
|
text.WriteString(line)
|
||||||
|
if severityTag != nil {
|
||||||
|
spans = append(spans, tagSpan{severityTag, offset, offset + utf8.RuneCountInString(entry.Message)})
|
||||||
|
}
|
||||||
|
offset += utf8.RuneCountInString(line)
|
||||||
}
|
}
|
||||||
a.consoleBuffer.SetText(text.String())
|
a.consoleBuffer.SetText(text.String())
|
||||||
|
applyTagSpans(a.consoleBuffer, spans)
|
||||||
if follow {
|
if follow {
|
||||||
scrollTextViewToEnd(a.consoleView, a.consoleScroll)
|
scrollTextViewToEnd(a.consoleView, a.consoleScroll)
|
||||||
}
|
}
|
||||||
@@ -193,6 +245,13 @@ func copyBuffer(buffer *gtk.TextBuffer) {
|
|||||||
gdk.DisplayGetDefault().Clipboard().SetText(buffer.Text(start, end, true))
|
gdk.DisplayGetDefault().Clipboard().SetText(buffer.Text(start, end, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bufferTag(buffer *gtk.TextBuffer, name, foreground string) *gtk.TextTag {
|
||||||
|
tag := gtk.NewTextTag(name)
|
||||||
|
tag.SetObjectProperty("foreground", foreground)
|
||||||
|
buffer.TagTable().Add(tag)
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
func labelWithClass(text, class string) *gtk.Label {
|
func labelWithClass(text, class string) *gtk.Label {
|
||||||
label := gtk.NewLabel(text)
|
label := gtk.NewLabel(text)
|
||||||
label.AddCSSClass(class)
|
label.AddCSSClass(class)
|
||||||
|
|||||||
+23
-25
@@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/gotk4/pkg/gio/v2"
|
"github.com/diamondburned/gotk4/pkg/gio/v2"
|
||||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
|
||||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||||
"tea.chunkbyte.com/kato/captioneer/src/app"
|
"tea.chunkbyte.com/kato/captioneer/src/app"
|
||||||
"tea.chunkbyte.com/kato/captioneer/src/config"
|
"tea.chunkbyte.com/kato/captioneer/src/config"
|
||||||
@@ -17,8 +16,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type settingsControls struct {
|
type settingsControls struct {
|
||||||
startCaptions *gtk.Switch
|
startCaptions *gtk.CheckButton
|
||||||
alwaysOnTop *gtk.Switch
|
alwaysOnTop *gtk.CheckButton
|
||||||
|
|
||||||
mode *gtk.ComboBoxText
|
mode *gtk.ComboBoxText
|
||||||
chunkDuration *gtk.Entry
|
chunkDuration *gtk.Entry
|
||||||
@@ -27,11 +26,11 @@ type settingsControls struct {
|
|||||||
previewThresholdDB *gtk.SpinButton
|
previewThresholdDB *gtk.SpinButton
|
||||||
vadThreshold *gtk.SpinButton
|
vadThreshold *gtk.SpinButton
|
||||||
|
|
||||||
overlayEnabled *gtk.Switch
|
overlayEnabled *gtk.CheckButton
|
||||||
historyEnabled *gtk.Switch
|
historyEnabled *gtk.CheckButton
|
||||||
stdoutEnabled *gtk.Switch
|
stdoutEnabled *gtk.CheckButton
|
||||||
stderrEnabled *gtk.Switch
|
stderrEnabled *gtk.CheckButton
|
||||||
transcriptEnabled *gtk.Switch
|
transcriptEnabled *gtk.CheckButton
|
||||||
transcriptDirectory *gtk.Entry
|
transcriptDirectory *gtk.Entry
|
||||||
|
|
||||||
overlayOpacity *gtk.SpinButton
|
overlayOpacity *gtk.SpinButton
|
||||||
@@ -41,10 +40,10 @@ type settingsControls struct {
|
|||||||
overlayMaxWidth *gtk.SpinButton
|
overlayMaxWidth *gtk.SpinButton
|
||||||
overlayMonitor *gtk.Entry
|
overlayMonitor *gtk.Entry
|
||||||
overlayFinalTimeout *gtk.Entry
|
overlayFinalTimeout *gtk.Entry
|
||||||
overlayClickThrough *gtk.Switch
|
overlayClickThrough *gtk.CheckButton
|
||||||
|
|
||||||
overlayBackend *gtk.ComboBoxText
|
overlayBackend *gtk.ComboBoxText
|
||||||
modelAutoRestart *gtk.Switch
|
modelAutoRestart *gtk.CheckButton
|
||||||
modelTimeout *gtk.Entry
|
modelTimeout *gtk.Entry
|
||||||
modelShutdownTimeout *gtk.Entry
|
modelShutdownTimeout *gtk.Entry
|
||||||
}
|
}
|
||||||
@@ -101,8 +100,8 @@ func (a *Application) buildSettingsPage() *gtk.Box {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Application) createSettingsControls() {
|
func (a *Application) createSettingsControls() {
|
||||||
a.settings.startCaptions = gtk.NewSwitch()
|
a.settings.startCaptions = gtk.NewCheckButton()
|
||||||
a.settings.alwaysOnTop = gtk.NewSwitch()
|
a.settings.alwaysOnTop = gtk.NewCheckButton()
|
||||||
a.settings.mode = combo([]option{{"vad", "Voice activity detection (VAD)"}, {"fixed", "Fixed chunks"}})
|
a.settings.mode = combo([]option{{"vad", "Voice activity detection (VAD)"}, {"fixed", "Fixed chunks"}})
|
||||||
a.settings.chunkDuration = gtk.NewEntry()
|
a.settings.chunkDuration = gtk.NewEntry()
|
||||||
a.settings.modelsDirectory = gtk.NewEntry()
|
a.settings.modelsDirectory = gtk.NewEntry()
|
||||||
@@ -110,11 +109,11 @@ func (a *Application) createSettingsControls() {
|
|||||||
a.settings.previewThresholdDB = spin(-100, 0, 1, 1)
|
a.settings.previewThresholdDB = spin(-100, 0, 1, 1)
|
||||||
a.settings.vadThreshold = spin(0, 1, 0.01, 2)
|
a.settings.vadThreshold = spin(0, 1, 0.01, 2)
|
||||||
|
|
||||||
a.settings.overlayEnabled = gtk.NewSwitch()
|
a.settings.overlayEnabled = gtk.NewCheckButton()
|
||||||
a.settings.historyEnabled = gtk.NewSwitch()
|
a.settings.historyEnabled = gtk.NewCheckButton()
|
||||||
a.settings.stdoutEnabled = gtk.NewSwitch()
|
a.settings.stdoutEnabled = gtk.NewCheckButton()
|
||||||
a.settings.stderrEnabled = gtk.NewSwitch()
|
a.settings.stderrEnabled = gtk.NewCheckButton()
|
||||||
a.settings.transcriptEnabled = gtk.NewSwitch()
|
a.settings.transcriptEnabled = gtk.NewCheckButton()
|
||||||
a.settings.transcriptDirectory = gtk.NewEntry()
|
a.settings.transcriptDirectory = gtk.NewEntry()
|
||||||
|
|
||||||
a.settings.overlayOpacity = spin(0, 1, 0.01, 2)
|
a.settings.overlayOpacity = spin(0, 1, 0.01, 2)
|
||||||
@@ -125,10 +124,10 @@ func (a *Application) createSettingsControls() {
|
|||||||
a.settings.overlayMaxWidth = spin(100, 5000, 10, 0)
|
a.settings.overlayMaxWidth = spin(100, 5000, 10, 0)
|
||||||
a.settings.overlayMonitor = gtk.NewEntry()
|
a.settings.overlayMonitor = gtk.NewEntry()
|
||||||
a.settings.overlayFinalTimeout = gtk.NewEntry()
|
a.settings.overlayFinalTimeout = gtk.NewEntry()
|
||||||
a.settings.overlayClickThrough = gtk.NewSwitch()
|
a.settings.overlayClickThrough = gtk.NewCheckButton()
|
||||||
|
|
||||||
a.settings.overlayBackend = combo([]option{{"auto", "Automatic"}, {"wayland", "Wayland"}, {"x11", "X11"}})
|
a.settings.overlayBackend = combo([]option{{"auto", "Automatic"}, {"wayland", "Wayland"}, {"x11", "X11"}})
|
||||||
a.settings.modelAutoRestart = gtk.NewSwitch()
|
a.settings.modelAutoRestart = gtk.NewCheckButton()
|
||||||
a.settings.modelTimeout = gtk.NewEntry()
|
a.settings.modelTimeout = gtk.NewEntry()
|
||||||
a.settings.modelShutdownTimeout = gtk.NewEntry()
|
a.settings.modelShutdownTimeout = gtk.NewEntry()
|
||||||
}
|
}
|
||||||
@@ -368,16 +367,13 @@ func (a *Application) connectSettingsChanged() {
|
|||||||
control.ConnectChanged(changed)
|
control.ConnectChanged(changed)
|
||||||
}
|
}
|
||||||
a.settings.overlayFont.ConnectFontSet(changed)
|
a.settings.overlayFont.ConnectFontSet(changed)
|
||||||
switches := []*gtk.Switch{
|
checks := []*gtk.CheckButton{
|
||||||
a.settings.startCaptions, a.settings.alwaysOnTop, a.settings.overlayEnabled,
|
a.settings.startCaptions, a.settings.alwaysOnTop, a.settings.overlayEnabled,
|
||||||
a.settings.historyEnabled, a.settings.stdoutEnabled, a.settings.stderrEnabled,
|
a.settings.historyEnabled, a.settings.stdoutEnabled, a.settings.stderrEnabled,
|
||||||
a.settings.transcriptEnabled, a.settings.overlayClickThrough, a.settings.modelAutoRestart,
|
a.settings.transcriptEnabled, a.settings.overlayClickThrough, a.settings.modelAutoRestart,
|
||||||
}
|
}
|
||||||
for _, control := range switches {
|
for _, control := range checks {
|
||||||
control.ConnectStateSet(func(bool) bool {
|
control.ConnectToggled(changed)
|
||||||
glib.IdleAdd(changed)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +431,8 @@ func settingRow(title, description string, control gtk.Widgetter) *gtk.Box {
|
|||||||
detail.SetWrap(true)
|
detail.SetWrap(true)
|
||||||
text.Append(detail)
|
text.Append(detail)
|
||||||
row.Append(text)
|
row.Append(text)
|
||||||
|
widget := gtk.BaseWidget(control)
|
||||||
|
widget.SetVAlign(gtk.AlignCenter)
|
||||||
row.Append(control)
|
row.Append(control)
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|||||||
+455
-136
@@ -1,218 +1,537 @@
|
|||||||
|
/*
|
||||||
|
* Captioneer visual theme, modeled on the 2003-2005 Steam client:
|
||||||
|
* olive-gray raised chrome, sunken near-black wells, khaki/gold accents,
|
||||||
|
* 1px bevels (light top/left, dark bottom/right), no soft modern gradients.
|
||||||
|
*
|
||||||
|
* Palette:
|
||||||
|
* chrome #3E4637 (gradient stop #333A2D)
|
||||||
|
* raised panel #4C5844
|
||||||
|
* sunken well #1E221B
|
||||||
|
* body text #D8DED3 dim text #8A9384
|
||||||
|
* khaki accent #C4B550 selection #958831
|
||||||
|
* error #B05C4A warning #C79C4E
|
||||||
|
* bevel light #6A7862 bevel dark #242921
|
||||||
|
*/
|
||||||
|
|
||||||
window.captioneer-main {
|
window.captioneer-main {
|
||||||
background: #30372f;
|
background: #3E4637;
|
||||||
color: #e0e5d7;
|
color: #D8DED3;
|
||||||
|
font-size: 9.5pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Titlebar chrome */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
headerbar.captioneer-header {
|
headerbar.captioneer-header {
|
||||||
min-height: 40px;
|
|
||||||
padding: 4px 7px;
|
|
||||||
background-image: linear-gradient(to bottom, #4a5543, #2b332c);
|
|
||||||
border-bottom: 1px solid #171d18;
|
|
||||||
box-shadow: inset 0 1px #748069;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captioneer-brand {
|
|
||||||
color: #e8ebdf;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
color: #cbd3bd;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.status-running { color: #9eb96b; }
|
|
||||||
.status-dot.status-stopped { color: #7f8977; }
|
|
||||||
.status-dot.status-warning { color: #dfc078; }
|
|
||||||
.status-dot.status-error { color: #e58c7c; }
|
|
||||||
|
|
||||||
.captioneer-menu {
|
|
||||||
min-height: 24px;
|
|
||||||
padding: 0 4px;
|
|
||||||
background: #3a4337;
|
|
||||||
border-bottom: 1px solid #171d18;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captioneer-tabs {
|
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 3px 8px 0;
|
padding: 2px 6px;
|
||||||
background: #353d33;
|
background-image: linear-gradient(to bottom, #454E3C, #333A2D);
|
||||||
border-bottom: 1px solid #171d18;
|
border-bottom: 1px solid #242921;
|
||||||
|
box-shadow: inset 0 1px #6A7862;
|
||||||
|
color: #D8DED3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captioneer-tabs button {
|
headerbar.captioneer-header windowcontrols button {
|
||||||
min-height: 26px;
|
min-height: 17px;
|
||||||
padding: 3px 16px;
|
min-width: 19px;
|
||||||
border-radius: 2px 2px 0 0;
|
padding: 1px 3px;
|
||||||
background-image: linear-gradient(to bottom, #56604d, #414a3d);
|
margin: 0 1px;
|
||||||
border: 1px solid #1d241e;
|
border-radius: 0;
|
||||||
border-top-color: #748069;
|
background-image: none;
|
||||||
border-left-color: #68745e;
|
background: #4C5844;
|
||||||
color: #c9d0bf;
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
color: #B9C2AD;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headerbar.captioneer-header windowcontrols button:hover {
|
||||||
|
background: #5B684D;
|
||||||
|
color: #E8EDD8;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerbar.captioneer-header windowcontrols button:active {
|
||||||
|
background: #333A2D;
|
||||||
|
border-top-color: #242921;
|
||||||
|
border-left-color: #242921;
|
||||||
|
border-bottom-color: #6A7862;
|
||||||
|
border-right-color: #6A7862;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captioneer-brand {
|
||||||
|
color: #C4B550;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 10pt;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 1px 12px;
|
||||||
|
background: #2C3227;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-bottom-color: #58644E;
|
||||||
|
border-right-color: #58644E;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: #B9C2AD;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.status-running { color: #C4B550; }
|
||||||
|
.status-dot.status-stopped { color: #79826F; }
|
||||||
|
.status-dot.status-warning { color: #C79C4E; }
|
||||||
|
.status-dot.status-error { color: #B05C4A; }
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Menu bar */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
.captioneer-menu {
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: #3E4637;
|
||||||
|
border-bottom: 1px solid #242921;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captioneer-menu > item {
|
||||||
|
padding: 2px 9px;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #C3CBB6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captioneer-menu > item:hover,
|
||||||
|
.captioneer-menu > item:selected {
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
}
|
||||||
|
|
||||||
|
popover.menu contents {
|
||||||
|
background: #3E4637;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.35);
|
||||||
|
color: #D8DED3;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
popover.menu modelbutton {
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #D8DED3;
|
||||||
|
}
|
||||||
|
|
||||||
|
popover.menu modelbutton:hover {
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Tab strip */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
.captioneer-tabs {
|
||||||
|
min-height: 27px;
|
||||||
|
padding: 5px 8px 0;
|
||||||
|
background: #383F30;
|
||||||
|
border-bottom: 1px solid #242921;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captioneer-tabs button {
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 3px 16px;
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
border-radius: 1px 1px 0 0;
|
||||||
|
background-image: none;
|
||||||
|
background: #434D3B;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
color: #A8B29B;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captioneer-tabs button:hover {
|
||||||
|
color: #D8DED3;
|
||||||
|
background: #4A553F;
|
||||||
|
}
|
||||||
|
|
||||||
.captioneer-tabs button:checked {
|
.captioneer-tabs button:checked {
|
||||||
background: #515b49;
|
background: #4C5844;
|
||||||
color: #f0f2ea;
|
color: #C4B550;
|
||||||
border-bottom-color: #515b49;
|
border-bottom-color: #4C5844;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captioneer-content {
|
.captioneer-content {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
background: #515b49;
|
background: #4C5844;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Sunken wells and text views */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
.inset-panel,
|
.inset-panel,
|
||||||
textview,
|
textview,
|
||||||
entry,
|
entry,
|
||||||
spinbutton,
|
spinbutton,
|
||||||
combobox,
|
combobox button.combo,
|
||||||
fontbutton {
|
fontbutton button {
|
||||||
background: #252c26;
|
background-image: none;
|
||||||
color: #e0e5d7;
|
background: #1E221B;
|
||||||
border: 1px solid #171d18;
|
color: #D8DED3;
|
||||||
border-bottom-color: #748069;
|
border: 1px solid #191D16;
|
||||||
border-right-color: #68745e;
|
border-bottom-color: #6A7862;
|
||||||
|
border-right-color: #58644E;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
|
||||||
|
|
||||||
textview text {
|
|
||||||
background: #252c26;
|
|
||||||
color: #e0e5d7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-view text {
|
|
||||||
background: #202621;
|
|
||||||
color: #c5cfb9;
|
|
||||||
font-family: Monospace;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provisional-row {
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #394237;
|
|
||||||
color: #cbd5b8;
|
|
||||||
border: 1px solid #1a201b;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
padding: 4px;
|
|
||||||
background: #424b3e;
|
|
||||||
border: 1px solid #202720;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
min-height: 24px;
|
|
||||||
padding: 3px 9px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-image: linear-gradient(to bottom, #657058, #46503f);
|
|
||||||
color: #edf0e6;
|
|
||||||
border: 1px solid #1a201b;
|
|
||||||
border-top-color: #7d896f;
|
|
||||||
border-left-color: #748069;
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textview text {
|
||||||
|
background: #1E221B;
|
||||||
|
color: #D8DED3;
|
||||||
|
}
|
||||||
|
|
||||||
|
textview text selection {
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-view text {
|
||||||
|
background: #191D16;
|
||||||
|
color: #B9C4AC;
|
||||||
|
font-family: Monospace;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provisional-row {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: #2C3227;
|
||||||
|
color: #D3DCBF;
|
||||||
|
border: 1px solid #191D16;
|
||||||
|
border-bottom-color: #58644E;
|
||||||
|
border-right-color: #58644E;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-tag {
|
||||||
|
padding: 1px 7px;
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Toolbars and buttons */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
background: #525E48;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 21px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background-image: none;
|
||||||
|
background: #57634B;
|
||||||
|
color: #D8DED3;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-image: linear-gradient(to bottom, #758064, #515c48);
|
background: #63704F;
|
||||||
|
color: #EDF1DD;
|
||||||
|
border-top-color: #83917A;
|
||||||
|
border-left-color: #83917A;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active,
|
button:active,
|
||||||
button:checked {
|
button:checked {
|
||||||
background: #354033;
|
background: #3A4233;
|
||||||
border-top-color: #171d18;
|
border-top-color: #242921;
|
||||||
border-left-color: #171d18;
|
border-left-color: #242921;
|
||||||
border-bottom-color: #748069;
|
border-bottom-color: #6A7862;
|
||||||
border-right-color: #68745e;
|
border-right-color: #6A7862;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
switch:disabled,
|
checkbutton:disabled,
|
||||||
entry:disabled,
|
entry:disabled,
|
||||||
spinbutton:disabled,
|
spinbutton:disabled,
|
||||||
combobox:disabled {
|
combobox:disabled {
|
||||||
color: #7f8977;
|
color: #79826F;
|
||||||
background: #363d35;
|
background: #414A38;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
entry:focus-visible,
|
entry:focus-visible,
|
||||||
spinbutton:focus-visible,
|
spinbutton:focus-visible,
|
||||||
combobox:focus-visible,
|
combobox:focus-visible,
|
||||||
switch:focus-visible,
|
checkbutton:focus-visible,
|
||||||
textview:focus-visible {
|
textview:focus-visible {
|
||||||
outline: 2px solid #d5dea7;
|
outline: 1px dotted #C4B550;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-action {
|
.primary-action {
|
||||||
background-image: linear-gradient(to bottom, #899b5f, #637344);
|
color: #C4B550;
|
||||||
color: #ffffff;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
border-top-color: #83917A;
|
||||||
|
border-left-color: #7E8C74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-action:hover {
|
||||||
|
color: #DED08A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-action {
|
.danger-action {
|
||||||
background-image: linear-gradient(to bottom, #9e6556, #71463e);
|
color: #E0A18E;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.danger-action:hover {
|
||||||
|
color: #F0BCAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Typography helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
color: #eef1e8;
|
color: #C4B550;
|
||||||
|
font-size: 8.5pt;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: #adb7a1;
|
color: #8A9384;
|
||||||
font-size: 9.5pt;
|
font-size: 8.5pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-error {
|
.validation-error {
|
||||||
color: #e5a192;
|
color: #D98D7A;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 8.5pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-text {
|
.warning-text {
|
||||||
color: #dfc078;
|
color: #C79C4E;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Statusbar */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
.statusbar {
|
.statusbar {
|
||||||
min-height: 25px;
|
min-height: 23px;
|
||||||
padding: 3px 8px;
|
padding: 3px 6px;
|
||||||
background: #343c32;
|
background: #3E4637;
|
||||||
color: #adb7a1;
|
border-top: 1px solid #242921;
|
||||||
border-top: 1px solid #748069;
|
box-shadow: inset 0 1px #58644E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-segment {
|
||||||
|
min-height: 17px;
|
||||||
|
padding: 0 9px;
|
||||||
|
background: #343B2D;
|
||||||
|
color: #98A28C;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-bottom-color: #58644E;
|
||||||
|
border-right-color: #58644E;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Settings */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
.settings-sidebar {
|
||||||
|
background: #434D3B;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-bottom-color: #58644E;
|
||||||
|
border-right-color: #58644E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar list {
|
||||||
|
background: #434D3B;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-sidebar row {
|
.settings-sidebar row {
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
padding: 7px 10px;
|
min-height: 24px;
|
||||||
border-radius: 1px;
|
padding: 4px 10px;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #B4BEA6;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar row:hover {
|
||||||
|
background: #4E5943;
|
||||||
|
color: #D8DED3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar row:selected {
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel {
|
.settings-panel {
|
||||||
background: #454f41;
|
background: #46513D;
|
||||||
border: 1px solid #1d241e;
|
border: 1px solid #242921;
|
||||||
|
border-bottom-color: #58644E;
|
||||||
|
border-right-color: #58644E;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row {
|
.setting-row {
|
||||||
min-height: 48px;
|
min-height: 36px;
|
||||||
padding: 6px 8px;
|
padding: 4px 8px;
|
||||||
background: #4b5546;
|
background: #4E5A45;
|
||||||
border: 1px solid #252c26;
|
border: 1px solid #3A4233;
|
||||||
|
border-top-color: #5C6950;
|
||||||
|
border-left-color: #5C6950;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Form controls */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
checkbutton check {
|
||||||
|
min-height: 13px;
|
||||||
|
min-width: 13px;
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background-image: none;
|
||||||
|
background: #1E221B;
|
||||||
|
border: 1px solid #191D16;
|
||||||
|
border-bottom-color: #6A7862;
|
||||||
|
border-right-color: #58644E;
|
||||||
|
color: #C4B550;
|
||||||
|
box-shadow: none;
|
||||||
|
-gtk-icon-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbutton check:hover {
|
||||||
|
background: #262B22;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbutton check:checked {
|
||||||
|
background: #1E221B;
|
||||||
|
color: #C4B550;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry,
|
||||||
|
spinbutton {
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
caret-color: #C4B550;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry selection,
|
||||||
|
spinbutton selection {
|
||||||
|
background: #958831;
|
||||||
|
color: #1E221B;
|
||||||
|
}
|
||||||
|
|
||||||
|
spinbutton button {
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 18px;
|
||||||
|
padding: 0 3px;
|
||||||
|
margin: 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #4C5844;
|
||||||
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
}
|
||||||
|
|
||||||
|
spinbutton button:hover {
|
||||||
|
background: #5B684D;
|
||||||
|
}
|
||||||
|
|
||||||
|
combobox button.combo,
|
||||||
|
fontbutton button {
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
combobox arrow {
|
||||||
|
color: #C4B550;
|
||||||
|
min-width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Scrollbars */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
scrollbar {
|
||||||
|
background: #2C3227;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollbar trough {
|
||||||
|
background: #2C3227;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollbar slider {
|
scrollbar slider {
|
||||||
min-width: 12px;
|
min-width: 11px;
|
||||||
min-height: 12px;
|
min-height: 24px;
|
||||||
|
margin: 1px;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
background: #68745e;
|
background: #5C6850;
|
||||||
border: 1px solid #222921;
|
border: 1px solid #242921;
|
||||||
|
border-top-color: #6A7862;
|
||||||
|
border-left-color: #6A7862;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollbar slider:hover {
|
||||||
|
background: #68755A;
|
||||||
|
}
|
||||||
|
|
||||||
|
separator {
|
||||||
|
background: #242921;
|
||||||
|
min-height: 1px;
|
||||||
|
min-width: 1px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ package overlay
|
|||||||
#include <gdk/wayland/gdkwayland.h>
|
#include <gdk/wayland/gdkwayland.h>
|
||||||
#include <X11/Xlib.h>
|
#include <X11/Xlib.h>
|
||||||
#include <X11/Xatom.h>
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xutil.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -188,10 +189,24 @@ static gboolean captioneer_reposition_x11(gpointer data) {
|
|||||||
gdk_monitor_get_geometry(overlay->monitor, &geometry);
|
gdk_monitor_get_geometry(overlay->monitor, &geometry);
|
||||||
int width = gdk_surface_get_width(surface);
|
int width = gdk_surface_get_width(surface);
|
||||||
int height = gdk_surface_get_height(surface);
|
int height = gdk_surface_get_height(surface);
|
||||||
|
if (width <= 1 || height <= 1) {
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
int bottom_margin = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(overlay->window), "bottom-margin"));
|
int bottom_margin = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(overlay->window), "bottom-margin"));
|
||||||
int x = geometry.x + (geometry.width - width) / 2;
|
int x = geometry.x + (geometry.width - width) / 2;
|
||||||
int y = geometry.y + geometry.height - height - bottom_margin;
|
int y = geometry.y + geometry.height - height - bottom_margin;
|
||||||
XMoveWindow(GDK_SURFACE_XDISPLAY(surface), GDK_SURFACE_XID(surface), x, y);
|
Display *display = GDK_SURFACE_XDISPLAY(surface);
|
||||||
|
Window xid = GDK_SURFACE_XID(surface);
|
||||||
|
XSizeHints *size_hints = XAllocSizeHints();
|
||||||
|
if (size_hints != NULL) {
|
||||||
|
size_hints->flags = USPosition | PPosition;
|
||||||
|
size_hints->x = x;
|
||||||
|
size_hints->y = y;
|
||||||
|
XSetWMNormalHints(display, xid, size_hints);
|
||||||
|
XFree(size_hints);
|
||||||
|
}
|
||||||
|
XMoveWindow(display, xid, x, y);
|
||||||
|
XFlush(display);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +342,11 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
gtk_layer_init_for_window(overlay->window);
|
gtk_layer_init_for_window(overlay->window);
|
||||||
gtk_layer_set_namespace(overlay->window, "captioneer");
|
gtk_layer_set_namespace(overlay->window, "captioneer");
|
||||||
gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY);
|
gtk_layer_set_layer(overlay->window, GTK_LAYER_SHELL_LAYER_OVERLAY);
|
||||||
|
// Stretch across the bottom edge so the compositor pins the surface
|
||||||
|
// there (single-edge anchoring can leave it floating mid-screen on
|
||||||
|
// Plasma). The bubble itself stays content-width and centered.
|
||||||
|
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
|
||||||
|
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_RIGHT, TRUE);
|
||||||
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE);
|
gtk_layer_set_anchor(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE);
|
||||||
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin);
|
gtk_layer_set_margin(overlay->window, GTK_LAYER_SHELL_EDGE_BOTTOM, bottom_margin);
|
||||||
gtk_layer_set_exclusive_zone(overlay->window, 0);
|
gtk_layer_set_exclusive_zone(overlay->window, 0);
|
||||||
@@ -363,6 +383,9 @@ static CaptioneerOverlay *captioneer_overlay_new(
|
|||||||
overlay->bubble = box;
|
overlay->bubble = box;
|
||||||
gtk_widget_add_css_class(box, "captioneer-bubble");
|
gtk_widget_add_css_class(box, "captioneer-bubble");
|
||||||
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
|
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
|
||||||
|
gtk_widget_set_valign(box, GTK_ALIGN_END);
|
||||||
|
gtk_widget_set_hexpand(box, FALSE);
|
||||||
|
gtk_widget_set_vexpand(box, FALSE);
|
||||||
gtk_widget_set_size_request(box, effective_width, -1);
|
gtk_widget_set_size_request(box, effective_width, -1);
|
||||||
|
|
||||||
overlay->final_label = gtk_label_new("");
|
overlay->final_label = gtk_label_new("");
|
||||||
|
|||||||
Reference in New Issue
Block a user