|
|
1.1 root 1: #!/usr/bin/env python
2: #
3: # Misc common helper classes and functions for the Hatari UI
4: #
1.1.1.5 ! root 5: # Copyright (C) 2008-2012 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
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"""
1.1.1.5 ! root 37: version = "v1.1"
1.1 root 38: name = "Hatari UI"
39: logo = "hatari.png"
40: icon = "hatari-icon.png"
1.1.1.5 ! root 41: copyright = "UI copyright (C) 2008-2012 by Eero Tamminen"
1.1.1.2 root 42:
1.1 root 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":
1.1.1.3 root 86: splitter = ';'
1.1 root 87: else:
1.1.1.3 root 88: splitter = ':'
89: for i in os.environ['PATH'].split(splitter):
1.1 root 90: fname = os.path.join(i, name)
91: if os.access(fname, os.X_OK) and not os.path.isdir(fname):
92: return fname
93: return None
94:
95: def get_doc_path(self):
96: """return path or URL to Hatari docs or None"""
97: # first try whether there are local Hatari docs in standard place
98: # for this Hatari/UI version
99: sep = os.sep
100: path = self.get_binary_path("hatari")
101: path = sep.join(path.split(sep)[:-2]) # remove "bin/hatari"
102: path = path + sep + "share" + sep + "doc" + sep + "hatari" + sep
103: if os.path.exists(path + "manual.html"):
104: return path
105: # if not, point to latest Hatari HG version docs
1.1.1.3 root 106: print("WARNING: Hatari manual not found at:", path + "manual.html")
1.1.1.4 root 107: return "http://hg.tuxfamily.org/mercurialroot/hatari/hatari/raw-file/tip/doc/"
1.1 root 108:
109: def set_mainwin(self, widget):
110: self.mainwin = widget
111:
112: def view_url(self, url, name):
113: """view given URL or file path, or error use 'name' as its name"""
114: if self._view and "://" in url or os.path.exists(url):
1.1.1.3 root 115: print("RUN: '%s' '%s'" % (self._view, url))
1.1 root 116: os.spawnlp(os.P_NOWAIT, self._view, self._view, url)
117: return
118: if not self._view:
119: msg = "Cannot view %s, HTML viewer missing" % name
120: else:
121: msg = "Cannot view %s,\n'%s' file is missing" % (name, url)
122: from dialogs import ErrorDialog
123: ErrorDialog(self.mainwin).run(msg)
124:
125: def view_hatari_manual(self, dummy=None):
126: self.view_url(self._path + "manual.html", "Hatari manual")
127:
128: def view_hatari_compatibility(self, dummy=None):
129: self.view_url(self._path + "compatibility.html", "Hatari compatibility list")
130:
131: def view_hatari_releasenotes(self, dummy=None):
132: self.view_url(self._path + "release-notes.txt", "Hatari release notes")
133:
134: def view_hatari_todo(self, dummy=None):
135: self.view_url(self._path + "todo.txt", "Hatari TODO items")
136:
137: def view_hatari_mails(self, dummy=None):
1.1.1.4 root 138: self.view_url("http://hatari.tuxfamily.org/contact.html", "Hatari mailing lists")
1.1 root 139:
140: def view_hatari_repository(self, dummy=None):
1.1.1.4 root 141: self.view_url("http://hg.tuxfamily.org/mercurialroot/hatari/hatari", "latest Hatari changes")
1.1 root 142:
143: def view_hatari_authors(self, dummy=None):
144: self.view_url(self._path + "authors.txt", "Hatari authors")
145:
146: def view_hatari_page(self, dummy=None):
1.1.1.4 root 147: self.view_url("http://hatari.tuxfamily.org/", "Hatari home page")
1.1 root 148:
149: def view_hatariui_page(self, dummy=None):
150: self.view_url("http://koti.mbnet.fi/tammat/hatari/hatari-ui.shtml", "Hatari UI home page")
151:
152:
153: # --------------------------------------------------------
154: # auxiliary class+callback to be used with the PasteDialog
155:
156: class HatariTextInsert:
157: def __init__(self, hatari, text):
158: self.index = 0
159: self.text = text
160: self.pressed = False
161: self.hatari = hatari
1.1.1.3 root 162: print("OUTPUT '%s'" % text)
1.1 root 163: gobject.timeout_add(100, _text_insert_cb, self)
164:
1.1.1.3 root 165: # callback to insert text object to Hatari character at the time
166: # (first key down, on next call up), at given interval
1.1 root 167: def _text_insert_cb(textobj):
168: char = textobj.text[textobj.index]
1.1.1.3 root 169: if char == ' ':
170: # white space gets stripped, use scancode instead
171: char = "57"
1.1 root 172: if textobj.pressed:
173: textobj.pressed = False
1.1.1.3 root 174: textobj.hatari.insert_event("keyup %s" % char)
1.1 root 175: textobj.index += 1
176: if textobj.index >= len(textobj.text):
177: del(textobj)
178: return False
179: else:
180: textobj.pressed = True
1.1.1.3 root 181: textobj.hatari.insert_event("keydown %s" % char)
182: # call again
1.1 root 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:
1.1.1.2 root 220: def create_table_dialog(parent, title, rows, cols, oktext = gtk.STOCK_APPLY):
221: "create_table_dialog(parent,title,rows, cols, oktext) -> (table,dialog)"
1.1 root 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:
1.1.1.2 root 227: table = gtk.Table(rows, cols)
228: table.set_data("col_offset", 0)
1.1 root 229: table.set_col_spacings(8)
230: dialog.vbox.add(table)
231: return (table, dialog)
232:
1.1.1.2 root 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:
1.1 root 237: def table_add_entry_row(table, row, label, size = None):
238: "table_add_entry_row(table,row,label,[entry size]) -> entry"
1.1.1.2 root 239: # add given label to given row in given table
240: # return entry for that line
1.1 root 241: label = gtk.Label(label)
242: align = gtk.Alignment(1) # right aligned
243: align.add(label)
1.1.1.2 root 244: col = table.get_data("col_offset")
245: table.attach(align, col, col+1, row, row+1, gtk.FILL)
246: col += 1
1.1 root 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)
1.1.1.2 root 252: table.attach(align, col, col+1, row, row+1)
1.1 root 253: else:
254: entry = gtk.Entry()
1.1.1.2 root 255: table.attach(entry, col, col+1, row, row+1)
1.1 root 256: return entry
257:
1.1.1.2 root 258: def table_add_widget_row(table, row, label, widget, fullspan = False):
1.1 root 259: "table_add_widget_row(table,row,label,widget) -> widget"
1.1.1.2 root 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")
1.1 root 267: if label:
268: label = gtk.Label(label)
269: align = gtk.Alignment(1)
270: align.add(label)
1.1.1.2 root 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)
1.1 root 277: return widget
278:
1.1.1.2 root 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:
1.1 root 304: def table_add_separator(table, row):
305: "table_add_separator(table,row)"
306: widget = gtk.HSeparator()
1.1.1.2 root 307: endcol = table.get_data("n-columns")
308: # separator for whole table width
309: table.attach(widget, 0, endcol, row, row+1, gtk.FILL)
1.1 root 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:
1.1.1.3 root 346:
347: # File selection button with eject button
348: class FselAndEjectFactory:
349: def __init__(self):
350: pass
351:
352: def get(self, label, path, filename, action):
353: "returns file selection button and box having that + eject button"
354: fsel = gtk.FileChooserButton(label)
355: # Hatari cannot access URIs
356: fsel.set_local_only(True)
357: fsel.set_width_chars(12)
358: fsel.set_action(action)
359: if filename:
360: fsel.set_filename(filename)
361: elif path:
362: fsel.set_current_folder(path)
363: eject = create_button("Eject", self._eject, fsel)
364:
365: box = gtk.HBox()
366: box.pack_start(fsel)
367: box.pack_start(eject, False, False)
368: return (fsel, box)
369:
370: def _eject(self, widget, fsel):
371: fsel.unselect_all()
372:
373:
1.1 root 374: # Gtk is braindead, there's no way to set a default filename
375: # for file chooser button unless it already exists
376: # - set_filename() works only for files that already exist
377: # - set_current_name() works only for SAVE action,
378: # but file chooser button doesn't support that
379: # i.e. I had to do my own (less nice) container widget...
1.1.1.3 root 380: class FselEntry:
1.1 root 381: def __init__(self, parent, validate = None, data = None):
382: self._parent = parent
383: self._validate = validate
384: self._validate_data = data
385: entry = gtk.Entry()
386: entry.set_width_chars(12)
387: entry.set_editable(False)
388: hbox = gtk.HBox()
389: hbox.add(entry)
390: button = create_button("Select...", self._select_file_cb)
391: hbox.pack_start(button, False, False)
392: self._entry = entry
393: self._hbox = hbox
394:
395: def _select_file_cb(self, widget):
396: fname = self._entry.get_text()
397: while True:
398: fname = get_save_filename("Select file", self._parent, fname)
399: if not fname:
400: # assume cancel
401: return
402: if self._validate:
403: # filename needs validation and is valid?
404: if not self._validate(self._validate_data, fname):
405: continue
406: self._entry.set_text(fname)
407: return
408:
409: def set_filename(self, fname):
410: self._entry.set_text(fname)
411:
412: def get_filename(self):
413: return self._entry.get_text()
414:
415: def get_container(self):
416: return self._hbox
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.