|
|
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.7 ! root 37: version = "v1.3"
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.7 ! root 42: copyright = "UI copyright (C) 2008-2015 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])
46:
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
81:
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):
151: self.view_url("http://koti.mbnet.fi/tammat/hatari/hatari-ui.shtml", "Hatari UI home page")
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 root 164: gobject.timeout_add(100, _text_insert_cb, self)
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"
192: button = gtk.Button(label)
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"
201: button = gtk.ToolButton(stock_id)
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"
210: button = gtk.ToggleButton(label)
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
220:
1.1.1.2 root 221: def create_table_dialog(parent, title, rows, cols, oktext = gtk.STOCK_APPLY):
222: "create_table_dialog(parent,title,rows, cols, oktext) -> (table,dialog)"
1.1 root 223: dialog = gtk.Dialog(title, parent,
224: gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
225: (oktext, gtk.RESPONSE_APPLY,
226: gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
227:
1.1.1.2 root 228: table = gtk.Table(rows, cols)
229: table.set_data("col_offset", 0)
1.1 root 230: table.set_col_spacings(8)
231: dialog.vbox.add(table)
232: return (table, dialog)
233:
1.1.1.2 root 234: def table_set_col_offset(table, offset):
235: "set column offset for successive table_* ops on given table"
236: table.set_data("col_offset", offset)
237:
1.1 root 238: def table_add_entry_row(table, row, label, size = None):
239: "table_add_entry_row(table,row,label,[entry size]) -> entry"
1.1.1.2 root 240: # add given label to given row in given table
241: # return entry for that line
1.1 root 242: label = gtk.Label(label)
243: align = gtk.Alignment(1) # right aligned
244: align.add(label)
1.1.1.2 root 245: col = table.get_data("col_offset")
246: table.attach(align, col, col+1, row, row+1, gtk.FILL)
247: col += 1
1.1 root 248: if size:
249: entry = gtk.Entry(size)
250: entry.set_width_chars(size)
251: align = gtk.Alignment(0) # left aligned (default is centered)
252: align.add(entry)
1.1.1.2 root 253: table.attach(align, col, col+1, row, row+1)
1.1 root 254: else:
255: entry = gtk.Entry()
1.1.1.2 root 256: table.attach(entry, col, col+1, row, row+1)
1.1 root 257: return entry
258:
1.1.1.2 root 259: def table_add_widget_row(table, row, label, widget, fullspan = False):
1.1 root 260: "table_add_widget_row(table,row,label,widget) -> widget"
1.1.1.2 root 261: # add given label right aligned to given row in given table
262: # add given widget to the right column and returns it
263: # return entry for that line
264: if fullspan:
265: col = 0
266: else:
267: col = table.get_data("col_offset")
1.1 root 268: if label:
269: label = gtk.Label(label)
270: align = gtk.Alignment(1)
271: align.add(label)
1.1.1.2 root 272: table.attach(align, col, col+1, row, row+1, gtk.FILL)
273: if fullspan:
274: col = table.get_data("col_offset")
275: table.attach(widget, 1, col+2, row, row+1)
276: else:
277: table.attach(widget, col+1, col+2, row, row+1)
1.1 root 278: return widget
279:
1.1.1.2 root 280: def table_add_radio_rows(table, row, label, texts, cb = None):
281: "table_add_radio_rows(table,row,label,texts[,cb]) -> [radios]"
282: # - add given label right aligned to given row in given table
283: # - create/add radio buttons with given texts to next row, set
284: # the one given as "active" as active and set 'cb' as their
285: # "toggled" callback handler
286: # - return array or radiobuttons
287: label = gtk.Label(label)
288: align = gtk.Alignment(1)
289: align.add(label)
290: col = table.get_data("col_offset")
291: table.attach(align, col, col+1, row, row+1, gtk.FILL)
292:
293: radios = []
294: radio = None
295: box = gtk.VBox()
296: for text in texts:
297: radio = gtk.RadioButton(radio, text)
298: if cb:
299: radio.connect("toggled", cb, text)
300: radios.append(radio)
301: box.add(radio)
302: table.attach(box, col+1, col+2, row, row+1)
303: return radios
304:
1.1 root 305: def table_add_separator(table, row):
306: "table_add_separator(table,row)"
307: widget = gtk.HSeparator()
1.1.1.2 root 308: endcol = table.get_data("n-columns")
309: # separator for whole table width
310: table.attach(widget, 0, endcol, row, row+1, gtk.FILL)
1.1 root 311:
312:
313: # -----------------------------
314: # File selection helpers
315:
316: def get_open_filename(title, parent, path = None):
317: buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
318: fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_OPEN, buttons)
319: fsel.set_local_only(True)
320: if path:
321: fsel.set_filename(path)
322: if fsel.run() == gtk.RESPONSE_OK:
323: filename = fsel.get_filename()
324: else:
325: filename = None
326: fsel.destroy()
327: return filename
328:
329: def get_save_filename(title, parent, path = None):
330: buttons = (gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
331: fsel = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_SAVE, buttons)
332: fsel.set_local_only(True)
333: fsel.set_do_overwrite_confirmation(True)
334: if path:
335: fsel.set_filename(path)
336: if not os.path.exists(path):
337: # above set only folder, this is needed to set
338: # the file name when the file doesn't exist
339: fsel.set_current_name(os.path.basename(path))
340: if fsel.run() == gtk.RESPONSE_OK:
341: filename = fsel.get_filename()
342: else:
343: filename = None
344: fsel.destroy()
345: return filename
346:
1.1.1.3 root 347:
348: # File selection button with eject button
349: class FselAndEjectFactory:
350: def __init__(self):
351: pass
352:
353: def get(self, label, path, filename, action):
354: "returns file selection button and box having that + eject button"
355: fsel = gtk.FileChooserButton(label)
356: # Hatari cannot access URIs
357: fsel.set_local_only(True)
358: fsel.set_width_chars(12)
359: fsel.set_action(action)
360: if filename:
361: fsel.set_filename(filename)
362: elif path:
363: fsel.set_current_folder(path)
364: eject = create_button("Eject", self._eject, fsel)
365:
366: box = gtk.HBox()
367: box.pack_start(fsel)
368: box.pack_start(eject, False, False)
369: return (fsel, box)
370:
371: def _eject(self, widget, fsel):
372: fsel.unselect_all()
373:
374:
1.1 root 375: # Gtk is braindead, there's no way to set a default filename
376: # for file chooser button unless it already exists
377: # - set_filename() works only for files that already exist
378: # - set_current_name() works only for SAVE action,
379: # but file chooser button doesn't support that
380: # i.e. I had to do my own (less nice) container widget...
1.1.1.3 root 381: class FselEntry:
1.1 root 382: def __init__(self, parent, validate = None, data = None):
383: self._parent = parent
384: self._validate = validate
385: self._validate_data = data
386: entry = gtk.Entry()
387: entry.set_width_chars(12)
388: entry.set_editable(False)
389: hbox = gtk.HBox()
390: hbox.add(entry)
391: button = create_button("Select...", self._select_file_cb)
392: hbox.pack_start(button, False, False)
393: self._entry = entry
394: self._hbox = hbox
395:
396: def _select_file_cb(self, widget):
397: fname = self._entry.get_text()
398: while True:
399: fname = get_save_filename("Select file", self._parent, fname)
400: if not fname:
401: # assume cancel
402: return
403: if self._validate:
404: # filename needs validation and is valid?
405: if not self._validate(self._validate_data, fname):
406: continue
407: self._entry.set_text(fname)
408: return
409:
410: def set_filename(self, fname):
411: self._entry.set_text(fname)
412:
413: def get_filename(self):
414: return self._entry.get_text()
415:
416: def get_container(self):
417: return self._hbox
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.