|
|
1.1.1.9 ! root 1: #!/usr/bin/env python 1.1 root 2: # 3: # Misc common helper classes and functions for the Hatari UI 4: # 1.1.1.9 ! root 5: # Copyright (C) 2008-2019 by Eero Tamminen 1.1 root 6: # 7: # This program is free software; you can redistribute it and/or modify 8: # it under the terms of the GNU General Public License as published by 9: # the Free Software Foundation; either version 2 of the License, or 10: # (at your option) any later version. 11: # 12: # This program is distributed in the hope that it will be useful, 13: # but WITHOUT ANY WARRANTY; without even the implied warranty of 14: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15: # GNU General Public License for more details. 16: 17: import os 18: import sys 1.1.1.9 ! root 19: import gi ! 20: # use correct version of gtk ! 21: gi.require_version('Gtk', '3.0') ! 22: from gi.repository import Gtk ! 23: from gi.repository import GObject 1.1 root 24: 25: 26: # leak debugging 27: #import gc 28: #gc.set_debug(gc.DEBUG_UNCOLLECTABLE) 29: 30: 31: # --------------------- 32: # Hatari UI information 33: 34: class UInfo: 35: """singleton constants for the UI windows, 36: one instance is needed to initialize these properly""" 1.1.1.9 ! root 37: version = "v1.4" 1.1 root 38: name = "Hatari UI" 1.1.1.7 root 39: logo = "hatari-logo.png" 40: # TODO: use share/icons/hicolor/*/apps/hatari.png instead 1.1 root 41: icon = "hatari-icon.png" 1.1.1.9 ! root 42: copyright = "UI copyright (C) 2008-2019 by Eero Tamminen" 1.1.1.2 root 43: 1.1 root 44: # path to the directory where the called script resides 45: path = os.path.dirname(sys.argv[0]) 1.1.1.9 ! root 46: 1.1 root 47: def __init__(self, path = None): 48: "UIinfo([path]), set suitable paths for resources from CWD and path" 49: if path: 50: self.path = path 51: if not os.path.exists(UInfo.icon): 52: UInfo.icon = self._get_path(UInfo.icon) 53: if not os.path.exists(UInfo.logo): 54: UInfo.logo = self._get_path(UInfo.logo) 55: 56: def _get_path(self, filename): 57: sep = os.path.sep 58: testpath = "%s%s%s" % (self.path, sep, filename) 59: if os.path.exists(testpath): 60: return testpath 61: 62: 63: # -------------------------------------------------------- 64: # functions for showing HTML files 65: 66: class UIHelp: 67: def __init__(self): 68: """determine HTML viewer and where docs are""" 69: self._view = self.get_html_viewer() 70: self._path = self.get_doc_path() 71: 72: def get_html_viewer(self): 73: """return name of html viewer or None""" 74: path = self.get_binary_path("xdg-open") 75: if path: 76: return path 77: path = self.get_binary_path("firefox") 78: if path: 79: return path 80: return None 1.1.1.9 ! root 81: 1.1 root 82: def get_binary_path(self, name): 83: """return true if given binary is in path""" 84: # could also try running the binary with "--version" arg 85: # and check the exec return value 86: if os.sys.platform == "win32": 1.1.1.3 root 87: splitter = ';' 1.1 root 88: else: 1.1.1.3 root 89: splitter = ':' 90: for i in os.environ['PATH'].split(splitter): 1.1 root 91: fname = os.path.join(i, name) 92: if os.access(fname, os.X_OK) and not os.path.isdir(fname): 93: return fname 94: return None 95: 96: def get_doc_path(self): 97: """return path or URL to Hatari docs or None""" 98: # first try whether there are local Hatari docs in standard place 99: # for this Hatari/UI version 100: sep = os.sep 101: path = self.get_binary_path("hatari") 102: path = sep.join(path.split(sep)[:-2]) # remove "bin/hatari" 103: path = path + sep + "share" + sep + "doc" + sep + "hatari" + sep 104: if os.path.exists(path + "manual.html"): 105: return path 106: # if not, point to latest Hatari HG version docs 1.1.1.3 root 107: print("WARNING: Hatari manual not found at:", path + "manual.html") 1.1.1.4 root 108: return "http://hg.tuxfamily.org/mercurialroot/hatari/hatari/raw-file/tip/doc/" 1.1 root 109: 110: def set_mainwin(self, widget): 111: self.mainwin = widget 112: 113: def view_url(self, url, name): 114: """view given URL or file path, or error use 'name' as its name""" 115: if self._view and "://" in url or os.path.exists(url): 1.1.1.3 root 116: print("RUN: '%s' '%s'" % (self._view, url)) 1.1 root 117: os.spawnlp(os.P_NOWAIT, self._view, self._view, url) 118: return 119: if not self._view: 120: msg = "Cannot view %s, HTML viewer missing" % name 121: else: 122: msg = "Cannot view %s,\n'%s' file is missing" % (name, url) 123: from dialogs import ErrorDialog 124: ErrorDialog(self.mainwin).run(msg) 125: 126: def view_hatari_manual(self, dummy=None): 127: self.view_url(self._path + "manual.html", "Hatari manual") 128: 129: def view_hatari_compatibility(self, dummy=None): 130: self.view_url(self._path + "compatibility.html", "Hatari compatibility list") 131: 132: def view_hatari_releasenotes(self, dummy=None): 133: self.view_url(self._path + "release-notes.txt", "Hatari release notes") 134: 135: def view_hatari_todo(self, dummy=None): 136: self.view_url(self._path + "todo.txt", "Hatari TODO items") 137: 138: def view_hatari_mails(self, dummy=None): 1.1.1.4 root 139: self.view_url("http://hatari.tuxfamily.org/contact.html", "Hatari mailing lists") 1.1 root 140: 141: def view_hatari_repository(self, dummy=None): 1.1.1.4 root 142: self.view_url("http://hg.tuxfamily.org/mercurialroot/hatari/hatari", "latest Hatari changes") 1.1 root 143: 144: def view_hatari_authors(self, dummy=None): 145: self.view_url(self._path + "authors.txt", "Hatari authors") 146: 147: def view_hatari_page(self, dummy=None): 1.1.1.4 root 148: self.view_url("http://hatari.tuxfamily.org/", "Hatari home page") 1.1 root 149: 150: def view_hatariui_page(self, dummy=None): 1.1.1.8 root 151: self.view_url("http://eerott.mbnet.fi/hatari/hatari-ui.shtml", "Hatari UI home page") 1.1 root 152: 153: 154: # -------------------------------------------------------- 155: # auxiliary class+callback to be used with the PasteDialog 156: 157: class HatariTextInsert: 158: def __init__(self, hatari, text): 159: self.index = 0 160: self.text = text 161: self.pressed = False 162: self.hatari = hatari 1.1.1.3 root 163: print("OUTPUT '%s'" % text) 1.1.1.9 ! root 164: GObject.timeout_add(100, _text_insert_cb, self) 1.1 root 165: 1.1.1.3 root 166: # callback to insert text object to Hatari character at the time 167: # (first key down, on next call up), at given interval 1.1 root 168: def _text_insert_cb(textobj): 169: char = textobj.text[textobj.index] 1.1.1.3 root 170: if char == ' ': 171: # white space gets stripped, use scancode instead 172: char = "57" 1.1 root 173: if textobj.pressed: 174: textobj.pressed = False 1.1.1.3 root 175: textobj.hatari.insert_event("keyup %s" % char) 1.1 root 176: textobj.index += 1 177: if textobj.index >= len(textobj.text): 178: del(textobj) 179: return False 180: else: 181: textobj.pressed = True 1.1.1.3 root 182: textobj.hatari.insert_event("keydown %s" % char) 183: # call again 1.1 root 184: return True 185: 186: 187: # ---------------------------- 188: # helper functions for buttons 189: 190: def create_button(label, cb, data = None): 191: "create_button(label,cb[,data]) -> button widget" 1.1.1.9 ! root 192: button = Gtk.Button(label) 1.1 root 193: if data == None: 194: button.connect("clicked", cb) 195: else: 196: button.connect("clicked", cb, data) 197: return button 198: 199: def create_toolbutton(stock_id, cb, data = None): 200: "create_toolbutton(stock_id,cb[,data]) -> toolbar button with stock icon+label" 1.1.1.9 ! root 201: button = Gtk.ToolButton(stock_id) 1.1 root 202: if data == None: 203: button.connect("clicked", cb) 204: else: 205: button.connect("clicked", cb, data) 206: return button 207: 208: def create_toggle(label, cb, data = None): 209: "create_toggle(label,cb[,data]) -> toggle button widget" 1.1.1.9 ! root 210: button = Gtk.ToggleButton(label) 1.1 root 211: if data == None: 212: button.connect("toggled", cb) 213: else: 214: button.connect("toggled", cb, data) 215: return button 216: 217: 218: # ----------------------------- 219: # Table dialog helper functions 1.1.1.9 ! root 220: # ! 221: # TODO: rewrite to use Gtk.Grid instead of Gtk.Table 1.1 root 222: 1.1.1.9 ! root 223: def create_table_dialog(parent, title, rows, cols, oktext = Gtk.STOCK_APPLY): 1.1.1.2 root 224: "create_table_dialog(parent,title,rows, cols, oktext) -> (table,dialog)" 1.1.1.9 ! root 225: dialog = Gtk.Dialog(title, parent, ! 226: Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, ! 227: (oktext, Gtk.ResponseType.APPLY, ! 228: Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)) 1.1 root 229: 1.1.1.9 ! root 230: table = Gtk.Table(rows, cols) 1.1 root 231: table.set_col_spacings(8) 232: dialog.vbox.add(table) 233: return (table, dialog) 234: 1.1.1.9 ! root 235: def table_add_entry_row(table, row, col, label, size = None): ! 236: "table_add_entry_row(table,row,col,label,[entry size]) -> entry" 1.1.1.2 root 237: # add given label to given row in given table 238: # return entry for that line 1.1.1.9 ! root 239: label = Gtk.Label(label=label, halign=Gtk.Align.END) ! 240: table.attach(label, col, col+1, row, row+1, Gtk.AttachOptions.FILL) 1.1.1.2 root 241: col += 1 1.1 root 242: if size: 1.1.1.9 ! root 243: entry = Gtk.Entry(max_length=size, width_chars=size, halign=Gtk.Align.START) ! 244: table.attach(entry, col, col+1, row, row+1) 1.1 root 245: else: 1.1.1.9 ! root 246: entry = Gtk.Entry() 1.1.1.2 root 247: table.attach(entry, col, col+1, row, row+1) 1.1 root 248: return entry 249: 1.1.1.9 ! root 250: def table_add_widget_row(table, row, col, label, widget, fullspan = False): ! 251: "table_add_widget_row(table,row,col,label,widget) -> widget" 1.1.1.2 root 252: # add given label right aligned to given row in given table 253: # add given widget to the right column and returns it 254: # return entry for that line 1.1 root 255: if label: 1.1.1.9 ! root 256: if fullspan: ! 257: lcol = 0 ! 258: else: ! 259: lcol = col ! 260: label = Gtk.Label(label=label, halign=Gtk.Align.END) ! 261: table.attach(label, lcol, lcol+1, row, row+1, Gtk.AttachOptions.FILL) 1.1.1.2 root 262: if fullspan: 263: table.attach(widget, 1, col+2, row, row+1) 264: else: 265: table.attach(widget, col+1, col+2, row, row+1) 1.1 root 266: return widget 267: 1.1.1.9 ! root 268: def table_add_radio_rows(table, row, col, label, texts, cb = None): ! 269: "table_add_radio_rows(table,row,col,label,texts[,cb]) -> [radios]" 1.1.1.2 root 270: # - add given label right aligned to given row in given table 271: # - create/add radio buttons with given texts to next row, set 272: # the one given as "active" as active and set 'cb' as their 273: # "toggled" callback handler 274: # - return array or radiobuttons 1.1.1.9 ! root 275: label = Gtk.Label(label=label, halign=Gtk.Align.END) ! 276: table.attach(label, col, col+1, row, row+1) 1.1.1.2 root 277: 278: radios = [] 279: radio = None 1.1.1.9 ! root 280: box = Gtk.VBox() 1.1.1.2 root 281: for text in texts: 1.1.1.9 ! root 282: radio = Gtk.RadioButton(group=radio, label=text) 1.1.1.2 root 283: if cb: 284: radio.connect("toggled", cb, text) 285: radios.append(radio) 286: box.add(radio) 287: table.attach(box, col+1, col+2, row, row+1) 288: return radios 289: 1.1 root 290: def table_add_separator(table, row): 291: "table_add_separator(table,row)" 1.1.1.9 ! root 292: widget = Gtk.HSeparator() ! 293: endcol = table.get_property("n-columns") 1.1.1.2 root 294: # separator for whole table width 1.1.1.9 ! root 295: table.attach(widget, 0, endcol, row, row+1, Gtk.AttachOptions.FILL) 1.1 root 296: 297: 298: # ----------------------------- 299: # File selection helpers 300: 301: def get_open_filename(title, parent, path = None): 1.1.1.9 ! root 302: buttons = (Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) ! 303: fsel = Gtk.FileChooserDialog(title, parent, Gtk.FileChooserAction.OPEN, buttons) 1.1 root 304: fsel.set_local_only(True) 305: if path: 306: fsel.set_filename(path) 1.1.1.9 ! root 307: if fsel.run() == Gtk.ResponseType.OK: 1.1 root 308: filename = fsel.get_filename() 309: else: 310: filename = None 311: fsel.destroy() 312: return filename 313: 314: def get_save_filename(title, parent, path = None): 1.1.1.9 ! root 315: buttons = (Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) ! 316: fsel = Gtk.FileChooserDialog(title, parent, Gtk.FileChooserAction.SAVE, buttons) 1.1 root 317: fsel.set_local_only(True) 318: fsel.set_do_overwrite_confirmation(True) 319: if path: 320: fsel.set_filename(path) 321: if not os.path.exists(path): 322: # above set only folder, this is needed to set 323: # the file name when the file doesn't exist 324: fsel.set_current_name(os.path.basename(path)) 1.1.1.9 ! root 325: if fsel.run() == Gtk.ResponseType.OK: 1.1 root 326: filename = fsel.get_filename() 327: else: 328: filename = None 329: fsel.destroy() 330: return filename 331: 1.1.1.3 root 332: 333: # File selection button with eject button 334: class FselAndEjectFactory: 335: def __init__(self): 336: pass 337: 338: def get(self, label, path, filename, action): 339: "returns file selection button and box having that + eject button" 1.1.1.9 ! root 340: fsel = Gtk.FileChooserButton(label) 1.1.1.3 root 341: # Hatari cannot access URIs 342: fsel.set_local_only(True) 343: fsel.set_width_chars(12) 344: fsel.set_action(action) 345: if filename: 346: fsel.set_filename(filename) 347: elif path: 348: fsel.set_current_folder(path) 349: eject = create_button("Eject", self._eject, fsel) 350: 1.1.1.9 ! root 351: box = Gtk.HBox() ! 352: box.pack_start(fsel, True, True, 0) ! 353: box.pack_start(eject, False, False, 0) 1.1.1.3 root 354: return (fsel, box) 355: 356: def _eject(self, widget, fsel): 357: fsel.unselect_all() 358: 359: 1.1 root 360: # Gtk is braindead, there's no way to set a default filename 361: # for file chooser button unless it already exists 362: # - set_filename() works only for files that already exist 363: # - set_current_name() works only for SAVE action, 364: # but file chooser button doesn't support that 365: # i.e. I had to do my own (less nice) container widget... 1.1.1.3 root 366: class FselEntry: 1.1 root 367: def __init__(self, parent, validate = None, data = None): 368: self._parent = parent 369: self._validate = validate 370: self._validate_data = data 1.1.1.9 ! root 371: entry = Gtk.Entry() 1.1 root 372: entry.set_width_chars(12) 373: entry.set_editable(False) 1.1.1.9 ! root 374: hbox = Gtk.HBox() 1.1 root 375: hbox.add(entry) 376: button = create_button("Select...", self._select_file_cb) 1.1.1.9 ! root 377: hbox.pack_start(button, False, False, 0) 1.1 root 378: self._entry = entry 379: self._hbox = hbox 1.1.1.9 ! root 380: 1.1 root 381: def _select_file_cb(self, widget): 382: fname = self._entry.get_text() 383: while True: 384: fname = get_save_filename("Select file", self._parent, fname) 385: if not fname: 386: # assume cancel 387: return 388: if self._validate: 389: # filename needs validation and is valid? 390: if not self._validate(self._validate_data, fname): 391: continue 392: self._entry.set_text(fname) 393: return 1.1.1.9 ! root 394: 1.1 root 395: def set_filename(self, fname): 396: self._entry.set_text(fname) 1.1.1.9 ! root 397: 1.1 root 398: def get_filename(self): 399: return self._entry.get_text() 400: 401: def get_container(self): 402: return self._hbox
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.