|
|
1.1 ! root 1: #!/usr/bin/env python ! 2: # ! 3: # Misc common helper classes and functions for the Hatari UI ! 4: # ! 5: # Copyright (C) 2008-2009 by Eero Tamminen <eerot at berlios> ! 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 ! 19: # use correct version of pygtk/gtk ! 20: import pygtk ! 21: pygtk.require('2.0') ! 22: import gtk ! 23: import gobject ! 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""" ! 37: version = "v1.0" ! 38: name = "Hatari UI" ! 39: logo = "hatari.png" ! 40: icon = "hatari-icon.png" ! 41: copyright = "UI copyright (C) 2008-2010 by Eero Tamminen" ! 42: ! 43: # path to the directory where the called script resides ! 44: path = os.path.dirname(sys.argv[0]) ! 45: ! 46: def __init__(self, path = None): ! 47: "UIinfo([path]), set suitable paths for resources from CWD and path" ! 48: if path: ! 49: self.path = path ! 50: if not os.path.exists(UInfo.icon): ! 51: UInfo.icon = self._get_path(UInfo.icon) ! 52: if not os.path.exists(UInfo.logo): ! 53: UInfo.logo = self._get_path(UInfo.logo) ! 54: ! 55: def _get_path(self, filename): ! 56: sep = os.path.sep ! 57: testpath = "%s%s%s" % (self.path, sep, filename) ! 58: if os.path.exists(testpath): ! 59: return testpath ! 60: ! 61: ! 62: # -------------------------------------------------------- ! 63: # functions for showing HTML files ! 64: ! 65: class UIHelp: ! 66: def __init__(self): ! 67: """determine HTML viewer and where docs are""" ! 68: self._view = self.get_html_viewer() ! 69: self._path = self.get_doc_path() ! 70: ! 71: def get_html_viewer(self): ! 72: """return name of html viewer or None""" ! 73: path = self.get_binary_path("xdg-open") ! 74: if path: ! 75: return path ! 76: path = self.get_binary_path("firefox") ! 77: if path: ! 78: return path ! 79: return None ! 80: ! 81: def get_binary_path(self, name): ! 82: """return true if given binary is in path""" ! 83: # could also try running the binary with "--version" arg ! 84: # and check the exec return value ! 85: if os.sys.platform == "win32": ! 86: for i in os.environ['PATH'].split(';'): ! 87: fname = os.path.join(i, name) ! 88: if os.access(fname, os.X_OK) and not os.path.isdir(fname): ! 89: return fname ! 90: else: ! 91: for i in os.environ['PATH'].split(':'): ! 92: fname = os.path.join(i, name) ! 93: if os.access(fname, os.X_OK) and not os.path.isdir(fname): ! 94: return fname ! 95: return None ! 96: ! 97: def get_doc_path(self): ! 98: """return path or URL to Hatari docs or None""" ! 99: # first try whether there are local Hatari docs in standard place ! 100: # for this Hatari/UI version ! 101: sep = os.sep ! 102: path = self.get_binary_path("hatari") ! 103: path = sep.join(path.split(sep)[:-2]) # remove "bin/hatari" ! 104: path = path + sep + "share" + sep + "doc" + sep + "hatari" + sep ! 105: if os.path.exists(path + "manual.html"): ! 106: return path ! 107: # if not, point to latest Hatari HG version docs ! 108: print "WARNING: Hatari manual not found at:", path + "manual.html" ! 109: return "http://hg.berlios.de/repos/hatari/raw-file/tip/doc/" ! 110: ! 111: def set_mainwin(self, widget): ! 112: self.mainwin = widget ! 113: ! 114: def view_url(self, url, name): ! 115: """view given URL or file path, or error use 'name' as its name""" ! 116: if self._view and "://" in url or os.path.exists(url): ! 117: print "RUN: '%s' '%s'" % (self._view, url) ! 118: os.spawnlp(os.P_NOWAIT, self._view, self._view, url) ! 119: return ! 120: if not self._view: ! 121: msg = "Cannot view %s, HTML viewer missing" % name ! 122: else: ! 123: msg = "Cannot view %s,\n'%s' file is missing" % (name, url) ! 124: from dialogs import ErrorDialog ! 125: ErrorDialog(self.mainwin).run(msg) ! 126: ! 127: def view_hatari_manual(self, dummy=None): ! 128: self.view_url(self._path + "manual.html", "Hatari manual") ! 129: ! 130: def view_hatari_compatibility(self, dummy=None): ! 131: self.view_url(self._path + "compatibility.html", "Hatari compatibility list") ! 132: ! 133: def view_hatari_releasenotes(self, dummy=None): ! 134: self.view_url(self._path + "release-notes.txt", "Hatari release notes") ! 135: ! 136: def view_hatari_todo(self, dummy=None): ! 137: self.view_url(self._path + "todo.txt", "Hatari TODO items") ! 138: ! 139: def view_hatari_bugs(self, dummy=None): ! 140: self.view_url("http://developer.berlios.de/bugs/?group_id=10436", "Hatari bug tracker") ! 141: ! 142: def view_hatari_mails(self, dummy=None): ! 143: self.view_url("http://developer.berlios.de/mail/?group_id=10436", "Hatari mailing lists") ! 144: ! 145: def view_hatari_repository(self, dummy=None): ! 146: self.view_url("http://hg.berlios.de/repos/hatari", "latest Hatari changes") ! 147: ! 148: def view_hatari_authors(self, dummy=None): ! 149: self.view_url(self._path + "authors.txt", "Hatari authors") ! 150: ! 151: def view_hatari_page(self, dummy=None): ! 152: self.view_url("http://hatari.berlios.de/", "Hatari home page") ! 153: ! 154: def view_hatariui_page(self, dummy=None): ! 155: self.view_url("http://koti.mbnet.fi/tammat/hatari/hatari-ui.shtml", "Hatari UI home page") ! 156: ! 157: ! 158: # -------------------------------------------------------- ! 159: # auxiliary class+callback to be used with the PasteDialog ! 160: ! 161: class HatariTextInsert: ! 162: def __init__(self, hatari, text): ! 163: self.index = 0 ! 164: self.text = text ! 165: self.pressed = False ! 166: self.hatari = hatari ! 167: print "OUTPUT '%s'" % text ! 168: gobject.timeout_add(100, _text_insert_cb, self) ! 169: ! 170: # callback to insert text object to Hatari character at the time, at given interval ! 171: def _text_insert_cb(textobj): ! 172: char = textobj.text[textobj.index] ! 173: if textobj.pressed: ! 174: textobj.pressed = False ! 175: textobj.hatari.insert_event("keyrelease %c" % char) ! 176: textobj.index += 1 ! 177: if textobj.index >= len(textobj.text): ! 178: del(textobj) ! 179: return False ! 180: else: ! 181: textobj.pressed = True ! 182: textobj.hatari.insert_event("keypress %c" % char) ! 183: return True ! 184: ! 185: ! 186: # ---------------------------- ! 187: # helper functions for buttons ! 188: ! 189: def create_button(label, cb, data = None): ! 190: "create_button(label,cb[,data]) -> button widget" ! 191: button = gtk.Button(label) ! 192: if data == None: ! 193: button.connect("clicked", cb) ! 194: else: ! 195: button.connect("clicked", cb, data) ! 196: return button ! 197: ! 198: def create_toolbutton(stock_id, cb, data = None): ! 199: "create_toolbutton(stock_id,cb[,data]) -> toolbar button with stock icon+label" ! 200: button = gtk.ToolButton(stock_id) ! 201: if data == None: ! 202: button.connect("clicked", cb) ! 203: else: ! 204: button.connect("clicked", cb, data) ! 205: return button ! 206: ! 207: def create_toggle(label, cb, data = None): ! 208: "create_toggle(label,cb[,data]) -> toggle button widget" ! 209: button = gtk.ToggleButton(label) ! 210: if data == None: ! 211: button.connect("toggled", cb) ! 212: else: ! 213: button.connect("toggled", cb, data) ! 214: return button ! 215: ! 216: ! 217: # ----------------------------- ! 218: # Table dialog helper functions ! 219: ! 220: def create_table_dialog(parent, title, rows, cols, oktext = gtk.STOCK_APPLY): ! 221: "create_table_dialog(parent,title,rows, cols, oktext) -> (table,dialog)" ! 222: dialog = gtk.Dialog(title, parent, ! 223: gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, ! 224: (oktext, gtk.RESPONSE_APPLY, ! 225: gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) ! 226: ! 227: table = gtk.Table(rows, cols) ! 228: table.set_data("col_offset", 0) ! 229: table.set_col_spacings(8) ! 230: dialog.vbox.add(table) ! 231: return (table, dialog) ! 232: ! 233: def table_set_col_offset(table, offset): ! 234: "set column offset for successive table_* ops on given table" ! 235: table.set_data("col_offset", offset) ! 236: ! 237: def table_add_entry_row(table, row, label, size = None): ! 238: "table_add_entry_row(table,row,label,[entry size]) -> entry" ! 239: # add given label to given row in given table ! 240: # return entry for that line ! 241: label = gtk.Label(label) ! 242: align = gtk.Alignment(1) # right aligned ! 243: align.add(label) ! 244: col = table.get_data("col_offset") ! 245: table.attach(align, col, col+1, row, row+1, gtk.FILL) ! 246: col += 1 ! 247: if size: ! 248: entry = gtk.Entry(size) ! 249: entry.set_width_chars(size) ! 250: align = gtk.Alignment(0) # left aligned (default is centered) ! 251: align.add(entry) ! 252: table.attach(align, col, col+1, row, row+1) ! 253: else: ! 254: entry = gtk.Entry() ! 255: table.attach(entry, col, col+1, row, row+1) ! 256: return entry ! 257: ! 258: def table_add_widget_row(table, row, label, widget, fullspan = False): ! 259: "table_add_widget_row(table,row,label,widget) -> widget" ! 260: # add given label right aligned to given row in given table ! 261: # add given widget to the right column and returns it ! 262: # return entry for that line ! 263: if fullspan: ! 264: col = 0 ! 265: else: ! 266: col = table.get_data("col_offset") ! 267: if label: ! 268: label = gtk.Label(label) ! 269: align = gtk.Alignment(1) ! 270: align.add(label) ! 271: table.attach(align, col, col+1, row, row+1, gtk.FILL) ! 272: if fullspan: ! 273: col = table.get_data("col_offset") ! 274: table.attach(widget, 1, col+2, row, row+1) ! 275: else: ! 276: table.attach(widget, col+1, col+2, row, row+1) ! 277: return widget ! 278: ! 279: def table_add_radio_rows(table, row, label, texts, cb = None): ! 280: "table_add_radio_rows(table,row,label,texts[,cb]) -> [radios]" ! 281: # - add given label right aligned to given row in given table ! 282: # - create/add radio buttons with given texts to next row, set ! 283: # the one given as "active" as active and set 'cb' as their ! 284: # "toggled" callback handler ! 285: # - return array or radiobuttons ! 286: label = gtk.Label(label) ! 287: align = gtk.Alignment(1) ! 288: align.add(label) ! 289: col = table.get_data("col_offset") ! 290: table.attach(align, col, col+1, row, row+1, gtk.FILL) ! 291: ! 292: radios = [] ! 293: radio = None ! 294: box = gtk.VBox() ! 295: for text in texts: ! 296: radio = gtk.RadioButton(radio, text) ! 297: if cb: ! 298: radio.connect("toggled", cb, text) ! 299: radios.append(radio) ! 300: box.add(radio) ! 301: table.attach(box, col+1, col+2, row, row+1) ! 302: return radios ! 303: ! 304: def table_add_separator(table, row): ! 305: "table_add_separator(table,row)" ! 306: widget = gtk.HSeparator() ! 307: endcol = table.get_data("n-columns") ! 308: # separator for whole table width ! 309: table.attach(widget, 0, endcol, row, row+1, gtk.FILL) ! 310: ! 311: ! 312: # ----------------------------- ! 313: # File selection helpers ! 314: ! 315: def get_open_filename(title, parent, path = None): ! 316: buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) ! 317: fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_OPEN, buttons) ! 318: fsel.set_local_only(True) ! 319: if path: ! 320: fsel.set_filename(path) ! 321: if fsel.run() == gtk.RESPONSE_OK: ! 322: filename = fsel.get_filename() ! 323: else: ! 324: filename = None ! 325: fsel.destroy() ! 326: return filename ! 327: ! 328: def get_save_filename(title, parent, path = None): ! 329: buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) ! 330: fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_SAVE, buttons) ! 331: fsel.set_local_only(True) ! 332: fsel.set_do_overwrite_confirmation(True) ! 333: if path: ! 334: fsel.set_filename(path) ! 335: if not os.path.exists(path): ! 336: # above set only folder, this is needed to set ! 337: # the file name when the file doesn't exist ! 338: fsel.set_current_name(os.path.basename(path)) ! 339: if fsel.run() == gtk.RESPONSE_OK: ! 340: filename = fsel.get_filename() ! 341: else: ! 342: filename = None ! 343: fsel.destroy() ! 344: return filename ! 345: ! 346: # Gtk is braindead, there's no way to set a default filename ! 347: # for file chooser button unless it already exists ! 348: # - set_filename() works only for files that already exist ! 349: # - set_current_name() works only for SAVE action, ! 350: # but file chooser button doesn't support that ! 351: # i.e. I had to do my own (less nice) container widget... ! 352: class FselEntry(): ! 353: def __init__(self, parent, validate = None, data = None): ! 354: self._parent = parent ! 355: self._validate = validate ! 356: self._validate_data = data ! 357: entry = gtk.Entry() ! 358: entry.set_width_chars(12) ! 359: entry.set_editable(False) ! 360: hbox = gtk.HBox() ! 361: hbox.add(entry) ! 362: button = create_button("Select...", self._select_file_cb) ! 363: hbox.pack_start(button, False, False) ! 364: self._entry = entry ! 365: self._hbox = hbox ! 366: ! 367: def _select_file_cb(self, widget): ! 368: fname = self._entry.get_text() ! 369: while True: ! 370: fname = get_save_filename("Select file", self._parent, fname) ! 371: if not fname: ! 372: # assume cancel ! 373: return ! 374: if self._validate: ! 375: # filename needs validation and is valid? ! 376: if not self._validate(self._validate_data, fname): ! 377: continue ! 378: self._entry.set_text(fname) ! 379: return ! 380: ! 381: def set_filename(self, fname): ! 382: self._entry.set_text(fname) ! 383: ! 384: def get_filename(self): ! 385: return self._entry.get_text() ! 386: ! 387: def get_container(self): ! 388: return self._hbox
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.