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