- Added recanim, recsound, config load/save and memory snapshot

load/save features.  If config changed in UI, give a temporary
  config file to Hatari whenever it's re-run
- Fullscreen isn't anymore a toggle as user cannot use this
  option if Hatari is fullscreen... Removed fullscreen config
  option handling as it will be changed also directly from Hatari
- To support drag&drop for the UI, it forwards non-option args
  (floppy name) to Hatari from the UI command line
- Disk image and joystick settings to separate dialogs
- Add dummy paths and peripherals settings dialogs
- Check that Hatari supports --control-socket argument
- Make dialog run() methods more consistent
- Python code compatible to Python 2.4 (I'm testing with 2.5
  which accepts '()' for classes not inheriting anything) and
  wrapper scripts to older SH (use exit when outside func body)
This commit is contained in:
Eero Tamminen 2008-07-26 00:18:43 +03:00
parent e83c646bda
commit 5793fdb70b
7 changed files with 467 additions and 246 deletions

View File

@ -63,38 +63,49 @@ def text_to_value(text):
# ------------------------------------------------------
# Handle INI style configuration files as used by Hatari
class ConfigStore():
defaultpath = "%s%c.hatari" % (os.getenv("HOME"), os.path.sep)
class ConfigStore:
defaultpath = "%s%c.hatari" % (os.path.expanduser("~"), os.path.sep)
def __init__(self, cfgfile, defaults = {}, miss_is_error = True):
"ConfigStore(cfgfile[,defaults,miss_is_error])"
self.changed = False
self.defaults = defaults
self.miss_is_error = miss_is_error
path = self.get_path(cfgfile)
if path:
self.sections = self.load(path)
if self.sections:
print "Loaded configuration file:", path
else:
print "WARNING: configuration file '%' loading failed" % path
path = None
else:
print "WARNING: configuration file '%s' missing, using defaults" % cfgfile
self.sections = defaults
self.original = self.get_checkpoint()
path = self._get_fullpath(cfgfile)
self.cfgfile = cfgfile
self.load(path)
def load(self, path):
"load(path), load given configuration file"
if path:
sections = self._read(path)
if sections:
print "Loaded configuration file:", path
self.cfgfile = os.path.basename(path)
self.sections = sections
else:
print "ERROR: configuration file '%' loading failed" % path
return
else:
print "WARNING: configuration file missing, using defaults"
self.sections = self.defaults
self.path = path
self.original = self.get_checkpoint()
self.changed = False
def is_loaded(self):
"is_loaded() -> True if configuration loading succeeded"
if self.sections:
return True
return False
def get_path(self, cfgfile):
"get_path(cfgfile) -> path or None, check first CWD & then HOME for cfgfile"
def get_path(self):
"get_path() -> configuration file path"
return self.path
def _get_fullpath(self, cfgfile):
"get_fullpath(cfgfile) -> path or None, check first CWD & then HOME for cfgfile"
# hatari.cfg can be in home or current work dir
for path in (os.getcwd(), os.getenv("HOME")):
for path in (os.getcwd(), os.path.expanduser("~")):
if path:
path = self._check_path(path, cfgfile)
if path:
@ -114,8 +125,8 @@ class ConfigStore():
return testpath
return None
def load(self, path):
"load(path) -> (all keys, section2key mappings)"
def _read(self, path):
"_read(path) -> (all keys, section2key mappings)"
config = open(path, "r")
if not config:
return ({}, {})
@ -211,19 +222,42 @@ class ConfigStore():
fileobj.write("\n")
def save(self):
"save(), if configuration changed, save it"
"save() -> path, if configuration changed, save it"
if not self.changed:
print "No configuration changes to save, skipping"
return
return None
if not self.path:
print "WARNING: no existing configuration file, trying to create one"
if not os.path.exists(self.defaultpath):
os.mkdir(self.defaultpath)
self.path = "%s%c%s" % (self.defaultpath, os.path.sep, self.cfgfile)
#fileobj = sys.stdout
fileobj = open(self.path, "w")
if fileobj:
self.write(fileobj)
print "Saved configuration file:", self.path
else:
if not fileobj:
print "ERROR: opening '%s' for saving failed" % self.path
return None
self.write(fileobj)
print "Saved configuration file:", self.path
self.changed = False
return path
def save_as(self, path):
"save_as(path) -> path, save configuration to given file and select it"
assert(path)
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
self.path = path
self.changed = True
self.save()
return path
def save_tmp(self, path):
"save_tmp(path) -> path, save configuration to given file without selecting it"
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
fileobj = open(path, "w")
if not fileobj:
print "ERROR: opening '%s' for saving failed" % path
return None
self.write(fileobj)
print "Saved temporary configuration file:", path
return path

View File

@ -23,7 +23,8 @@ import pango
from config import ConfigStore
from uihelpers import UInfo, create_button, create_toggle, \
create_table_dialog, table_add_entry_row, table_add_widget_row
create_table_dialog, table_add_entry_row, table_add_widget_row, \
get_save_filename
from dialogs import TodoDialog, ErrorDialog, AskDialog, KillDialog
@ -34,7 +35,7 @@ def dialog_apply_cb(widget, dialog):
# -------------
# Table dialogs
class SaveDialog():
class SaveDialog:
def __init__(self, parent):
entry = gtk.Entry()
entry.set_width_chars(12)
@ -51,14 +52,11 @@ class SaveDialog():
self.address.connect("activate", dialog_apply_cb, self.dialog)
self.length = table_add_entry_row(table, 2, "Number of bytes:", 6)
self.length.connect("activate", dialog_apply_cb, self.dialog)
def _select_file_cb(self, widget):
buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
fsel = gtk.FileChooserDialog("Select save file", self.dialog, gtk.FILE_CHOOSER_ACTION_SAVE, buttons)
fsel.set_local_only(True)
if fsel.run() == gtk.RESPONSE_OK:
self.file.set_text(fsel.get_filename())
fsel.destroy()
filename = get_save_filename("Select save file", self.dialog)
if filename:
self.file.set_text(filename)
def run(self, address):
"run(address) -> (filename,address,length), all as strings"
@ -96,7 +94,7 @@ class SaveDialog():
return (filename, address, length)
class LoadDialog():
class LoadDialog:
def __init__(self, parent):
chooser = gtk.FileChooserButton('Select a File')
chooser.set_local_only(True) # Hatari cannot access URIs
@ -132,7 +130,7 @@ class LoadDialog():
return (filename, address)
class OptionsDialog():
class OptionsDialog:
def __init__(self, parent):
table, self.dialog = create_table_dialog(parent, "Debugger UI options", 1)
self.lines = table_add_entry_row(table, 0, "Memdump/disasm lines:", 2)
@ -178,7 +176,7 @@ class Constants:
# class for the memory address entry, view (label) and
# the logic for memory dump modes and moving in memory
class MemoryAddress():
class MemoryAddress:
# class variables
debug_output = None
hatari = None
@ -346,7 +344,7 @@ class MemoryAddress():
# the Hatari debugger UI class and methods
class HatariDebugUI():
class HatariDebugUI:
def __init__(self, hatariobj, do_destroy = False):
self.address = MemoryAddress(hatariobj)

View File

@ -29,7 +29,7 @@ from uihelpers import UInfo, HatariTextInsert, create_table_dialog, \
# -----------------
# Dialog base class
class HatariUIDialog():
class HatariUIDialog:
def __init__(self, parent):
"<any>Dialog(parent) -> object"
self.parent = parent
@ -182,14 +182,14 @@ class QuitSaveDialog(HatariUIDialog):
self.dialog = dialog
def run(self, config):
"run(config) -> RESPONSE_CANCEL if dialog is canceled"
"run(config) -> False if canceled, True otherwise or if no changes"
changes = []
for key, value in config.get_changes():
changes.append("%s = %s" % (key, str(value)))
if not changes:
return gtk.RESPONSE_NO
return True
child = self.viewport.get_child()
child.set_text("\n".join(changes))
child.set_text(config.get_path() + ":\n" + "\n".join(changes))
width, height = child.get_size_request()
if height < 320:
self.scrolledwindow.set_size_request(width, height)
@ -199,9 +199,11 @@ class QuitSaveDialog(HatariUIDialog):
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_CANCEL:
return False
if response == gtk.RESPONSE_YES:
config.save()
return response
return True
# ---------------------------
@ -218,16 +220,16 @@ Hatari emulator is already/still running and it needs to be terminated first. Ho
Terminate Hatari anyway?""")
def run(self, hatari):
"run(hatari) -> False if Hatari killed, True if left running"
"run(hatari) -> True if Hatari killed, False if left running"
if not hatari.is_running():
return False
return True
# Hatari is running, OK to kill?
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_OK:
hatari.kill()
return False
return True
return True
return False
# ---------------------------
@ -260,9 +262,58 @@ class ResetDialog(HatariUIDialog):
return False
return True
# ----------------------------------
# Disk image dialog
class DiskDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Disk image settings", 9)
row = 0
self.floppy = []
path = config.get_floppydir()
for drive in ("A", "B"):
label = "Disk %c:" % drive
fsel = gtk.FileChooserButton(label)
# Hatari cannot access URIs
fsel.set_local_only(True)
fsel.set_width_chars(12)
filename = config.get_floppy(row)
if filename:
fsel.set_filename(filename)
elif path:
fsel.set_current_folder(path)
self.floppy.append(fsel)
eject = create_button("Eject", self._eject, fsel)
box = gtk.HBox()
box.pack_start(fsel)
box.pack_start(eject, False, False)
table_add_widget_row(table, row, label, box)
row += 1
table.show_all()
def _eject(self, widget, fsel):
fsel.unselect_all()
def run(self, config):
"run(config), show disk image dialog"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_APPLY:
config.lock_updates()
for drive in range(2):
config.set_floppy(drive, self.floppy[drive].get_filename())
config.flush_updates()
# ---------------------------
# Hatari screen dialog
# Display dialog
class DisplayDialog(HatariUIDialog):
@ -325,8 +376,83 @@ class DisplayDialog(HatariUIDialog):
config.flush_updates()
# ----------------------------------
# Joystick dialog
class JoystickDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Joystick settings", 9)
joy = 0
self.joy = []
joytypes = config.get_joystick_types()
for label in config.get_joystick_names():
combo = gtk.combo_box_new_text()
for text in joytypes:
combo.append_text(text)
combo.set_active(config.get_joystick(joy))
widget = table_add_widget_row(table, joy, "%s:" % label, combo)
self.joy.append(widget)
joy += 1
table.show_all()
def run(self, config):
"run(config), show joystick dialog"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_APPLY:
config.lock_updates()
for joy in range(6):
config.set_joystick(joy, self.joy[joy].get_active())
config.flush_updates()
# ---------------------------------------
# Peripherals (midi,printer,rs232) dialog
class PeripheralDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Peripheral settings", 9)
print "TODO: get midi/printer/rs232 stuff"
table.show_all()
def run(self, config):
"run(config), show peripherals dialog"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_APPLY:
print "TODO: set midi/printer/rs232 stuff"
# ---------------------------------------
# Path dialog
class PathDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Path settings", 2)
print "TODO: get path stuff"
table.show_all()
def run(self, config):
"run(config), show paths dialog"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_APPLY:
print "TODO: set path stuff"
# ---------------------------
# Hatari sound dialog
# Sound dialog
class SoundDialog(HatariUIDialog):
@ -349,7 +475,7 @@ class SoundDialog(HatariUIDialog):
self.dialog = dialog
def run(self, config):
"run(config), show display dialog"
"run(config), show sound dialog"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
@ -465,77 +591,10 @@ class TraceDialog(HatariUIDialog):
return self.savedpoints
# ----------------------------------
# Peripherals (disk/joystick) dialog
class PeripheralsDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Peripheral settings", 9)
row = 0
self.floppy = []
path = config.get_floppydir()
for drive in ("A", "B"):
label = "Disk %c:" % drive
fsel = gtk.FileChooserButton(label)
# Hatari cannot access URIs
fsel.set_local_only(True)
fsel.set_width_chars(12)
filename = config.get_floppy(row)
if filename:
fsel.set_filename(filename)
elif path:
fsel.set_current_folder(path)
self.floppy.append(fsel)
eject = create_button("Eject", self._eject, fsel)
box = gtk.HBox()
box.pack_start(fsel)
box.pack_start(eject, False, False)
table_add_widget_row(table, row, label, box)
row += 1
table_add_separator(table, row)
row += 1
joy = 0
self.joy = []
joytypes = config.get_joystick_types()
for label in config.get_joystick_names():
combo = gtk.combo_box_new_text()
for text in joytypes:
combo.append_text(text)
combo.set_active(config.get_joystick(joy))
widget = table_add_widget_row(table, row + joy, "%s:" % label, combo)
self.joy.append(widget)
joy += 1
# TODO: add printer, serial, midi, RTC to peripherals?
table.show_all()
def _eject(self, widget, fsel):
fsel.unselect_all()
def run(self, config):
"run() -> file name, file name for given disk"
if not self.dialog:
self._create_dialog(config)
response = self.dialog.run()
self.dialog.hide()
if response == gtk.RESPONSE_APPLY:
config.lock_updates()
for drive in range(2):
config.set_floppy(drive, self.floppy[drive].get_filename())
for joy in range(6):
config.set_joystick(joy, self.joy[joy].get_active())
config.flush_updates()
# ----------------------------------------
# Setup dialog for settings needing reboot
# Machine dialog for settings needing reboot
class SetupDialog(HatariUIDialog):
class MachineDialog(HatariUIDialog):
def __init__(self, parent):
self.setups = []
self.parent = parent
@ -547,7 +606,7 @@ class SetupDialog(HatariUIDialog):
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
("Set and reboot", gtk.RESPONSE_APPLY,
gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
self.editdialog = EditSetupDialog(dialog)
self.editdialog = EditMachineDialog(dialog)
box1 = gtk.HBox()
box1.add(create_button("Add", self._add_setup, config))
@ -641,7 +700,7 @@ class SetupDialog(HatariUIDialog):
self.label.set_text("\n".join(info))
def run(self, config):
"run() -> bool, whether to reboot"
"run(config) -> bool, whether to reboot"
if not self.dialog:
self._create_dialog(config)
@ -655,7 +714,7 @@ class SetupDialog(HatariUIDialog):
# ----------------------------------------------
# Dialog for adding/editing setup configurations
class EditSetupDialog(HatariUIDialog):
class EditMachineDialog(HatariUIDialog):
def _create_dialog(self, config):
table, self.dialog = create_table_dialog(self.parent, "Add/edit setup", 9)

View File

@ -25,7 +25,7 @@ from config import ConfigStore
# Running Hatari instance
class Hatari():
class Hatari:
"running hatari instance and methods for communicating with it"
basepath = "/tmp/hatari-ui-" + str(os.getpid())
logpath = basepath + ".log"
@ -184,10 +184,10 @@ class Hatari():
self._set_embed_env(env, parent_win)
# callers need to take care of confirming quitting
args = [self.hataribin, "--confirm-quit", "off"]
if extra_args:
args += extra_args
if self.server:
args += ["--control-socket", self.controlpath]
if extra_args:
args += extra_args
print "RUN:", args
os.execvpe(self.hataribin, args, env)
@ -436,17 +436,6 @@ class HatariConfigMapping(ConfigStore):
self.set("[Screen]", "nSpec512Threshold", value)
self._change_option("--spec512 %d" % value)
# ------------ show fullscreen ---------------
def get_fullscreen(self):
return self.get("[Screen]", "bFullScreen")
def set_fullscreen(self, value):
self.set("[Screen]", "bFullScreen", value)
if value:
self._change_option("--fullscreen")
else:
self._change_option("--window")
# ------------ show borders ---------------
def get_borders(self):
return self.get("[Screen]", "bAllowOverscan")

View File

@ -4,14 +4,15 @@ path=${0%/*}
name=${0##*/}
# example setup for Hatari UI
$path/$name.py --right "about,|,run,pause,|,reset,debug,|,quit" --embed
return
$path/$name.py --right "about,|,run,pause,forward,|,reset,|,quit" --embed $*
exit 0
# test setup without embedding, dupplicate toggles
$path/$name.py --top "about,run,pause,quit" \
--panel "Testpanel,pause,>,close" \
--bottom "sound,|,pause,|,fast,|,Testpanel"
return
--bottom "sound,|,pause,|,fast,|,Testpanel" \
$*
exit 0
# test setup with embedding and all available controls
$path/$name.py --embed \
@ -20,5 +21,6 @@ $path/$name.py --embed \
--panel "Keys,F1=59,F2=60,F3=61,F4=62,F5=63,F6=64,F7=65,F8=66,F9=67,F10=68,>,Macro=Test,Undo=97,Help=98,Enter=114,>,close" \
--panel "Misc,fast,|,full,|,sound,>,shot,>,close" \
--bottom "fast,full,Misc,Keys,input,devices,display,debug,trace" \
--right "fast,full,Misc,Keys,input,devices,display,Help=98"
return
--right "fast,full,Misc,Keys,input,devices,display,Help=98" \
$*
exit 0

View File

@ -29,10 +29,11 @@ import gobject
from debugui import HatariDebugUI
from hatari import Hatari, HatariConfigMapping
from uihelpers import UInfo, create_button, create_toolbutton, create_toggle, \
HatariTextInsert
from dialogs import AboutDialog, InputDialog, KillDialog, QuitSaveDialog, \
ResetDialog, SetupDialog, TraceDialog, PeripheralsDialog, ErrorDialog, \
DisplayDialog, SoundDialog
HatariTextInsert, get_open_filename, get_save_filename
from dialogs import AboutDialog, TodoDialog, ErrorDialog, InputDialog, \
KillDialog, QuitSaveDialog, ResetDialog, TraceDialog, DiskDialog, \
DisplayDialog, JoystickDialog, MachineDialog, PeripheralDialog, \
PathDialog, SoundDialog
# helper functions to match callback args
@ -45,7 +46,8 @@ def window_hide_cb(window, arg):
# Class with Hatari and configuration instances which methods are
# called to change those (with additional dialogs or directly).
# Owns the application window and socket widget embedding Hatari.
class UICallbacks():
class UICallbacks:
tmpconfpath = os.path.expanduser("~/.hatari/.tmp.cfg")
def __init__(self):
# Hatari and configuration
self.hatari = Hatari()
@ -59,47 +61,59 @@ class UICallbacks():
self.hatariwin = None
self.debugui = None
self.panels = {}
# dialogs are created when needed
self.aboutdialog = None
self.resetdialog = None
self.inputdialog = None
self.devicesdialog = None
self.displaydialog = None
self.sounddialog = None
self.pastedialog = None
self.quitdialog = None
self.setupdialog = None
self.tracedialog = None
self.resetdialog = None
self.quitdialog = None
self.killdialog = None
# TODO: Hatari UI configuration settings save/load
self.tracepoints = None
self.diskdialog = None
self.displaydialog = None
self.joystickdialog = None
self.machinedialog = None
self.peripheraldialog = None
self.sounddialog = None
self.pathdialog = None
# used by run()
self.memstate = None
self.floppy = None
self.io_id = None
# TODO: Hatari UI own configuration settings save/load
self.tracepoints = None
def _reset_config_dialogs(self):
self.diskdialog = None
self.displaydialog = None
self.joystickdialog = None
self.machinedialog = None
self.peripheraldialog = None
self.sounddialog = None
self.pathdialog = None
# ---------- create UI ----------------
def create_ui(self, accelgroup, menu, toolbars, fullscreen, embed):
"create_ui(menu, toolbars, fullscreen, embed)"
# add horizontal elements
hbox = gtk.HBox()
if toolbars["left"]:
#hbox.add(toolbars["left"])
hbox.pack_start(toolbars["left"], False, True)
if embed:
self._add_uisocket(hbox)
if toolbars["right"]:
#hbox.add(toolbars["right"])
hbox.pack_start(toolbars["right"], False, True)
# add vertical elements
vbox = gtk.VBox()
if menu:
vbox.add(menu)
if toolbars["top"]:
#vbox.add(toolbars["top"])
vbox.pack_start(toolbars["top"], False, True)
vbox.add(hbox)
if toolbars["bottom"]:
#vbox.add(toolbars["bottom"])
vbox.pack_start(toolbars["bottom"], False, True)
# put them to main window
mainwin = gtk.Window(gtk.WINDOW_TOPLEVEL)
@ -145,15 +159,25 @@ class UICallbacks():
# force also mainwin smaller (it automatically grows)
self.mainwin.resize(width, height)
return True
def run(self, widget=None):
if self.killdialog.run(self.hatari):
def run(self, widget = None):
if not self.killdialog.run(self.hatari):
return
if self.io_id:
gobject.source_remove(self.io_id)
args = ["--configfile"]
# whether to use Hatari config or unsaved Hatari UI config?
if self.config.is_changed():
args += [self.config.save_tmp(self.tmpconfpath)]
else:
args += [self.config.get_path()]
if self.memstate:
args += self.memstate
if self.floppy:
args += self.floppy
if self.hatariwin:
size = self.hatariwin.window.get_size()
self.hatari.run(None, self.hatariwin.window)
self.hatari.run(args, self.hatariwin.window)
# get notifications of Hatari window size changes
self.hatari.enable_embed_info()
socket = self.hatari.get_control_socket().fileno()
@ -162,7 +186,42 @@ class UICallbacks():
# all keyboard events should go to Hatari window
self.hatariwin.grab_focus()
else:
self.hatari.run()
self.hatari.run(args)
def set_floppy(self, floppy):
self.floppy = floppy
# ------- quit callback -----------
def quit(self, widget, arg = None):
# due to Gtk API, needs to return True when *not* quitting
if not self.killdialog.run(self.hatari):
return True
if self.io_id:
gobject.source_remove(self.io_id)
if self.config.is_changed():
if not self.quitdialog:
self.quitdialog = QuitSaveDialog(self.mainwin)
if not self.quitdialog.run(self.config):
return True
gtk.main_quit()
if os.path.exists(self.tmpconfpath):
os.unlink(self.tmpconfpath)
# continue to mainwin destroy if called by delete_event
return False
# ------- pause callback -----------
def pause(self, widget):
if widget.get_active():
self.hatari.pause()
else:
self.hatari.unpause()
# dialogs
# ------- reset callback -----------
def reset(self, widget):
if not self.resetdialog:
self.resetdialog = ResetDialog(self.mainwin)
self.resetdialog.run(self.hatari)
# ------- about callback -----------
def about(self, widget):
@ -176,46 +235,11 @@ class UICallbacks():
self.inputdialog = InputDialog(self.mainwin)
self.inputdialog.run(self.hatari)
# ------- pause callback -----------
def pause(self, widget):
if widget.get_active():
self.hatari.pause()
else:
self.hatari.unpause()
# ------- reset callback -----------
def reset(self, widget):
if not self.resetdialog:
self.resetdialog = ResetDialog(self.mainwin)
self.resetdialog.run(self.hatari)
# ------- setup callback -----------
def setup(self, widget):
if not self.setupdialog:
self.setupdialog = SetupDialog(self.mainwin)
if self.setupdialog.run(self.config):
self.hatari.trigger_shortcut("warmreset")
# ------- quit callback -----------
def quit(self, widget, arg = None):
if self.killdialog.run(self.hatari):
return True
if self.io_id:
gobject.source_remove(self.io_id)
if self.config.is_changed():
if not self.quitdialog:
self.quitdialog = QuitSaveDialog(self.mainwin)
if self.quitdialog.run(self.config) == gtk.RESPONSE_CANCEL:
return True
gtk.main_quit()
# continue to mainwin destroy if called by delete_event
return False
# ------- devices callback -----------
def devices(self, widget):
if not self.devicesdialog:
self.devicesdialog = PeripheralsDialog(self.mainwin)
self.devicesdialog.run(self.config)
# ------- disk callback -----------
def disk(self, widget):
if not self.diskdialog:
self.diskdialog = DiskDialog(self.mainwin)
self.diskdialog.run(self.config)
# ------- display callback -----------
def display(self, widget):
@ -223,6 +247,37 @@ class UICallbacks():
self.displaydialog = DisplayDialog(self.mainwin)
self.displaydialog.run(self.config)
# ------- joystick callback -----------
def joystick(self, widget):
if not self.joystickdialog:
self.joystickdialog = JoystickDialog(self.mainwin)
self.joystickdialog.run(self.config)
# ------- machine callback -----------
def machine(self, widget):
if not self.machinedialog:
self.machinedialog = MachineDialog(self.mainwin)
if self.machinedialog.run(self.config):
self.hatari.trigger_shortcut("coldreset")
# ------- peripheral callback -----------
def peripheral(self, widget):
if not self.peripheraldialog:
self.peripheraldialog = PeripheralDialog(self.mainwin)
self.peripheraldialog.run(self.config)
# ------- sound callback -----------
def sound(self, widget):
if not self.sounddialog:
self.sounddialog = SoundDialog(self.mainwin)
self.sounddialog.run(self.config)
# ------- path callback -----------
def path(self, widget):
if not self.pathdialog:
self.pathdialog = PathDialog(self.mainwin)
self.pathdialog.run(self.config)
# ------- debug callback -----------
def debugger(self, widget):
if not self.debugui:
@ -235,6 +290,38 @@ class UICallbacks():
self.tracedialog = TraceDialog(self.mainwin)
self.tracepoints = self.tracedialog.run(self.hatari, self.tracepoints)
# ------ snapshot load/save callbacks ---------
def load(self, widget):
path = os.path.expanduser("~/.hatari/hatari.sav")
filename = get_open_filename("Select snapshot", self.mainwin, path)
if filename:
self.memstate = ["--memstate", filename]
self.run()
return True
return False
def save(self, widget):
self.hatari.trigger_shortcut("savemem")
# ------ config load/save callbacks ---------
def config_load(self, widget):
path = self.config.get_path()
filename = get_open_filename("Select configuration file", self.mainwin, path)
if filename:
self.hatari.change_option("--configfile %s" % filename)
self.config.load(filename)
self._reset_config_dialogs()
return True
return False
def config_save(self, widget):
path = self.config.get_path()
filename = get_save_filename("Save configuration as...", self.mainwin, path)
if filename:
self.config.save_as(filename)
return True
return False
# ------- fast forward callback -----------
def set_fastforward(self, widget):
self.config.set_fastforward(widget.get_active())
@ -244,22 +331,20 @@ class UICallbacks():
# ------- fullscreen callback -----------
def set_fullscreen(self, widget):
self.config.set_fullscreen(widget.get_active())
def get_fullscreen(self):
return self.config.get_fullscreen()
# ------- sound callback -----------
def sound(self, widget):
if not self.sounddialog:
self.sounddialog = SoundDialog(self.mainwin)
self.sounddialog.run(self.config)
# if user can select this, Hatari isn't in fullscreen
self.hatari.change_option("--fullscreen")
# ------- screenshot callback -----------
def screenshot(self, widget):
print "TODO: Support converting screenshot to PNG and giving its name?"
self.hatari.trigger_shortcut("screenshot")
# ------- record callbacks -----------
def recanim(self, widget):
self.hatari.trigger_shortcut("recanim")
def recsound(self, widget):
self.hatari.trigger_shortcut("recsound")
# ------- insert key special callback -----------
def keypress(self, widget, code):
self.hatari.insert_event("keypress %s" % code)
@ -286,39 +371,51 @@ class UICallbacks():
window.deiconify()
# ---------------------------------------------------------------
# class for creating menus, toolbars and panels
# and managing actions bound to them
class UIActions:
def __init__(self):
cb = self.callbacks = UICallbacks()
self.actions = gtk.ActionGroup("All")
# name, icon ID, label, accel, tooltip, callback
self.actions.add_toggle_actions((
("pause", gtk.STOCK_MEDIA_PAUSE, "Pause", "<Ctrl>P", "Pause Hatari to save battery", cb.pause),
# TODO: how to know when these are changed from inside Hatari?
("fast", gtk.STOCK_MEDIA_FORWARD, "FastForward", "<Ctrl>F", "Whether to fast forward Hatari (needs fast machine)", cb.set_fastforward, cb.get_fastforward()),
("full", gtk.STOCK_FULLSCREEN, "Fullscreen", "<Ctrl>U", "Toggle whether Hatari is fullscreen", cb.set_fullscreen, cb.get_fullscreen())
("recanim", gtk.STOCK_MEDIA_RECORD, "Record animation", "<Ctrl>A", "Record animation", cb.recanim),
("recsound", gtk.STOCK_MEDIA_RECORD, "Record sound", "<Ctrl>W", "Record YM/Wav", cb.recsound),
("pause", gtk.STOCK_MEDIA_PAUSE, "Pause", "<Ctrl>P", "Pause Hatari to save battery", cb.pause),
("forward", gtk.STOCK_MEDIA_FORWARD, "Forward", "<Ctrl>F", "Whether to fast forward Hatari (needs fast machine)", cb.set_fastforward, cb.get_fastforward())
))
# name, icon ID, label, accel, tooltip, callback
self.actions.add_actions((
("about", gtk.STOCK_INFO, "About", "<Ctrl>A", "Hatari UI information", cb.about),
("shot", gtk.STOCK_MEDIA_RECORD, "Screenshot", "<Ctrl>C", "Take a screenshot", cb.screenshot),
("quit", gtk.STOCK_QUIT, "Quit", None, "Quit Hatari UI", cb.quit),
("load", gtk.STOCK_OPEN, "Load snapshot...", "<Ctrl>L", "Load emulation snapshot", cb.load),
("save", gtk.STOCK_SAVE, "Save snapshot", "<Ctrl>S", "Save emulation snapshot", cb.save),
("shot", gtk.STOCK_MEDIA_RECORD, "Grab screenshot", "<Ctrl>G", "Grab a screenshot", cb.screenshot),
("quit", gtk.STOCK_QUIT, "Quit", "<Ctrl>Q", "Quit Hatari UI", cb.quit),
("run", gtk.STOCK_MEDIA_PLAY, "Run", "<Ctrl>R", "(Re-)run Hatari", cb.run),
("full", gtk.STOCK_FULLSCREEN, "Fullscreen", "<Ctrl>U", "Toggle whether Hatari is fullscreen", cb.set_fullscreen),
("input", gtk.STOCK_SPELL_CHECK, "Inputs...", "<Ctrl>N", "Simulate text input and mouse clicks", cb.inputs),
("reset", gtk.STOCK_REFRESH, "Reset...", "<Ctrl>E", "Warm or cold reset Hatari", cb.reset),
("sound", gtk.STOCK_YES, "Sound...", "<Ctrl>S", "Sound settings", cb.sound),
("display", gtk.STOCK_PROPERTIES, "Display...", "<Ctrl>D", "Display settings", cb.display),
("devices", gtk.STOCK_FLOPPY, "Peripherals...", "<Ctrl>H", "Floppy and joystick settings", cb.devices),
("machine", gtk.STOCK_PREFERENCES, "Machine...", "<Ctrl>M", "Hatari st/e/tt/falcon configuration", cb.setup),
("disk", gtk.STOCK_FLOPPY, "Disks...", "<Ctrl>D", "Floppy settings", cb.disk),
("display", gtk.STOCK_PREFERENCES, "Display...", "<Ctrl>Y", "Display settings", cb.display),
("joystick", gtk.STOCK_CONNECT, "Joysticks...", "<Ctrl>J", "Joystick settings", cb.joystick),
("machine", gtk.STOCK_HARDDISK, "Machine...", "<Ctrl>M", "Hatari st/e/tt/falcon configuration", cb.machine),
("device", gtk.STOCK_PRINT, "Peripherals...", "<Ctrl>V", "Toggle Midi, Printer, RS232 peripherals", cb.peripheral),
("sound", gtk.STOCK_PROPERTIES, "Sound...", "<Ctrl>O", "Sound settings", cb.sound),
("path", gtk.STOCK_DIRECTORY, "Paths...", None, "Device & save file paths", cb.path),
("lconfig", gtk.STOCK_OPEN, "Load config...", None, "Load configuration", self.config_load),
("sconfig", gtk.STOCK_SAVE_AS, "Save config as...", None, "Save configuration", cb.config_save),
("debug", gtk.STOCK_FIND, "Debugger...", "<Ctrl>B", "Activate Hatari debugger", cb.debugger),
("trace", gtk.STOCK_EXECUTE, "Trace settings...", "<Ctrl>T", "Hatari tracing setup", cb.trace)
("trace", gtk.STOCK_EXECUTE, "Trace settings...", "<Ctrl>T", "Hatari tracing setup", cb.trace),
("about", gtk.STOCK_INFO, "About", "<Ctrl>I", "Hatari UI information", cb.about)
))
self.action_names = [x.get_name() for x in self.actions.list_actions()]
@ -326,6 +423,11 @@ class UIActions:
self.toolbars = {}
self.panels = []
def config_load(self, widget):
# user loads a new configuration?
if self.callbacks.config_load(widget):
print "TODO: reset toggle actions"
# ----- toolbar / panel additions ---------
def set_actions(self, action_str, place):
"set_actions(actions,place) -> error string, None if all OK"
@ -402,7 +504,6 @@ class UIActions:
"Simulate Atari key press/release and string inserting"
if not textcode:
return (None, None)
# TODO: icon for the key button
widget = gtk.ToolButton(gtk.STOCK_PASTE)
widget.set_label(name)
try:
@ -480,10 +581,12 @@ class UIActions:
def _get_menu(self):
allmenus = (
("File", ("about", None, "shot", None, "quit")),
("Emulation", ("run", "pause", None, "fast", None, "full", None, "input", None, "reset")),
("Setup", ("sound", "display", "devices", "machine")),
("Debug", ("debug", "trace"))
("File", ("load", "save", None, "shot", "recanim", "recsound", None, "quit")),
("Emulation", ("run", "pause", "forward", None, "full", None, "input", None, "reset")),
("Devices", ("disk", "display", "joystick", "machine", "device", "sound")),
("Configuration", ("path", None, "lconfig", "sconfig")),
("Debug", ("debug", "trace")),
("Help", ("about",))
)
bar = gtk.MenuBar()
@ -496,7 +599,7 @@ class UIActions:
return bar
# ------------- run the whole UI -------------
def run(self, havemenu, fullscreen, embed):
def run(self, floppy, havemenu, fullscreen, embed):
accelgroup = None
# create menu?
if havemenu:
@ -519,6 +622,7 @@ class UIActions:
toolbars[side] = self._get_container(self.toolbars[side], True)
self.callbacks.create_ui(accelgroup, menu, toolbars, fullscreen, embed)
self.callbacks.set_floppy(floppy)
# ugly, Hatari socket window ID can be gotten only
# after Socket window is realized by gtk_main()
@ -532,7 +636,7 @@ def usage(actions, msg=None):
uiname = "%s %s" % (UInfo.name, UInfo.version)
print "\n%s" % uiname
print "=" * len(uiname)
print "\nUsage: %s [options]" % name
print "\nUsage: %s [options] [floppy image]" % name
print "\nOptions:"
print "\t-h, --help\t\tthis help"
print "\t-n, --nomenu\t\tomit menus"
@ -562,7 +666,7 @@ For example:
\t%s --embed \\
\t-t "about,run,pause,quit" \\
\t-p "MyPanel,Macro=Test,Undo=97,Help=98,>,F1=59,F2=60,F3=61,F4=62,>,close" \\
\t-r "paste,debug,trace,setup,MyPanel" \\
\t-r "paste,debug,trace,machine,MyPanel" \\
\t-b "sound,|,fastforward,|,fullscreen"
if no options are given, the UI uses basic controls.
@ -578,12 +682,10 @@ def main():
try:
longopts = ["embed", "fullscreen", "nomenu", "help",
"left=", "right=", "top=", "bottom=", "panel="]
opts, args = getopt.getopt(sys.argv[1:], "efnhl:r:t:b:p:", longopts)
opts, floppies = getopt.getopt(sys.argv[1:], "efnhl:r:t:b:p:", longopts)
del longopts
except getopt.GetoptError, err:
usage(actions, err)
if args:
usage(actions, "arguments given although only options accepted")
menu = True
embed = False
@ -615,7 +717,13 @@ def main():
if error:
usage(actions, error)
actions.run(menu, fullscreen, embed)
if len(floppies) > 1:
usage(actions, "multiple floppy images given: %s" % str(floppies))
if floppies:
if not os.path.exists(floppies[0]):
usage(actions, "floppy image '%s' doesn't exist" % floppies[0])
actions.run(floppies, menu, fullscreen, embed)
if __name__ == "__main__":

View File

@ -39,7 +39,7 @@ class UInfo:
logo = "hatari.png"
icon = "hatari-icon.png"
# path to the directory where the called script resides
path = sys.argv[0][:sys.argv[0].rfind(os.path.sep)]
path = os.path.dirname(sys.argv[0])
def __init__(self, path = None):
"UIinfo([path]), set suitable paths for resources from CWD and path"
@ -60,7 +60,7 @@ class UInfo:
# --------------------------------------------------------
# auxiliary class+callback to be used with the PasteDialog
class HatariTextInsert():
class HatariTextInsert:
def __init__(self, hatari, text):
self.index = 0
self.text = text
@ -167,3 +167,34 @@ def table_add_separator(table, row):
"table_add_separator(table,row)"
widget = gtk.HSeparator()
table.attach(widget, 0, 2, row, row+1, gtk.FILL)
# -----------------------------
# File selection helpers
def get_open_filename(title, parent, path = None):
buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_OPEN, buttons)
fsel.set_local_only(True)
if path:
fsel.set_filename(path)
if fsel.run() == gtk.RESPONSE_OK:
filename = fsel.get_filename()
else:
filename = None
fsel.destroy()
return filename
def get_save_filename(title, parent, path = None):
buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_SAVE, buttons)
fsel.set_local_only(True)
fsel.set_do_overwrite_confirmation(True)
if path:
fsel.set_filename(path)
if fsel.run() == gtk.RESPONSE_OK:
filename = fsel.get_filename()
else:
filename = None
fsel.destroy()
return filename