Annotation of hatari/python-ui/hatariui.py, revision 1.1.1.8

1.1.1.8 ! root        1: #!/usr/bin/env python
1.1       root        2: #
1.1.1.8 ! root        3: # A Python Gtk UI for Hatari that can embed the Hatari emulator window.
1.1       root        4: #
1.1.1.8 ! root        5: # Requires Gtk 3.x and Python GObject Introspection libraries.
1.1       root        6: #
1.1.1.8 ! root        7: # Copyright (C) 2008-2019 by Eero Tamminen
1.1       root        8: #
                      9: # This program is free software; you can redistribute it and/or modify
                     10: # it under the terms of the GNU General Public License as published by
                     11: # the Free Software Foundation; either version 2 of the License, or
                     12: # (at your option) any later version.
                     13: #
                     14: # This program is distributed in the hope that it will be useful,
                     15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
                     16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
                     17: # GNU General Public License for more details.
                     18: 
                     19: import os
                     20: import sys
                     21: import getopt
                     22: 
1.1.1.8 ! root       23: import gi
        !            24: # use correct version of gtk
        !            25: gi.require_version('Gtk', '3.0')
        !            26: from gi.repository import Gtk
        !            27: from gi.repository import Gdk
        !            28: from gi.repository import GObject
1.1       root       29: 
                     30: from debugui import HatariDebugUI
                     31: from hatari import Hatari, HatariConfigMapping
                     32: from uihelpers import UInfo, UIHelp, create_button, create_toolbutton, \
                     33:      create_toggle, HatariTextInsert, get_open_filename, get_save_filename
1.1.1.2   root       34: from dialogs import AboutDialog, TodoDialog, NoteDialog, ErrorDialog, \
                     35:      InputDialog, KillDialog, QuitSaveDialog, ResetDialog, TraceDialog, \
                     36:      FloppyDialog, HardDiskDialog, DisplayDialog, JoystickDialog, \
                     37:      MachineDialog, PeripheralDialog, PathDialog, SoundDialog
1.1       root       38: 
                     39: 
                     40: # helper functions to match callback args
                     41: def window_hide_cb(window, arg):
                     42:     window.hide()
                     43:     return True
                     44: 
                     45: 
                     46: # ---------------------------------------------------------------
                     47: # Class with Hatari and configuration instances which methods are
                     48: # called to change those (with additional dialogs or directly).
                     49: # Owns the application window and socket widget embedding Hatari.
                     50: class UICallbacks:
                     51:     tmpconfpath = os.path.expanduser("~/.hatari/.tmp.cfg")
                     52:     def __init__(self):
                     53:         # Hatari and configuration
                     54:         self.hatari = Hatari()
1.1.1.2   root       55:         error = self.hatari.is_compatible()
                     56:         if error:
                     57:             ErrorDialog(None).run(error)
                     58:             sys.exit(-1)
1.1.1.8 ! root       59: 
1.1       root       60:         self.config = HatariConfigMapping(self.hatari)
1.1.1.2   root       61:         try:
                     62:             self.config.validate()
                     63:         except (KeyError, AttributeError):
                     64:             NoteDialog(None).run("Loading Hatari configuration failed!\nRetrying after saving Hatari configuration.")
                     65:             self.hatari.save_config()
                     66:             self.config = HatariConfigMapping(self.hatari)
                     67:             self.config.validate()
1.1.1.8 ! root       68: 
1.1       root       69:         # windows are created when needed
                     70:         self.mainwin = None
                     71:         self.hatariwin = None
                     72:         self.debugui = None
                     73:         self.panels = {}
                     74: 
                     75:         # dialogs are created when needed
                     76:         self.aboutdialog = None
                     77:         self.inputdialog = None
                     78:         self.tracedialog = None
                     79:         self.resetdialog = None
                     80:         self.quitdialog = None
                     81:         self.killdialog = None
                     82: 
1.1.1.2   root       83:         self.floppydialog = None
                     84:         self.harddiskdialog = None
1.1       root       85:         self.displaydialog = None
                     86:         self.joystickdialog = None
                     87:         self.machinedialog = None
                     88:         self.peripheraldialog = None
                     89:         self.sounddialog = None
                     90:         self.pathdialog = None
1.1.1.8 ! root       91: 
1.1       root       92:         # used by run()
                     93:         self.memstate = None
                     94:         self.floppy = None
                     95:         self.io_id = None
                     96: 
                     97:         # TODO: Hatari UI own configuration settings save/load
                     98:         self.tracepoints = None
                     99: 
                    100:     def _reset_config_dialogs(self):
1.1.1.2   root      101:         self.floppydialog = None
                    102:         self.harddiskdialog = None
1.1       root      103:         self.displaydialog = None
                    104:         self.joystickdialog = None
                    105:         self.machinedialog = None
                    106:         self.peripheraldialog = None
                    107:         self.sounddialog = None
                    108:         self.pathdialog = None
1.1.1.8 ! root      109: 
1.1       root      110:     # ---------- create UI ----------------
                    111:     def create_ui(self, accelgroup, menu, toolbars, fullscreen, embed):
                    112:         "create_ui(menu, toolbars, fullscreen, embed)"
                    113:         # add horizontal elements
1.1.1.8 ! root      114:         hbox = Gtk.HBox()
1.1       root      115:         if toolbars["left"]:
1.1.1.8 ! root      116:             hbox.pack_start(toolbars["left"], False, True, 0)
1.1       root      117:         if embed:
                    118:             self._add_uisocket(hbox)
                    119:         if toolbars["right"]:
1.1.1.8 ! root      120:             hbox.pack_start(toolbars["right"], False, True, 0)
1.1       root      121:         # add vertical elements
1.1.1.8 ! root      122:         vbox = Gtk.VBox()
1.1       root      123:         if menu:
                    124:             vbox.add(menu)
                    125:         if toolbars["top"]:
1.1.1.8 ! root      126:             vbox.pack_start(toolbars["top"], False, True, 0)
1.1       root      127:         vbox.add(hbox)
                    128:         if toolbars["bottom"]:
1.1.1.8 ! root      129:             vbox.pack_start(toolbars["bottom"], False, True, 0)
1.1       root      130:         # put them to main window
1.1.1.8 ! root      131:         mainwin = Gtk.Window(Gtk.WindowType.TOPLEVEL)
1.1       root      132:         mainwin.set_title("%s %s" % (UInfo.name, UInfo.version))
                    133:         mainwin.set_icon_from_file(UInfo.icon)
                    134:         if accelgroup:
                    135:             mainwin.add_accel_group(accelgroup)
                    136:         if fullscreen:
                    137:             mainwin.fullscreen()
                    138:         mainwin.add(vbox)
                    139:         mainwin.show_all()
                    140:         # for run and quit callbacks
                    141:         self.killdialog = KillDialog(mainwin)
                    142:         mainwin.connect("delete_event", self.quit)
                    143:         self.mainwin = mainwin
1.1.1.8 ! root      144: 
1.1       root      145:     def _add_uisocket(self, box):
                    146:         # add Hatari parent container to given box
1.1.1.8 ! root      147:         socket = Gtk.Socket(can_focus=True)
1.1       root      148:         # without this, closing Hatari would remove the socket widget
                    149:         socket.connect("plug-removed", lambda obj: True)
1.1.1.8 ! root      150:         socket.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("black"))
        !           151:         socket.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
1.1.1.4   root      152:         # set max Hatari window size = desktop size
1.1.1.8 ! root      153:         self.config.set_desktop_size(Gdk.Screen.width(), Gdk.Screen.height())
1.1       root      154:         # set initial embedded hatari size
                    155:         width, height = self.config.get_window_size()
                    156:         socket.set_size_request(width, height)
                    157:         # no resizing for the Hatari window
1.1.1.8 ! root      158:         box.pack_start(socket, False, False, 0)
1.1       root      159:         self.hatariwin = socket
                    160: 
                    161:     # ------- run callback -----------
                    162:     def _socket_cb(self, fd, event):
1.1.1.8 ! root      163:         if event != GObject.IO_IN:
1.1       root      164:             # hatari process died, make sure Hatari instance notices
                    165:             self.hatari.kill()
                    166:             return False
                    167:         width, height = self.hatari.get_embed_info()
1.1.1.3   root      168:         print("New size = %d x %d" % (width, height))
1.1       root      169:         oldwidth, oldheight = self.hatariwin.get_size_request()
                    170:         self.hatariwin.set_size_request(width, height)
                    171:         if width < oldwidth or height < oldheight:
                    172:             # force also mainwin smaller (it automatically grows)
                    173:             self.mainwin.resize(width, height)
                    174:         return True
1.1.1.8 ! root      175: 
1.1       root      176:     def run(self, widget = None):
                    177:         if not self.killdialog.run(self.hatari):
                    178:             return
                    179:         if self.io_id:
1.1.1.8 ! root      180:             GObject.source_remove(self.io_id)
1.1       root      181:         args = ["--configfile"]
                    182:         # whether to use Hatari config or unsaved Hatari UI config?
                    183:         if self.config.is_changed():
                    184:             args += [self.config.save_tmp(self.tmpconfpath)]
                    185:         else:
                    186:             args += [self.config.get_path()]
                    187:         if self.memstate:
                    188:             args += self.memstate
1.1.1.2   root      189:         # only way to change boot order is to specify disk on command line
1.1       root      190:         if self.floppy:
                    191:             args += self.floppy
                    192:         if self.hatariwin:
1.1.1.7   root      193:             self.hatari.run(args, self.hatariwin.get_id())
1.1       root      194:             # get notifications of Hatari window size changes
                    195:             self.hatari.enable_embed_info()
                    196:             socket = self.hatari.get_control_socket().fileno()
1.1.1.8 ! root      197:             events = GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR
        !           198:             self.io_id = GObject.io_add_watch(socket, events, self._socket_cb)
1.1       root      199:             # all keyboard events should go to Hatari window
                    200:             self.hatariwin.grab_focus()
                    201:         else:
                    202:             self.hatari.run(args)
                    203: 
                    204:     def set_floppy(self, floppy):
                    205:         self.floppy = floppy
                    206: 
                    207:     # ------- quit callback -----------
                    208:     def quit(self, widget, arg = None):
                    209:         # due to Gtk API, needs to return True when *not* quitting
                    210:         if not self.killdialog.run(self.hatari):
                    211:             return True
                    212:         if self.io_id:
1.1.1.8 ! root      213:             GObject.source_remove(self.io_id)
1.1       root      214:         if self.config.is_changed():
                    215:             if not self.quitdialog:
                    216:                 self.quitdialog = QuitSaveDialog(self.mainwin)
                    217:             if not self.quitdialog.run(self.config):
                    218:                 return True
1.1.1.8 ! root      219:         Gtk.main_quit()
1.1       root      220:         if os.path.exists(self.tmpconfpath):
                    221:             os.unlink(self.tmpconfpath)
                    222:         # continue to mainwin destroy if called by delete_event
                    223:         return False
                    224: 
                    225:     # ------- pause callback -----------
                    226:     def pause(self, widget):
                    227:         if widget.get_active():
                    228:             self.hatari.pause()
                    229:         else:
                    230:             self.hatari.unpause()
                    231: 
                    232:     # dialogs
                    233:     # ------- reset callback -----------
                    234:     def reset(self, widget):
                    235:         if not self.resetdialog:
                    236:             self.resetdialog = ResetDialog(self.mainwin)
                    237:         self.resetdialog.run(self.hatari)
                    238: 
                    239:     # ------- about callback -----------
                    240:     def about(self, widget):
                    241:         if not self.aboutdialog:
                    242:             self.aboutdialog = AboutDialog(self.mainwin)
                    243:         self.aboutdialog.run()
                    244: 
                    245:     # ------- input callback -----------
                    246:     def inputs(self, widget):
                    247:         if not self.inputdialog:
                    248:             self.inputdialog = InputDialog(self.mainwin)
                    249:         self.inputdialog.run(self.hatari)
                    250: 
1.1.1.2   root      251:     # ------- floppydisk callback -----------
                    252:     def floppydisk(self, widget):
                    253:         if not self.floppydialog:
                    254:             self.floppydialog = FloppyDialog(self.mainwin)
                    255:         self.floppydialog.run(self.config)
                    256: 
                    257:     # ------- harddisk callback -----------
                    258:     def harddisk(self, widget):
                    259:         if not self.harddiskdialog:
                    260:             self.harddiskdialog = HardDiskDialog(self.mainwin)
                    261:         self.harddiskdialog.run(self.config)
1.1       root      262: 
                    263:     # ------- display callback -----------
                    264:     def display(self, widget):
                    265:         if not self.displaydialog:
                    266:             self.displaydialog = DisplayDialog(self.mainwin)
                    267:         self.displaydialog.run(self.config)
                    268: 
                    269:     # ------- joystick callback -----------
                    270:     def joystick(self, widget):
                    271:         if not self.joystickdialog:
                    272:             self.joystickdialog = JoystickDialog(self.mainwin)
                    273:         self.joystickdialog.run(self.config)
                    274: 
                    275:     # ------- machine callback -----------
                    276:     def machine(self, widget):
                    277:         if not self.machinedialog:
                    278:             self.machinedialog = MachineDialog(self.mainwin)
                    279:         if self.machinedialog.run(self.config):
                    280:             self.hatari.trigger_shortcut("coldreset")
                    281: 
                    282:     # ------- peripheral callback -----------
                    283:     def peripheral(self, widget):
                    284:         if not self.peripheraldialog:
                    285:             self.peripheraldialog = PeripheralDialog(self.mainwin)
                    286:         self.peripheraldialog.run(self.config)
                    287: 
                    288:     # ------- sound callback -----------
                    289:     def sound(self, widget):
                    290:         if not self.sounddialog:
                    291:             self.sounddialog = SoundDialog(self.mainwin)
                    292:         self.sounddialog.run(self.config)
                    293: 
                    294:     # ------- path callback -----------
                    295:     def path(self, widget):
                    296:         if not self.pathdialog:
                    297:             self.pathdialog = PathDialog(self.mainwin)
                    298:         self.pathdialog.run(self.config)
                    299: 
                    300:     # ------- debug callback -----------
                    301:     def debugger(self, widget):
                    302:         if not self.debugui:
                    303:             self.debugui = HatariDebugUI(self.hatari)
                    304:         self.debugui.show()
                    305: 
                    306:     # ------- trace callback -----------
                    307:     def trace(self, widget):
                    308:         if not self.tracedialog:
                    309:             self.tracedialog = TraceDialog(self.mainwin)
                    310:         self.tracepoints = self.tracedialog.run(self.hatari, self.tracepoints)
                    311: 
                    312:     # ------ snapshot load/save callbacks ---------
                    313:     def load(self, widget):
                    314:         path = os.path.expanduser("~/.hatari/hatari.sav")
                    315:         filename = get_open_filename("Select snapshot", self.mainwin, path)
                    316:         if filename:
                    317:             self.memstate = ["--memstate", filename]
                    318:             self.run()
                    319:             return True
                    320:         return False
                    321: 
                    322:     def save(self, widget):
                    323:         self.hatari.trigger_shortcut("savemem")
                    324: 
                    325:     # ------ config load/save callbacks ---------
                    326:     def config_load(self, widget):
                    327:         path = self.config.get_path()
                    328:         filename = get_open_filename("Select configuration file", self.mainwin, path)
                    329:         if filename:
                    330:             self.hatari.change_option("--configfile %s" % filename)
                    331:             self.config.load(filename)
                    332:             self._reset_config_dialogs()
                    333:             return True
                    334:         return False
                    335: 
                    336:     def config_save(self, widget):
                    337:         path = self.config.get_path()
                    338:         filename = get_save_filename("Save configuration as...", self.mainwin, path)
                    339:         if filename:
                    340:             self.config.save_as(filename)
                    341:             return True
                    342:         return False
                    343: 
                    344:     # ------- fast forward callback -----------
                    345:     def set_fastforward(self, widget):
                    346:         self.config.set_fastforward(widget.get_active())
                    347: 
                    348:     def get_fastforward(self):
                    349:         return self.config.get_fastforward()
                    350: 
                    351:     # ------- fullscreen callback -----------
                    352:     def set_fullscreen(self, widget):
                    353:         # if user can select this, Hatari isn't in fullscreen
                    354:         self.hatari.change_option("--fullscreen")
                    355: 
                    356:     # ------- screenshot callback -----------
                    357:     def screenshot(self, widget):
                    358:         self.hatari.trigger_shortcut("screenshot")
                    359: 
                    360:     # ------- record callbacks -----------
                    361:     def recanim(self, widget):
                    362:         self.hatari.trigger_shortcut("recanim")
                    363: 
                    364:     def recsound(self, widget):
                    365:         self.hatari.trigger_shortcut("recsound")
                    366: 
                    367:     # ------- insert key special callback -----------
                    368:     def keypress(self, widget, code):
                    369:         self.hatari.insert_event("keypress %s" % code)
                    370: 
                    371:     def textinsert(self, widget, text):
                    372:         HatariTextInsert(self.hatari, text)
                    373: 
                    374:     # ------- panel callback -----------
                    375:     def panel(self, action, box):
                    376:         title = action.get_name()
                    377:         if title not in self.panels:
1.1.1.8 ! root      378:             window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
1.1       root      379:             window.set_transient_for(self.mainwin)
                    380:             window.set_icon_from_file(UInfo.icon)
                    381:             window.set_title(title)
                    382:             window.add(box)
1.1.1.8 ! root      383:             window.set_type_hint(Gdk.WindowTypeHint.DIALOG)
1.1       root      384:             window.connect("delete_event", window_hide_cb)
                    385:             self.panels[title] = window
                    386:         else:
                    387:             window = self.panels[title]
                    388:         window.show_all()
                    389:         window.deiconify()
                    390: 
                    391: 
                    392: # ---------------------------------------------------------------
                    393: # class for creating menus, toolbars and panels
                    394: # and managing actions bound to them
                    395: class UIActions:
                    396:     def __init__(self):
                    397:         cb = self.callbacks = UICallbacks()
                    398: 
                    399:         self.help = UIHelp()
1.1.1.8 ! root      400: 
        !           401:         self.actions = Gtk.ActionGroup("All")
        !           402: 
1.1       root      403:         # name, icon ID, label, accel, tooltip, callback
                    404:         self.actions.add_toggle_actions((
                    405:         # TODO: how to know when these are changed from inside Hatari?
1.1.1.8 ! root      406:         ("recanim", Gtk.STOCK_MEDIA_RECORD, "Record animation", "<Ctrl>A", "Record animation", cb.recanim),
        !           407:         ("recsound", Gtk.STOCK_MEDIA_RECORD, "Record sound", "<Ctrl>W", "Record YM/Wav", cb.recsound),
        !           408:         ("pause", Gtk.STOCK_MEDIA_PAUSE, "Pause", "<Ctrl>P", "Pause Hatari to save battery", cb.pause),
        !           409:         ("forward", Gtk.STOCK_MEDIA_FORWARD, "Forward", "<Ctrl>F", "Whether to fast forward Hatari (needs fast machine)", cb.set_fastforward, cb.get_fastforward())
1.1       root      410:         ))
1.1.1.8 ! root      411: 
1.1       root      412:         # name, icon ID, label, accel, tooltip, callback
                    413:         self.actions.add_actions((
1.1.1.8 ! root      414:         ("load", Gtk.STOCK_OPEN, "Load snapshot...", "<Ctrl>L", "Load emulation snapshot", cb.load),
        !           415:         ("save", Gtk.STOCK_SAVE, "Save snapshot", "<Ctrl>S", "Save emulation snapshot", cb.save),
        !           416:         ("shot", Gtk.STOCK_MEDIA_RECORD, "Grab screenshot", "<Ctrl>G", "Grab a screenshot", cb.screenshot),
        !           417:         ("quit", Gtk.STOCK_QUIT, "Quit", "<Ctrl>Q", "Quit Hatari UI", cb.quit),
        !           418: 
        !           419:         ("run", Gtk.STOCK_MEDIA_PLAY, "Run", "<Ctrl>R", "(Re-)run Hatari", cb.run),
        !           420:         ("full", Gtk.STOCK_FULLSCREEN, "Fullscreen", "<Ctrl>U", "Toggle whether Hatari is fullscreen", cb.set_fullscreen),
        !           421:         ("input", Gtk.STOCK_SPELL_CHECK, "Inputs...", "<Ctrl>N", "Simulate text input and mouse clicks", cb.inputs),
        !           422:         ("reset", Gtk.STOCK_REFRESH, "Reset...", "<Ctrl>E", "Warm or cold reset Hatari", cb.reset),
        !           423: 
        !           424:         ("display", Gtk.STOCK_PREFERENCES, "Display...", "<Ctrl>Y", "Display settings", cb.display),
        !           425:         ("floppy", Gtk.STOCK_FLOPPY, "Floppies...", "<Ctrl>D", "Floppy images", cb.floppydisk),
        !           426:         ("harddisk", Gtk.STOCK_HARDDISK, "Hard disks...", "<Ctrl>H", "Hard disk images and directories", cb.harddisk),
        !           427:         ("joystick", Gtk.STOCK_CONNECT, "Joysticks...", "<Ctrl>J", "Joystick settings", cb.joystick),
        !           428:         ("machine", Gtk.STOCK_HARDDISK, "Machine...", "<Ctrl>M", "Hatari st/e/tt/falcon configuration", cb.machine),
        !           429:         ("device", Gtk.STOCK_PRINT, "Peripherals...", "<Ctrl>V", "Toggle Midi, Printer, RS232 peripherals", cb.peripheral),
        !           430:         ("sound", Gtk.STOCK_PROPERTIES, "Sound...", "<Ctrl>O", "Sound settings", cb.sound),
        !           431: 
        !           432:         ("path", Gtk.STOCK_DIRECTORY, "Paths...", None, "Device & save file paths", cb.path),
        !           433:         ("lconfig", Gtk.STOCK_OPEN, "Load config...", "<Ctrl>C", "Load configuration", self.config_load),
        !           434:         ("sconfig", Gtk.STOCK_SAVE_AS, "Save config as...", None, "Save configuration", cb.config_save),
        !           435: 
        !           436:         ("debug", Gtk.STOCK_FIND, "Debugger...", "<Ctrl>B", "Activate Hatari debugger", cb.debugger),
        !           437:         ("trace", Gtk.STOCK_EXECUTE, "Trace settings...", "<Ctrl>T", "Hatari tracing setup", cb.trace),
        !           438: 
1.1       root      439:         ("manual", None, "Hatari manual", None, None, self.help.view_hatari_manual),
                    440:         ("compatibility", None, "Hatari compatibility list", None, None, self.help.view_hatari_compatibility),
                    441:         ("release", None, "Hatari release notes", None, None, self.help.view_hatari_releasenotes),
                    442:         ("todo", None, "Hatari TODO", None, None, self.help.view_hatari_todo),
                    443:         ("mails", None, "Hatari mailing lists", None, None, self.help.view_hatari_mails),
                    444:         ("changes", None, "Latest Hatari changes", None, None, self.help.view_hatari_repository),
                    445:         ("authors", None, "Hatari authors", None, None, self.help.view_hatari_authors),
                    446:         ("hatari", None, "Hatari home page", None, None, self.help.view_hatari_page),
                    447:         ("hatariui", None, "Hatari UI home page", None, None, self.help.view_hatariui_page),
1.1.1.8 ! root      448:         ("about", Gtk.STOCK_INFO, "Hatari UI info", "<Ctrl>I", "Hatari UI information", cb.about)
1.1       root      449:         ))
                    450:         self.action_names = [x.get_name() for x in self.actions.list_actions()]
                    451: 
                    452:         # no actions set yet to panels or toolbars
                    453:         self.toolbars = {}
                    454:         self.panels = []
                    455: 
                    456:     def config_load(self, widget):
                    457:         # user loads a new configuration?
                    458:         if self.callbacks.config_load(widget):
1.1.1.3   root      459:             print("TODO: reset toggle actions")
1.1       root      460: 
                    461:     # ----- toolbar / panel additions ---------
                    462:     def set_actions(self, action_str, place):
                    463:         "set_actions(actions,place) -> error string, None if all OK"
                    464:         actions = action_str.split(",")
                    465:         for action in actions:
                    466:             if action in self.action_names:
                    467:                 # regular action
                    468:                 continue
                    469:             if action in self.panels:
                    470:                 # user specified panel
                    471:                 continue
                    472:             if action in ("close", ">"):
                    473:                 if place != "panel":
                    474:                     return "'close' and '>' can be only in a panel"
                    475:                 continue
                    476:             if action == "|":
                    477:                 # divider
                    478:                 continue
                    479:             if action.find("=") >= 0:
                    480:                 # special keycode/string action
                    481:                 continue
                    482:             return "unrecognized action '%s'" % action
                    483: 
                    484:         if place in ("left", "right", "top", "bottom"):
                    485:             self.toolbars[place] = actions
                    486:             return None
                    487:         if place == "panel":
                    488:             if len(actions) < 3:
                    489:                 return "panel has too few items to be useful"
                    490:             return None
                    491:         return "unknown actions position '%s'" % place
                    492: 
                    493:     def add_panel(self, spec):
                    494:         "add_panel(panel_specification) -> error string, None if all is OK"
                    495:         offset = spec.find(",")
                    496:         if offset <= 0:
                    497:             return "invalid panel specification '%s'" % spec
                    498: 
                    499:         name, panelcontrols = spec[:offset], spec[offset+1:]
                    500:         error = self.set_actions(panelcontrols, "panel")
                    501:         if error:
                    502:             return error
                    503: 
                    504:         if ",>," in panelcontrols:
1.1.1.8 ! root      505:             box = Gtk.VBox()
1.1       root      506:             splitcontrols = panelcontrols.split(",>,")
                    507:             for controls in splitcontrols:
                    508:                 box.add(self._get_container(controls.split(",")))
                    509:         else:
                    510:             box = self._get_container(panelcontrols.split(","))
1.1.1.8 ! root      511: 
1.1       root      512:         self.panels.append(name)
                    513:         self.actions.add_actions(
1.1.1.8 ! root      514:             ((name, Gtk.STOCK_ADD, name, None, name, self.callbacks.panel),),
1.1       root      515:             box
                    516:         )
                    517:         return None
                    518: 
                    519:     def list_actions(self):
                    520:         yield ("|", "Separator between controls")
1.1.1.6   root      521:         yield (">", "Start next toolbar row in panel windows")
1.1       root      522:         # generate the list from action information
                    523:         for act in self.actions.list_actions():
1.1.1.2   root      524:             note = act.get_property("tooltip")
                    525:             if not note:
                    526:                 note = act.get_property("label")
                    527:             yield(act.get_name(), note)
1.1       root      528:         yield ("<panel name>", "Button for the specified panel window")
                    529:         yield ("<name>=<string/code>", "Synthetize string or single key <code>")
                    530: 
                    531:     # ------- panel special actions -----------
                    532:     def _close_cb(self, widget):
                    533:         widget.get_toplevel().hide()
                    534: 
                    535:     # ------- key special action -----------
                    536:     def _create_key_control(self, name, textcode):
                    537:         "Simulate Atari key press/release and string inserting"
                    538:         if not textcode:
1.1.1.3   root      539:             return None
1.1.1.8 ! root      540:         widget = Gtk.ToolButton(Gtk.STOCK_PASTE)
1.1       root      541:         widget.set_label(name)
                    542:         try:
                    543:             # part after "=" converts to an int?
                    544:             code = int(textcode, 0)
                    545:             widget.connect("clicked", self.callbacks.keypress, code)
                    546:             tip = "keycode: %d" % code
                    547:         except ValueError:
                    548:             # no, assume a string macro is wanted instead
                    549:             widget.connect("clicked", self.callbacks.textinsert, textcode)
                    550:             tip = "string '%s'" % textcode
1.1.1.3   root      551:         widget.set_tooltip_text("Insert " + tip)
                    552:         return widget
1.1       root      553: 
                    554:     def _get_container(self, actions, horiz = True):
                    555:         "return Gtk container with the specified actions or None for no actions"
                    556:         if not actions:
                    557:             return None
                    558: 
1.1.1.3   root      559:         #print("ACTIONS:", actions)
1.1       root      560:         if len(actions) > 1:
1.1.1.8 ! root      561:             bar = Gtk.Toolbar()
1.1       root      562:             if horiz:
1.1.1.8 ! root      563:                 bar.set_orientation(Gtk.Orientation.HORIZONTAL)
1.1       root      564:             else:
1.1.1.8 ! root      565:                 bar.set_orientation(Gtk.Orientation.VERTICAL)
        !           566:             bar.set_style(Gtk.ToolbarStyle.BOTH)
1.1       root      567:             # disable overflow menu to get toolbar sized correctly for panels
                    568:             bar.set_show_arrow(False)
                    569:         else:
                    570:             bar = None
1.1.1.8 ! root      571: 
1.1       root      572:         for action in actions:
1.1.1.3   root      573:             #print(action)
1.1       root      574:             offset = action.find("=")
                    575:             if offset >= 0:
                    576:                 # handle "<name>=<keycode>" action specification
                    577:                 name = action[:offset]
                    578:                 text = action[offset+1:]
1.1.1.3   root      579:                 widget = self._create_key_control(name, text)
1.1       root      580:             elif action == "|":
1.1.1.8 ! root      581:                 widget = Gtk.SeparatorToolItem()
1.1       root      582:             elif action == "close":
                    583:                 if bar:
1.1.1.8 ! root      584:                     widget = create_toolbutton(Gtk.STOCK_CLOSE, self._close_cb)
1.1       root      585:                 else:
                    586:                     widget = create_button("Close", self._close_cb)
                    587:             else:
                    588:                 widget = self.actions.get_action(action).create_tool_item()
                    589:             if not widget:
                    590:                 continue
                    591:             if bar:
                    592:                 if action != "|":
                    593:                     widget.set_expand(True)
                    594:                 bar.insert(widget, -1)
                    595:         if bar:
                    596:             return bar
                    597:         return widget
                    598: 
                    599:     # ------------- handling menu -------------
                    600:     def _add_submenu(self, bar, title, items):
1.1.1.8 ! root      601:         submenu = Gtk.Menu()
1.1       root      602:         for name in items:
                    603:             if name:
                    604:                 action = self.actions.get_action(name)
                    605:                 item = action.create_menu_item()
                    606:             else:
1.1.1.8 ! root      607:                 item = Gtk.SeparatorMenuItem()
1.1       root      608:             submenu.add(item)
1.1.1.8 ! root      609:         baritem = Gtk.MenuItem(title, False)
1.1       root      610:         baritem.set_submenu(submenu)
                    611:         bar.add(baritem)
                    612: 
                    613:     def _get_menu(self):
                    614:         allmenus = (
                    615:         ("File", ("load", "save", None, "shot", "recanim", "recsound", None, "quit")),
                    616:         ("Emulation", ("run", "pause", "forward", None, "full", None, "input", None, "reset")),
1.1.1.2   root      617:         ("Devices", ("display", "floppy", "harddisk", "joystick", "machine", "device", "sound")),
1.1       root      618:         ("Configuration", ("path", None, "lconfig", "sconfig")),
                    619:         ("Debug", ("debug", "trace")),
1.1.1.4   root      620:         ("Help", ("manual", "compatibility", "release", "todo", None, "mails", "changes", None, "authors", "hatari", "hatariui", "about",))
1.1       root      621:         )
1.1.1.8 ! root      622:         bar = Gtk.MenuBar()
1.1       root      623: 
                    624:         for title, items in allmenus:
                    625:             self._add_submenu(bar, title, items)
                    626: 
                    627:         if self.panels:
                    628:             self._add_submenu(bar, "Panels", self.panels)
                    629: 
                    630:         return bar
                    631: 
                    632:     # ------------- run the whole UI -------------
                    633:     def run(self, floppy, havemenu, fullscreen, embed):
                    634:         accelgroup = None
                    635:         # create menu?
                    636:         if havemenu:
                    637:             # this would steal keys from embedded Hatari
                    638:             if not embed:
1.1.1.8 ! root      639:                 accelgroup = Gtk.AccelGroup()
1.1       root      640:                 for action in self.actions.list_actions():
                    641:                     action.set_accel_group(accelgroup)
                    642:             menu = self._get_menu()
                    643:         else:
                    644:             menu = None
                    645: 
                    646:         # create toolbars
                    647:         toolbars = { "left":None, "right":None, "top":None, "bottom":None}
                    648:         for side in ("left", "right"):
                    649:             if side in self.toolbars:
                    650:                 toolbars[side] = self._get_container(self.toolbars[side], False)
                    651:         for side in ("top", "bottom"):
                    652:             if side in self.toolbars:
                    653:                 toolbars[side] = self._get_container(self.toolbars[side], True)
                    654: 
                    655:         self.callbacks.create_ui(accelgroup, menu, toolbars, fullscreen, embed)
                    656:         self.help.set_mainwin(self.callbacks.mainwin)
                    657:         self.callbacks.set_floppy(floppy)
                    658: 
                    659:         # ugly, Hatari socket window ID can be gotten only
                    660:         # after Socket window is realized by gtk_main()
1.1.1.8 ! root      661:         GObject.idle_add(self.callbacks.run)
        !           662:         Gtk.main()
1.1       root      663: 
                    664: 
                    665: # ------------- usage / argument handling --------------
                    666: def usage(actions, msg=None):
                    667:     name = os.path.basename(sys.argv[0])
                    668:     uiname = "%s %s" % (UInfo.name, UInfo.version)
1.1.1.3   root      669:     print("\n%s" % uiname)
                    670:     print("=" * len(uiname))
                    671:     print("\nUsage: %s [options] [directory|disk image|Atari program]" % name)
                    672:     print("\nOptions:")
                    673:     print("\t-h, --help\t\tthis help")
                    674:     print("\t-n, --nomenu\t\tomit menus")
                    675:     print("\t-e, --embed\t\tembed Hatari window in middle of controls")
                    676:     print("\t-f, --fullscreen\tstart in fullscreen")
                    677:     print("\t-l, --left <controls>\ttoolbar at left")
                    678:     print("\t-r, --right <controls>\ttoolbar at right")
                    679:     print("\t-t, --top <controls>\ttoolbar at top")
                    680:     print("\t-b, --bottom <controls>\ttoolbar at bottom")
                    681:     print("\t-p, --panel <name>,<controls>")
                    682:     print("\t\t\t\tseparate window with given name and controls")
                    683:     print("\nAvailable (panel/toolbar) controls:")
1.1       root      684:     for action, description in actions.list_actions():
                    685:         size = len(action)
                    686:         if size < 8:
                    687:             tabs = "\t\t"
                    688:         elif size < 16:
                    689:             tabs = "\t"
                    690:         else:
                    691:             tabs = "\n\t\t\t"
1.1.1.3   root      692:         print("\t%s%s%s" % (action, tabs, description))
                    693:     print("""
1.1       root      694: You can have as many panels as you wish.  For each panel you need to add
                    695: a control with the name of the panel (see "MyPanel" below).
                    696: 
                    697: For example:
                    698: \t%s --embed \\
                    699: \t-t "about,run,pause,quit" \\
                    700: \t-p "MyPanel,Macro=Test,Undo=97,Help=98,>,F1=59,F2=60,F3=61,F4=62,>,close" \\
                    701: \t-r "paste,debug,trace,machine,MyPanel" \\
                    702: \t-b "sound,|,fastforward,|,fullscreen"
                    703: 
                    704: if no options are given, the UI uses basic controls.
1.1.1.3   root      705: """ % name)
1.1       root      706:     if msg:
1.1.1.3   root      707:         print("ERROR: %s\n" % msg)
1.1       root      708:     sys.exit(1)
                    709: 
                    710: 
                    711: def main():
                    712:     info = UInfo()
                    713:     actions = UIActions()
                    714:     try:
                    715:         longopts = ["embed", "fullscreen", "nomenu", "help",
                    716:             "left=", "right=", "top=", "bottom=", "panel="]
                    717:         opts, floppies = getopt.getopt(sys.argv[1:], "efnhl:r:t:b:p:", longopts)
                    718:         del longopts
1.1.1.3   root      719:     except getopt.GetoptError as err:
1.1       root      720:         usage(actions, err)
                    721: 
                    722:     menu = True
                    723:     embed = False
                    724:     fullscreen = False
                    725: 
                    726:     error = None
                    727:     for opt, arg in opts:
1.1.1.3   root      728:         print(opt, arg)
1.1       root      729:         if opt in ("-e", "--embed"):
                    730:             embed = True
                    731:         elif opt in ("-f", "--fullscreen"):
                    732:             fullscreen = True
                    733:         elif opt in ("-n", "--nomenu"):
                    734:             menu = False
                    735:         elif opt in ("-h", "--help"):
                    736:             usage(actions)
                    737:         elif opt in ("-l", "--left"):
                    738:             error = actions.set_actions(arg, "left")
                    739:         elif opt in ("-r", "--right"):
                    740:             error = actions.set_actions(arg, "right")
                    741:         elif opt in ("-t", "--top"):
                    742:             error = actions.set_actions(arg, "top")
                    743:         elif opt in ("-b", "--bottom"):
                    744:             error = actions.set_actions(arg, "bottom")
                    745:         elif opt in ("-p", "--panel"):
                    746:             error = actions.add_panel(arg)
                    747:         else:
                    748:             assert False, "getopt returned unhandled option"
                    749:         if error:
                    750:             usage(actions, error)
                    751: 
                    752:     if len(floppies) > 1:
                    753:         usage(actions, "multiple floppy images given: %s" % str(floppies))
                    754:     if floppies:
                    755:         if not os.path.exists(floppies[0]):
                    756:             usage(actions, "floppy image '%s' doesn't exist" % floppies[0])
                    757: 
                    758:     actions.run(floppies, menu, fullscreen, embed)
                    759: 
                    760: 
                    761: if __name__ == "__main__":
                    762:     main()

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.