1#!/usr/bin/env python 2# 3# Classes for Hatari emulator instance and mapping its congfiguration 4# variables with its command line option. 5# 6# Copyright (C) 2008-2012 by Eero Tamminen 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17 18import os 19import sys 20import time 21import signal 22import socket 23import select 24from config import ConfigStore 25 26 27# Running Hatari instance 28class Hatari: 29 "running hatari instance and methods for communicating with it" 30 basepath = "/tmp/hatari-ui-" + str(os.getpid()) 31 logpath = basepath + ".log" 32 tracepath = basepath + ".trace" 33 debugpath = basepath + ".debug" 34 controlpath = basepath + ".socket" 35 server = None # singleton due to path being currently one per user 36 37 def __init__(self, hataribin = None): 38 # collect hatari process zombies without waitpid() 39 signal.signal(signal.SIGCHLD, signal.SIG_IGN) 40 if hataribin: 41 self.hataribin = hataribin 42 else: 43 self.hataribin = "hatari" 44 self._create_server() 45 self.control = None 46 self.paused = False 47 self.pid = 0 48 49 def is_compatible(self): 50 "check Hatari compatibility and return error string if it's not" 51 error = "Hatari not found or it doesn't support the required --control-socket option!" 52 pipe = os.popen(self.hataribin + " -h") 53 for line in pipe.readlines(): 54 if line.find("--control-socket") >= 0: 55 error = None 56 break 57 try: 58 pipe.close() 59 except IOError: 60 pass 61 return error 62 63 def save_config(self): 64 os.popen(self.hataribin + " --saveconfig") 65 66 def _create_server(self): 67 if self.server: 68 return 69 self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 70 if os.path.exists(self.controlpath): 71 os.unlink(self.controlpath) 72 self.server.bind(self.controlpath) 73 self.server.listen(1) 74 75 def _send_message(self, msg): 76 if self.control: 77 self.control.send(msg) 78 return True 79 else: 80 print("ERROR: no Hatari (control socket)") 81 return False 82 83 def change_option(self, option): 84 "change_option(option), changes given Hatari cli option" 85 return self._send_message("hatari-option %s\n" % option) 86 87 def set_path(self, key, path): 88 "set_path(key, path), sets path with given key" 89 return self._send_message("hatari-path %s %s\n" % (key, path)) 90 91 def set_device(self, device, enabled): 92 # needed because CLI options cannot disable devices, only enable 93 "set_path(device, enabled), sets whether given device is enabled or not" 94 if enabled: 95 return self._send_message("hatari-enable %s\n" % device) 96 else: 97 return self._send_message("hatari-disable %s\n" % device) 98 99 def trigger_shortcut(self, shortcut): 100 "trigger_shortcut(shortcut), triggers given Hatari (keyboard) shortcut" 101 return self._send_message("hatari-shortcut %s\n" % shortcut) 102 103 def insert_event(self, event): 104 "insert_event(event), synthetizes given key/mouse Atari event" 105 return self._send_message("hatari-event %s\n" % event) 106 107 def debug_command(self, cmd): 108 "debug_command(command), runs given Hatari debugger command" 109 return self._send_message("hatari-debug %s\n" % cmd) 110 111 def pause(self): 112 "pause(), pauses Hatari emulation" 113 return self._send_message("hatari-stop\n") 114 115 def unpause(self): 116 "unpause(), continues Hatari emulation" 117 return self._send_message("hatari-cont\n") 118 119 def _open_output_file(self, hataricommand, option, path): 120 if os.path.exists(path): 121 os.unlink(path) 122 # TODO: why fifo doesn't work properly (blocks forever on read or 123 # reads only byte at the time and stops after first newline)? 124 #os.mkfifo(path) 125 #raw_input("attach strace now, then press Enter\n") 126 127 # ask Hatari to open/create the requested output file... 128 hataricommand("%s %s" % (option, path)) 129 wait = 0.025 130 # ...and wait for it to appear before returning it 131 for i in range(0, 8): 132 time.sleep(wait) 133 if os.path.exists(path): 134 return open(path, "r") 135 wait += wait 136 return None 137 138 def open_debug_output(self): 139 "open_debug_output() -> file, opens Hatari debugger output file" 140 return self._open_output_file(self.debug_command, "f", self.debugpath) 141 142 def open_trace_output(self): 143 "open_trace_output() -> file, opens Hatari tracing output file" 144 return self._open_output_file(self.change_option, "--trace-file", self.tracepath) 145 146 def open_log_output(self): 147 "open_trace_output() -> file, opens Hatari debug log file" 148 return self._open_output_file(self.change_option, "--log-file", self.logpath) 149 150 def get_lines(self, fileobj): 151 "get_lines(file) -> list of lines readable from given Hatari output file" 152 # wait until data is available, then wait for some more 153 # and only then the data can be read, otherwise its old 154 print("Request&wait data from Hatari...") 155 select.select([fileobj], [], []) 156 time.sleep(0.1) 157 print("...read the data lines") 158 lines = fileobj.readlines() 159 print("".join(lines)) 160 return lines 161 162 def enable_embed_info(self): 163 "enable_embed_info(), request embedded Hatari window ID change information" 164 self._send_message("hatari-embed-info\n") 165 166 def get_embed_info(self): 167 "get_embed_info() -> (width, height), get embedded Hatari window size" 168 width, height = self.control.recv(12).split("x") 169 return (int(width), int(height)) 170 171 def get_control_socket(self): 172 "get_control_socket() -> socket which can be checked for embed ID changes" 173 return self.control 174 175 def is_running(self): 176 "is_running() -> bool, True if Hatari is running, False otherwise" 177 if not self.pid: 178 return False 179 try: 180 os.waitpid(self.pid, os.WNOHANG) 181 except OSError as value: 182 print("Hatari PID %d had exited in the meanwhile:\n\t%s" % (self.pid, value)) 183 self.pid = 0 184 if self.control: 185 self.control.close() 186 self.control = None 187 return False 188 return True 189 190 def run(self, extra_args = None, parent_win = None): 191 "run([parent window][,embedding args]), runs Hatari" 192 # if parent_win given, embed Hatari to it 193 pid = os.fork() 194 if pid < 0: 195 print("ERROR: fork()ing Hatari failed!") 196 return 197 if pid: 198 # in parent 199 self.pid = pid 200 if self.server: 201 print("WAIT hatari to connect to control socket...") 202 (self.control, addr) = self.server.accept() 203 print("connected!") 204 else: 205 # child runs Hatari 206 env = os.environ 207 if parent_win: 208 self._set_embed_env(env, parent_win) 209 # callers need to take care of confirming quitting 210 args = [self.hataribin, "--confirm-quit", "off"] 211 if self.server: 212 args += ["--control-socket", self.controlpath] 213 if extra_args: 214 args += extra_args 215 print("RUN:", args) 216 os.execvpe(self.hataribin, args, env) 217 218 def _set_embed_env(self, env, parent_win): 219 if sys.platform == 'win32': 220 win_id = parent_win.handle 221 else: 222 win_id = parent_win.xid 223 # tell SDL to use given widget's window 224 #env["SDL_WINDOWID"] = str(win_id) 225 226 # above is broken: when SDL uses a window it hasn't created itself, 227 # it for some reason doesn't listen to any events delivered to that 228 # window nor implements XEMBED protocol to get them in a way most 229 # friendly to embedder: 230 # http://standards.freedesktop.org/xembed-spec/latest/ 231 # 232 # Instead we tell hatari to reparent itself after creating 233 # its own window into this program widget window 234 env["PARENT_WIN_ID"] = str(win_id) 235 236 def kill(self): 237 "kill(), kill Hatari if it's running" 238 if self.is_running(): 239 os.kill(self.pid, signal.SIGKILL) 240 print("killed hatari with PID %d" % self.pid) 241 self.pid = 0 242 if self.control: 243 self.control.close() 244 self.control = None 245 246 247# Mapping of requested values both to Hatari configuration 248# and command line options. 249# 250# By default this doesn't allow setting any other configuration 251# variables than the ones that were read from the configuration 252# file i.e. you get an exception if configuration variables 253# don't match to current Hatari. So before using this the current 254# Hatari configuration should have been saved at least once. 255# 256# Because of some inconsistencies in the values (see e.g. sound), 257# this cannot just do these according to some mapping table, but 258# it needs actual method for (each) setting. 259class HatariConfigMapping(ConfigStore): 260 _paths = { 261 "memauto": ("[Memory]", "szAutoSaveFileName", "Automatic memory snapshot"), 262 "memsave": ("[Memory]", "szMemoryCaptureFileName", "Manual memory snapshot"), 263 "midiin": ("[Midi]", "sMidiInFileName", "Midi input"), 264 "midiout": ("[Midi]", "sMidiOutFileName", "Midi output"), 265 "rs232in": ("[RS232]", "szInFileName", "RS232 I/O input"), 266 "rs232out": ("[RS232]", "szOutFileName", "RS232 I/O output"), 267 "printout": ("[Printer]", "szPrintToFileName", "Printer output"), 268 "soundout": ("[Sound]", "szYMCaptureFileName", "Sound output") 269 } 270 "access methods to Hatari configuration file variables and command line options" 271 def __init__(self, hatari): 272 userconfdir = ".hatari" 273 ConfigStore.__init__(self, userconfdir) 274 conffilename = "hatari.cfg" 275 self.load(self.get_filepath(conffilename)) 276 277 self._hatari = hatari 278 self._lock_updates = False 279 self._desktop_w = 0 280 self._desktop_h = 0 281 self._options = [] 282 283 def validate(self): 284 "exception is thrown if the loaded configuration isn't compatible" 285 for method in dir(self): 286 if '_' not in method: 287 continue 288 # check class getters 289 starts = method[:method.find("_")] 290 if starts != "get": 291 continue 292 # but ignore getters for other things than config 293 ends = method[method.rfind("_")+1:] 294 if ends in ("types", "names", "values", "changes", "checkpoint", "filepath"): 295 continue 296 if ends in ("floppy", "joystick"): 297 # use port '0' for checks 298 getattr(self, method)(0) 299 else: 300 getattr(self, method)() 301 302 def _change_option(self, option, quoted = None): 303 "handle option changing, and quote spaces for quoted part of it" 304 if quoted: 305 option = "%s %s" % (option, quoted.replace(" ", "\\ ")) 306 if self._lock_updates: 307 self._options.append(option) 308 else: 309 self._hatari.change_option(option) 310 311 def lock_updates(self): 312 "lock_updates(), collect Hatari configuration changes" 313 self._lock_updates = True 314 315 def flush_updates(self): 316 "flush_updates(), apply collected Hatari configuration changes" 317 self._lock_updates = False 318 if not self._options: 319 return 320 self._hatari.change_option(" ".join(self._options)) 321 self._options = [] 322 323 # ------------ paths --------------- 324 def get_paths(self): 325 paths = [] 326 for key, item in self._paths.items(): 327 paths.append((key, self.get(item[0], item[1]), item[2])) 328 return paths 329 330 def set_paths(self, paths): 331 for key, path in paths: 332 self.set(self._paths[key][0], self._paths[key][1], path) 333 self._hatari.set_path(key, path) 334 335 # ------------ midi --------------- 336 def get_midi(self): 337 return self.get("[Midi]", "bEnableMidi") 338 339 def set_midi(self, value): 340 self.set("[Midi]", "bEnableMidi", value) 341 self._hatari.set_device("midi", value) 342 343 # ------------ printer --------------- 344 def get_printer(self): 345 return self.get("[Printer]", "bEnablePrinting") 346 347 def set_printer(self, value): 348 self.set("[Printer]", "bEnablePrinting", value) 349 self._hatari.set_device("printer", value) 350 351 # ------------ RS232 --------------- 352 def get_rs232(self): 353 return self.get("[RS232]", "bEnableRS232") 354 355 def set_rs232(self, value): 356 self.set("[RS232]", "bEnableRS232", value) 357 self._hatari.set_device("rs232", value) 358 359 # ------------ machine --------------- 360 def get_machine_types(self): 361 return ("ST", "STE", "TT", "Falcon") 362 363 def get_machine(self): 364 return self.get("[System]", "nMachineType") 365 366 def set_machine(self, value): 367 self.set("[System]", "nMachineType", value) 368 self._change_option("--machine %s" % ("st", "ste", "tt", "falcon")[value]) 369 370 # ------------ CPU level --------------- 371 def get_cpulevel_types(self): 372 return ("68000", "68010", "68020", "68EC030+FPU", "68040") 373 374 def get_cpulevel(self): 375 return self.get("[System]", "nCpuLevel") 376 377 def set_cpulevel(self, value): 378 self.set("[System]", "nCpuLevel", value) 379 self._change_option("--cpulevel %d" % value) 380 381 # ------------ CPU clock --------------- 382 def get_cpuclock_types(self): 383 return ("8 MHz", "16 MHz", "32 MHz") 384 385 def get_cpuclock(self): 386 clocks = {8:0, 16: 1, 32:2} 387 return clocks[self.get("[System]", "nCpuFreq")] 388 389 def set_cpuclock(self, value): 390 clocks = [8, 16, 32] 391 if value < 0 or value > 2: 392 print("WARNING: CPU clock idx %d, clock fixed to 8 Mhz" % value) 393 value = 8 394 else: 395 value = clocks[value] 396 self.set("[System]", "nCpuFreq", value) 397 self._change_option("--cpuclock %d" % value) 398 399 # ------------ DSP type --------------- 400 def get_dsp_types(self): 401 return ("None", "Dummy", "Emulated") 402 403 def get_dsp(self): 404 return self.get("[System]", "nDSPType") 405 406 def set_dsp(self, value): 407 self.set("[System]", "nDSPType", value) 408 self._change_option("--dsp %s" % ("none", "dummy", "emu")[value]) 409 410 # ------------ compatible --------------- 411 def get_compatible(self): 412 return self.get("[System]", "bCompatibleCpu") 413 414 def set_compatible(self, value): 415 self.set("[System]", "bCompatibleCpu", value) 416 self._change_option("--compatible %s" % str(value)) 417 418 # ------------ Timer-D --------------- 419 def get_timerd(self): 420 return self.get("[System]", "bPatchTimerD") 421 422 def set_timerd(self, value): 423 self.set("[System]", "bPatchTimerD", value) 424 self._change_option("--timer-d %s" % str(value)) 425 426 # ------------ RTC --------------- 427 def get_rtc(self): 428 return self.get("[System]", "bRealTimeClock") 429 430 def set_rtc(self, value): 431 self.set("[System]", "bRealTimeClock", value) 432 self._change_option("--rtc %s" % str(value)) 433 434 # ------------ fastforward --------------- 435 def get_fastforward(self): 436 return self.get("[System]", "bFastForward") 437 438 def set_fastforward(self, value): 439 self.set("[System]", "bFastForward", value) 440 self._change_option("--fast-forward %s" % str(value)) 441 442 # ------------ sound --------------- 443 def get_sound_values(self): 444 # 48kHz, 44.1kHz and STE/TT/Falcon DMA 50066Hz divisable values 445 return ("6000", "6258", "8000", "11025", "12000", "12517", 446 "16000", "22050", "24000", "25033", "32000", 447 "44100", "48000", "50066") 448 449 def get_sound(self): 450 enabled = self.get("[Sound]", "bEnableSound") 451 hz = str(self.get("[Sound]", "nPlaybackFreq")) 452 idx = self.get_sound_values().index(hz) 453 return (enabled, idx) 454 455 def set_sound(self, enabled, idx): 456 # map get_sound_values() index to Hatari config 457 hz = self.get_sound_values()[idx] 458 self.set("[Sound]", "nPlaybackFreq", int(hz)) 459 self.set("[Sound]", "bEnableSound", enabled) 460 # and to cli option 461 if enabled: 462 self._change_option("--sound %s" % hz) 463 else: 464 self._change_option("--sound off") 465 466 def get_ymmixer_types(self): 467 return ("linear", "table", "model") 468 469 def get_ymmixer(self): 470 # values for types are start from 1, not 0 471 return self.get("[Sound]", "YmVolumeMixing")-1 472 473 def set_ymmixer(self, value): 474 self.set("[Sound]", "YmVolumeMixing", value+1) 475 self._change_option("--ym-mixing %s" % self.get_ymmixer_types()[value]) 476 477 def get_bufsize(self): 478 return self.get("[Sound]", "nSdlAudioBufferSize") 479 480 def set_bufsize(self, value): 481 value = int(value) 482 if value < 10: value = 10 483 if value > 100: value = 100 484 self.set("[Sound]", "nSdlAudioBufferSize", value) 485 self._change_option("--sound-buffer-size %d" % value) 486 487 def get_sync(self): 488 return self.get("[Sound]", "bEnableSoundSync") 489 490 def set_sync(self, value): 491 self.set("[Sound]", "bEnableSoundSync", value) 492 self._change_option("--sound-sync %s" % str(value)) 493 494 def get_mic(self): 495 return self.get("[Sound]", "bEnableMicrophone") 496 497 def set_mic(self, value): 498 self.set("[Sound]", "bEnableMicrophone", value) 499 self._change_option("--mic %s" % str(value)) 500 501 # ----------- joystick -------------- 502 def get_joystick_types(self): 503 return ("Disabled", "Real joystick", "Keyboard") 504 505 def get_joystick_names(self): 506 return ( 507 "ST Joystick 0", 508 "ST Joystick 1", 509 "STE Joypad A", 510 "STE Joypad B", 511 "Parport stick 1", 512 "Parport stick 2" 513 ) 514 515 def get_joystick(self, port): 516 # return index to get_joystick_values() array 517 return self.get("[Joystick%d]" % port, "nJoystickMode") 518 519 def set_joystick(self, port, value): 520 # map get_sound_values() index to Hatari config 521 self.set("[Joystick%d]" % port, "nJoystickMode", value) 522 joytype = ("none", "real", "keys")[value] 523 self._change_option("--joy%d %s" % (port, joytype)) 524 525 # ------------ floppy handling --------------- 526 def get_floppydir(self): 527 return self.get("[Floppy]", "szDiskImageDirectory") 528 529 def set_floppydir(self, path): 530 return self.set("[Floppy]", "szDiskImageDirectory", path) 531 532 def get_floppy(self, drive): 533 return self.get("[Floppy]", "szDisk%cFileName" % ("A", "B")[drive]) 534 535 def set_floppy(self, drive, filename): 536 self.set("[Floppy]", "szDisk%cFileName" % ("A", "B")[drive], filename) 537 self._change_option("--disk-%c" % ("a", "b")[drive], str(filename)) 538 539 def get_floppy_drives(self): 540 return (self.get("[Floppy]", "EnableDriveA"), self.get("[Floppy]", "EnableDriveB")) 541 542 def set_floppy_drives(self, drives): 543 idx = 0 544 for drive in ("A", "B"): 545 value = drives[idx] 546 self.set("[Floppy]", "EnableDrive%c" % drive, value) 547 self._change_option("--drive-%c %s" % (drive.lower(), str(value))) 548 idx += 1 549 550 def get_fastfdc(self): 551 return self.get("[Floppy]", "FastFloppy") 552 553 def set_fastfdc(self, value): 554 self.set("[Floppy]", "FastFloppy", value) 555 self._change_option("--fastfdc %s" % str(value)) 556 557 def get_doublesided(self): 558 driveA = self.get("[Floppy]", "DriveA_NumberOfHeads") 559 driveB = self.get("[Floppy]", "DriveB_NumberOfHeads") 560 if driveA > 1 or driveB > 1: 561 return True 562 return False 563 564 def set_doublesided(self, value): 565 if value: sides = 2 566 else: sides = 1 567 for drive in ("A", "B"): 568 self.set("[Floppy]", "Drive%c_NumberOfHeads" % drive, sides) 569 self._change_option("--drive-%c-heads %d" % (drive.lower(), sides)) 570 571 # ------------- disk protection ------------- 572 def get_protection_types(self): 573 return ("Off", "On", "Auto") 574 575 def get_floppy_protection(self): 576 return self.get("[Floppy]", "nWriteProtection") 577 578 def get_hd_protection(self): 579 return self.get("[HardDisk]", "nWriteProtection") 580 581 def set_floppy_protection(self, value): 582 self.set("[Floppy]", "nWriteProtection", value) 583 self._change_option("--protect-floppy %s" % self.get_protection_types()[value]) 584 585 def set_hd_protection(self, value): 586 self.set("[HardDisk]", "nWriteProtection", value) 587 self._change_option("--protect-hd %s" % self.get_protection_types()[value]) 588 589 # ------------ GEMDOS HD (dir) emulation --------------- 590 def get_hd_cases(self): 591 return ("No conversion", "Upper case", "Lower case") 592 593 def get_hd_case(self): 594 return self.get("[HardDisk]", "nGemdosCase") 595 596 def set_hd_case(self, value): 597 values = ("off", "upper", "lower") 598 self.set("[HardDisk]", "nGemdosCase", value) 599 self._change_option("--gemdos-case %s" % values[value]) 600 601 def get_gemdos_dir(self): 602 self.get("[HardDisk]", "bUseHardDiskDirectory") # for validation 603 return self.get("[HardDisk]", "szHardDiskDirectory") 604 605 def set_gemdos_dir(self, dirname): 606 if dirname and os.path.isdir(dirname): 607 self.set("[HardDisk]", "bUseHardDiskDirectory", True) 608 self.set("[HardDisk]", "szHardDiskDirectory", dirname) 609 self._change_option("--harddrive", str(dirname)) 610 611 # ------------ ACSI HD (file) --------------- 612 def get_acsi_image(self): 613 self.get("[HardDisk]", "bUseHardDiskImage") # for validation 614 return self.get("[HardDisk]", "szHardDiskImage") 615 616 def set_acsi_image(self, filename): 617 if filename and os.path.isfile(filename): 618 self.set("[HardDisk]", "bUseHardDiskImage", True) 619 self.set("[HardDisk]", "szHardDiskImage", filename) 620 self._change_option("--acsi", str(filename)) 621 622 # ------------ IDE master (file) --------------- 623 def get_idemaster_image(self): 624 self.get("[HardDisk]", "bUseIdeMasterHardDiskImage") # for validation 625 return self.get("[HardDisk]", "szIdeMasterHardDiskImage") 626 627 def set_idemaster_image(self, filename): 628 if filename and os.path.isfile(filename): 629 self.set("[HardDisk]", "bUseIdeMasterHardDiskImage", True) 630 self.set("[HardDisk]", "szIdeMasterHardDiskImage", filename) 631 self._change_option("--ide-master", str(filename)) 632 633 # ------------ IDE slave (file) --------------- 634 def get_ideslave_image(self): 635 self.get("[HardDisk]", "bUseIdeSlaveHardDiskImage") # for validation 636 return self.get("[HardDisk]", "szIdeSlaveHardDiskImage") 637 638 def set_ideslave_image(self, filename): 639 if filename and os.path.isfile(filename): 640 self.set("[HardDisk]", "bUseIdeSlaveHardDiskImage", True) 641 self.set("[HardDisk]", "szIdeSlaveHardDiskImage", filename) 642 self._change_option("--ide-slave", str(filename)) 643 644 # ------------ TOS ROM --------------- 645 def get_tos(self): 646 return self.get("[ROM]", "szTosImageFileName") 647 648 def set_tos(self, filename): 649 self.set("[ROM]", "szTosImageFileName", filename) 650 self._change_option("--tos", str(filename)) 651 652 # ------------ memory --------------- 653 def get_memory_names(self): 654 # empty item in list shouldn't be shown, filter them out 655 return ("512kB", "1MB", "2MB", "4MB", "8MB", "14MB") 656 657 def get_memory(self): 658 "return index to what get_memory_names() returns" 659 sizemap = (0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5) 660 memsize = self.get("[Memory]", "nMemorySize") 661 if memsize >= 0 and memsize < len(sizemap): 662 return sizemap[memsize] 663 return 1 # default = 1BM 664 665 def set_memory(self, idx): 666 # map memory item index to memory size 667 sizemap = (0, 1, 2, 4, 8, 14) 668 if idx >= 0 and idx < len(sizemap): 669 memsize = sizemap[idx] 670 else: 671 memsize = 1 672 self.set("[Memory]", "nMemorySize", memsize) 673 self._change_option("--memsize %d" % memsize) 674 675 # ------------ monitor --------------- 676 def get_monitor_types(self): 677 return ("Mono", "RGB", "VGA", "TV") 678 679 def get_monitor(self): 680 return self.get("[Screen]", "nMonitorType") 681 682 def set_monitor(self, value): 683 self.set("[Screen]", "nMonitorType", value) 684 self._change_option("--monitor %s" % ("mono", "rgb", "vga", "tv")[value]) 685 686 # ------------ frameskip --------------- 687 def get_frameskip_names(self): 688 return ( 689 "Disabled", 690 "1 frame", 691 "2 frames", 692 "3 frames", 693 "4 frames", 694 "Automatic" 695 ) 696 697 def get_frameskip(self): 698 fs = self.get("[Screen]", "nFrameSkips") 699 if fs < 0 or fs > 5: 700 return 5 701 return fs 702 703 def set_frameskip(self, value): 704 value = int(value) # guarantee correct type 705 self.set("[Screen]", "nFrameSkips", value) 706 self._change_option("--frameskips %d" % value) 707 708 # ------------ VBL slowdown --------------- 709 def get_slowdown_names(self): 710 return ("Disabled", "2x", "3x", "4x", "5x", "6x", "8x") 711 712 def set_slowdown(self, value): 713 value = 1 + int(value) 714 self._change_option("--slowdown %d" % value) 715 716 # ------------ spec512 --------------- 717 def get_spec512threshold(self): 718 return self.get("[Screen]", "nSpec512Threshold") 719 720 def set_spec512threshold(self, value): 721 value = int(value) # guarantee correct type 722 self.set("[Screen]", "nSpec512Threshold", value) 723 self._change_option("--spec512 %d" % value) 724 725 # --------- keep desktop res ----------- 726 def get_desktop(self): 727 return self.get("[Screen]", "bKeepResolution") 728 729 def set_desktop(self, value): 730 self.set("[Screen]", "bKeepResolution", value) 731 self._change_option("--desktop %s" % str(value)) 732 733 # --------- keep desktop res - st ------ 734 def get_desktop_st(self): 735 return self.get("[Screen]", "bKeepResolutionST") 736 737 def set_desktop_st(self, value): 738 self.set("[Screen]", "bKeepResolutionST", value) 739 self._change_option("--desktop-st %s" % str(value)) 740 741 # ------------ force max --------------- 742 def get_force_max(self): 743 return self.get("[Screen]", "bForceMax") 744 745 def set_force_max(self, value): 746 self.set("[Screen]", "bForceMax", value) 747 self._change_option("--force-max %s" % str(value)) 748 749 # ------------ show borders --------------- 750 def get_borders(self): 751 return self.get("[Screen]", "bAllowOverscan") 752 753 def set_borders(self, value): 754 self.set("[Screen]", "bAllowOverscan", value) 755 self._change_option("--borders %s" % str(value)) 756 757 # ------------ show statusbar --------------- 758 def get_statusbar(self): 759 return self.get("[Screen]", "bShowStatusbar") 760 761 def set_statusbar(self, value): 762 self.set("[Screen]", "bShowStatusbar", value) 763 self._change_option("--statusbar %s" % str(value)) 764 765 # ------------ crop statusbar --------------- 766 def get_crop(self): 767 return self.get("[Screen]", "bCrop") 768 769 def set_crop(self, value): 770 self.set("[Screen]", "bCrop", value) 771 self._change_option("--crop %s" % str(value)) 772 773 # ------------ show led --------------- 774 def get_led(self): 775 return self.get("[Screen]", "bShowDriveLed") 776 777 def set_led(self, value): 778 self.set("[Screen]", "bShowDriveLed", value) 779 self._change_option("--drive-led %s" % str(value)) 780 781 # ------------ monitor aspect ratio --------------- 782 def get_aspectcorrection(self): 783 return self.get("[Screen]", "bAspectCorrect") 784 785 def set_aspectcorrection(self, value): 786 self.set("[Screen]", "bAspectCorrect", value) 787 self._change_option("--aspect %s" % str(value)) 788 789 # ------------ max window size --------------- 790 def set_desktop_size(self, w, h): 791 self._desktop_w = w 792 self._desktop_h = h 793 794 def get_desktop_size(self): 795 return (self._desktop_w, self._desktop_h) 796 797 def get_max_size(self): 798 w = self.get("[Screen]", "nMaxWidth") 799 h = self.get("[Screen]", "nMaxHeight") 800 # default to desktop size? 801 if not (w or h): 802 w = self._desktop_w 803 h = self._desktop_h 804 return (w, h) 805 806 def set_max_size(self, w, h): 807 # guarantee correct type (Gtk float -> config int) 808 w = int(w); h = int(h) 809 self.set("[Screen]", "nMaxWidth", w) 810 self.set("[Screen]", "nMaxHeight", h) 811 self._change_option("--max-width %d" % w) 812 self._change_option("--max-height %d" % h) 813 814 # TODO: remove once UI doesn't need this anymore 815 def set_zoom(self, value): 816 print("Just setting Zoom, configuration doesn't anymore have setting for this.") 817 if value: 818 zoom = 2 819 else: 820 zoom = 1 821 self._change_option("--zoom %d" % zoom) 822 823 # ------------ configured Hatari window size --------------- 824 def get_window_size(self): 825 if self.get("[Screen]", "bFullScreen"): 826 print("WARNING: don't start Hatari UI with fullscreened Hatari!") 827 828 # VDI resolution? 829 if self.get("[Screen]", "bUseExtVdiResolutions"): 830 width = self.get("[Screen]", "nVdiWidth") 831 height = self.get("[Screen]", "nVdiHeight") 832 return (width, height) 833 834 # window sizes for other than ST & STE can differ 835 if self.get("[System]", "nMachineType") not in (0, 1): 836 print("WARNING: neither ST nor STE machine, window size inaccurate!") 837 videl = True 838 else: 839 videl = False 840 841 # mono monitor? 842 if self.get_monitor() == 0: 843 return (640, 400) 844 845 # no, color 846 width = 320 847 height = 200 848 # statusbar? 849 if self.get_statusbar(): 850 sbar = 12 851 height += sbar 852 else: 853 sbar = 0 854 # zoom? 855 maxw, maxh = self.get_max_size() 856 if 2*width <= maxw and 2*height <= maxh: 857 width *= 2 858 height *= 2 859 zoom = 2 860 else: 861 zoom = 1 862 # overscan borders? 863 if self.get_borders() and not videl: 864 # properly aligned borders on top of zooming 865 leftx = (maxw-width)/zoom 866 borderx = 2*(min(48,leftx/2)/16)*16 867 lefty = (maxh-height)/zoom 868 bordery = min(29+47, lefty) 869 width += zoom*borderx 870 height += zoom*bordery 871 872 return (width, height) 873