Annotation of hatari/python-ui/uihelpers.py, revision 1.1.1.9

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

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.