mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
d32262adff
debugger no longer sends play, pause notifications to the gui. the gui polls for that information as required govern package now has SubState type to complement the State type. StateIntegrity() function enforces combinations of State and SubState, called from debugger.setState() function playmode notifications reworked and contained in a single playmode_overlay.go file. this includes the FPS and screen detail preference value sdlimgui.playmode.fpsOverlay replaced with sdlimgui.playmode.fpsDetail. still toggled with F7 key coproc icon moved to top-left corner of playmode overlay and only visible when FPS detail is showing when FPS detail is showing multiple (small) icons care shown. when it is not showing, a single (large) icon is shown according to the priority of the icon. eg. pause indicator has higher priority than the mute indicator
504 lines
13 KiB
Go
504 lines
13 KiB
Go
// This file is part of Gopher2600.
|
|
//
|
|
// Gopher2600 is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Gopher2600 is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package sdlimgui
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jetsetilly/gopher2600/debugger"
|
|
"github.com/jetsetilly/gopher2600/debugger/govern"
|
|
"github.com/jetsetilly/gopher2600/debugger/terminal"
|
|
"github.com/jetsetilly/gopher2600/gui/crt"
|
|
"github.com/jetsetilly/gopher2600/gui/sdlaudio"
|
|
"github.com/jetsetilly/gopher2600/gui/sdlimgui/caching"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
"github.com/jetsetilly/gopher2600/prefs"
|
|
"github.com/jetsetilly/gopher2600/reflection"
|
|
"github.com/jetsetilly/gopher2600/resources"
|
|
"github.com/jetsetilly/gopher2600/userinput"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
|
|
"github.com/inkyblackness/imgui-go/v4"
|
|
)
|
|
|
|
// imguiIniFile is where imgui will store the coordinates of the imgui windows
|
|
const imguiIniFile = "debugger_imgui.ini"
|
|
|
|
// the number of frames to count before resetting fonts
|
|
const resetFontFrames = 2
|
|
|
|
// the amount to fade a widget by when disabled
|
|
const disabledAlpha = 0.3
|
|
|
|
// the phantom input system allows windows to obey key presses without any
|
|
// specific widget being active
|
|
type phantomInput int
|
|
|
|
// list of valid phantomInput values
|
|
const (
|
|
phantomInputNone phantomInput = iota
|
|
phantomInputRune
|
|
phantomInputBackSpace
|
|
)
|
|
|
|
// SdlImgui is an sdl based visualiser using imgui.
|
|
type SdlImgui struct {
|
|
// the mechanical requirements for the gui
|
|
plt *platform
|
|
rnd renderer
|
|
fonts fontAtlas
|
|
|
|
// resetFonts value will be reduced to 0 each GUI frame. at value 1 the
|
|
// fonts will be reset.
|
|
//
|
|
// set to 1 to reset immediately and higher values to delay the reset by
|
|
// that number of frames. higher values are useful in the prefs window to
|
|
// allow sufficient time to close the font size selector combo box
|
|
resetFonts int
|
|
|
|
// the current mode of the emulation
|
|
mode atomic.Value // govern.Mode
|
|
|
|
// references to parent emulation
|
|
dbg *debugger.Debugger
|
|
|
|
// cached values from the emulation
|
|
cache caching.Cache
|
|
|
|
// terminal interface to the debugger. this is distinct from the
|
|
// winTerminal type.
|
|
term *term
|
|
|
|
// implementations of television protocols, PixelRenderer and AudioMixer
|
|
screen *screen
|
|
audio *sdlaudio.Audio
|
|
|
|
// the playscreen is drawn to the background of the platform window
|
|
playScr *playScr
|
|
|
|
// imgui window management
|
|
wm *manager
|
|
|
|
// if true then a tooltip has been requested. reset after menu is drawn
|
|
tooltipIndicator bool
|
|
|
|
// tooltips will show regardless of show tooltip preferences
|
|
tooltipForce bool
|
|
|
|
// the colors used by the imgui system. includes the TV colors converted to
|
|
// a suitable format
|
|
cols *imguiColors
|
|
|
|
// polling encapsulates the programmatic communication to the service loop.
|
|
// how the feature requests, pushed functions etc. are handled by the
|
|
// service loop is important to the GUI's responsiveness.
|
|
polling *polling
|
|
|
|
// the phantom input system allows windows to obey key presses without any
|
|
// specific widget being active
|
|
phantomInput phantomInput
|
|
phantomInputRune rune
|
|
|
|
// the time the window was focused. we use this to prevent the TAB key from
|
|
// opening the ROM requester if the window was focused with the alt-TAB key
|
|
// combo used by Windows and many Linux desktops
|
|
windowFocusedTime time.Time
|
|
|
|
// gui specific preferences. crt preferences are handled separately. all
|
|
// other preferences are handled by the emulation
|
|
prefs *preferences
|
|
crtPrefs *crt.Preferences
|
|
|
|
// modal window
|
|
modal modal
|
|
|
|
// functions that should only be run after gui rendering
|
|
postRenderFunctions chan func()
|
|
}
|
|
|
|
// NewSdlImgui is the preferred method of initialisation for type SdlImgui
|
|
//
|
|
// MUST ONLY be called from the gui thread.
|
|
func NewSdlImgui(dbg *debugger.Debugger) (*SdlImgui, error) {
|
|
img := &SdlImgui{
|
|
dbg: dbg,
|
|
postRenderFunctions: make(chan func(), 100),
|
|
cache: caching.NewCache(),
|
|
}
|
|
|
|
// create imgui context
|
|
imgui.CreateContext(nil)
|
|
|
|
// mode is in the none state at the beginning
|
|
img.mode.Store(govern.ModeNone)
|
|
|
|
// path to dear imgui ini file
|
|
iniPath, err := resources.JoinPath(imguiIniFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
imgui.CurrentIO().SetIniFilename(iniPath)
|
|
|
|
// define colors
|
|
img.cols = newColors()
|
|
|
|
// new renderer
|
|
img.rnd = newRenderer(img)
|
|
|
|
// create platform
|
|
img.plt, err = newPlatform(img)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
// initialise preferences after platform initialisation because there are
|
|
// preference hooks that reference platform fields
|
|
img.prefs, err = newPreferences(img)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
// start renderer after platform and preferences
|
|
err = img.rnd.start()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
img.screen = newScreen(img)
|
|
img.term = newTerm()
|
|
|
|
img.playScr = newPlayScr(img)
|
|
|
|
img.wm, err = newManager(img)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
err = img.wm.loadManagerState()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
err = img.wm.loadManagerHotkeys()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
// initialise new polling type
|
|
img.polling = newPolling(img)
|
|
|
|
// connect pixel renderer/referesher to television and texture renderer to
|
|
// pixel renderer
|
|
img.dbg.VCS().TV.AddPixelRenderer(img.screen)
|
|
|
|
// texture renderers are added depending on playmode
|
|
|
|
// this audio mixer produces the sound. there is another AudioMixer
|
|
// implementation in winAudio which visualises the sound
|
|
img.audio, err = sdlaudio.NewAudio()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
img.dbg.VCS().TV.AddRealtimeAudioMixer(img.audio)
|
|
|
|
// initialise crt preferences
|
|
img.crtPrefs, err = crt.NewPreferences()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sdlimgui: %w", err)
|
|
}
|
|
|
|
// set scaling when IntegerScaling prefs value is changed
|
|
img.crtPrefs.IntegerScaling.SetHookPost(func(v prefs.Value) error {
|
|
select {
|
|
case img.postRenderFunctions <- func() {
|
|
img.screen.crit.section.Lock()
|
|
img.playScr.setScaling()
|
|
img.screen.crit.section.Unlock()
|
|
}:
|
|
default:
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// set event filter for SDL see comment for serviceWindowEvent()
|
|
sdl.AddEventWatchFunc(img.serviceWindowEvent, nil)
|
|
|
|
// reset fonts as soon as possible
|
|
img.resetFonts = 1
|
|
|
|
// container window is open on setEmulationMode()
|
|
|
|
// load preferences
|
|
err = img.prefs.load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
// Destroy implements GuiCreator interface
|
|
//
|
|
// MUST ONLY be called from the gui thread.
|
|
func (img *SdlImgui) Destroy() {
|
|
err := img.prefs.saveOnExitDsk.Save()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
|
|
err = img.audio.EndMixing()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
|
|
err = img.wm.saveManagerState()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
|
|
err = img.wm.saveManagerHotkeys()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
|
|
err = img.plt.destroy()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
|
|
img.rnd.destroy()
|
|
|
|
ctx, err := imgui.CurrentContext()
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
ctx.Destroy()
|
|
}
|
|
|
|
// GetTerminal implements terminal.Broker interface.
|
|
func (img *SdlImgui) GetTerminal() terminal.Terminal {
|
|
return img.term
|
|
}
|
|
|
|
// GetReflectionRenderer implements reflection.Broker interface.
|
|
func (img *SdlImgui) GetReflectionRenderer() reflection.Renderer {
|
|
return img.screen
|
|
}
|
|
|
|
// quit application sends a request to the emulation.
|
|
func (img *SdlImgui) quit() {
|
|
select {
|
|
case img.dbg.UserInput() <- userinput.EventQuit{}:
|
|
default:
|
|
logger.Log("sdlimgui", "dropped quit event")
|
|
}
|
|
}
|
|
|
|
// end program. this differs from quit in that this function is called when we
|
|
// receive a ReqEnd, which *may* have been sent in reponse to a userinput.EventQuit
|
|
func (img *SdlImgui) end() {
|
|
img.prefs.saveWindowPreferences()
|
|
}
|
|
|
|
// draw gui. called from service loop.
|
|
func (img *SdlImgui) draw() {
|
|
mode := img.mode.Load().(govern.Mode)
|
|
|
|
if mode == govern.ModeNone {
|
|
return
|
|
}
|
|
|
|
if img.dbg.State() == govern.EmulatorStart {
|
|
return
|
|
}
|
|
|
|
imgui.PushFont(img.fonts.gui)
|
|
defer imgui.PopFont()
|
|
|
|
if mode == govern.ModePlay {
|
|
img.playScr.draw()
|
|
}
|
|
|
|
img.wm.draw()
|
|
img.modalDraw()
|
|
}
|
|
|
|
// is the gui in playmode or not. thread safe. called from emulation thread
|
|
// and gui thread.
|
|
func (img *SdlImgui) isPlaymode() bool {
|
|
return img.mode.Load().(govern.Mode) == govern.ModePlay
|
|
}
|
|
|
|
// set emulation and handle the changeover gracefully. this includes the saving
|
|
// and loading of preference groups.
|
|
//
|
|
// should only be called from gui thread.
|
|
func (img *SdlImgui) setEmulationMode(mode govern.Mode) error {
|
|
// release captured mouse before switching emulation modes. if we don't do
|
|
// this then the capture state will remain if we flip back to the emulation
|
|
// mode later. at this point the captured mouse can cause confusion
|
|
img.setCapture(false)
|
|
|
|
img.mode.Store(mode)
|
|
img.prefs.loadWindowPreferences()
|
|
|
|
switch mode {
|
|
case govern.ModeDebugger:
|
|
img.screen.clearTextureRenderers()
|
|
img.screen.addTextureRenderer(img.wm.dbgScr)
|
|
img.plt.window.Show()
|
|
img.plt.window.Raise()
|
|
|
|
case govern.ModePlay:
|
|
img.screen.clearTextureRenderers()
|
|
img.screen.addTextureRenderer(img.playScr)
|
|
img.plt.window.Show()
|
|
img.plt.window.Raise()
|
|
}
|
|
|
|
img.applyAudioMutePreference()
|
|
|
|
// small delay before calling smartHideCursor(). this is because SDL will
|
|
// detect a mouse motion on startup or when the window changes between
|
|
// emulation modes. rather than dealing with thresholds in the event
|
|
// handler, it's easy and cleaner to set a short delay
|
|
time.AfterFunc(250*time.Millisecond, func() {
|
|
img.smartCursorVisibility(true)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (img *SdlImgui) toggleAudioMute() {
|
|
if img.isPlaymode() {
|
|
m := img.prefs.audioMutePlaymode.Get().(bool)
|
|
img.prefs.audioMutePlaymode.Set(!m)
|
|
} else {
|
|
m := img.prefs.audioMuteDebugger.Get().(bool)
|
|
img.prefs.audioMuteDebugger.Set(!m)
|
|
}
|
|
|
|
// the act of setting the prefs value means that setAudioMute() is called
|
|
// indirectly, so there's no need to call it here
|
|
}
|
|
|
|
// apply the preference for audio mute according to the current playmode
|
|
func (img *SdlImgui) applyAudioMutePreference() {
|
|
// if there is no prefs instance then return without error. this can happen
|
|
// when the prefs are being loaded from disk for the first time and the
|
|
// prefs instance hasn't been returned
|
|
if img.prefs == nil {
|
|
return
|
|
}
|
|
|
|
var mute bool
|
|
|
|
if img.isPlaymode() {
|
|
mute = img.prefs.audioMutePlaymode.Get().(bool)
|
|
img.dbg.VCS().RIOT.Ports.MutePeripherals(mute)
|
|
} else {
|
|
mute = img.prefs.audioMuteDebugger.Get().(bool)
|
|
img.dbg.VCS().RIOT.Ports.MutePeripherals(mute)
|
|
}
|
|
|
|
img.audio.Mute(mute)
|
|
}
|
|
|
|
// has mouse been grabbed. only called from gui thread.
|
|
func (img *SdlImgui) isCaptured() bool {
|
|
if img.isPlaymode() {
|
|
return img.playScr.isCaptured
|
|
}
|
|
return img.wm.dbgScr.isCaptured
|
|
}
|
|
|
|
// grab mouse. only called from gui thread.
|
|
func (img *SdlImgui) setCapture(set bool) {
|
|
if img.isPlaymode() {
|
|
img.playScr.isCaptured = set
|
|
} else {
|
|
if set {
|
|
img.wm.dbgScr.debuggerSetOpen(true)
|
|
img.wm.dbgScr.debuggerGeometry().raise = true
|
|
}
|
|
img.wm.dbgScr.isCaptured = set
|
|
}
|
|
|
|
img.plt.setCapture(set)
|
|
}
|
|
|
|
// "captured running" is when the emulation is running inside the debugger and
|
|
// the input has been captured
|
|
//
|
|
// always returns false if the emulation is in playmode
|
|
func (img *SdlImgui) isCapturedRunning() bool {
|
|
if img.isPlaymode() {
|
|
return false
|
|
}
|
|
return img.wm.dbgScr.isCaptured && img.dbg.State() == govern.Running
|
|
}
|
|
|
|
// set "captured running". does nothing if the emulation is in playmode
|
|
func (img *SdlImgui) setCapturedRunning(set bool) {
|
|
if img.isPlaymode() {
|
|
return
|
|
}
|
|
|
|
if set {
|
|
img.setCapture(true)
|
|
img.term.pushCommand("RUN")
|
|
} else {
|
|
img.setCapture(false)
|
|
img.term.pushCommand("HALT")
|
|
if img.wm.refocusWindow != nil {
|
|
geom := img.wm.refocusWindow.debuggerGeometry()
|
|
geom.raise = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// set visiblity of cursor
|
|
func (img *SdlImgui) cursorVisibility(hidden bool) {
|
|
if hidden {
|
|
_, err := sdl.ShowCursor(sdl.DISABLE)
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
} else {
|
|
_, err := sdl.ShowCursor(sdl.ENABLE)
|
|
if err != nil {
|
|
logger.Log("sdlimgui", err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// set the visibility of the cursor based on playmode and capture state
|
|
func (img *SdlImgui) smartCursorVisibility(hidden bool) {
|
|
if img.isPlaymode() && !img.isCaptured() {
|
|
img.cursorVisibility(hidden)
|
|
} else {
|
|
img.cursorVisibility(false)
|
|
}
|
|
}
|
|
|
|
func (img *SdlImgui) setReasonableWindowConstraints() {
|
|
x := img.plt.displaySize()[0] * 0.95
|
|
y := img.plt.displaySize()[1] * 0.95
|
|
imgui.SetNextWindowSizeConstraints(imgui.Vec2{300, 300}, imgui.Vec2{X: x, Y: y})
|
|
}
|