|
|
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()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.