diff --git a/GUI/Cocoa/Application.py b/GUI/Cocoa/Application.py new file mode 100644 index 0000000..a456aab --- /dev/null +++ b/GUI/Cocoa/Application.py @@ -0,0 +1,356 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Application class - PyObjC +# +#------------------------------------------------------------------------------ + +import os, sys, traceback +import objc +from Foundation import NSObject, NSBundle, NSDefaultRunLoopMode, NSData, NSDate +import AppKit +from AppKit import NSApplication, NSResponder, NSScreen, NSMenu, NSMenuItem, \ + NSKeyDown, NSKeyUp, NSMouseMoved, NSLeftMouseDown, NSSystemDefined, \ + NSCommandKeyMask, NSPasteboard, NSStringPboardType, NSModalPanelRunLoopMode +NSAnyEventMask = 0xffffffff +from GUI import Globals, GApplications +from GUI import application, export +from GUI.GApplications import Application as GApplication +from GUI import Event + +#------------------------------------------------------------------------------ + +Globals.ns_screen_height = None +Globals.ns_last_mouse_moved_event = None +Globals.pending_exception = None +Globals.ns_application = None + +ns_distant_future = NSDate.distantFuture() + +#------------------------------------------------------------------------------ + +class Application(GApplication): + # _ns_app _PyGui_NSApplication + # _ns_pasteboard NSPasteboard + # _ns_key_window Window + + _ns_menubar_update_pending = False + _ns_files_opened = False + _ns_using_clargs = False + _ns_menus_updated = False + + def __init__(self, **kwds): + self._ns_app = Globals.ns_application + self._ns_app.pygui_app = self + self._ns_pasteboard = NSPasteboard.generalPasteboard() + self._ns_key_window = None + GApplication.__init__(self, **kwds) + self.ns_init_application_name() + + def destroy(self): + del self.menus[:] + import Windows + Windows._ns_zombie_window = None + self._ns_app.pygui_app = None + self._ns_app = None + self._ns_pasteboard = None + GApplication.destroy(self) + + def set_menus(self, menu_list): + GApplication.set_menus(self, menu_list) + self._update_menubar() + + def _update_menubar(self): + ns_app = self._ns_app + ns_menubar = NSMenu.alloc().initWithTitle_("") + menu_list = self._effective_menus() + for menu in menu_list: + ns_item = NSMenuItem.alloc() + ns_item.initWithTitle_action_keyEquivalent_(menu.title, '', "") + ns_menubar.addItem_(ns_item) + ns_menu = menu._ns_menu + # An NSMenu can only be a submenu of one menu at a time, so + # remove it from the old menubar if necessary. + old_supermenu = ns_menu.supermenu() + if old_supermenu: + i = old_supermenu.indexOfItemWithSubmenu_(ns_menu) + old_supermenu.removeItemAtIndex_(i) + ns_menubar.setSubmenu_forItem_(ns_menu, ns_item) + # The menu you pass to setAppleMenu_ must *also* be a member of the + # main menu. + ns_app.setMainMenu_(ns_menubar) + if menu_list: + ns_app_menu = menu_list[0]._ns_menu + ns_app.setAppleMenu_(ns_app_menu) + + def handle_next_event(self, modal_window = None): + ns_app = self._ns_app + if modal_window: + ns_mode = NSModalPanelRunLoopMode + ns_modal_window = modal_window._ns_window + else: + ns_mode = NSDefaultRunLoopMode + ns_modal_window = None + ns_event = ns_app.nextEventMatchingMask_untilDate_inMode_dequeue_( + NSAnyEventMask, ns_distant_future, ns_mode, True) + if ns_event: + ns_window = ns_event.window() + if not ns_window or not ns_modal_window or ns_window == ns_modal_window: + ns_app.sendEvent_(ns_event) + + def get_target_window(self): + # NSApplication.keyWindow() isn't reliable enough. We keep track + # of the key window ourselves. + return self._ns_key_window + + def zero_windows_allowed(self): + return 1 + + def query_clipboard(self): + pb = self._ns_pasteboard + pb_types = pb.types() + return NSStringPboardType in pb_types + + def get_clipboard(self): + pb = self._ns_pasteboard + ns_data = pb.dataForType_(NSStringPboardType) + if ns_data: + return ns_data.bytes().tobytes() + + def set_clipboard(self, data): + ns_data = NSData.dataWithBytes_length_(data, len(data)) + pb = self._ns_pasteboard + pb.clearContents() + pb.setData_forType_(ns_data, NSStringPboardType) + + def setup_menus(self, m): + m.hide_app_cmd.enabled = True + m.hide_other_apps_cmd.enabled = True + m.show_all_apps_cmd.enabled = True + if not self._ns_app.modalWindow(): + GApplication.setup_menus(self, m) + + def process_args(self, args): + # Note: When using py2app, argv_emulation should be disabled. + if args and args[0].startswith("-psn"): + # Launched from MacOSX Finder -- wait for file open/app launch messages + pass + else: + # Not launched from Finder or using argv emulation + self._ns_using_clargs = True + GApplication.process_args(self, args) + + def run(self, fast_exit = True): + try: + GApplication.run(self) + except (KeyboardInterrupt, SystemExit): + pass + except: + traceback.print_exc() + # A py2app bundled application seems to crash on exit if we don't + # bail out really quickly here (Python 2.3, PyObjC 1.3.7, py2app 0.2.1, + # MacOSX 10.4.4) + if fast_exit: + os._exit(0) + + def event_loop(self): + self._ns_app.run() + + def _quit(self): + self._quit_flag = True + self._ns_app.stop_(self._ns_app) + + def hide_app_cmd(self): + self._ns_app.hide_(self) + + def hide_other_apps_cmd(self): + self._ns_app.hideOtherApplications_(self) + + def show_all_apps_cmd(self): + self._ns_app.unhideAllApplications_(self) + + def ns_process_key_event(self, ns_event): + # Perform menu setup before command-key events. + # Send non-command key events to associated window if any, + # otherwise pass them to the pygui application. This is necessary + # because otherwise there is no way of receiving key events when + # there are no windows. + if ns_event.modifierFlags() & NSCommandKeyMask: + NSApplication.sendEvent_(self._ns_app, ns_event) + else: + ns_window = ns_event.window() + if ns_window: + ns_window.sendEvent_(ns_event) + else: + event = Event(ns_event) + self.handle(event.kind, event) + + def ns_menu_needs_update(self, ns_menu): + try: + if not self._ns_menus_updated: + self._perform_menu_setup() + self._ns_menus_updated = True + except Exception: + self.report_exception() + + def ns_init_application_name(self): + # Arrange for the application name to be used as the title + # of the application menu. + ns_bundle = NSBundle.mainBundle() + if ns_bundle: + ns_info = ns_bundle.localizedInfoDictionary() + if not ns_info: + ns_info = ns_bundle.infoDictionary() + if ns_info: + if ns_info['CFBundleName'] == "Python": + #print "GUI.Application: NSBundle infoDictionary =", ns_info ### + ns_info['CFBundleName'] = Globals.application_name + return + +#------------------------------------------------------------------------------ + +_ns_key_event_mask = AppKit.NSKeyDownMask | AppKit.NSKeyUpMask + +#------------------------------------------------------------------------------ + +class _PyGui_NSApplication(NSApplication): + + pygui_app = None + + def sendEvent_(self, ns_event): + # Perform special processing of key events. + # Perform menu setup when menu bar is clicked. + # Remember the most recent mouse-moved event to use as the + # location of event types which do not have a location themselves. + if Globals.pending_exception: + raise_pending_exception() + ns_type = ns_event.type() + self.pygui_app._ns_menus_updated = False + if (1 << ns_type) & _ns_key_event_mask: + self.pygui_app.ns_process_key_event(ns_event) + else: + if ns_type == NSMouseMoved: + Globals.ns_last_mouse_moved_event = ns_event + ns_window = ns_event.window() + if ns_window: + ns_view = ns_window.contentView().hitTest_(ns_event.locationInWindow()) + if ns_view: + ns_view.mouseMoved_(ns_event) + else: + NSApplication.sendEvent_(self, ns_event) + + def menuNeedsUpdate_(self, ns_menu): + self.pygui_app.ns_menu_needs_update(ns_menu) + + def menuSelection_(self, ns_menu_item): + try: + command = ns_menu_item.representedObject() + index = ns_menu_item.tag() + if index >= 0: + dispatch_to_app(self, command, index) + else: + dispatch_to_app(self, command) + except: + self.pygui_app.report_error() + + def validateMenuItem_(self, item): + return False + + def undo_(self, sender): + dispatch_to_app(self, 'undo_cmd') + + def redo_(self, sender): + dispatch_to_app(self, 'redo_cmd') + + def cut_(self, sender): + dispatch_to_app(self, 'cut_cmd') + + def copy_(self, sender): + dispatch_to_app(self, 'copy_cmd') + + def paste_(self, sender): + dispatch_to_app(self, 'paste_cmd') + + def clear_(self, sender): + dispatch_to_app(self, 'clear_cmd') + + def selectAll_(self, sender): + dispatch_to_app(self, 'select_all_cmd') + + def application_openFile_(self, ns_app, path): + app = self.pygui_app + if app._ns_using_clargs: + return True + # Bizarrely, argv[0] gets passed to application_openFile_ under + # some circumstances. We don't want to try to open it! + if path == sys.argv[0]: + return True + app._ns_files_opened = True + try: + app.open_path(path) + return True + except Exception, e: + app.report_error() + return False + + def applicationDidFinishLaunching_(self, notification): + app = self.pygui_app + if app._ns_using_clargs: + return + try: + if not app._ns_files_opened: + app.open_app() + except Exception, e: + app.report_error() + return False + +export(Application) + +#------------------------------------------------------------------------------ + +def raise_pending_exception(): + exc_type, exc_value, exc_tb = Globals.pending_exception + Globals.pending_exception = None + raise exc_type, exc_value, exc_tb + +def create_ns_application(): + ns_app = _PyGui_NSApplication.sharedApplication() + ns_app.setDelegate_(ns_app) + Globals.ns_application = ns_app + +def dispatch_to_app(ns_app, *args): + app = ns_app.pygui_app + if app: + app.dispatch(*args) + +Globals.ns_screen_height = NSScreen.mainScreen().frame().size.height + +create_ns_application() + +#------------------------------------------------------------------------------ + +# Disable this for now, since MachSignals.signal segfaults. :-( +# +#def _install_sigint_handler(): +# print "_install_sigint_handler" ### +# from Foundation import NSRunLoop +# run_loop = NSRunLoop.currentRunLoop() +# if not run_loop: +# print "...No current run loop" ### +# sys.exit(1) ### +# MachSignals.signal(signal.SIGINT, _sigint_handler) +# #from PyObjCTools.AppHelper import installMachInterrupt +# #installMachInterrupt() +# print "...done" ### +# +#def _sigint_handler(signum): +# print "_sigint_handler" ### +# raise KeyboardInterrupt + +#def _install_sigint_handler(): +# import signal +# signal.signal(signal.SIGINT, _raise_keyboard_interrupt) +# +#def _raise_keyboard_interrupt(signum, frame): +# raise KeyboardInterrupt + +#_install_sigint_handler() diff --git a/GUI/Cocoa/BaseAlertFunctions.py b/GUI/Cocoa/BaseAlertFunctions.py new file mode 100644 index 0000000..07c9efe --- /dev/null +++ b/GUI/Cocoa/BaseAlertFunctions.py @@ -0,0 +1,29 @@ +# +# Python GUI - Basic alert functions - Cocoa +# + +from AppKit import \ + NSRunAlertPanel, NSRunCriticalAlertPanel, NSRunInformationalAlertPanel + +def alert(kind, prompt, ok_label, **kwds): + alert_n(kind, prompt, ok_label, None, None) + +def alert2(kind, prompt, yes_label, no_label, **kwds): + return alert_n(kind, prompt, yes_label, no_label, None) + +def alert3(kind, prompt, yes_label, no_label, other_label, **kwds): + return alert_n(kind, prompt, yes_label, no_label, other_label) + +def alert_n(kind, prompt, label1, label2, label3): + splat = prompt.split("\n", 1) + title = splat[0] + if len(splat) > 1: + msg = splat[1] + else: + msg = "" + if kind == 'caution': + return NSRunCriticalAlertPanel(title, msg, label1, label2, label3) + elif kind == 'note': + return NSRunInformationalAlertPanel(title, msg, label1, label2, label3) + else: + return NSRunAlertPanel(title, msg, label1, label2, label3) diff --git a/GUI/Cocoa/BaseFileDialogs.py b/GUI/Cocoa/BaseFileDialogs.py new file mode 100644 index 0000000..600f19b --- /dev/null +++ b/GUI/Cocoa/BaseFileDialogs.py @@ -0,0 +1,63 @@ +# +# Python GUI - File selection dialogs - Cocoa +# + +from AppKit import NSOpenPanel, NSSavePanel, NSOKButton +from GUI.Files import FileRef +from GUI import application + +#------------------------------------------------------------------ + +def _request_old(prompt, default_dir, file_types, dir, multiple): + ns_panel = NSOpenPanel.openPanel() + if prompt.endswith(":"): + prompt = prompt[:-1] + ns_panel.setTitle_(prompt) + ns_panel.setCanChooseFiles_(not dir) + ns_panel.setCanChooseDirectories_(dir) + ns_panel.setAllowsMultipleSelection_(multiple) + if default_dir: + ns_dir = default_dir.path + else: + ns_dir = None + if file_types: + ns_types = [] + for type in file_types: + ns_types.extend(type._ns_file_types()) + else: + ns_types = None + result = ns_panel.runModalForDirectory_file_types_(ns_dir, None, ns_types) + if result == NSOKButton: + if multiple: + return [FileRef(path = path) for path in ns_panel.filenames()] + else: + return FileRef(path = ns_panel.filename()) + else: + return None + +#------------------------------------------------------------------ + +def _request_new(prompt, default_dir, default_name, file_type, dir): + ns_panel = NSSavePanel.savePanel() + #if prompt.endswith(":"): + # prompt = prompt[:-1] + #if prompt.lower().endswith(" as"): + # prompt = prompt[:-3] + #ns_panel.setTitle_(prompt) + #print "_request_new: setting label to", repr(prompt) ### + ns_panel.setNameFieldLabel_(prompt) + if default_dir: + ns_dir = default_dir.path + else: + ns_dir = None + if file_type: + suffix = file_type.suffix + if suffix: + ns_panel.setCanSelectHiddenExtension_(True) + if not file_type.mac_type or file_type.mac_force_suffix: + ns_panel.setRequiredFileType_(suffix) + result = ns_panel.runModalForDirectory_file_(ns_dir, default_name) + if result == NSOKButton: + return FileRef(path = ns_panel.filename()) + else: + return None diff --git a/GUI/Cocoa/Button.py b/GUI/Cocoa/Button.py new file mode 100644 index 0000000..d12deae --- /dev/null +++ b/GUI/Cocoa/Button.py @@ -0,0 +1,45 @@ +# +# Python GUI - Buttons - PyObjC version +# + +import AppKit +from GUI import export +from GUI.StdFonts import system_font +from GUI.ButtonBasedControls import ButtonBasedControl +from GUI.GButtons import Button as GButton + +_style_to_ns_key_equivalent = { + 'default': "\x0d", + 'cancel': "\x1b", +} + +_ns_key_equivalent_to_style = { + "\x0d": 'default', + "\x1b": 'cancel', +} + +class Button(ButtonBasedControl, GButton): + + def __init__(self, title = "New Button", font = system_font, **kwds): + ns_button = self._create_ns_button(title = title, font = font, + ns_button_type = AppKit.NSMomentaryLight, + ns_bezel_style = AppKit.NSRoundedBezelStyle, + padding = (10, 2) + ) + GButton.__init__(self, _ns_view = ns_button, **kwds) + + def get_style(self): + ns_key = self._ns_view.getKeyEquivalent() + return _ns_key_equivalent_to_style.get(ns_key, 'normal') + + def set_style(self, style): + ns_key = _style_to_ns_key_equivalent.get(style, "") + self._ns_view.setKeyEquivalent_(ns_key) + + def activate(self): + self._ns_view.performClick_(None) + +# def key_down(self, e): ### +# print "Button.key_down:", e ### + +export(Button) diff --git a/GUI/Cocoa/ButtonBasedControls.py b/GUI/Cocoa/ButtonBasedControls.py new file mode 100644 index 0000000..fee4a95 --- /dev/null +++ b/GUI/Cocoa/ButtonBasedControls.py @@ -0,0 +1,86 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - PyObjC version +# +# Mixin class for controls based on NSButton +# +#------------------------------------------------------------------------------ + +from Foundation import NSMutableDictionary, NSAttributedString +from AppKit import NSMutableParagraphStyle, NSFontAttributeName, \ + NSForegroundColorAttributeName, NSParagraphStyleAttributeName, \ + NSButton +from GUI.Utils import NSMultiClass, PyGUI_NS_EventHandler, \ + ns_set_action, ns_size_to_fit +from GUI import Control +from GUI.StdColors import black + +#------------------------------------------------------------------------------ + +class ButtonBasedControl(object): + + _ns_handle_mouse = True + + _color = None + + def _create_ns_button(self, title, font, ns_button_type, ns_bezel_style, + padding = (0, 0)): + ns_button = PyGUI_NSButton.alloc().init() + ns_button.pygui_component = self + ns_button.setButtonType_(ns_button_type) + ns_button.setBezelStyle_(ns_bezel_style) + ns_button.setTitle_(title) + ns_button.setFont_(font._ns_font) + num_lines = title.count("\n") + 1 + ns_size_to_fit(ns_button, padding = padding, + height = font.line_height * num_lines + 5) + ns_set_action(ns_button, 'doAction:') + return ns_button + + def set_title(self, title): + Control.set_title(self, title) + self._ns_update_attributed_title() + + def set_font(self, font): + Control.set_font(self, font) + self._ns_update_attributed_title() + + def set_just(self, just): + Control.set_just(self, just) + self._ns_update_attributed_title() + + def get_color(self): + if self._color: + return self._color + else: + return black + + def set_color(self, color): + self._color = color + self._ns_update_attributed_title() + + # There is no direct way of setting the text colour of the title; + # it must be done using an attributed string. But when doing + # this, the attributes must include the font and alignment + # as well. So when using a custom color, we construct a new + # attributed string whenever the title, font, alignment or color + # is changed. + + def _ns_update_attributed_title(self): + if self._color: + ns_button = self._ns_view + ns_attrs = NSMutableDictionary.alloc().init() + ns_attrs[NSFontAttributeName] = ns_button.font() + ns_attrs[NSForegroundColorAttributeName] = self._color._ns_color + ns_parstyle = NSMutableParagraphStyle.alloc().init() + ns_parstyle.setAlignment_(ns_button.alignment()) + ns_attrs[NSParagraphStyleAttributeName] = ns_parstyle + ns_attstr = NSAttributedString.alloc().initWithString_attributes_( + ns_button.title(), ns_attrs) + ns_button.setAttributedTitle_(ns_attstr) + +#------------------------------------------------------------------------------ + +class PyGUI_NSButton(NSButton, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] diff --git a/GUI/Cocoa/Canvas.py b/GUI/Cocoa/Canvas.py new file mode 100644 index 0000000..e65d6b4 --- /dev/null +++ b/GUI/Cocoa/Canvas.py @@ -0,0 +1,321 @@ +# +# Python GUI - Drawing - PyObjC +# + +from array import array +from Foundation import NSPoint, NSMakeRect, NSString +from AppKit import NSGraphicsContext, NSBezierPath, NSEvenOddWindingRule, \ + NSFontAttributeName, NSForegroundColorAttributeName, \ + NSCompositeCopy, NSCompositeSourceOver, NSAffineTransform +from GUI import export +from GUI.StdColors import black, white +from GUI.GCanvases import Canvas as GCanvas +import math + +class Canvas(GCanvas): + + def __init__(self): + self._ns_path = NSBezierPath.bezierPath() + self._ns_path.setWindingRule_(NSEvenOddWindingRule) + self._stack = [] + ctx = NSGraphicsContext.currentContext() + ctx.setCompositingOperation_(NSCompositeSourceOver) + GCanvas.__init__(self) + self._printing = not ctx.isDrawingToScreen() + self.initgraphics() + self.transformstack = [[]] + + def get_pencolor(self): + return self._pencolor + + def set_pencolor(self, c): + self._pencolor = c + + def get_fillcolor(self): + return self._fillcolor + + def set_fillcolor(self, c): + self._fillcolor = c + + def get_textcolor(self): + return self._textcolor + + def set_textcolor(self, c): + self._textcolor = c + + def get_backcolor(self): + return self._backcolor + + def set_backcolor(self, c): + self._backcolor = c + + def get_pensize(self): + return self._pensize + + def set_pensize(self, d): + self._pensize = d + self._ns_path.setLineWidth_(d) + + def get_font(self): + return self._font + + def set_font(self, f): + self._font = f + + def get_current_point(self): + return self._ns_path.currentPoint() + + def newpath(self): + self._ns_path.removeAllPoints() + #for i in range(len(self.transformstack)): + #j = self.transformstack.pop() + #transforms = {"translate":self.translate,"rotate":self.rotate,"scale":self.scale} + #transforms[j[0]](*j[1:]) + + def moveto(self, x, y): + x, y = self._transform(x, y) + self._ns_path.moveToPoint_((x, y)) + + def rmoveto(self, dx, dy): + self._ns_path.relativeMoveToPoint_((dx, dy)) + + def lineto(self, x, y): + x, y = self._transform(x, y) + self._ns_path.lineToPoint_((x, y)) + + def rlineto(self, dx, dy): + self._ns_path.relativeLineToPoint_((dx, dy)) + + def curveto(self, cp1, cp2, ep): + cp1 = self._transform(*cp1) + cp2 = self._transform(*cp2) + ep = self._transform(*ep) + self._ns_path.curveToPoint_controlPoint1_controlPoint2_( + ep, cp1, cp2) + + def rcurveto(self, cp1, cp2, ep): + self._ns_path.relativeCurveToPoint_controlPoint1_controlPoint2_( + ep, cp1, cp2) + + def arc(self, c, r, a0, a1): + c = self._transform(*c) + self._ns_path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_( + c, r, a0, a1) + + def rect(self, rect): + try: + rect = self._transform(rect[0],rect[1]) + self._transform(rect[2], rect[3]) + except: + print "Error here - line 113" + self._ns_path.appendBezierPathWithRect_(_ns_rect(rect)) + + def oval(self, rect): + rect = (self._transform(*rect[0]), self._transform(*rect[1])) + self._ns_path.appendBezierPathWithOvalInRect(_ns_rect(rect)) + + def lines(self, points): + # Due to a memory leak in PyObjC 2.3, we need to be very careful + # about the type of object that we pass to appendBezierPathWithPoints_count_. + # If 'points' is a numpy array, we convert it to an array.array of type 'f', + # else we fall back on iterating over the points in Python. +# ns = self._ns_path +# ns.moveToPoint_(points[0]) +# ns.appendBezierPathWithPoints_count_(points, len(points)) + try: + p = points.flat + except AttributeError: + GCanvas.lines(self, points) + else: + a = array('f', p) + ns = self._ns_path + ns.moveToPoint_(points[0]) + ns.appendBezierPathWithPoints_count_(a, len(points)) + + + def poly(self, points): +# ns = self._ns_path +# ns.moveToPoint_(points[0]) +# ns.appendBezierPathWithPoints_count_(points, len(points)) +# ns.closePath() + self.lines(points) + self.closepath() + + def closepath(self): + self._ns_path.closePath() + + def clip(self): + ns = self._ns_path + ns.addClip() + + def rectclip(self, (l, t, r, b)): + ns_rect = NSMakeRect(l, t, r - l, b - t) + NSBezierPath.clipRect_(ns_rect) + + def gsave(self): + self._stack.append(( + self._pencolor, self._fillcolor, self._textcolor, self._backcolor, + self._pensize, self._font)) + self.transformstack.append([]) + NSGraphicsContext.currentContext().saveGraphicsState() + + def grestore(self): + (self._pencolor, self._fillcolor, self._textcolor, self._backcolor, + self._pensize, self._font) = self._stack.pop() + self.transformstack.pop() + NSGraphicsContext.currentContext().restoreGraphicsState() + + def stroke(self): + ns = self._ns_path + self._pencolor._ns_color.set() + ns.stroke() + + def fill(self): + ns = self._ns_path + self._fillcolor._ns_color.set() + ns.fill() + + def erase(self): + ns = self._ns_path + self._backcolor._ns_color.set() + ctx = NSGraphicsContext.currentContext() + ctx.setCompositingOperation_(NSCompositeCopy) + ns.fill() + ctx.setCompositingOperation_(NSCompositeSourceOver) + + def fill_stroke(self): + ns = self._ns_path + self._pencolor._ns_color.set() + ns.stroke() + self._fillcolor._ns_color.set() + ns.fill() + + def show_text(self, text): + x, y = self._ns_path.currentPoint() + font = self._font + ns_font = font._ns_font + ns_color = self._textcolor._ns_color + ns_string = NSString.stringWithString_(text) + ns_attrs = { + NSFontAttributeName: ns_font, + NSForegroundColorAttributeName: ns_color, + } +# print "Canvas.show_text:", repr(text) ### +# print "family:", ns_font.familyName() ### +# print "size:", ns_font.pointSize() ### +# print "ascender:", ns_font.ascender() ### +# print "descender:", ns_font.descender() ### +# print "capHeight:", ns_font.capHeight() ### +# print "leading:", ns_font.leading() ### +# print "matrix:", ns_font.matrix() ### +# print "defaultLineHeightForFont:", ns_font.defaultLineHeightForFont() ### + h = ns_font.defaultLineHeightForFont() + d = -ns_font.descender() + dy = h - d + if ns_font.familyName() == "Courier New": + dy += ns_font.pointSize() * 0.229167 + ns_point = NSPoint(x, y - dy) + #print "drawing at:", ns_point ### + ns_string.drawAtPoint_withAttributes_(ns_point, ns_attrs) + dx = ns_font.widthOfString_(ns_string) + #self._ns_path.relativeMoveToPoint_(NSPoint(x + dx, y)) + self._ns_path.relativeMoveToPoint_((dx, 0)) + + def _ns_frame_rect(self, (l, t, r, b)): + p = self._pensize + q = 0.5 * p + return NSMakeRect(l + q, t + q, r - l - p, b - t - p) + + def stroke_rect(self, r): + self._pencolor._ns_color.set() + NSBezierPath.setDefaultLineWidth_(self._pensize) + NSBezierPath.strokeRect_(_ns_rect(r)) + + def frame_rect(self, r): + self._pencolor._ns_color.set() + NSBezierPath.setDefaultLineWidth_(self._pensize) + NSBezierPath.strokeRect_(self._ns_frame_rect(r)) + + def fill_rect(self, r): + self._fillcolor._ns_color.set() + NSBezierPath.fillRect_(_ns_rect(r)) + + def erase_rect(self, r): + self._backcolor._ns_color.set() + NSBezierPath.fillRect_(_ns_rect(r)) + + def _ns_oval_path(self, ns_rect): + ns_path = NSBezierPath.bezierPathWithOvalInRect_(ns_rect) + ns_path.setLineWidth_(self._pensize) + return ns_path + + def stroke_oval(self, r): + self._pencolor._ns_color.set() + self._ns_oval_path(_ns_rect(r)).stroke() + + def frame_oval(self, r): + self._pencolor._ns_color.set() + self._ns_oval_path(self._ns_frame_rect(r)).stroke() + + def fill_oval(self, r): + self._fillcolor._ns_color.set() + self._ns_oval_path(_ns_rect(r)).fill() + + def erase_oval(self, r): + self._backcolor._ns_color.set() + self._ns_oval_path(_ns_rect(r)).fill() + + def _ns_arc_path(self, c, r, sa, ea): + ns_path = NSBezierPath.bezierPath() + ns_path.setLineWidth_(self._pensize) + ns_path.\ + appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_( + c, r, sa, ea) + return ns_path + + def stroke_arc(self, center, radius, start_angle, arc_angle): + ns_path = self._ns_arc_path(center, radius, start_angle, arc_angle) + self._pencolor._ns_color.set() + ns_path.stroke() + + def frame_arc(self, center, radius, start_angle, arc_angle): + r = radius - 0.5 * self._pensize + ns_path = self._ns_arc_path(center, r, start_angle, arc_angle) + self._pencolor._ns_color.set() + ns_path.stroke() + + def translate(self, dx, dy): + matrix = NSAffineTransform.transform() + matrix.translateXBy_yBy_(dx, dy) + self._ns_path.transformUsingAffineTransform_(matrix) + self.transformstack[-1].append(["translate",dx,dy]) + + def rotate(self, rotation): + matrix = NSAffineTransform.transform() + matrix.rotateByDegrees_(rotation) + self._ns_path.transformUsingAffineTransform_(matrix) + self.transformstack[-1].append(["rotate",rotation]) + + def scale(self, sx, sy): + matrix = NSAffineTransform.transform() + matrix.scaleXBy_yBy_(sx, sy) + self._ns_path.transformUsingAffineTransform_(matrix) + self.transformstack[-1].append(["scale",sx,sy]) + def _transform(self, x, y): + for i in self.transformstack: #reversed(self.transformstack): + for j in i: # reversed(i): + if j[0]=="translate": + x = x+j[1] + y = y+j[2] + elif j[0]=="rotate": + x = x*math.cos(j[1])-y*math.sin(j[1]) + y = x*math.sin(j[1])+y*math.cos(j[1]) + elif j[0]=="scale": + x = x*j[1] + y = y*j[2] + return x, y + +def _ns_rect((l, t, r, b)): + return NSMakeRect(l, t, r - l, b - t) + +export(Canvas) + diff --git a/GUI/Cocoa/CheckBox.py b/GUI/Cocoa/CheckBox.py new file mode 100644 index 0000000..bc0732e --- /dev/null +++ b/GUI/Cocoa/CheckBox.py @@ -0,0 +1,53 @@ +# +# Python GUI - Check boxes - PyObjC +# + +import AppKit +from AppKit import NSOnState, NSOffState, NSMixedState +from GUI import export +from GUI.Actions import Action +from GUI.StdFonts import system_font +from GUI.ButtonBasedControls import ButtonBasedControl +from GUI.GCheckBoxes import CheckBox as GCheckBox + +class CheckBox(ButtonBasedControl, GCheckBox): + + _ns_mixed = False + + def __init__(self, title = "New Check Box", font = system_font, **kwds): + ns_button = self._create_ns_button(title = title, font = font, + ns_button_type = AppKit.NSSwitchButton, + ns_bezel_style = AppKit.NSRoundedBezelStyle) + #if mixed: + # self._ns_mixed = True + # ns_button.setAllowsMixedState_(True) + GCheckBox.__init__(self, _ns_view = ns_button, **kwds) + + def get_mixed(self): + return self._ns_view.allowsMixedState() + + def set_mixed(self, x): + self._ns_view.setAllowsMixedState_(x) + + def get_on(self): + state = self._ns_view.state() + if state == NSMixedState: + return 'mixed' + else: + return state <> NSOffState + + def set_on(self, v): + if v == 'mixed' and self.mixed: + state = NSMixedState + elif v: + state = NSOnState + else: + state = NSOffState + self._ns_view.setState_(state) + + def do_action(self): + if not self._auto_toggle: + self.on = not self.on + Action.do_action(self) + +export(CheckBox) diff --git a/GUI/Cocoa/Color.py b/GUI/Cocoa/Color.py new file mode 100644 index 0000000..a756d38 --- /dev/null +++ b/GUI/Cocoa/Color.py @@ -0,0 +1,47 @@ +# +# Python GUI - Colors - PyObjC +# + +from AppKit import NSColor, NSCalibratedRGBColorSpace +from GUI import export +from GUI.GColors import Color as GColor + +NSColor.setIgnoresAlpha_(False) + +class Color(GColor): + + def _from_ns_color(cls, ns_color): + color = cls.__new__(cls) + color._ns_color = ns_color.colorUsingColorSpaceName_( + NSCalibratedRGBColorSpace) + return color + + _from_ns_color = classmethod(_from_ns_color) + + def __init__(self, red, green, blue, alpha = 1.0): + self._ns_color = NSColor.colorWithCalibratedRed_green_blue_alpha_( + red, green, blue, alpha) + + def get_red(self): + return self._ns_color.redComponent() + + def get_green(self): + return self._ns_color.greenComponent() + + def get_blue(self): + return self._ns_color.blueComponent() + + def get_alpha(self): + return self._ns_color.alphaComponent() + + def get_rgb(self): + return self.get_rgba()[:3] + + def get_rgba(self): + m = self._ns_color.getRed_green_blue_alpha_ + try: + return m() + except TypeError: + return m(None, None, None, None) + +export(Color) diff --git a/GUI/Cocoa/Colors.py b/GUI/Cocoa/Colors.py new file mode 100644 index 0000000..cf159f4 --- /dev/null +++ b/GUI/Cocoa/Colors.py @@ -0,0 +1,11 @@ +# +# Python GUI - Color constants and functions - Cocoa +# + +from AppKit import NSColor +from GUI import Color + +rgb = Color + +selection_forecolor = Color._from_ns_color(NSColor.selectedTextColor()) +selection_backcolor = Color._from_ns_color(NSColor.selectedTextBackgroundColor()) diff --git a/GUI/Cocoa/Component.py b/GUI/Cocoa/Component.py new file mode 100644 index 0000000..895a9c2 --- /dev/null +++ b/GUI/Cocoa/Component.py @@ -0,0 +1,128 @@ +# +# Python GUI - Components - PyObjC +# + +from Foundation import NSRect, NSPoint, NSSize, NSObject +from GUI import export +from GUI import Globals, application +from GUI import Event +from GUI.GComponents import Component as GComponent + +#------------------------------------------------------------------------------ + +Globals._ns_view_to_component = {} # Mapping from NSView to corresponding Component + +#------------------------------------------------------------------------------ + +class Component(GComponent): + + _has_local_coords = True + _generic_tabbing = False + _ns_pass_mouse_events_to_platform = False + _ns_handle_mouse = False + _ns_accept_first_responder = False + + def __init__(self, _ns_view, _ns_inner_view = None, _ns_responder = None, + _ns_set_autoresizing_mask = True, **kwds): + self._ns_view = _ns_view + if not _ns_inner_view: + _ns_inner_view = _ns_view + self._ns_inner_view = _ns_inner_view + self._ns_responder = _ns_responder or _ns_inner_view + Globals._ns_view_to_component[_ns_view] = self + GComponent.__init__(self, **kwds) + + def destroy(self): + #print "Component.destroy:", self ### + GComponent.destroy(self) + _ns_view = self._ns_view + if _ns_view in Globals._ns_view_to_component: + #print "Component.destroy: removing", _ns_view, "from mapping" ### + del Globals._ns_view_to_component[_ns_view] + #print "Component.destroy: breaking link to", self._ns_view ### + self._ns_view = None + #if self._ns_inner_view: print "Component.destroy: breaking inner link to", self._ns_inner_view ### + self._ns_inner_view = None + self._ns_responder = None + + def get_bounds(self): + (l, t), (w, h) = self._ns_view.frame() + return (l, t, l + w, t + h) + + def set_bounds(self, (l, t, r, b)): + ns = self._ns_view + w0, h0 = ns.frame().size + w1 = r - l + h1 = b - t + ns_frame = ((l, t), (w1, h1)) + old_ns_frame = ns.frame() + ns.setFrame_(ns_frame) + sv = ns.superview() + if sv: + sv.setNeedsDisplayInRect_(old_ns_frame) + sv.setNeedsDisplayInRect_(ns_frame) + if w0 != w1 or h0 != h1: + self._resized((w1 - w0, h1 - h0)) + + def become_target(self): + ns_view = self._ns_view + ns_window = ns_view.window() + if ns_window: + self._ns_accept_first_responder = True + ns_window.makeFirstResponder_(ns_view) + self._ns_accept_first_responder = False + + def _ns_pass_to_platform(self, event, method_name): + #print "Component._ns_pass_to_platform:", self ### + h = self._ns_responder + b = h.__class__.__bases__[0] + m = getattr(b, method_name) + #print "...ns responder =", object.__repr__(h) ### + #print "...ns base class =", b ### + #print "...ns method =", m ### + m(h, event._ns_event) + + def mouse_down(self, event): + if self._ns_handle_mouse: + self._ns_pass_to_platform(event, ns_mouse_down_methods[event.button]) + + def mouse_drag(self, event): + if self._ns_handle_mouse: + self._ns_pass_to_platform(event, 'mouseDragged_') + + def mouse_up(self, event): + if self._ns_handle_mouse: + self._ns_pass_to_platform(event, ns_mouse_up_methods[event.button]) + + def mouse_move(self, event): + #self._ns_pass_to_platform(event, 'mouseMoved_') + pass + + def mouse_enter(self, event): + #self._ns_pass_to_platform(event, 'mouseEntered_') + pass + + def mouse_leave(self, event): + #self._ns_pass_to_platform(event, 'mouseExited_') + pass + + def key_down(self, event): + #print "Component.key_down:", repr(event.char), "for", self ### + self._ns_pass_to_platform(event, 'keyDown_') + + def key_up(self, event): + self._ns_pass_to_platform(event, 'keyUp_') + +#------------------------------------------------------------------------------ + +ns_mouse_down_methods = { + 'left': 'mouseDown_', 'middle': 'otherMouseDown_', 'right': 'rightMouseDown_' +} + +ns_mouse_up_methods = { + 'left': 'mouseUp_', 'middle': 'otherMouseUp_', 'right': 'rightMouseUp_' +} + +#------------------------------------------------------------------------------ + +export(Component) diff --git a/GUI/Cocoa/Container.py b/GUI/Cocoa/Container.py new file mode 100644 index 0000000..e578481 --- /dev/null +++ b/GUI/Cocoa/Container.py @@ -0,0 +1,31 @@ +# +# Python GUI - Containers - PyObjC version +# + +from AppKit import NSView +from GUI.Utils import PyGUI_Flipped_NSView +from GUI import export +from GUI.GContainers import Container as GContainer + +class Container(GContainer): + # _ns_inner_view NSView Containing NSView for subcomponents + +# def __init__(self, _ns_view, **kwds): +# GContainer.__init__(self, _ns_view = _ns_view, **kwds) + +# def destroy(self): +# #print "Container.destroy:", self ### +# GContainer.destroy(self) +# #print "Container.destroy: breaking inner link to", self._ns_inner_view ### + + def _add(self, comp): + GContainer._add(self, comp) + self._ns_inner_view.addSubview_(comp._ns_view) + + def _remove(self, comp): + GContainer._remove(self, comp) + comp._ns_view.removeFromSuperview() + +#------------------------------------------------------------------------------ + +export(Container) diff --git a/GUI/Cocoa/Control.py b/GUI/Cocoa/Control.py new file mode 100644 index 0000000..17bc4a0 --- /dev/null +++ b/GUI/Cocoa/Control.py @@ -0,0 +1,68 @@ +# +# Python GUI - Controls - PyObjC +# + +from math import ceil +from Foundation import NSSize +import AppKit +from GUI import export +from GUI import StdColors +from GUI import Color +from GUI import Font +from GUI.GControls import Control as GControl + +_ns_alignment_from_just = { + 'left': AppKit.NSLeftTextAlignment, + 'center': AppKit.NSCenterTextAlignment, + 'centre': AppKit.NSCenterTextAlignment, + 'right': AppKit.NSRightTextAlignment, + 'flush': AppKit.NSJustifiedTextAlignment, + '': AppKit.NSNaturalTextAlignment, +} + +_ns_alignment_to_just = { + AppKit.NSLeftTextAlignment: 'left', + AppKit.NSCenterTextAlignment: 'center', + AppKit.NSRightTextAlignment: 'right', + AppKit.NSJustifiedTextAlignment: 'flush', + AppKit.NSNaturalTextAlignment: '', +} + +class Control(GControl): + + #_vertical_padding = 5 + + def get_title(self): + return self._ns_cell().title() + + def set_title(self, v): + self._ns_cell().setTitle_(v) + + def get_enabled(self): + return self._ns_cell().enabled() + + def set_enabled(self, v): + self._ns_cell().setEnabled_(v) + + def get_color(self): + return StdColors.black + + def set_color(self, v): + pass + + def get_font(self): + return Font._from_ns_font(self._ns_cell().font()) + + def set_font(self, f): + self._ns_cell().setFont_(f._ns_font) + + def get_just(self): + return _ns_alignment_to_just[self._ns_cell().alignment()] + + def set_just(self, v): + self._ns_cell().setAlignment_(_ns_alignment_from_just[v]) + + def _ns_cell(self): + return self._ns_inner_view.cell() + +export(Control) diff --git a/GUI/Cocoa/Cursor.py b/GUI/Cocoa/Cursor.py new file mode 100644 index 0000000..48a86f0 --- /dev/null +++ b/GUI/Cocoa/Cursor.py @@ -0,0 +1,27 @@ +# +# Python GUI - Cursors - Cocoa +# + +from AppKit import NSCursor +from GUI import export +from GUI.GCursors import Cursor as GCursor + +class Cursor(GCursor): + # + # _ns_cursor NSCursor + + def _from_ns_cursor(cls, ns_cursor): + cursor = cls.__new__(cls) + cursor._ns_cursor = ns_cursor + return cursor + + _from_ns_cursor = classmethod(_from_ns_cursor) + + def _init_from_image_and_hotspot(self, image, hotspot): + #print "Cursor._init_from_image_and_hotspot:", image, hotspot ### + ns_image = image._ns_image.copy() + ns_image.setFlipped_(False) + self._ns_cursor = NSCursor.alloc().initWithImage_hotSpot_( + ns_image, hotspot) + +export(Cursor) diff --git a/GUI/Cocoa/Dialog.py b/GUI/Cocoa/Dialog.py new file mode 100644 index 0000000..92016a6 --- /dev/null +++ b/GUI/Cocoa/Dialog.py @@ -0,0 +1,17 @@ +# +# Python GUI - Dialogs - Cocoa +# + +from GUI import export +from GUI.GDialogs import Dialog #as GDialog + +#class Dialog(GDialog): +# +# _default_keys = ['\r'] +# _cancel_keys = ['\x1b'] +# +# def key_down(self, event): +# # Cocoa already takes care of default/cancel button activation +# self.pass_to_next_handler('key_down', event) + +export(Dialog) diff --git a/GUI/Cocoa/DrawableContainer.py b/GUI/Cocoa/DrawableContainer.py new file mode 100644 index 0000000..5ddb84f --- /dev/null +++ b/GUI/Cocoa/DrawableContainer.py @@ -0,0 +1,86 @@ +# +# Python GUI - DrawableContainers - PyObjC +# + +from Foundation import NSMakeRect +from AppKit import NSView, NSScrollView, NSColor +from GUI import export +from GUI.Utils import PyGUI_Flipped_NSView +from GUI import Canvas +from GUI.Geometry import rect_to_ns_rect +from GUI.Utils import NSMultiClass, PyGUI_NS_ViewBase +from GUI.GDrawableContainers import default_size, \ + DrawableContainer as GDrawableContainer + +ns_gray = NSColor.grayColor() + +class DrawableContainer(GDrawableContainer): + + def __init__(self, **kwds): + width, height = default_size + ns_frame = NSMakeRect(0, 0, width, height) + ns_inner_view = PyGUI_User_NSView.alloc().initWithFrame_(ns_frame) + if self._ns_scrollable: + ns_view = NSScrollView.alloc().initWithFrame_(ns_frame) + ns_view.setDocumentView_(ns_inner_view) + ns_view.setBackgroundColor_(ns_gray) + else: + ns_view = ns_inner_view + ns_inner_view.pygui_component = self + GDrawableContainer.__init__(self, _ns_view = ns_view, _ns_inner_view = ns_inner_view) + self.set(**kwds) + + def destroy(self): + #print "View.destroy:", self ### + ns_inner_view = self._ns_inner_view + GDrawableContainer.destroy(self) + if ns_inner_view: + #print "View.destroy: breaking back link from", ns_inner_view ### + ns_inner_view.pygui_component = None + + def get_background_color(self): + ns_view = self._ns_inner_view + if ns_view.drawsBackground(): + return Color._from_ns_color(ns_view.backgroundColor()) + + def set_background_color(self, x): + ns_view = self._ns_inner_view + if x: + ns_view.setBackgroundColor_(x._ns_color) + ns_view.setDrawsBackground_(True) + else: + ns_view.setDrawsBackground_(False) + + def invalidate(self): + self._ns_inner_view.setNeedsDisplay_(True) + + def invalidate_rect(self, r): + self._ns_inner_view.setNeedsDisplayInRect_(rect_to_ns_rect(r)) + + def with_canvas(self, proc): + ns_view = self._ns_view + ns_view.lockFocus() + proc(Canvas()) + ns_view.unlockFocus() + + def update(self): + self._ns_view.displayIfNeeded() + + def track_mouse(self): + return self._ns_track_mouse(self._ns_inner_view) + +#------------------------------------------------------------------------------ + +class PyGUI_User_NSView(PyGUI_Flipped_NSView, PyGUI_NS_ViewBase): + # + # pygui_component View + + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + + def drawRect_(self, ns_rect): + (l, t), (w, h) = ns_rect + rect = (l, t, l + w, t + h) + self.pygui_component.draw(Canvas(), rect) + +export(DrawableContainer) diff --git a/GUI/Cocoa/EditCmdHandler.py b/GUI/Cocoa/EditCmdHandler.py new file mode 100644 index 0000000..876a752 --- /dev/null +++ b/GUI/Cocoa/EditCmdHandler.py @@ -0,0 +1,62 @@ +# +# PyGUI - Edit command handling - Cocoa +# + +from AppKit import NSMenuItem +from GUI import export + +class EditCmdHandler(object): + # Mixin for Components whose _ns_responder handles the + # standard editing commands. + + def setup_menus(self, m): + def validate(cmd_name, ns_selector): + ns_menu_item = NSMenuItem.alloc().\ + initWithTitle_action_keyEquivalent_("", ns_selector, "") + m[cmd_name].enabled = ns_target.validateMenuItem_(ns_menu_item) + ns_target = self.window._ns_window + if ns_target: + validate('undo_cmd', 'undo:') + validate('redo_cmd', 'redo:') + ns_target = self._ns_edit_cmd_target() + if ns_target: + validate('cut_cmd', 'cut:') + validate('copy_cmd', 'copy:') + validate('paste_cmd', 'paste:') + validate('clear_cmd', 'delete:') + validate('select_all_cmd', 'selectAll:') + + def undo_cmd(self): + ns_window = self.window._ns_window + if ns_window: + ns_window.undo_(None) + + def redo_cmd(self): + ns_window = self.window._ns_window + if ns_window: + ns_window.redo_(None) + + def cut_cmd(self): + self._ns_edit_cmd('cut_') + + def copy_cmd(self): + self._ns_edit_cmd('copy_') + + def paste_cmd(self): + self._ns_edit_cmd('paste_') + + def clear_cmd(self): + self._ns_edit_cmd('delete_') + + def select_all_cmd(self): + self._ns_edit_cmd('selectAll_') + + def _ns_edit_cmd(self, ns_method_name): + ns_target = self._ns_edit_cmd_target() + if ns_target: + getattr(ns_target, ns_method_name)(None) + + def _ns_edit_cmd_target(self): + return self._ns_responder + +export(EditCmdHandler) diff --git a/GUI/Cocoa/Event.py b/GUI/Cocoa/Event.py new file mode 100644 index 0000000..6941271 --- /dev/null +++ b/GUI/Cocoa/Event.py @@ -0,0 +1,167 @@ +# +# Python GUI - Events - PyObjC version +# + +import AppKit +from AppKit import NSEvent, \ + NSShiftKeyMask, NSControlKeyMask, NSCommandKeyMask, NSAlternateKeyMask +from GUI import export +from GUI import Globals +from GUI.GEvents import Event as GEvent + +_ns_event_type_to_kind = { + AppKit.NSLeftMouseDown: 'mouse_down', + AppKit.NSLeftMouseUp: 'mouse_up', + AppKit.NSRightMouseDown: 'mouse_down', + AppKit.NSRightMouseUp: 'mouse_up', + AppKit.NSOtherMouseDown: 'mouse_down', + AppKit.NSOtherMouseUp: 'mouse_up', + AppKit.NSMouseMoved: 'mouse_move', + AppKit.NSLeftMouseDragged: 'mouse_drag', + AppKit.NSRightMouseDragged: 'mouse_drag', + AppKit.NSOtherMouseDragged: 'mouse_drag', + AppKit.NSMouseEntered: 'mouse_enter', + AppKit.NSMouseExited: 'mouse_leave', + AppKit.NSKeyDown: 'key_down', + AppKit.NSKeyUp: 'key_up', + AppKit.NSFlagsChanged: 'flags_changed', + AppKit.NSAppKitDefined: 'app_kit_defined', + AppKit.NSSystemDefined: 'system_defined', + AppKit.NSApplicationDefined: 'application_defined', + AppKit.NSPeriodic: 'periodic', + AppKit.NSCursorUpdate: 'cursor_update', +} + +_ns_event_type_to_button = { + AppKit.NSLeftMouseDown: 'left', + AppKit.NSLeftMouseUp: 'left', + AppKit.NSRightMouseDown: 'right', + AppKit.NSRightMouseUp: 'right', + AppKit.NSOtherMouseDown: 'middle', + AppKit.NSOtherMouseUp: 'middle', + AppKit.NSLeftMouseDragged: 'left', + AppKit.NSRightMouseDragged: 'right', + AppKit.NSOtherMouseDragged: 'middle', +} + +_ns_keycode_to_keyname = { + AppKit.NSUpArrowFunctionKey: 'up_arrow', + AppKit.NSDownArrowFunctionKey: 'down_arrow', + AppKit.NSLeftArrowFunctionKey: 'left_arrow', + AppKit.NSRightArrowFunctionKey: 'right_arrow', + AppKit.NSF1FunctionKey: 'f1', + AppKit.NSF2FunctionKey: 'f2', + AppKit.NSF3FunctionKey: 'f3', + AppKit.NSF4FunctionKey: 'f4', + AppKit.NSF5FunctionKey: 'f5', + AppKit.NSF6FunctionKey: 'f6', + AppKit.NSF7FunctionKey: 'f7', + AppKit.NSF8FunctionKey: 'f8', + AppKit.NSF9FunctionKey: 'f9', + AppKit.NSF10FunctionKey: 'f10', + AppKit.NSF11FunctionKey: 'f11', + AppKit.NSF12FunctionKey: 'f12', + AppKit.NSF13FunctionKey: 'f13', + AppKit.NSF14FunctionKey: 'f14', + AppKit.NSF15FunctionKey : 'f15', + AppKit.NSDeleteFunctionKey: 'delete', + AppKit.NSHomeFunctionKey: 'home', + AppKit.NSEndFunctionKey: 'end', + AppKit.NSPageUpFunctionKey: 'page_up', + AppKit.NSPageDownFunctionKey: 'page_down', + AppKit.NSClearLineFunctionKey: 'clear', + #AppKit.NSHelpFunctionKey: 'help', + AppKit.NSHelpFunctionKey: 'insert', + "\r": 'return', + "\x03": 'enter', +} + +_mouse_events = [ + 'mouse_down', 'mouse_drag', 'mouse_up', + 'mouse_move', 'mouse_enter', 'mouse_exit' +] + +_key_events = [ + 'key_down', 'key_up' +] + +_ns_screen_height = None + +class Event(GEvent): + """Platform-dependent modifiers (boolean): + command The Macintosh Command key. + option The Macintosh Option key. + """ + + global_position = (0, 0) + position = (0, 0) + button = '' + num_clicks = 0 + char = "" + unichars = "" + key = '' + auto = False + delta = (0, 0) + + def __init__(self, ns_event): + self._ns_event = ns_event + _ns_type = ns_event.type() + kind = _ns_event_type_to_kind[_ns_type] + self.kind = kind + self.time = ns_event.timestamp() + ns_window = ns_event.window() + is_mouse_event = kind in _mouse_events + if is_mouse_event: + ns_win_pos = ns_event.locationInWindow() + x, y = ns_window.convertBaseToScreen_(ns_win_pos) + else: + ns_last_mouse = Globals.ns_last_mouse_moved_event + if ns_last_mouse: + ns_window = ns_last_mouse.window() + if ns_window: + ns_win_pos = ns_last_mouse.locationInWindow() + x, y = ns_window.convertBaseToScreen_(ns_win_pos) + else: + x, y = ns_last_mouse.locationInWindow() + else: + x, y = NSEvent.mouseLocation() + h = Globals.ns_screen_height + self.global_position = (x, h - y) + if is_mouse_event: + self.button = _ns_event_type_to_button.get(_ns_type, '') + if kind == 'mouse_down': + self.num_clicks = ns_event.clickCount() + self.delta = (ns_event.deltaX(), ns_event.deltaY()) + ns_flags = ns_event.modifierFlags() + self.shift = self.extend_contig = (ns_flags & NSShiftKeyMask) <> 0 + self.control = (ns_flags & NSControlKeyMask) <> 0 + self.command = self.extend_noncontig = (ns_flags & NSCommandKeyMask) <> 0 + self.option = (ns_flags & NSAlternateKeyMask) <> 0 + if kind in _key_events: + self.auto = ns_event.isARepeat() + ns_chars = ns_event.characters() + #print "Event.__init__: ns_chars =", repr(ns_chars) ### + self.unichars = ns_chars + if len(ns_chars) == 1: + if ns_chars == "\x19" and ns_event.keyCode() == 48: + self.char = "\t" + elif ns_chars == "\x7f": + self.char = "\x08" + elif ns_chars <= "\x7e": + self.char = str(ns_chars) + #else: + # self.char = ns_chars + ns_unmod = ns_event.charactersIgnoringModifiers() + key = _ns_keycode_to_keyname.get(ns_chars, '') + if not key and u"\x20" <= ns_unmod <= u"\x7e": + key = str(ns_unmod) + self.key = key + if key == 'enter': + self.char = "\r" + elif key == 'delete': + self.char = "\x7f" + + def _platform_modifiers_str(self): + return " command:%s option:%s" % (self.command, self.option) + +export(Event) diff --git a/GUI/Cocoa/Files.py b/GUI/Cocoa/Files.py new file mode 100644 index 0000000..3d78a3f --- /dev/null +++ b/GUI/Cocoa/Files.py @@ -0,0 +1,43 @@ +# +# Python GUI - File references and types - Cocoa +# + +from struct import unpack +from Foundation import NSFileTypeForHFSTypeCode, \ + NSFileManager, NSFileHFSCreatorCode, NSFileHFSTypeCode +from GUI.GFiles import FileRef as GFileRef, DirRef, FileType as GFileType + +class FileType(GFileType): + + def _ns_file_types(self): + # Return list of Cocoa file type specifications matched + # by this file type. + result = [] + mac_type = self._mac_type + if mac_type: + result.append(NSFileTypeForHFSTypeCode(mac_type)) + suffix = self._suffix + if suffix: + result.append(suffix) + return result + + +class FileRef(GFileRef): + + def _set_type(self, file_type): + creator = file_type.mac_creator + type = file_type.mac_type + if creator is not None or type is not None: + fm = NSFileManager.defaultManager() + attrs = {} + if creator is not None: + attrs[NSFileHFSCreatorCode] = four_char_code(creator) + if type is not None: + attrs[NSFileHFSTypeCode] = four_char_code(type) + #print "FileRef: Setting attributes of %r to %s" % ( ### + # self.path, attrs) ### + fm.changeFileAttributes_atPath_(attrs, self.path) + + +def four_char_code(chars): + return unpack(">L", chars)[0] diff --git a/GUI/Cocoa/Font.py b/GUI/Cocoa/Font.py new file mode 100644 index 0000000..586ee46 --- /dev/null +++ b/GUI/Cocoa/Font.py @@ -0,0 +1,77 @@ +# +# Python GUI - Fonts - PyObjC +# + +import sys +from AppKit import NSFont, NSFontManager, NSBoldFontMask, NSItalicFontMask, \ + NSLayoutManager +from GUI import export +from GUI.GFonts import Font as GFont + +_ns_font_manager = NSFontManager.sharedFontManager() +_ns_layout_manager = NSLayoutManager.alloc().init() + +class Font(GFont): + # _ns_font NSFont + + def _from_ns_font(cls, ns_font): + font = cls.__new__(cls) + font._ns_font = ns_font + return font + + _from_ns_font = classmethod(_from_ns_font) + + def __init__(self, family, size = 12, style = []): + traits = 0 + if 'bold' in style: + traits |= NSBoldFontMask + if 'italic' in style: + traits |= NSItalicFontMask + self._ns_font = _ns_font_manager.fontWithFamily_traits_weight_size_( + family, traits, 5, size) + if not self._ns_font: + import StdFonts + self._ns_font = StdFonts.application_font._ns_font + + def get_family(self): + return self._ns_font.familyName() + + def get_size(self): + return self._ns_font.pointSize() + + def get_style(self): + style = [] + traits = _ns_font_manager.traitsOfFont_(self._ns_font) + if traits & NSBoldFontMask: + style.append('bold') + if traits & NSItalicFontMask: + style.append('italic') + return style + + def get_ascent(self): + return self._ns_font.ascender() + + def get_descent(self): + return -self._ns_font.descender() + + def get_height(self): + ns_font = self._ns_font + a = ns_font.ascender() + d = ns_font.descender() + return a - d + + def get_cap_height(self): + return self._ns_font.capHeight() + + def get_x_height(self): + return self._ns_font.xHeight() + + def get_line_height(self): + # Adding 1 here to match what NSTextField seems to do + return _ns_layout_manager.defaultLineHeightForFont_(self._ns_font) + 1 + + def width(self, s, start = 0, end = sys.maxint): + return self._ns_font.widthOfString_(s[start:end]) + +export(Font) + diff --git a/GUI/Cocoa/Frame.py b/GUI/Cocoa/Frame.py new file mode 100644 index 0000000..3c8e510 --- /dev/null +++ b/GUI/Cocoa/Frame.py @@ -0,0 +1,25 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Frames - Cocoa +# +#------------------------------------------------------------------------------ + +from GUI.GFrames import Frame as GFrame +from GUI import export +from GUI.Utils import NSMultiClass +from GUI.Utils import PyGUI_NS_EventHandler, PyGUI_Flipped_NSView + +class Frame(GFrame): + + def __init__(self, **kwds): + ns_view = PyGUI_Frame.alloc().initWithFrame_(((0, 0), (100, 100))) + ns_view.pygui_component = self + GFrame.__init__(self, _ns_view = ns_view, **kwds) + +#------------------------------------------------------------------------------ + +class PyGUI_Frame(PyGUI_Flipped_NSView, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + +export(Frame) diff --git a/GUI/Cocoa/GL.py b/GUI/Cocoa/GL.py new file mode 100644 index 0000000..da2453c --- /dev/null +++ b/GUI/Cocoa/GL.py @@ -0,0 +1,185 @@ +# +# PyGUI - OpenGL View - Cocoa +# + +__all__ = ['GLConfig', 'GLView', 'GLPixmap'] + +import AppKit +from Foundation import NSSize +from AppKit import NSOpenGLPixelFormat, NSOpenGLView, \ + NSBitmapImageRep, NSCachedImageRep, NSImage, NSAlphaFirstBitmapFormat, \ + NSFloatingPointSamplesBitmapFormat +from Foundation import NSMakeRect +from OpenGL.GL import glViewport, glFlush, glFinish, glReadPixels, \ + GL_RGB, GL_RGBA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, \ + GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT, GL_FLOAT, \ + glReadPixelsub, glTexImage2D, glPixelStorei, GL_UNPACK_ALIGNMENT +from OpenGL.GLU import gluBuild2DMipmaps +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI.GGLConfig import GLConfig as GGLConfig +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture +from GUI.GLDisplayLists import DisplayList +from GUI.Utils import NSMultiClass, PyGUI_NS_ViewBase + + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + # _ns_pixel_format NSOpenGLPixelFormat + + def _ns_get_pixel_format(self, offscreen = False): + attrs = [AppKit.NSOpenGLPFAColorSize, self._color_size] + if self._double_buffer: + attrs += [AppKit.NSOpenGLPFADoubleBuffer] + if self._alpha: + attrs += [AppKit.NSOpenGLPFAAlphaSize, self._alpha_size] + if self._stereo: + attrs += [AppKit.NSOpenGLPFAStereo] + if self._aux_buffers: + attrs += [AppKit.NSOpenGLPFAAuxBuffers, self._aux_buffers] + if self._depth_buffer: + attrs += [AppKit.NSOpenGLPFADepthSize, self._depth_size] + if self._stencil_buffer: + attrs += [AppKit.NSOpenGLPFAStencilSize, self._stencil_size] + if self._accum_buffer: + attrs += [AppKit.NSOpenGLPFAAccumSize, self._accum_size] + if self._multisample: + attrs += [AppKit.NSOpenGLPFASampleBuffers, 1] + attrs += [AppKit.NSOpenGLPFASamples, self._samples_per_pixel] + if offscreen: + attrs += [AppKit.NSOpenGLPFAOffScreen] + attrs.append(0) + ns_pf = NSOpenGLPixelFormat.alloc().initWithAttributes_(attrs) + if not ns_pf and self._double_buffer: + attrs.remove(AppKit.NSOpenGLPFADoubleBuffer) + ns_pf = NSOpenGLPixelFormat.alloc().initWithAttributes_(attrs) + if not ns_pf: + raise GLConfigError + return ns_pf + + def _ns_set_pixel_format(self, ns_pf): + def ns_attr(attr): + return ns_pf.getValues_forAttribute_forVirtualScreen_(attr, 0)[0] + self._ns_pixel_format = ns_pf + self._double_buffer = ns_attr(AppKit.NSOpenGLPFADoubleBuffer) + self._color_size = ns_attr(AppKit.NSOpenGLPFAColorSize) + self._alpha_size = ns_attr(AppKit.NSOpenGLPFAAlphaSize) + self._alpha = self._alpha_size > 0 + self._stereo = ns_attr(AppKit.NSOpenGLPFAStereo) + self._aux_buffers = ns_attr(AppKit.NSOpenGLPFAAuxBuffers) + self._depth_size = ns_attr(AppKit.NSOpenGLPFADepthSize) + self._depth_buffer = self._depth_size > 0 + self._stencil_size = ns_attr(AppKit.NSOpenGLPFAStencilSize) + self._stencil_buffer = self._stencil_size > 0 + self._accum_size = ns_attr(AppKit.NSOpenGLPFAAccumSize) + self._accum_buffer = self._accum_size > 0 + self._multisample = ns_attr(AppKit.NSOpenGLPFASampleBuffers) > 0 + self._samples_per_pixel = ns_attr(AppKit.NSOpenGLPFASamples) + + def supported(self, mode = 'both'): + try: + ns_pf = self._ns_get_pixel_format() + pf = GLConfig.__new__() + pf._ns_set_pixel_format(ns_pf) + return pf + except GLConfigError: + return None + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + # _ns_view NSOpenGLView + # _ns_context NSOpenGLContext + # _ns_flush function for flushing/swapping buffers + + def __init__(self, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + ns_pf = pf._ns_get_pixel_format() + width, height = GGLView._default_size + ns_rect = NSMakeRect(0, 0, width, height) + ns_view = _PyGUI_NSOpenGLView.alloc().initWithFrame_pixelFormat_( + ns_rect, ns_pf) + ns_view.pygui_component = self + GGLView.__init__(self, _ns_view = ns_view) + GLContext.__init__(self, share_group = share_group, _ns_pixel_format = ns_pf) + ns_context = self._ns_context + ns_view.setOpenGLContext_(ns_context) + #ns_context.setView_(ns_view) # Docs say this is needed, but + # prints warning and seems to work without. + if pf.double_buffer: + self._ns_flush = ns_context.flushBuffer + else: + self._ns_flush = glFlush + self.set(**kwds) + self.with_context(self._init_context) + + def destroy(self): + #print "GLView.destroy:", self ### + ns_view = self._ns_view + GGLView.destroy(self) + #print "GLView.destroy: breaking back link from", ns_view ### + ns_view.pygui_component = None + + def invalidate(self): + self._ns_view.setNeedsDisplay_(True) + + def update(self): + self._ns_view.displayIfNeeded() + + def track_mouse(self): + return self._ns_track_mouse(self._ns_view) + +#------------------------------------------------------------------------------ + +class GLPixmap(GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + ns_pf = pf._ns_get_pixel_format() + ns_size = NSSize(width, height) + ns_cache = NSCachedImageRep.alloc().initWithSize_depth_separate_alpha_( + ns_size, 0, True, True) + ns_image = NSImage.alloc().initWithSize_(ns_size) + GLContext.__init__(self, share_group = share_group, _ns_pixel_format = ns_pf) + self._ns_context.setView_(ns_cache.window().contentView()) + self._init_with_ns_image(ns_image, flipped = False) + self._ns_cache = ns_cache + self.with_context(self._init_context) + + def _ns_flush(self): + glFlush() + width, height = self.size + pixels = glReadPixels(0, 0, int(width), int(height), GL_RGBA, GL_UNSIGNED_BYTE) + bytes_per_row = int(width) * 4 + ns_new_bitmap = NSBitmapImageRep.alloc().\ + initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel_( + (pixels, "", "", "", ""), int(width), int(height), 8, 4, True, False, AppKit.NSDeviceRGBColorSpace, bytes_per_row, 0) + ns_image = NSImage.alloc().initWithSize_(NSSize(width, height)) + ns_image.addRepresentation_(ns_new_bitmap) + ns_image.lockFocus() + ns_image.unlockFocus() + self._ns_image = ns_image + self._ns_bitmap_image_rep = ns_new_bitmap + +#------------------------------------------------------------------------------ + +class _PyGUI_NSOpenGLView(NSOpenGLView, PyGUI_NS_ViewBase): + __metaclass__ = NSMultiClass + # + # pygui_component GLView + + __slots__ = ['pygui_component'] + + def isFlipped(self): + return True + + def reshape(self): + comp = self.pygui_component + if comp.window: + comp.with_context(comp._update_viewport) + + def drawRect_(self, rect): + comp = self.pygui_component + comp.with_context(comp._render, flush = True) diff --git a/GUI/Cocoa/GLContexts.py b/GUI/Cocoa/GLContexts.py new file mode 100644 index 0000000..ec0f154 --- /dev/null +++ b/GUI/Cocoa/GLContexts.py @@ -0,0 +1,35 @@ +# +# PyGUI - OpenGL Contexts - Cocoa +# + +from AppKit import NSOpenGLContext +from GUI.GGLContexts import GLContext as GGLContext + +class GLContext(GGLContext): + # _ns_context NSOpenGLContext + + def __init__(self, share_group, _ns_pixel_format): + GGLContext.__init__(self, share_group) + shared_context = self._get_shared_context() + if shared_context: + ns_share = shared_context._ns_context + else: + ns_share = None + ns_context = NSOpenGLContext.alloc().initWithFormat_shareContext_( + _ns_pixel_format, ns_share) + self._ns_context = ns_context + + def _with_context(self, proc, flush): + #print "GLContext._with_context: Entering context", self._ns_context ### + old_context = NSOpenGLContext.currentContext() + self._ns_context.makeCurrentContext() + try: + self._with_share_group(proc) + if flush: + self._ns_flush() + finally: + #print "GL: Restoring previous context" ### + if old_context: + old_context.makeCurrentContext() + else: + NSOpenGLContext.clearCurrentContext() diff --git a/GUI/Cocoa/GLTextures.py b/GUI/Cocoa/GLTextures.py new file mode 100644 index 0000000..aa7c054 --- /dev/null +++ b/GUI/Cocoa/GLTextures.py @@ -0,0 +1,69 @@ +# +# PyGUI - OpenGL Textures - Cocoa +# + +from AppKit import NSAlphaFirstBitmapFormat, NSFloatingPointSamplesBitmapFormat +from OpenGL import GL +from GUI.GGLTextures import Texture as GTexture + +class Texture(GTexture): + + def _gl_get_texture_data(self, image): + ns_rep = image._ns_bitmap_image_rep + if ns_rep.numberOfPlanes() <> 1: + raise ValueError("Cannot use planar image data as GL texture") + ns_format = ns_rep.bitmapFormat() + if ns_format & NSAlphaFirstBitmapFormat: + raise ValueError("Cannot use alpha-first image data as GL texture") + fp_samples = ns_format & NSFloatingPointSamplesBitmapFormat <> 0 + bits_per_pixel = ns_rep.bitsPerPixel() + bytes_per_row = ns_rep.bytesPerRow() + samples_per_pixel = ns_rep.samplesPerPixel() + if bits_per_pixel % samples_per_pixel <> 0: + raise ValueError("Image data format not usable as GL texture") + bits_per_sample = bits_per_pixel / samples_per_pixel + try: + gl_format = format_map[samples_per_pixel] + gl_type = type_map[bits_per_sample, fp_samples] + except KeyError: + raise ValueError("Image data format not usable as GL texture") + data = ns_rep.bitmapData() + if 0: + print "GUI.GLTexture._gl_get_texture_data_and_format:" ### + print "format =", gl_format_map.get(gl_format) ### + print "type =", gl_type_map.get(gl_type) ### + print "data length =", len(data) ### + print repr(data[:16]) ### + GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1) + return gl_format, gl_type, str(data) + +#------------------------------------------------------------------------------ + +format_map = { + 3: GL.GL_RGB, + 4: GL.GL_RGBA, + 1: GL.GL_LUMINANCE, + 2: GL.GL_LUMINANCE_ALPHA, +} + +type_map = { + (8, 0): GL.GL_UNSIGNED_BYTE, + (16, 0): GL.GL_UNSIGNED_SHORT, + (32, 0): GL.GL_UNSIGNED_INT, + (32, 1): GL.GL_FLOAT, +} + +gl_format_map = { + GL.GL_RGB: 'GL_RGB', + GL.GL_RGBA: 'GL_RGBA', + GL.GL_LUMINANCE: 'GL_LUMINANCE', + GL.GL_LUMINANCE_ALPHA: 'GL_LUMINANCE_ALPHA', +} + +gl_type_map = { + GL.GL_UNSIGNED_BYTE: 'GL_UNSIGNED_BYTE', + GL.GL_UNSIGNED_SHORT: 'GL_UNSIGNED_SHORT', + GL.GL_UNSIGNED_INT: 'GL_UNSIGNED_INT', + GL.GL_FLOAT: 'GL_FLOAT', +} + diff --git a/GUI/Cocoa/Geometry.py b/GUI/Cocoa/Geometry.py new file mode 100644 index 0000000..3a9bd54 --- /dev/null +++ b/GUI/Cocoa/Geometry.py @@ -0,0 +1,12 @@ +# +# Python GUI - Points and Rectangles - PyObjC +# + +from Foundation import NSMakeRect +from GUI.GGeometry import * + +def rect_to_ns_rect((l, t, r, b)): + return NSMakeRect(l, t, r - l, b - t) + +def ns_rect_to_rect(((l, t), (w, h))): + return (l, t, l + w, t + h) diff --git a/GUI/Cocoa/Image.py b/GUI/Cocoa/Image.py new file mode 100644 index 0000000..a40ab1e --- /dev/null +++ b/GUI/Cocoa/Image.py @@ -0,0 +1,31 @@ +# +# Python GUI - Images - Cocoa +# + +from Foundation import NSData +from AppKit import NSImage, NSBitmapImageRep +from GUI import export +from GUI.GImages import Image as GImage + +class Image(GImage): + # _ns_bitmap_image_rep + + def _init_from_file(self, file): + #ns_image = NSImage.alloc().initWithContentsOfFile_(file) + #if not ns_image: + ns_data = NSData.dataWithContentsOfFile_(file) + if not ns_data: + raise EnvironmentError("Unable to read image file: %s" % file) + ns_rep = NSBitmapImageRep.imageRepWithData_(ns_data) + if not ns_rep: + raise ValueError("Unrecognised image file type: %s" % file) + ns_rep.setSize_((ns_rep.pixelsWide(), ns_rep.pixelsHigh())) + self._init_from_ns_rep(ns_rep) + + def _init_from_ns_rep(self, ns_rep): + ns_image = NSImage.alloc().init() + ns_image.addRepresentation_(ns_rep) + self._ns_bitmap_image_rep = ns_rep + self._init_with_ns_image(ns_image, flipped = True) + +export(Image) diff --git a/GUI/Cocoa/ImageBase.py b/GUI/Cocoa/ImageBase.py new file mode 100644 index 0000000..4405648 --- /dev/null +++ b/GUI/Cocoa/ImageBase.py @@ -0,0 +1,33 @@ +# +# Python GUI - Common Image/Pixmap code - Cocoa +# + +from AppKit import NSCompositeSourceOver +from GUI import export +from GUI.Geometry import rect_to_ns_rect +from GUI.GImageBases import ImageBase as GImageBase + +class ImageBase(GImageBase): + # + # Code common to Image, Pixmap and GLPixmap classes + + def _init_with_ns_image(self, ns_image, flipped): + ns_image.setFlipped_(flipped) + self._ns_image = ns_image + + def get_size(self): + return tuple(self._ns_image.size()) + + def get_width(self): + return self._ns_image.size()[0] + + def get_height(self): + return self._ns_image.size()[1] + + def draw(self, canvas, src_rect, dst_rect): + ns_src_rect = rect_to_ns_rect(src_rect) + ns_dst_rect = rect_to_ns_rect(dst_rect) + self._ns_image.drawInRect_fromRect_operation_fraction_( + ns_dst_rect, ns_src_rect, NSCompositeSourceOver, 1.0) + +export(ImageBase) diff --git a/GUI/Cocoa/Label.py b/GUI/Cocoa/Label.py new file mode 100644 index 0000000..082f69e --- /dev/null +++ b/GUI/Cocoa/Label.py @@ -0,0 +1,28 @@ +# +# Python GUI - Labels - PyObjC +# + +import AppKit +from AppKit import NSView +from GUI import export +from GUI.StdFonts import system_font +from GUI.TextFieldBasedControls import TextFieldBasedControl +from GUI.GLabels import Label as GLabel + +ns_label_autoresizing_mask = (AppKit.NSViewWidthSizable + | AppKit.NSViewHeightSizable) + +class Label(TextFieldBasedControl, GLabel): + + def __init__(self, text = "New Label", font = system_font, **kwds): + ns_textfield = self._create_ns_textfield(editable = False, + text = text, font = font) +# width, height = ns_textfield.frame().size +# ns_view = NSView.alloc().initWithFrame_(((0, 0), (width, height + 5))) +# ns_view.addSubview_(ns_textfield) +# ns_textfield.setFrameOrigin_((0, 2)) +# ns_textfield.setAutoresizingMask_(ns_label_autoresizing_mask) + ns_view = ns_textfield + GLabel.__init__(self, _ns_view = ns_view, _ns_inner_view = ns_textfield, **kwds) + +export(Label) diff --git a/GUI/Cocoa/ListButton.py b/GUI/Cocoa/ListButton.py new file mode 100644 index 0000000..671a8a8 --- /dev/null +++ b/GUI/Cocoa/ListButton.py @@ -0,0 +1,48 @@ +#-------------------------------------------------------------- +# +# PyGUI - Pop-up list control - Cocoa +# +#-------------------------------------------------------------- + +from AppKit import NSPopUpButton +from GUI import export +from GUI.GListButtons import ListButton as GListButton +from GUI.Utils import NSMultiClass, PyGUI_NS_EventHandler, \ + ns_set_action, ns_size_to_fit + +class ListButton(GListButton): + + _ns_handle_mouse = True + + def __init__(self, **kwds): + titles, values = self._extract_initial_items(kwds) + self._titles = titles + self._values = values + frame = ((0, 0), (100, 20)) + ns = PyGUI_NSPopUpButton.alloc().initWithFrame_pullsDown_(frame, False) + ns.pygui_component = self + ns_set_action(ns, 'doAction:') + self._ns_update_items(ns) + ns_size_to_fit(ns) + GListButton.__init__(self, _ns_view = ns, **kwds) + + def _update_items(self): + self._ns_update_items(self._ns_view) + + def _ns_update_items(self, ns): + ns.removeAllItems() + ns.addItemsWithTitles_(self._titles) + + def _get_selected_index(self): + return self._ns_view.indexOfSelectedItem() + + def _set_selected_index(self, i): + self._ns_view.selectItemAtIndex_(i) + +#-------------------------------------------------------------- + +class PyGUI_NSPopUpButton(NSPopUpButton, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + +export(ListButton) diff --git a/GUI/Cocoa/Menu.py b/GUI/Cocoa/Menu.py new file mode 100644 index 0000000..6a82454 --- /dev/null +++ b/GUI/Cocoa/Menu.py @@ -0,0 +1,67 @@ +# +# Python GUI - Menus - PyObjC +# + +from AppKit import NSMenu, NSMenuItem, NSOnState, \ + NSCommandKeyMask, NSShiftKeyMask, NSAlternateKeyMask +from GUI import export +from GUI import Globals +from GUI.GMenus import Menu as GMenu, MenuItem + +#_ns_standard_actions = { +# 'undo_cmd': 'undo:', +# 'redo_cmd': 'redo:', +# 'cut_cmd': 'cut:', +# 'copy_cmd': 'copy:', +# 'paste_cmd': 'paste:', +# 'clear_cmd': 'clear:', +# 'select_all_cmd': 'selectAll:', +#} + +class Menu(GMenu): + + def __init__(self, title, items, **kwds): + #print "Menu: creating with items", items ### + GMenu.__init__(self, title, items, **kwds) + ns_menu = NSMenu.alloc().initWithTitle_(title) + ns_menu.setAutoenablesItems_(False) + ns_menu.setDelegate_(Globals.ns_application) + self._ns_menu = ns_menu + + def _clear_platform_menu(self): + ns_menu = self._ns_menu + n = ns_menu.numberOfItems() + while n: + n -= 1 + ns_menu.removeItemAtIndex_(n) + + def _add_separator_to_platform_menu(self): + ns_item = NSMenuItem.separatorItem() + self._ns_menu.addItem_(ns_item) + + def _add_item_to_platform_menu(self, item, name, command = None, index = None): + key = item._key or "" + if item._shift: + key = key.upper() + else: + key = key.lower() + ns_item = NSMenuItem.alloc() + #ns_action = _ns_standard_actions.get(command, 'menuSelection:') + ns_action = 'menuSelection:' + ns_item.initWithTitle_action_keyEquivalent_(name, ns_action, key) + ns_item.setEnabled_(item.enabled) + if item.checked: + ns_item.setState_(NSOnState) + ns_modifiers = NSCommandKeyMask + if item._option: + ns_modifiers |= NSAlternateKeyMask + ns_item.setKeyEquivalentModifierMask_(ns_modifiers) + ns_item.setRepresentedObject_(command) + if index is not None: + ns_tag = index + else: + ns_tag = -1 + ns_item.setTag_(ns_tag) + self._ns_menu.addItem_(ns_item) + +export(Menu) diff --git a/GUI/Cocoa/Numerical.py b/GUI/Cocoa/Numerical.py new file mode 100644 index 0000000..f53f3ba --- /dev/null +++ b/GUI/Cocoa/Numerical.py @@ -0,0 +1,50 @@ +#-------------------------------------------------------------- +# +# PyGUI - NumPy interface - Cocoa +# +#-------------------------------------------------------------- + +from AppKit import NSBitmapImageRep, \ + NSAlphaNonpremultipliedBitmapFormat, NSCalibratedRGBColorSpace +from GUI import Image + +# HACK! PyObjC 2.3 incorrectly wraps the following method, so we change the +# signature and pass the bitmap data in using ctypes. +NSBitmapImageRep.__dict__['initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_'].signature = '@52@0:4^v8i12i16i20i24c28c32@36I40i44i48' +import ctypes +planes_t = ctypes.c_void_p * 5 + +def image_from_ndarray(array, format, size = None): + """ + Creates an Image from a numpy ndarray object. The format + may be 'RGB' or 'RGBA'. If a size is specified, the array + will be implicitly reshaped to that size, otherwise the size + is inferred from the first two dimensions of the array. + """ + if array.itemsize <> 1: + raise ValueError("Color component size must be 1 byte") + if size is not None: + width, height = size + data_size = array.size + pixel_size = data_size // (width * height) + if pixel_size <> len(format): + raise ValueError("Array has wrong shape for specified size and format") + else: + height, width, pixel_size = array.shape + if pixel_size <> len(format): + raise ValueError("Array has wrong shape for specified format") + bps = 8 + spp = pixel_size + alpha = format.endswith("A") + csp = NSCalibratedRGBColorSpace + bpp = bps * spp + bpr = width * pixel_size + fmt = NSAlphaNonpremultipliedBitmapFormat + ns_rep = NSBitmapImageRep.alloc() + planes = planes_t(array.ctypes.data, 0, 0, 0, 0) + ns_rep.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_( + ctypes.addressof(planes), width, height, bps, spp, alpha, False, csp, fmt, bpr, bpp) + image = Image.__new__(Image) + image._init_from_ns_rep(ns_rep) + image._data = array + return image diff --git a/GUI/Cocoa/PIL.py b/GUI/Cocoa/PIL.py new file mode 100644 index 0000000..31f223b --- /dev/null +++ b/GUI/Cocoa/PIL.py @@ -0,0 +1,78 @@ +#-------------------------------------------------------------- +# +# PyGUI - PIL interface - Cocoa +# +#-------------------------------------------------------------- + +import ctypes +from AppKit import NSBitmapImageRep, \ + NSAlphaNonpremultipliedBitmapFormat, NSFloatingPointSamplesBitmapFormat, \ + NSDeviceCMYKColorSpace, NSCalibratedRGBColorSpace +from GUI import Image + +def hack_objc_sig(): + #print "GUI[Cocoa].PIL: Hacking objc method signature" ### + # HACK! PyObjC 2.3 incorrectly wraps the following method, so we change the + # signature and pass the bitmap data in using ctypes. + NSBitmapImageRep.__dict__['initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_'].signature = '@52@0:4^v8i12i16i20i24c28c32@36I40i44i48' + +planes_t = ctypes.c_char_p * 5 + +debug_pil = False + +def image_from_pil_image(pil_image): + """Creates an Image from a Python Imaging Library (PIL) + Image object.""" + mode = pil_image.mode + w, h = pil_image.size + data = pil_image.tostring() + alpha = False + cmyk = False + floating = False + if mode == "1": + bps = 1; spp = 1 + elif mode == "L": + bps = 8; spp = 1 + elif mode == "RGB": + bps = 8; spp = 3 + elif mode == "RGBA": + bps = 8; spp = 4; alpha = True + elif mode == "CMYK": + bps = 8; spp = 4; cmyk = True + elif mode == "I": + bps = 32; spp = 1 + elif mode == "F": + bps = 32; spp = 1; floating = True + else: + raise ValueError("Unsupported PIL image mode '%s'" % mode) + if cmyk: + csp = NSDeviceCMYKColorSpace + else: + csp = NSCalibratedRGBColorSpace + fmt = NSAlphaNonpremultipliedBitmapFormat + if floating: + fmt |= NSFloatingPointSamplesBitmapFormat + bpp = bps * spp + bpr = w * ((bpp + 7) // 8) + if debug_pil: + print "GUI.PIL:" + print "image size =", (w, h) + print "data size =", len(data) + print "bits per sample =", bps + print "samples per pixel =", spp + print "bits per pixel =", bpp + print "bytes per row =", bpr + hack_objc_sig() + ns_rep = NSBitmapImageRep.alloc() + planes = planes_t(data, "", "", "", "") + ns_rep.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bitmapFormat_bytesPerRow_bitsPerPixel_( + ctypes.addressof(planes), w, h, bps, spp, alpha, False, csp, fmt, bpr, bpp) +# planes = (data, "", "", "", "") +# ns_rep.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel_( +# planes, w, h, bps, spp, alpha, False, csp, bpr, bpp) + image = Image.__new__(Image) + image._init_from_ns_rep(ns_rep) + image._data = data + return image + + diff --git a/GUI/Cocoa/Pixmap.py b/GUI/Cocoa/Pixmap.py new file mode 100644 index 0000000..3b3a4a2 --- /dev/null +++ b/GUI/Cocoa/Pixmap.py @@ -0,0 +1,80 @@ +# +# Python GUI - Pixmaps - Cocoa +# + +from Foundation import NSSize +from AppKit import NSImage, NSCachedImageRep, NSBitmapImageRep, \ + NSCalibratedRGBColorSpace, NSImageCacheNever, NSGraphicsContext, \ + NSAffineTransform +from GUI import export +from GUI import Canvas +from GUI.GPixmaps import Pixmap as GPixmap + +class Pixmap(GPixmap): + # _ns_bitmap_image_rep NSBitmapImageRep + + def __init__(self, width, height): + GPixmap.__init__(self) + #ns_size = NSSize(width, height) + #ns_image = NSImage.alloc().initWithSize_(ns_size) + ns_image = NSImage.alloc().init() + ns_image.setCacheMode_(NSImageCacheNever) + row_bytes = 4 * width + ns_bitmap = NSBitmapImageRep.alloc().\ + initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel_( + None, width, height, 8, 4, True, False, NSCalibratedRGBColorSpace, row_bytes, 32) + ns_image.addRepresentation_(ns_bitmap) + ns_bitmap_context = NSGraphicsContext.graphicsContextWithBitmapImageRep_(ns_bitmap) + ns_graphics_context = FlippedNSGraphicsContext.alloc().initWithBase_(ns_bitmap_context) + ns_tr = NSAffineTransform.transform() + ns_tr.translateXBy_yBy_(0.0, height) + ns_tr.scaleXBy_yBy_(1.0, -1.0) + # Using __class__ to get +saveGraphicsState instead of -saveGraphicsState + NSGraphicsContext.__class__.saveGraphicsState() + try: + NSGraphicsContext.setCurrentContext_(ns_graphics_context) + ns_tr.concat() + finally: + NSGraphicsContext.__class__.restoreGraphicsState() + self._init_with_ns_image(ns_image, flipped = True) #False) + self._ns_bitmap_image_rep = ns_bitmap + self._ns_graphics_context = ns_graphics_context + + def with_canvas(self, proc): + NSGraphicsContext.__class__.saveGraphicsState() + NSGraphicsContext.setCurrentContext_(self._ns_graphics_context) + try: + canvas = Canvas() + proc(canvas) + finally: + NSGraphicsContext.__class__.restoreGraphicsState() + +class FlippedNSGraphicsContext(NSGraphicsContext): + + def initWithBase_(self, base): + self.base = base + self.graphics_port = base.graphicsPort() + return self + + def isFlipped(self): + return True + + def graphicsPort(self): + return self.graphics_port + + def isDrawingToScreen(self): + return self.base.isDrawingToScreen() + + def setCompositingOperation_(self, x): + self.base.setCompositingOperation_(x) + + def focusStack(self): + return self.base.focusStack() + + def saveGraphicsState(self): + return self.base.saveGraphicsState() + + def restoreGraphicsState(self): + return self.base.restoreGraphicsState() + +export(Pixmap) diff --git a/GUI/Cocoa/Printing.py b/GUI/Cocoa/Printing.py new file mode 100644 index 0000000..21aba9f --- /dev/null +++ b/GUI/Cocoa/Printing.py @@ -0,0 +1,132 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Printing - Cocoa +# +#------------------------------------------------------------------------------ + +from AppKit import NSPrintInfo, NSPageLayout, NSPrintOperation, \ + NSKeyedArchiver, NSKeyedUnarchiver, NSData, NSAutoPagination, \ + NSPortraitOrientation, NSLandscapeOrientation, NSOKButton +from GUI.GPrinting import PageSetup as GPageSetup, Printable as GPrintable + +ns_to_generic_orientation = { + NSPortraitOrientation: 'portrait', + NSLandscapeOrientation: 'landscape', +} + +generic_to_ns_orientation = { + 'portrait': NSPortraitOrientation, + 'landscape': NSLandscapeOrientation, +} + +#------------------------------------------------------------------------------ + +class PageSetup(GPageSetup): + + def __init__(self): + ns_pi = NSPrintInfo.sharedPrintInfo().copy() + ns_pi.setLeftMargin_(36) + ns_pi.setTopMargin_(36) + ns_pi.setRightMargin_(36) + ns_pi.setBottomMargin_(36) + ns_pi.setHorizontalPagination_(NSAutoPagination) + self._ns_print_info = ns_pi + + def __getstate__(self): + state = GPageSetup.__getstate__(self) + data = NSKeyedArchiver.archivedDataWithRootObject_(self._ns_print_info) + state['_ns_print_info'] = data.bytes() + return state + + def __setstate__(self, state): + bytes = state.pop('_ns_print_info', None) + if bytes: + data = NSData.dataWithBytes_length_(bytes, len(bytes)) + self._ns_print_info = NSKeyedArchiver.unarchiveObjectWithData_(data) + else: + GPageSetup.__setstate__(self, state) + + def copy(self, other): + result = PageSetup.__new__() + result._ns_print_info = other._ns_print_info.copy() + + def get_paper_name(self): + return self._ns_print_info.paperName() + + def set_paper_name(self, x): + self._ns_print_info.setPaperName_(x) + + def get_paper_size(self): + return tuple(self._ns_print_info.paperSize()) + + def set_paper_size(self, x): + self._ns_print_info.setPaperSize_(x) + + def get_paper_width(self): + return self.paper_size[0] + + def set_paper_width(self, x): + self.paper_size = x, self.paper_height + + def get_paper_height(self): + return self.paper_size[1] + + def set_paper_height(self, x): + self.paper_size = self.paper_width, x + + def get_left_margin(self): + return self._ns_print_info.leftMargin() + + def set_get_left_margin(self, x): + self._ns_print_info.setLefMargin_(x) + + def get_right_margin(self): + return self._ns_print_info.rightMargin() + + def set_get_right_margin(self, x): + self._ns_print_info.setRightMargin_(x) + + def get_top_margin(self): + return self._ns_print_info.topMargin() + + def set_get_top_margin(self, x): + self._ns_print_info.setTopMargin_(x) + + def get_bottom_margin(self): + return self._ns_print_info.bottomMargin() + + def set_get_bottom_margin(self, x): + self._ns_print_info.setBottomMargin_(x) + + def get_orientation(self): + return ns_to_generic_orientation[self._ns_print_info.orientation()] + + def set_orientation(self, x): + nso = generic_to_ns_orientation.get(x, 'portrait') + self._ns_print_info.setOrientation_(nso) + + def get_printable_rect(self): + l, b, w, h = self._ns_print_info.imageablePageBounds() + return (l, b - h, l + w, b) + + def get_printer_name(self): + return self._ns_print_info.printer().name() + + def set_printer_name(self, x): + self._ns_print_info.setPrinter_(NSPrinter.printerWithName_(x)) + +#------------------------------------------------------------------------------ + +class Printable(GPrintable): + + def print_view(self, page_setup, prompt = True): + ns_op = NSPrintOperation.printOperationWithView_printInfo_( + self._ns_inner_view, page_setup._ns_print_info) + ns_op.setShowsPrintPanel_(prompt) + ns_op.runOperation() + +#------------------------------------------------------------------------------ + +def present_page_setup_dialog(page_setup): + result = NSPageLayout.pageLayout().runModalWithPrintInfo_(page_setup._ns_print_info) + return result == NSOKButton diff --git a/GUI/Cocoa/RadioButton.py b/GUI/Cocoa/RadioButton.py new file mode 100644 index 0000000..4b77a54 --- /dev/null +++ b/GUI/Cocoa/RadioButton.py @@ -0,0 +1,36 @@ +# +# Python GUI - Radio buttons - PyObjC +# + +import AppKit +from AppKit import NSOnState, NSOffState +from GUI import export +from GUI.StdFonts import system_font +from GUI.ButtonBasedControls import ButtonBasedControl +from GUI.GRadioButtons import RadioButton as GRadioButton + +class RadioButton(ButtonBasedControl, GRadioButton): + + def __init__(self, title = "New Radio Button", font = system_font, **kwds): + ns_button = self._create_ns_button(title = title, font = font, + ns_button_type = AppKit.NSRadioButton, + ns_bezel_style = AppKit.NSRoundedBezelStyle) + GRadioButton.__init__(self, _ns_view = ns_button, **kwds) + + def do_action(self): + if self._group: + self._group.value = self._value + else: + self._ns_view.setState_(NSOffState) + + def _value_changed(self): + self._update() + + def _update(self): + if self._group and self._value == self._group._value: + state = NSOnState + else: + state = NSOffState + self._ns_view.setState_(state) + +export(RadioButton) diff --git a/GUI/Cocoa/RadioGroup.py b/GUI/Cocoa/RadioGroup.py new file mode 100644 index 0000000..985990b --- /dev/null +++ b/GUI/Cocoa/RadioGroup.py @@ -0,0 +1,23 @@ +# +# Python GUI - Radio groups - PyObjC +# + +from GUI import export +from GUI.GRadioGroups import RadioGroup as GRadioGroup + +class RadioGroup(GRadioGroup): + + def __init__(self, items = [], **kwds): + GRadioGroup.__init__(self, items, **kwds) + + def _item_added(self, item): + item._update() + + def _item_removed(self, item): + pass + + def _value_changed(self): + for item in self._items: + item._update() + +export(RadioGroup) diff --git a/GUI/Cocoa/ScrollableView.py b/GUI/Cocoa/ScrollableView.py new file mode 100644 index 0000000..c6bdd83 --- /dev/null +++ b/GUI/Cocoa/ScrollableView.py @@ -0,0 +1,96 @@ +# +# Python GUI - Scrollable Views - PyObjC +# + +from Foundation import NSPoint, NSMakeRect +from AppKit import NSScrollView +from GUI import export +from GUI.GScrollableViews import ScrollableView as GScrollableView, \ + default_extent, default_line_scroll_amount, default_scrolling +from GUI.Geometry import ns_rect_to_rect + +class ScrollableView(GScrollableView): + + _ns_scrollable = True + + def __init__(self, extent = default_extent, + line_scroll_amount = default_line_scroll_amount, + scrolling = default_scrolling, + **kwds): + GScrollableView.__init__(self, + extent = extent, line_scroll_amount = line_scroll_amount, + scrolling = scrolling, **kwds) + + def get_hscrolling(self): + return self._ns_view.hasHorizontalScroller() + + def set_hscrolling(self, value): + self._ns_view.setHasHorizontalScroller_(value) + + def get_vscrolling(self): + return self._ns_view.hasVerticalScroller() + + def set_vscrolling(self, value): + self._ns_view.setHasVerticalScroller_(value) + +# def get_extent(self): +# (l, t), (w, h) = self._ns_inner_view.bounds() +# return (l, t, l + w, t + h) + + def get_extent(self): + return self._ns_inner_view.bounds().size + +# def set_extent(self, (l, t, r, b)): +# w = r - l +# h = b - t +# ns_docview = self._ns_inner_view +# ns_docview.setFrame_(NSMakeRect(0, 0, w, h)) +# ns_docview.setBounds_(NSMakeRect(l, t, w, h)) +# self.invalidate() + + def set_extent(self, (w, h)): + r = NSMakeRect(0, 0, w, h) + ns_docview = self._ns_inner_view + ns_docview.setFrame_(r) + ns_docview.setBounds_(r) + self.invalidate() + + def get_content_size(self): + return self._ns_view.contentSize() + + def set_content_size(self, size): + ns = self._ns_view + self.size = NSScrollView.\ + frameSizeForContentSize_hasHorizontalScroller_hasVerticalScroller_borderType_( + size, ns.hasHorizontalScroller(), ns.hasVerticalScroller(), ns.borderType()) + + def get_scroll_offset(self): + ns_clip_view = self._ns_view.contentView() + x, y = ns_clip_view.bounds().origin + return x, y + + def set_scroll_offset(self, (x, y)): + ns_view = self._ns_view + ns_clip_view = ns_view.contentView() + new_pt = ns_clip_view.constrainScrollPoint_(NSPoint(x, y)) + ns_clip_view.scrollToPoint_(new_pt) + ns_view.reflectScrolledClipView_(ns_clip_view) + + def get_line_scroll_amount(self): + ns_view = self._ns_view + x = ns_view.horizontalLineScroll() + y = ns_view.verticalLineScroll() + return x, y + + def set_line_scroll_amount(self, (x, y)): + ns_view = self._ns_view + ns_view.setHorizontalLineScroll_(x) + ns_view.setVerticalLineScroll_(y) + ns_view.setHorizontalPageScroll_(x) + ns_view.setVerticalPageScroll_(y) + + def viewed_rect(self): + ns_rect = self._ns_view.contentView().documentVisibleRect() + return ns_rect_to_rect(ns_rect) + +export(ScrollableView) diff --git a/GUI/Cocoa/Slider.py b/GUI/Cocoa/Slider.py new file mode 100644 index 0000000..d8c1ad2 --- /dev/null +++ b/GUI/Cocoa/Slider.py @@ -0,0 +1,85 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Slider - Cocoa +# +#------------------------------------------------------------------------------ + +from AppKit import NSSlider +from GUI import export +from GUI.StdFonts import system_font +from GUI.Utils import NSMultiClass, PyGUI_NS_EventHandler, \ + ns_set_action, ns_size_to_fit +from GUI.GSliders import Slider as GSlider + +class Slider(GSlider): + + _ns_handle_mouse = True + + def __init__(self, orient = 'h', ticks = 0, **kwds): + length = 100 + if ticks: + breadth = 30 + else: + breadth = 22 # Same as default height of a text-containing control + if orient == 'h': + ns_frame = ((0, 0), (length, breadth)) + elif orient == 'v': + ns_frame = ((0, 0), (breadth, length)) + else: + raise ValueError("Invalid orientation, should be 'h' or 'v'") + ns_slider = PyGUI_NSSlider.alloc().initWithFrame_(ns_frame) + ns_slider.pygui_component = self + ns_set_action(ns_slider, 'doAction:') + GSlider.__init__(self, _ns_view = ns_slider, **kwds) + self.set_ticks(ticks) + self._last_value = None + + def get_min_value(self): + return self._ns_view.minValue() + + def set_min_value(self, x): + self._ns_view.setMinValue_(x) + + def get_max_value(self): + return self._ns_view.maxValue() + + def set_max_value(self, x): + self._ns_view.setMaxValue_(x) + + def get_value(self): + return self._ns_view.doubleValue() + + def set_value(self, x): + self._ns_view.setDoubleValue_(x) + + def get_ticks(self): + return self._ns_view.numberOfTickMarks() + + def set_ticks(self, x): + self._ns_view.setNumberOfTickMarks_(x) + + def get_discrete(self): + return self._ns_view.allowsTickMarkValuesOnly() + + def set_discrete(self, x): + self._ns_view.setAllowsTickMarkValuesOnly_(x) + + def get_live(self): + return self._ns_view.isContinuous() + + def set_live(self, x): + self._ns_view.setContinuous_(x) + + def do_action(self): + value = self._ns_view.doubleValue() + if value <> self._last_value: + self._last_value = value + GSlider.do_action(self) + +#------------------------------------------------------------------------------ + +class PyGUI_NSSlider(NSSlider, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + +export(Slider) diff --git a/GUI/Cocoa/StdCursors.py b/GUI/Cocoa/StdCursors.py new file mode 100644 index 0000000..e26c851 --- /dev/null +++ b/GUI/Cocoa/StdCursors.py @@ -0,0 +1,56 @@ +# +# Python GUI - Standard Cursors - Cocoa +# + +from AppKit import NSCursor +from GUI import Cursor + +__all__ = [ + 'arrow', + 'ibeam', + 'crosshair', + 'fist', + 'hand', + 'finger', + 'invisible', +] + +_empty_cursor = None + +def _make_empty_cursor(): + global _empty_cursor + if not _empty_cursor: + from AppKit import NSCursor, NSImage, NSBitmapImageRep, NSDeviceRGBColorSpace + from GUI import Cursor + import sys + if sys.version_info >= (3, 0): + b = bytes([0]) + else: + b = "\x00" + d = b * 1024 + ns_bitmap = NSBitmapImageRep.alloc().\ + initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel_\ + ((d, d, d, d, d), 16, 16, 8, 4, True, False, NSDeviceRGBColorSpace, 64, 32) + ns_image = NSImage.alloc().initWithSize_((16, 16)) + ns_image.addRepresentation_(ns_bitmap) + ns_cursor = NSCursor.alloc().initWithImage_hotSpot_(ns_image, (0, 0)) + _empty_cursor = Cursor._from_ns_cursor(ns_cursor) + _empty_cursor._data = d + return _empty_cursor + +arrow = Cursor._from_ns_cursor(NSCursor.arrowCursor()) +ibeam = Cursor._from_ns_cursor(NSCursor.IBeamCursor()) +crosshair = Cursor._from_ns_cursor(NSCursor.crosshairCursor()) +fist = Cursor._from_ns_cursor(NSCursor.closedHandCursor()) +hand = Cursor._from_ns_cursor(NSCursor.openHandCursor()) +finger = Cursor._from_ns_cursor(NSCursor.pointingHandCursor()) +invisible = _make_empty_cursor() + +mac_poof = Cursor._from_ns_cursor(NSCursor.disappearingItemCursor()) + +del NSCursor +del Cursor +del _make_empty_cursor + +def empty_cursor(): + return invisible diff --git a/GUI/Cocoa/StdFonts.py b/GUI/Cocoa/StdFonts.py new file mode 100644 index 0000000..91aa2ca --- /dev/null +++ b/GUI/Cocoa/StdFonts.py @@ -0,0 +1,9 @@ +# +# Python GUI - Standard Fonts - PyObjC +# + +from AppKit import NSFont +from GUI import Font + +system_font = Font._from_ns_font(NSFont.systemFontOfSize_(0)) +application_font = Font._from_ns_font(NSFont.userFontOfSize_(0)) diff --git a/GUI/Cocoa/StdMenus.py b/GUI/Cocoa/StdMenus.py new file mode 100644 index 0000000..1b1fe3f --- /dev/null +++ b/GUI/Cocoa/StdMenus.py @@ -0,0 +1,62 @@ +# +# Python GUI - Standard Menus - PyObjC +# + +from GUI.GStdMenus import build_menus, \ + fundamental_cmds, help_cmds, pref_cmds, file_cmds, print_cmds, edit_cmds + +fundamental_cmds += ['hide_app_cmd', 'hide_other_apps_cmd', 'show_all_apps_cmd'] + +_appl_menu_items = [ + ("About ", 'about_cmd'), + "-", + ("Preferences...", 'preferences_cmd'), + "-", + ("Hide /H", 'hide_app_cmd'), + ("Hide Others", 'hide_other_apps_cmd'), + ("Show All", 'show_all_apps_cmd'), + "-", + ("Quit /Q", 'quit_cmd'), +] + +_file_menu_items = [ + ("New/N", 'new_cmd'), + ("Open.../O", 'open_cmd'), + ("Close/W", 'close_cmd'), + "-", + ("Save/S", 'save_cmd'), + ("Save As...", 'save_as_cmd'), + ("Revert", 'revert_cmd'), + "-", + ("Page Setup...", 'page_setup_cmd'), + ("Print.../P", 'print_cmd'), +] + +_edit_menu_items = [ + ("Undo/Z", 'undo_cmd'), + ("Redo/^Z", 'redo_cmd'), + "-", + ("Cut/X", 'cut_cmd'), + ("Copy/C", 'copy_cmd'), + ("Paste/V", 'paste_cmd'), + ("Delete", 'clear_cmd'), + "-", + ("Select All/A", 'select_all_cmd'), +] + +_help_menu_items = [ + ("Help", 'help_cmd'), +] + +#------------------------------------------------------------------------------ + +def basic_menus(substitutions = {}, include = None, exclude = None): + return build_menus([ + ("@", _appl_menu_items, False), + ("File", _file_menu_items, False), + ("Edit", _edit_menu_items, False), + ("Help", _help_menu_items, True), + ], + substitutions = substitutions, + include = include, + exclude = exclude) diff --git a/GUI/Cocoa/Task.py b/GUI/Cocoa/Task.py new file mode 100644 index 0000000..9ad5a3e --- /dev/null +++ b/GUI/Cocoa/Task.py @@ -0,0 +1,89 @@ +# +# PyGUI - Tasks - Cocoa +# + +import sys +from weakref import WeakValueDictionary +from Foundation import NSTimer, NSRunLoop, NSDefaultRunLoopMode +from AppKit import NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode +from GUI import export +from GUI import Globals +from GUI.GTasks import Task as GTask + +#---------------------------------------------------------------------- +# +# Doing things this convoluted way to work around a memory +# leak in PyObjC. Need to avoid having the NSTimer trigger +# creation of a bound method each time it fires or the bound +# methods leak. Also can't use the userInfo of the NSTimer as +# it seems to leak too. + +ns_timer_to_task = WeakValueDictionary() + +class TaskTrigger(object): + pass + +def fire_(ns_timer): + ns_timer_to_task[ns_timer]._ns_fire() + +trigger = TaskTrigger() +trigger.fire_ = fire_ + +#---------------------------------------------------------------------- + +class Task(GTask): + + def __init__(self, proc, interval, repeat = 0, start = 1): + self._proc = proc + self._interval = interval + self._repeat = repeat + self._ns_timer = None + if start: + self.start() + + def destroy(self): + #print "Task.destroy:", self ### + self.stop() + + def get_scheduled(self): + return self._ns_timer is not None + + def get_interval(self): + return self._interval + + def get_repeat(self): + return self._repeat + + def start(self): + self.stop() + #ns_timer = \ + # NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_( + # self._interval, self._target, '_ns_fire', None, self._repeat) + ns_timer = \ + NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_( + self._interval, trigger, 'fire:', None, self._repeat) + self._ns_timer = ns_timer + ns_timer_to_task[ns_timer] = self + ns_run_loop = NSRunLoop.currentRunLoop() + ns_run_loop.addTimer_forMode_( + ns_timer, NSDefaultRunLoopMode) + ns_run_loop.addTimer_forMode_( + ns_timer, NSEventTrackingRunLoopMode) + ns_run_loop.addTimer_forMode_( + ns_timer, NSModalPanelRunLoopMode) + + def stop(self): + ns_timer = self._ns_timer + if ns_timer: + ns_timer.invalidate() + del ns_timer_to_task[ns_timer] + self._ns_timer = None + + def _ns_fire(self): + try: + self._proc() + except: + Globals.pending_exception = sys.exc_info() + self.stop() + +export(Task) diff --git a/GUI/Cocoa/TextEditor.py b/GUI/Cocoa/TextEditor.py new file mode 100644 index 0000000..501e260 --- /dev/null +++ b/GUI/Cocoa/TextEditor.py @@ -0,0 +1,104 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Text Editor - Cocoa +# +#------------------------------------------------------------------------------ + +from AppKit import NSTextView, NSScrollView, NSViewWidthSizable, \ + NSMutableParagraphStyle +from GUI import export +from GUI import StdFonts +from GUI.Utils import NSMultiClass, PyGUI_NS_EventHandler +from GUI.GTextEditors import TextEditor as GTextEditor + +NUM_TAB_STOPS = 32 + +class TextEditor(GTextEditor): + + _ns_handle_mouse = True + + def __init__(self, scrolling = 'hv', **kwds): + width = 100 + height = 100 + frame = ((0, 0), (width, height)) + ns_outer = NSScrollView.alloc().initWithFrame_(frame) + ns_outer.setHasHorizontalScroller_('h' in scrolling) + ns_outer.setHasVerticalScroller_('v' in scrolling) + if 'h' in scrolling: + cwidth = 2000 + else: + cwidth = ns_outer.contentSize()[0] + frame = ((0, 0), (cwidth, height)) + ns_inner = PyGUI_NSTextView.alloc().initWithFrame_(frame) + ns_inner.pygui_component = self + ps = NSMutableParagraphStyle.alloc().init() + ps.setDefaultTabInterval_(ps.tabStops()[0].location()) + ps.setTabStops_([]) + ns_inner.setDefaultParagraphStyle_(ps) + ns_inner.setAllowsUndo_(True) + ns_outer.setDocumentView_(ns_inner) + if 'h' not in scrolling: + ns_inner.setAutoresizingMask_(NSViewWidthSizable) + if 'font' not in kwds: + kwds['font'] = StdFonts.application_font + GTextEditor.__init__(self, ns_outer, + _ns_inner_view = ns_inner, **kwds) + + def get_text(self): + return self._ns_inner_view.string() + + def set_text(self, value): + self._ns_inner_view.setString_(value) + self._ns_apply_style() + + def get_text_length(self): + return self._ns_inner_view.textStorage().length() + + def get_selection(self): + start, length = self._ns_inner_view.selectedRanges()[0].rangeValue() + return (start, start + length) + + def set_selection(self, value): + start, stop = value + self._ns_inner_view.setSelectedRange_((start, stop - start)) + + def get_font(self): + return self._font + + def set_font(self, font): + self._font = font + self._ns_inner_view.setFont_(font._ns_font) + + def get_tab_spacing(self): + #ns_storage = self._ns_inner_view.textStorage() + #ps, _ = ns_storage.attribute_atIndex_effectiveRange_("NSParagraphStyle", 0) + ps = self._ns_inner_view.defaultParagraphStyle() + return ps.defaultTabInterval() + + def set_tab_spacing(self, x): + ps = NSMutableParagraphStyle.alloc().init() + ps.setTabStops_([]) + ps.setDefaultTabInterval_(x) + self._ns_inner_view.setDefaultParagraphStyle_(ps) + self._ns_apply_style() + + def paste_cmd(self): + GTextEditor.paste_cmd(self) + self._ns_apply_style() + + def _ns_apply_style(self): + ns_textview = self._ns_inner_view + ps = ns_textview.defaultParagraphStyle() + font = ns_textview.font() + ns_storage = self._ns_inner_view.textStorage() + ns_storage.setAttributes_range_( + {"NSParagraphStyle": ps, "NSFont": font}, + (0, self.text_length)) + +#------------------------------------------------------------------------------ + +class PyGUI_NSTextView(NSTextView, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + +export(TextEditor) diff --git a/GUI/Cocoa/TextField.py b/GUI/Cocoa/TextField.py new file mode 100644 index 0000000..bfc4587 --- /dev/null +++ b/GUI/Cocoa/TextField.py @@ -0,0 +1,49 @@ +# +# Python GUI - Text Fields - PyObjC +# + +from Foundation import NSRange +from GUI import export +from GUI.StdFonts import system_font #application_font +from GUI import EditCmdHandler +from GUI.TextFieldBasedControls import TextFieldBasedControl +from GUI.GTextFields import TextField as GTextField + +class TextField(TextFieldBasedControl, GTextField): + + #_vertical_padding = 5 + _intercept_tab_key = False + + def __init__(self, text = "", font = system_font, + multiline = False, password = False, border = True, **kwds): + ns_textfield = self._create_ns_textfield(editable = True, + multiline = multiline, password = password, + text = text, font = font, border = border) + GTextField.__init__(self, _ns_view = ns_textfield, + multiline = multiline, **kwds) + + def get_selection(self): + ns_editor = self._ns_editor() + if ns_editor: + start, length = ns_editor.selectedRange() + return (start, start + length) + else: + return (0, 0) + + def set_selection(self, (start, end)): + self.become_target() + ns_editor = self._ns_editor() + if ns_editor: + ns_editor.setSelectedRange_(NSRange(start, end - start)) + + def select_all(self): + self.become_target() + self._ns_view.selectText_(None) + + def _ns_editor(self): + return self._ns_view.currentEditor() + + def _ns_edit_cmd_target(self): + return self._ns_editor() + +export(TextField) diff --git a/GUI/Cocoa/TextFieldBasedControls.py b/GUI/Cocoa/TextFieldBasedControls.py new file mode 100644 index 0000000..9bafabf --- /dev/null +++ b/GUI/Cocoa/TextFieldBasedControls.py @@ -0,0 +1,90 @@ +# +# Python GUI - PyObjC +# +# Base class for controls based on an NSTextField +# + +from Foundation import NSRect, NSPoint, NSSize +from AppKit import NSTextField, NSSecureTextField, NSTextFieldCell +from GUI.Utils import NSMultiClass +from GUI import Color +from GUI.Utils import ns_size_to_fit, PyGUI_NS_EventHandler +from GUI.StdFonts import system_font + +class TextFieldBasedControl(object): + + _ns_handle_mouse = True + + def _create_ns_textfield(self, editable, text, font, + multiline = False, password = False, border = False, + padding = (0, 0)): + self._ns_is_password = password + if password: + ns_class = PyGUI_NSSecureTextField + else: + ns_class = PyGUI_NSTextField + ns_frame = NSRect(NSPoint(0, 0), NSSize(20, 10)) + ns_textfield = ns_class.alloc().initWithFrame_(ns_frame) + ns_textfield.pygui_component = self + if multiline and not password: + ns_textfield.pygui_multiline = True + # Be careful here -- calling setBordered_ seems to affect isBezeled as well + if editable: + ns_textfield.setBezeled_(border) + else: + ns_textfield.setBordered_(border) + if not editable: + ns_textfield.setDrawsBackground_(False) + ns_textfield.setEditable_(editable) + ns_textfield.setSelectable_(editable) + ns_textfield.setFont_(font._ns_font) + ns_textfield.setStringValue_(text) + ns_size_to_fit(ns_textfield, padding = padding) + return ns_textfield + + def get_border(self): + ns_textfield = self._ns_inner_view + if ns_textfield.isEditable(): + return ns_textfield.isBezeled() + else: + return ns_textfield.isBordered() + + def set_border(self, border): + ns_textfield = self._ns_inner_view + if ns_textfield.isEditable(): + ns_textfield.setBezeled_(border) + else: + ns_textfield.setBordered_(border) + + def get_text(self): + return self._ns_inner_view.stringValue() + + def set_text(self, v): + self._ns_inner_view.setStringValue_(v) + + def get_color(self): + return Color._from_ns_color(self._ns_inner_view.textColor()) + + def set_color(self, v): + self._ns_inner_view.setTextColor_(v._ns_color) + + def _get_vertical_padding(self): + if self.border: + return 5 + else: + return 0 + + _vertical_padding = property(_get_vertical_padding) + +#------------------------------------------------------------------------------ + +class PyGUI_NSTextField(NSTextField): #, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + + pygui_multiline = False + +class PyGUI_NSSecureTextField(NSSecureTextField): #, PyGUI_NS_EventHandler): + __metaclass__ = NSMultiClass + + pygui_multiline = False + diff --git a/GUI/Cocoa/Utils.py b/GUI/Cocoa/Utils.py new file mode 100644 index 0000000..c83e90c --- /dev/null +++ b/GUI/Cocoa/Utils.py @@ -0,0 +1,198 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Utilities - PyObjC +# +#------------------------------------------------------------------------------ + +from math import ceil +from inspect import getmro +from Foundation import NSObject +from AppKit import NSView +from GUI import Event, Globals + +def NSMultiClass(name, bases, dic): + # Workaround for PyObjC classes not supporting + # multiple inheritance properly. Note: MRO is + # right to left across the bases. + main = bases[0] + slots = list(dic.get('__slots__', ())) + dic2 = {} + for mix in bases[1:]: + for cls in getmro(mix)[::-1]: + slots2 = cls.__dict__.get('__slots__') + if slots2: + for slot in slots2: + if slot not in slots: + slots.append(slot) + dic2.update(cls.__dict__) + dic2.update(dic) + if slots: + dic2['__slots__'] = slots + cls = type(main)(name, (main,), dic2) + return cls + +#------------------------------------------------------------------------------ + +class PyGUI_Flipped_NSView(NSView): + # An NSView with a flipped coordinate system. + + def isFlipped(self): + return True + +#------------------------------------------------------------------------------ + +class PyGUI_NSActionTarget(NSObject): + # A shared instance of this class is used as the target of + # all action messages from the NSViews of Components. It + # performs the action by calling the similarly-named method of + # the corresponding Component. + + def doAction_(self, ns_sender): + self.call_method('do_action', ns_sender) + + def call_method(self, method_name, ns_sender): + component = Globals._ns_view_to_component.get(ns_sender) + if component: + getattr(component, method_name)() + +_ns_action_target = PyGUI_NSActionTarget.alloc().init() + +def ns_set_action(ns_control, method_name): + # Arrange for the 'action' message of the NSControl to + # invoke the indicated method of its associated Component. + ns_control.setAction_(method_name) + ns_control.setTarget_(_ns_action_target) + +#------------------------------------------------------------------------------ + +class PyGUI_NS_EventHandler: + # Methods to be mixed in with NSView subclasses that are + # to relay mouse and keyboard events to a Component. + # + # pygui_component Component + + def mouseDown_(self, ns_event): + self._ns_mouse_event(ns_event) + + def mouseUp_(self, ns_event): + self._ns_mouse_event(ns_event) + + def mouseDragged_(self, ns_event): + self._ns_mouse_event(ns_event) + + def rightMouseDown_(self, ns_event): + self._ns_mouse_event(ns_event) + + def rightMouseUp_(self, ns_event): + self._ns_mouse_event(ns_event) + + def rightMouseDragged_(self, ns_event): + self._ns_mouse_event(ns_event) + + def otherMouseDown_(self, ns_event): + self._ns_mouse_event(ns_event) + + def otherMouseUp_(self, ns_event): + self._ns_mouse_event(ns_event) + + def otherMouseDragged_(self, ns_event): + self._ns_mouse_event(ns_event) + + def mouseMoved_(self, ns_event): + self._ns_mouse_event(ns_event) + + def mouseEntered_(self, ns_event): + self._ns_mouse_event(ns_event) + + def mouseExited_(self, ns_event): + self._ns_mouse_event(ns_event) + + def keyDown_(self, ns_event): + #print "PyGUI_NS_EventHandler.keyDown_:", repr(ns_event.characters()), \ + # "for", object.__repr__(self) ### + self._ns_other_event(ns_event) + + def keyUp_(self, ns_event): + #print "PyGUI_NS_EventHandler.keyUp_ for", self ### + self._ns_other_event(ns_event) + + def _ns_mouse_event(self, ns_event): + #print "PyGUI_NS_EventHandler._ns_mouse_event" ### + event = self._ns_mouse_event_to_event(ns_event) + #print "...sending to", self.pygui_component ### + component = self.pygui_component + if component: + component.handle_event_here(event) + + def _ns_mouse_event_to_event(self, ns_event): + event = Event(ns_event) + event.position = tuple(self._ns_event_position(ns_event)) + return event + + def _ns_event_position(self, ns_event): + #print "PyGUI_NS_EventHandler._ns_event_position:", self ### + #print "...mro =", self.__class__.__mro__ ### + ns_win_pos = ns_event.locationInWindow() + return self.convertPoint_fromView_(ns_win_pos, None) + + def _ns_other_event(self, ns_event): + #print "PyGUI_NS_EventHandler._ns_other_event for", self ### + event = Event(ns_event) + component = self.pygui_component + if component: + #print "...passing", event.kind, "to", component ### + component.handle_event(event) + + def acceptsFirstResponder(self): + ###return True + return self.pygui_component._ns_accept_first_responder + +# def canBecomeKeyView(self): +# return self.pygui_component._tab_stop + +#------------------------------------------------------------------------------ + +class PyGUI_NS_ViewBase(PyGUI_NS_EventHandler): + # Methods to be mixed in with PyGUI_NSView classes. + # + # pygui_component ViewBase + + __slots__ = ['tracking_rect'] + +# tracking_rect = None + + def becomeFirstResponder(self): + self.pygui_component.targeted() + return True + + def resignFirstResponder(self): + self.pygui_component.untargeted() + return True + + def resetCursorRects(self): + #print "PyGUI_NS_ViewBase: resetCursorRects" ### + self.removeCursorRects() + self.tracking_rect = self.addTrackingRect_owner_userData_assumeInside_( + self.visibleRect(), self, 0, False) + self.pygui_component._ns_reset_cursor_rects() + + def removeCursorRects(self): + #print "PyGUI_NS_ViewBase: removeCursorRects" ### + tag = getattr(self, 'tracking_rect', None) + if tag: + self.removeTrackingRect_(tag) + self.tracking_rect = None + +#------------------------------------------------------------------------------ + +def ns_size_to_fit(ns_control, padding = (0, 0), height = None): + # Set size of control to fit its contents, plus the given padding. + # Height may be overridden, because some controls don't seem to + # calculate it properly. + # Auto sizing can result in fractional sizes, which seems to cause + # problems when NS autoresizing occurs later. So we round the size up + # to whole numbers of pixels. + ns_control.sizeToFit() + w, h = ns_control.frame().size + pw, ph = padding + ns_control.setFrameSize_((ceil(w + pw), ceil((height or h) + ph))) diff --git a/GUI/Cocoa/View.py b/GUI/Cocoa/View.py new file mode 100644 index 0000000..74a13e8 --- /dev/null +++ b/GUI/Cocoa/View.py @@ -0,0 +1,12 @@ +# +# Python GUI - Views - PyObjC +# + +from GUI import export +from GUI.GViews import View as GView + +class View(GView): + + _ns_scrollable = False + +export(View) diff --git a/GUI/Cocoa/ViewBase.py b/GUI/Cocoa/ViewBase.py new file mode 100644 index 0000000..0cf6d05 --- /dev/null +++ b/GUI/Cocoa/ViewBase.py @@ -0,0 +1,61 @@ +# +# Python GUI - View Base - PyObjC +# + +import Foundation +import AppKit +from GUI import Globals, export +from GUI.Properties import overridable_property +from GUI import Event +from GUI.Utils import PyGUI_NS_EventHandler +from GUI.GViewBases import ViewBase as GViewBase + +ns_tracking_mask = ( + AppKit.NSLeftMouseDraggedMask | + AppKit.NSRightMouseDraggedMask | + AppKit.NSOtherMouseDraggedMask | + AppKit.NSLeftMouseUpMask | + AppKit.NSRightMouseUpMask | + AppKit.NSOtherMouseUpMask) + +# Need to use NSDefaultRunLoopMode here otherwise timers don't fire. +ns_tracking_mode = Foundation.NSDefaultRunLoopMode # AppKit.NSEventTrackingRunLoopMode + +ns_distant_future = Foundation.NSDate.distantFuture() + + +class ViewBase(GViewBase): + + def _change_container(self, new_container): + self._ns_inner_view.removeCursorRects() + super(ViewBase, self)._change_container(new_container) + + def _ns_track_mouse(self, ns_view): + ns_app = Globals.ns_application + tracking = True + while tracking: + ns_event = ns_app.nextEventMatchingMask_untilDate_inMode_dequeue_( + ns_tracking_mask, ns_distant_future, ns_tracking_mode, True) + event = ns_view._ns_mouse_event_to_event(ns_event) + yield event + tracking = event.kind <> 'mouse_up' + + def _cursor_changed(self): + #print "ViewBase._cursor_changed:", self ### + ns_view = self._ns_view + ns_window = ns_view.window() + if ns_window: + # invalidateCursorRectsForView_ doesn't seem to trigger + # resetCursorRects on the view. + #ns_window.invalidateCursorRectsForView_(ns_view) + ns_window.resetCursorRects() + + def _ns_reset_cursor_rects(self): + #print "ViewBase._ns_reset_cursor_rects:", self ### + cursor = self._cursor + if cursor: + ns_view = self._ns_inner_view + ns_rect = ns_view.visibleRect() + ns_view.addCursorRect_cursor_(ns_rect, cursor._ns_cursor) + +export(ViewBase) diff --git a/GUI/Cocoa/Window.py b/GUI/Cocoa/Window.py new file mode 100644 index 0000000..62d4037 --- /dev/null +++ b/GUI/Cocoa/Window.py @@ -0,0 +1,292 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Windows - PyObjC version +# +#------------------------------------------------------------------------------ + +from Foundation import NSRect, NSPoint, NSSize, NSObject +import AppKit +from AppKit import NSWindow, NSScreen, NSTextView, NSMenu +from GUI import export +from GUI import Globals +from GUI.Utils import NSMultiClass, PyGUI_NS_EventHandler, PyGUI_Flipped_NSView +from GUI import application +from GUI import Event +from GUI.GWindows import Window as GWindow + +_default_options_for_style = { + 'standard': + {'movable': 1, 'closable': 1, 'hidable': 1, 'resizable': 1}, + 'nonmodal_dialog': + {'movable': 1, 'closable': 0, 'hidable': 1, 'resizable': 0}, + 'modal_dialog': + {'movable': 1, 'closable': 0, 'hidable': 0, 'resizable': 0}, + 'alert': + {'movable': 1, 'closable': 0, 'hidable': 0, 'resizable': 0}, + 'fullscreen': + {'movable': 0, 'closable': 0, 'hidable': 0, 'resizable': 0}, + #{'movable': 1, 'closable': 1, 'hidable': 1, 'resizable': 1}, +} + +#------------------------------------------------------------------------------ + +class Window(GWindow): + # _ns_window PyGUI_NSWindow + # _ns_style_mask int + + def __init__(self, style = 'standard', zoomable = None, **kwds): + # We ignore zoomable, since it's the same as resizable. + self._style = style + options = dict(_default_options_for_style[style]) + for option in ['movable', 'closable', 'hidable', 'resizable']: + if option in kwds: + options[option] = kwds.pop(option) + self._ns_style_mask = self._ns_window_style_mask(**options) + if style == 'fullscreen': + ns_rect = NSScreen.mainScreen().frame() + else: + ns_rect = NSRect(NSPoint(0, 0), NSSize(self._default_width, self._default_height)) + ns_window = PyGUI_NSWindow.alloc() + ns_window.initWithContentRect_styleMask_backing_defer_( + ns_rect, self._ns_style_mask, AppKit.NSBackingStoreBuffered, True) + ns_content = PyGUI_NS_ContentView.alloc() + ns_content.initWithFrame_(NSRect(NSPoint(0, 0), NSSize(0, 0))) + ns_content.pygui_component = self + ns_window.setContentView_(ns_content) + ns_window.setAcceptsMouseMovedEvents_(True) + ns_window.setDelegate_(ns_window) + ns_window.pygui_component = self + self._ns_window = ns_window + GWindow.__init__(self, style = style, closable = options['closable'], + _ns_view = ns_window.contentView(), _ns_responder = ns_window, + _ns_set_autoresizing_mask = False, + **kwds) + + def _ns_window_style_mask(self, movable, closable, hidable, resizable): + if movable or closable or hidable or resizable: + mask = AppKit.NSTitledWindowMask + if closable: + mask |= AppKit.NSClosableWindowMask + if hidable: + mask |= AppKit.NSMiniaturizableWindowMask + if resizable: + mask |= AppKit.NSResizableWindowMask + else: + mask = AppKit.NSBorderlessWindowMask + return mask + + def destroy(self): + #print "Window.destroy:", self ### + self.hide() + app = application() + if app._ns_key_window is self: + app._ns_key_window = None + GWindow.destroy(self) + # We can't drop all references to the NSWindow yet, because this method + # can be called from its windowShouldClose: method, and allowing an + # NSWindow to be released while executing one of its own methods seems + # to be a very bad idea (Cocoa hangs). So we hide the NSWindow and store + # a reference to it in a global. It will be released the next time a + # window is closed and the global is re-used. + global _ns_zombie_window + _ns_zombie_window = self._ns_window + self._ns_window.pygui_component = None + #self._ns_window = None + + def get_bounds(self): + ns_window = self._ns_window + ns_frame = ns_window.frame() + (l, y), (w, h) = ns_window.contentRectForFrameRect_styleMask_( + ns_frame, self._ns_style_mask) + b = Globals.ns_screen_height - y + result = (l, b - h, l + w, b) + return result + + def set_bounds(self, (l, t, r, b)): + y = Globals.ns_screen_height - b + ns_rect = NSRect(NSPoint(l, y), NSSize(r - l, b - t)) + ns_window = self._ns_window + ns_frame = ns_window.frameRectForContentRect_styleMask_( + ns_rect, self._ns_style_mask) + ns_window.setFrame_display_(ns_frame, False) + + def get_title(self): + return self._ns_window.title() + + def set_title(self, v): + self._ns_window.setTitle_(v) + + def get_visible(self): + return self._ns_window.isVisible() + + def set_visible(self, v): + # At some mysterious time between creating a window and showing + # it for the first time, Cocoa adjusts its position so that it + # doesn't extend above the menu bar. This is a nuisance for + # our fullscreen windows, so we need to readjust the position + # before showing. + if v: + if self._style == 'fullscreen': + self._ns_window.setFrameOrigin_(NSPoint(0, 0)) + self._ns_window.orderFront_(None) + else: + self._ns_window.orderOut_(None) + + def _show(self): + self.visible = True + self._ns_window.makeKeyWindow() + + def get_target(self): + ns_window = self._ns_window + ns_view = ns_window.firstResponder() + while ns_view and ns_view is not ns_window: + component = Globals._ns_view_to_component.get(ns_view) + if component: + return component + ns_view = ns_view.superview() + return self + + def center(self): + self._ns_window.center() + + def _stagger(self): + key_win = application()._ns_key_window + if key_win: + (x, y), (w, h) = key_win._ns_window.frame() + p = self._ns_window.cascadeTopLeftFromPoint_(NSPoint(x, y + h)) + self._ns_window.setFrameTopLeftPoint_(p) + else: + (x, y), (w, h) = NSScreen.mainScreen().visibleFrame() + ns_vis_topleft = NSPoint(x, y + h) + self._ns_window.setFrameTopLeftPoint_(ns_vis_topleft) + + def _document_needs_saving(self, state): + self._ns_window.setDocumentEdited_(state) + +#------------------------------------------------------------------------------ + +class PyGUI_NSWindow(NSWindow, PyGUI_NS_EventHandler): + # pygui_component Window + # resize_delta point or None + + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component', 'resize_delta', 'pygui_field_editor'] + +# pygui_component = None +# resize_delta = None +# pygui_field_editor = None + + def _ns_event_position(self, ns_event): + return ns_event.locationInWindow() + + def windowShouldClose_(self, sender): + # We use this to detect when the Aqua window closing button + # is pressed, and do the closing ourselves. + self.pygui_component.close_cmd() + return False + + # The NSWindow is made its own delegate. + + def windowWillResize_toSize_(self, ns_win, new_ns_size): + w0, h0 = self.frame().size + w1, h1 = new_ns_size + self.resize_delta = (w1 - w0, h1 - h0) + return new_ns_size + + def windowDidResize_(self, notification): + delta = getattr(self, 'resize_delta', None) + if delta: + self.pygui_component._resized(delta) + self.resize_delta = None + + def windowDidBecomeKey_(self, notification): + app = application() + app._ns_key_window = self.pygui_component + app._update_menubar() + + def windowDidResignKey_(self, notification): + app = application() + app._ns_key_window = None + app._update_menubar() + + def windowWillReturnFieldEditor_toObject_(self, ns_window, ns_obj): + # Return special field editor for newline handling in text fields. + #print "Window: Field editor requested for", object.__repr__(ns_obj) ### + #editor = self.pygui_field_editor + #if not editor: + try: + editor = self.pygui_field_editor + except AttributeError: + #print "...creating new field editor" ### + editor = PyGUI_FieldEditor.alloc().initWithFrame_( + NSRect(NSPoint(0, 0), NSSize(0, 0))) + editor.setFieldEditor_(True) + editor.setDrawsBackground_(False) + self.pygui_field_editor = editor + return editor + + # Need the following two methods so that a fullscreen window can become + # the main window. Otherwise it can't, because it has no title bar. + + def canBecomeKeyWindow(self): + return self.isVisible() + + def canBecomeMainWindow(self): + #print "PyGUI_NSWindow.canBecomeMainWindow" + return self.isVisible() + + def windowDidBecomeMain_(self, notification): + #print "PyGUI_NSWindow.windowDidBecomeMain_:", self.pygui_component.title ### + comp = self.pygui_component + if comp and comp._style == 'fullscreen': + #print "...hiding menu bar" ### + NSMenu.setMenuBarVisible_(False) + #self.setFrameOrigin_(NSPoint(0, 0)) + + def windowDidResignMain_(self, notification): + #print "PyGUI_NSWindow.windowDidResignMain_:", self.pygui_component.title ### + comp = self.pygui_component + if comp and comp._style == 'fullscreen': + #print "...showing menu bar" ### + NSMenu.setMenuBarVisible_(True) + +#------------------------------------------------------------------------------ + +class PyGUI_NS_ContentView(PyGUI_Flipped_NSView, PyGUI_NS_EventHandler): + # pygui_component Window + + __metaclass__ = NSMultiClass + __slots__ = ['pygui_component'] + + def acceptsFirstResponder(self): + return False + +#------------------------------------------------------------------------------ + +class PyGUI_FieldEditorBase(NSTextView): + # Special field editor for use by TextFields. Intercepts + # return key events and handles them our own way. + + def keyDown_(self, ns_event): + #print "PyGUI_FieldEditorBase.keyDown_ for", self.pygui_component ### + if ns_event.characters() == "\r": + if self.pygui_component._multiline: + self.insertText_("\n") + return + NSTextView.keyDown_(self, ns_event) + +#------------------------------------------------------------------------------ + +class PyGUI_FieldEditor(PyGUI_FieldEditorBase, PyGUI_NS_EventHandler): + + __metaclass__ = NSMultiClass + + def get_pygui_component(self): + pygui_nstextfield = self.superview().superview() + component = pygui_nstextfield.pygui_component + component._ns_responder = self + return component + + pygui_component = property(get_pygui_component) + +export(Window) diff --git a/GUI/Generic/Actions.py b/GUI/Generic/Actions.py new file mode 100644 index 0000000..7f2ea48 --- /dev/null +++ b/GUI/Generic/Actions.py @@ -0,0 +1,62 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Actions - Generic +# +#------------------------------------------------------------------------------ + +from GUI.Properties import overridable_property +from GUI.Exceptions import ApplicationError + +#------------------------------------------------------------------------------ + +def action_property(name, doc): + attr = intern('_' + name) + def getter(self): + return getattr(self, attr) + def setter(self, value): + setattr(self, attr, value) + return property(getter, setter, None, doc) + +#------------------------------------------------------------------------------ + +class ActionBase(object): + """Mixin class providing base support for action properties.""" + + def do_named_action(self, name): + #print "ActionBase.do_named_action:", repr(name) ### + action = getattr(self, name) + #print "...action =", repr(action) ### + if action: + try: + if isinstance(action, tuple): + args = action[1:] + action = action[0] + else: + args = () + if isinstance(action, str): + #print "...handling", action ### + self.handle(action, *args) + else: + action(*args) + except ApplicationError: + raise + except: + import sys + et, ev, tb = sys.exc_info() + raise et, et("%s (while doing action %r%r)" % (ev, action, args)), tb + +#------------------------------------------------------------------------------ + +class Action(ActionBase): + """Mixin class providing a single action property called 'action'.""" + + action = action_property('action', """Action to be performed. + May be or (, ...) where is either + a message name or a callable object.""") + + _action = None + + def do_action(self): + "Invoke the action." + self.do_named_action('action') + diff --git a/GUI/Generic/Alerts.py b/GUI/Generic/Alerts.py new file mode 100644 index 0000000..efc5351 --- /dev/null +++ b/GUI/Generic/Alerts.py @@ -0,0 +1,63 @@ +#----------------------------------------------------------------------- +# +# PyGUI - Alert functions - Generic +# +#----------------------------------------------------------------------- + +from GUI import BaseAlertFunctions + +def alert(kind, prompt, ok_label = "OK", **kwds): + """Displays an alert box with one button. Does not return a value. + Kind may be 'stop' for conditions preventing continuation, + 'caution' for warning messages, 'note' for informational + messages, and 'query' for asking a question of the user.""" + + BaseAlertFunctions.alert(kind, prompt, ok_label, **kwds) + + +def alert2(kind, prompt, yes_label = "Yes", no_label = "No", + **kwds): + """Displays an alert with two buttons. Returns 1 if the + first button is pressed, 0 if the second button is pressed. + The 'default' and 'cancel' arguments specify which buttons, + if any, are activated by the standard keyboard equivalents, + and take the values 1, 0 or None.""" + + return BaseAlertFunctions.alert2(kind, prompt, yes_label, no_label,**kwds) + + +def alert3(kind, prompt, + yes_label = "Yes", no_label = "No", other_label = "Cancel", + **kwds): + """Displays an alert with 3 buttons. Returns 1 if the + first button is pressed, 0 if the second button is pressed, + and -1 if the third button is pressed. The 'default' and 'cancel' + arguments specify which buttons, if any, are activated by the + standard keyboard equivalents, and take the values 1, 0, -1 or None.""" + + return BaseAlertFunctions.alert3(kind, prompt, yes_label, no_label, other_label, **kwds) + + +def stop_alert(*args, **kwds): + """Displays a 1-button alert of type 'stop'. See alert().""" + alert('stop', *args, **kwds) + +def note_alert(*args, **kwds): + """Displays a 1-button alert of type 'note'. See alert().""" + alert('note', *args, **kwds) + +def confirm(*args, **kwds): + """Displays a 2-button alert of type 'caution'. See alert2().""" + return alert2('caution', *args, **kwds) + +def ask(*args, **kwds): + """Displays a 2-button alert of type 'query'. See alert2().""" + return alert2('query', *args, **kwds) + +def confirm_or_cancel(*args, **kwds): + """Displays a 3-button alert of type 'caution'. See alert3().""" + return alert3('caution', *args, **kwds) + +def ask_or_cancel(*args, **kwds): + """Displays a 3-button alert of type 'query'. See alert3().""" + return alert3('query', *args, **kwds) diff --git a/GUI/Generic/BaseAlertFunctions.py b/GUI/Generic/BaseAlertFunctions.py new file mode 100644 index 0000000..84a0b38 --- /dev/null +++ b/GUI/Generic/BaseAlertFunctions.py @@ -0,0 +1,26 @@ +# +# Python GUI - Basic alert functions - Generic +# + +from GUI.AlertClasses import Alert, Alert2, Alert3 + +def present_and_destroy(dlog): + dlog.center() + try: + return dlog.present() + finally: + dlog.destroy() + + +def alert(kind, prompt, ok_label, **kwds): + present_and_destroy(Alert(kind, prompt, ok_label)) + + +def alert2(kind, prompt, yes_label, no_label, **kwds): + return present_and_destroy( + Alert2(kind, prompt, yes_label, no_label, **kwds)) + + +def alert3(kind, prompt, yes_label, no_label, other_label, **kwds): + return present_and_destroy( + Alert3(kind, prompt, yes_label, no_label, other_label, **kwds)) diff --git a/GUI/Generic/Column.py b/GUI/Generic/Column.py new file mode 100644 index 0000000..715d666 --- /dev/null +++ b/GUI/Generic/Column.py @@ -0,0 +1,52 @@ +#--------------------------------------------------------------------------- +# +# PyGUI - Column layout component - Generic +# +#--------------------------------------------------------------------------- + +from LayoutUtils import equalize_components +from GUI import Frame, export + +class Column(Frame): + + def __init__(self, items, spacing = 10, align = 'l', equalize = '', + expand = None, padding = (0, 0), **kwds): + Frame.__init__(self) + hpad, vpad = padding + if expand is not None and not isinstance(expand, int): + expand = items.index(expand) + equalize_components(items, equalize) + width = 0 + for item in items: + if item: + width = max(width, item.width) + y = vpad + gap = 0 + vanchor = 't' + hanchor = align + for i, item in enumerate(items): + if item: + y += gap + if 'l' in align: + x = 0 + if 'r' in align: + item.width = width + elif align == 'r': + x = width - item.width + else: + x = (width - item.width) // 2 + item.position = (x + hpad, y) + if i == expand: + item.anchor = 'tb' + hanchor + vanchor = 'b' + else: + item.anchor = vanchor + hanchor + y += item.height + if i == expand: + vanchor = 'b' + gap = spacing + self.size = (width + 2 * hpad, y + vpad) + self.add(items) + self.set(**kwds) + +export(Column) diff --git a/GUI/Generic/Compatibility.py b/GUI/Generic/Compatibility.py new file mode 100644 index 0000000..87eda05 --- /dev/null +++ b/GUI/Generic/Compatibility.py @@ -0,0 +1,10 @@ +#------------------------------------------------------------------------------- +# +# PyGUI - Facilities for compatibility across Python versions +# +#------------------------------------------------------------------------------- + +try: + from __builtin__ import set +except ImportError: + from sets import Set as set diff --git a/GUI/Generic/Document.py b/GUI/Generic/Document.py new file mode 100644 index 0000000..1ff797c --- /dev/null +++ b/GUI/Generic/Document.py @@ -0,0 +1,321 @@ +# +# Python GUI - Documents - Generic +# + +import os, tempfile +from GUI import export +from GUI.Alerts import confirm, confirm_or_cancel +from GUI.Properties import overridable_property +from GUI import Model +from GUI import MessageHandler +from GUI.Files import FileRef, DirRef +from GUI.FileDialogs import request_new_file +from GUI import application +from GUI.Exceptions import Cancel, UnimplementedMethod, ApplicationError +from GUI.Printing import PageSetup, present_page_setup_dialog + +_next_doc_number = 1 # Counter for generating default titles + +class Document(Model, MessageHandler): + """A Document represents an + application data structure that can be stored in a file. It + implements the standard parts of asking the user for file names and + reading and writing files. + + Each Document can have one or more windows associated with it. When + the last window belonging to a document is closed, the document itself + is closed. + + A Document provides support for keeping track of whether it has been + edited, and asking the user whether to save changes when it is + closed.""" + + # The following attribute prevents a Document that is the parent + # of a Model from being pickled along with that Model. + pickle_as_parent_model = False + + needs_saving = overridable_property('needs_saving', + "True if the document has been edited and needs to be saved.") + + file = overridable_property('file', + """FileRef of the file that the document was read from or last written + to, or None. Changing this causes update_title to be called.""") + + file_type = overridable_property('file_type', + """FileType specifying the type of file handled by this document.""") + + title = overridable_property('title', + """The title of the document. Changing this causes update_title of each + associated window to be called.""") + + windows = overridable_property('windows', + "List of windows associated with the document. Do not modify directly.") + + page_setup = overridable_property('page_setup', + "The PageSetup to be used for printing this document.") + + binary = True # True if files are to be opened in binary mode + + _file_type = None # Type of file to create when saving + _needs_saving = 0 # True if has been edited + _file = None # FileRef of associated file, if any + _title = None # Title for use in window banners, etc. + _windows = None # List of associated windows + _page_setup = None # Document-specific PageSetup instance + + # + # Initialisation and destruction + # + + def __init__(self, **kwds): + self._windows = [] + Model.__init__(self, **kwds) + application()._add_document(self) + + def destroy(self): + """Destroy any associated windows, then destroy document contents.""" + #print "Document.destroy:", self ### + for win in self._windows[:]: + win.destroy() + application()._remove_document(self) + self.destroy_contents() + Model.destroy(self) + + # + # Properties + # + + def get_needs_saving(self): + return self._needs_saving + + def set_needs_saving(self, x): + if self._needs_saving <> x: + self._needs_saving = x + for window in self._windows: + window._document_needs_saving(x) + + def get_file(self): + return self._file + + def set_file(self, x): + self._file = x + if x is not None: + application()._last_directory = x.dir + self.update_title() + + def get_file_type(self): + return self._file_type + + def set_file_type(self, x): + self._file_type = x + + def get_title(self): + t = self._title + if t == None: + t = self.make_title() + self._title = t + return t + + def set_title(self, x): + self._title = x + for win in self._windows: + win.update_title() + + def get_windows(self): + return self._windows + + def get_page_setup(self): + ps = self._page_setup + if not ps: + ps = PageSetup() + self._page_setup = ps + return ps + + def set_page_setup(self, ps): + self._page_setup = ps + + # + # Methods + # + + def changed(self): + "Set the needs_saving property to true." + self.needs_saving = 1 + + def new_contents(self): + """Should initialise the document to the appropriate state following a New + command.""" + pass + + def read_contents(self, file): + """Should initialise the document's contents by reading it from the given + file object.""" + raise UnimplementedMethod(self, 'read_contents') + + def write_contents(self, file): + """Should write the document's contents to the given file object.""" + raise UnimplementedMethod(self, 'write_contents') + + def destroy_contents(self): + """Called when the contents of the document are about to be discarded. + If the contents contains any Model objects, they should be destroyed.""" + + def save_changes(self): + """If the document has been edited, ask the user whether to save changes, + and do so if requested.""" + if self._needs_saving: + result = confirm_or_cancel('Save changes to "%s"?' % self.title, + "Save", "Don't Save", "Cancel") + if result < 0: + raise Cancel + if result: + self.save_cmd() + + def save_cmd(self): + """Implements the standard Save command. Writes the document to its + associated file, asking the user for one first if necessary.""" + if self._file == None: + self.get_new_file_name() + try: + self.write() + except EnvironmentError, e: + raise ApplicationError("Unable to save '%s'." % self._file.name, e) + + def save_as_cmd(self): + """Implements the standard Save As... command. Asks the user for a new file + and writes the document to it.""" + self.get_new_file_name() + self.save_cmd() + + def revert_cmd(self): + """Implements the standard Revert command. Discards the current contents + of the document and re-reads it from the associated file.""" + if self._file != None: + if confirm( + 'Revert to the last saved version of "%s"?' % self.title, + "Revert", "Cancel"): + self.destroy_contents() + self.read() + + def close_cmd(self): + """Implements the standard Close command. Asks whether to save any + changes, then destroys the document.""" + self.save_changes() + self.destroy() + + def page_setup_cmd(self): + if present_page_setup_dialog(self.page_setup): + self.changed() + + def make_title(self): + """Generates a title for the document. If associated with a file, + uses its last pathname component, else generates 'Untitled-n'.""" + global _next_doc_number + if self._file != None: + return os.path.basename(self._file) + else: + n = _next_doc_number + _next_doc_number = n + 1 + return "Untitled-%d" % n + + def update_title(self): + """Called when the file property changes, to update the + title property appropriately.""" + file = self._file + if file: + self.title = file.name + + def get_default_save_directory(self): + """Called when the user is about to be asked for a location in which + to save a document that has not been saved before, to find a default + directory for request_new_file(). Should return a DirRef or FileRef, + or None if there is no particular preferred location.""" + return None + + def get_default_save_filename(self): + """Called when the user is about to be asked for a location in which + to save a document that has not been saved before, to find a default + file name for request_new_file(). Should return a suggested file name, + or an empty string to require the user to enter a file name.""" + return "" + + # + # Internal methods + # + + def get_new_file_name(self): + """Ask the user for a new file and associate the document with it.""" + old_file = self.file + if old_file: + old_name = old_file.name + old_dir = old_file.dir + else: + old_name = self.get_default_save_filename() + old_dir = self.get_default_save_directory() + #print "Document.get_new_file_name: old_dir =", old_dir, "old_name =", old_name ### + new_file = request_new_file( + #'Save "%s" as:' % self.title, + default_dir = old_dir, + default_name = old_name, + file_type = self.file_type or application().save_file_type) + if new_file is None: + raise Cancel() + self.file = new_file + + def read(self): + """Read the document from its currently associated file. The + document must be associated with a file and not have any existing + contents when this is called.""" + if self.binary: + mode = "rb" + else: + mode = "rU" + file = self.file.open(mode) + try: + self.read_contents(file) + finally: + file.close() + self.needs_saving = 0 + + def write(self): + """Write the document to its currently associated file. The + document must be associated with a file when this is called. + The document is initially written to a temporary file which + is then renamed, so if writing fails part way through, the + original file is undisturbed.""" + if self.binary: + mode = "wb" + else: + mode = "w" + dir_path = self.file.dir.path + fd, temp_path = tempfile.mkstemp(dir = dir_path, text = not self.binary) + file = os.fdopen(fd, mode) + try: + try: + self.write_contents(file) + finally: + file.close() + except EnvironmentError: + os.unlink(fd) + raise + path = self.file.path + try: + os.unlink(path) + except EnvironmentError: + pass + os.rename(temp_path, path) + self.needs_saving = 0 + + def setup_menus(self, m): + #print "Document.setup_menus" ### + if self._needs_saving or not self._file: + m.save_cmd.enabled = 1 + if self._needs_saving and self._file: + m.revert_cmd.enabled = 1 + m.save_as_cmd.enabled = 1 + m.page_setup_cmd.enabled = 1 + + def next_handler(self): + return application() + +export(Document) diff --git a/GUI/Generic/Enumerations.py b/GUI/Generic/Enumerations.py new file mode 100644 index 0000000..310168f --- /dev/null +++ b/GUI/Generic/Enumerations.py @@ -0,0 +1,16 @@ +# +# PyGUI - Enumerated type facilities +# + +class EnumMap(dict): + + def __init__(self, __name__, *args, **kwds): + self.name = __name__ + dict.__init__(self, *args, **kwds) + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + raise ValueError("Invalid %s '%s', should be one of %s" % + (self.name, key, ", ".join(["'%s'" % val for val in self.keys()]))) diff --git a/GUI/Generic/Exceptions.py b/GUI/Generic/Exceptions.py new file mode 100644 index 0000000..470a80d --- /dev/null +++ b/GUI/Generic/Exceptions.py @@ -0,0 +1,67 @@ +# +# Exceptions.py - GUI exception classes +# + +class Cancel(Exception): + """Exception raised when user cancels an operation.""" + pass + + +#class Quit(Exception): +# """Exception raised to exit the main event loop.""" +# pass + + +class Error(StandardError): + + def __init__(self, obj, mess): + self.obj = obj + self.mess = mess + Exception.__init__(self, "%s: %s" % (obj, mess)) + + +class ApplicationError(StandardError): + """Exception used for reporting errors to the user.""" + + def __init__(self, message, detail = None): + self.message = message + self.detail = detail + if detail: + message = "%s\n\n%s" % (message, detail) + StandardError.__init__(self, message) + + +class InternalError(Exception): + pass + + +class UnimplementedMethod(NotImplementedError): + + def __init__(self, obj, meth_name): + self.obj = obj + StandardError.__init__(self, "%s.%s not implemented" % \ + (obj.__class__.__name__, meth_name)) + + +class ArgumentError(TypeError): + + def __init__(self, obj, meth_name, arg_name, value): + self.obj = obj + self.meth_name = meth_name + self.arg_name = arg_name + self.value = value + TypeError.__init__(self, + "%s: Invalid value %s for argument %s of method %s", + (obj, value, arg_name, meth_name)) + + +class SetAttributeError(AttributeError): + + def __init__(self, obj, attr): + self.obj = obj + self.attr = attr + AttributeError.__init__(self, "Attribute '%s' of %s cannot be set" % (attr, obj)) + + +class UsageError(StandardError): + pass diff --git a/GUI/Generic/FileDialogs.py b/GUI/Generic/FileDialogs.py new file mode 100644 index 0000000..ced310a --- /dev/null +++ b/GUI/Generic/FileDialogs.py @@ -0,0 +1,56 @@ +# +# Python GUI - File selection dialogs - Generic +# + +from GUI.BaseFileDialogs import _request_old, _request_new + + +def request_old_file(prompt = "Open File", default_dir = None, file_types = None): + """Present a dialog for selecting an existing file. + Returns a FileRef, or None if cancelled.""" + + return _request_old(prompt, default_dir, file_types, + dir = False, multiple = False) + + +def request_old_files(prompt = "Choose Files", default_dir = None, file_types = None): + """Present a dialog for selecting a set of existing files. + Returns a list of FileRefs, or None if cancelled.""" + + return _request_old(prompt, default_dir, file_types, + dir = False, multiple = True) + + +def request_old_directory(prompt = "Choose Folder", default_dir = None): + """Present a dialog for selecting an existing directory. + Returns a FileRef, or None if cancelled.""" + + return _request_old(prompt, default_dir, file_types = None, + dir = True, multiple = False) + + +def request_old_directories(prompt = "Choose Folders", default_dir = None, + multiple = False): + """Present a dialog for selecting a set of existing directories. + Returns a list of FileRefs, or None if cancelled.""" + + return _request_old(prompt, default_dir, file_types = None, + dir = True, multiple = True) + + +def request_new_file(prompt = "Save As:", default_dir = None, + default_name = "", file_type = None): + """Present a dialog requesting a name and location for a new file. + Returns a FileRef, or None if cancelled.""" + + return _request_new(prompt, default_dir, default_name, file_type, + dir = False) + + +def request_new_directory(prompt = "Create Folder:", default_dir = None, + default_name = ""): + """Present a dialog requesting a name and location for a new directory. + Returns a FileRef, or None if cancelled.""" + + return _request_new(prompt, default_dir, default_name, file_type = None, + dir = True) diff --git a/GUI/Generic/GAlertClasses.py b/GUI/Generic/GAlertClasses.py new file mode 100644 index 0000000..d8eba49 --- /dev/null +++ b/GUI/Generic/GAlertClasses.py @@ -0,0 +1,67 @@ +# +# Python GUI - Alerts - Generic +# + +from GUI import BaseAlert +from GUI import Button +from GUI.StdButtons import DefaultButton, CancelButton + + +class Alert(BaseAlert): + + def __init__(self, kind, prompt, + ok_label = "OK", default = 1, **kwds): + BaseAlert.__init__(self, kind, prompt, + button_labels = [ok_label], default = default, **kwds) + + def _create_buttons(self, ok_label): + self.yes_button = DefaultButton(title = ok_label, action = self.yes) + #self.default_button = self.ok_button + + def _layout_buttons(self): + self.place(self.yes_button, + right = self.label.right, + top = self.label + self._label_button_spacing) + + +class Alert2(BaseAlert): + + def __init__(self, kind, prompt, + yes_label = "Yes", no_label = "No", + default = 1, cancel = 0, **kwds): + BaseAlert.__init__(self, kind, prompt, + button_labels = [yes_label, no_label], + default = default, cancel = cancel, **kwds) + + def _create_buttons(self, yes_label, no_label): + self.yes_button = DefaultButton(title = yes_label, action = self.yes) + self.no_button = CancelButton(title = no_label, action = self.no) + + def _layout_buttons(self): + self.place_row([self.no_button, self.yes_button], + right = self.label.right, + top = self.label + self._label_button_spacing) + + +class Alert3(BaseAlert): + + _minimum_width = 300 + + def __init__(self, kind, prompt, + yes_label = "Yes", no_label = "No", other_label = "Cancel", + default = 1, cancel = -1, **kwds): + BaseAlert.__init__(self, kind, prompt, + button_labels = [yes_label, no_label, other_label], + default = default, cancel = cancel, **kwds) + + def _create_buttons(self, yes_label, no_label, cancel_label): + self.yes_button = DefaultButton(title = yes_label, action = self.yes) + self.no_button = CancelButton(title = no_label, action = self.no) + self.other_button = Button(title = cancel_label, action = self.other) + + def _layout_buttons(self): + self.place_row([self.other_button, self.yes_button], + right = self.label.right, + top = self.label + self._label_button_spacing) + self.place(self.no_button, + left = self._left_margin, top = self.label + self._label_button_spacing) diff --git a/GUI/Generic/GApplications.py b/GUI/Generic/GApplications.py new file mode 100644 index 0000000..fa7b2d6 --- /dev/null +++ b/GUI/Generic/GApplications.py @@ -0,0 +1,547 @@ +# +# Python GUI - Application class - Generic +# + +import os, sys, traceback +from GUI import Globals +from GUI.Properties import Properties, overridable_property +from GUI import MessageHandler +from GUI.Exceptions import Cancel, UnimplementedMethod, UsageError, \ + ApplicationError #, Quit +from GUI.StdMenus import basic_menus +from GUI.GMenus import MenuState +from GUI.Files import FileRef +from GUI.Printing import PageSetup, present_page_setup_dialog + +class Application(Properties, MessageHandler): + """The user should create exactly one Application object, + or subclass thereof. It implements the main event loop + and other application-wide behaviour.""" + + _windows = None # List of all existing Windows + _documents = None # List of all existing Documents + _menus = None # Menus to appear in all Windows + _clipboard = None + _save_file_type = None + _exit_event_loop_flag = False + _last_directory = None + + windows = overridable_property('windows', + """A list of all existing Windows.""") + + documents = overridable_property('documents', + """A list of all existing documents.""") + + menus = overridable_property('menus', + """A list of Menus that are to be available from all Windows.""") + + open_file_types = overridable_property('open_file_types', + """List of FileTypes openable by the default Open... command.""") + + save_file_type = overridable_property('save_file_type', + """Default FileType for Documents that do not specify their own.""") + + file_type = overridable_property('file_type', + """Write only. Sets open_file_types and save_file_type.""") + + target = overridable_property('target', + """Current target for key events and menu messages.""") + + target_window = overridable_property('target_window', + """Window containing the current target, or None if there are no windows.""") + + page_setup = overridable_property('page_setup', + """Default PageSetup instance.""") + + def __init__(self, title = None): + if Globals._application is not None: + raise UsageError("More than one Application instance created") + if title: + Globals.application_name = title + self._open_file_types = [] + self._windows = [] + self._documents = [] + self._update_list = [] + self._idle_tasks = [] + self._page_setup = None + Globals._application = self + self._quit_flag = False + + def destroy(self): + Globals._application = None + + # + # Constants + # + +# def get_std_menus(self): +# """Returns a list of Menus containing the standard +# framework-defined menu commands in their standard +# positions.""" +# return basic_menus() +# +# std_menus = property(get_std_menus) + + # + # Properties + # + + def get_windows(self): + return self._windows + + def get_documents(self): + return self._documents + + def get_menus(self): + menus = self._menus + if menus is None: + menus = [] + return menus + + def set_menus(self, menus): + self._menus = menus + + def get_open_file_types(self): + return self._open_file_types + + def set_open_file_types(self, x): + self._open_file_types = x + + def get_save_file_type(self): + return self._save_file_type + + def set_save_file_type(self, x): + self._save_file_type = x + + def set_file_type(self, x): + self._open_file_types = [x] + self._save_file_type = x + + def get_page_setup(self): + # This property is initialised lazily, because on Windows it turn out + # that calling PageSetupDlg() before the application's first window is + # created causes the app not to be brought to the foreground initially. + ps = self._page_setup + if not ps: + ps = PageSetup() + self._page_setup = ps + return ps + + def set_page_setup(self, x): + self._page_setup = x + + # + # Event loop + # + + def run(self): + """The main event loop. Runs until _quit() is called, or + KeyboardInterrupt or SystemExit is raised.""" + # Implementations may override this together with _quit() to use + # a different means of causing the main event loop to exit. + self.process_args(sys.argv[1:]) + if self._menus is None: + self.menus = basic_menus() + while not self._quit_flag: + try: + self.event_loop() + #except (KeyboardInterrupt, Quit), e: + except KeyboardInterrupt: + return + except SystemExit: + raise + except: + self.report_error() + + def _quit(self): + # Causes the main event loop to exit. + self._quit_flag = True + self._exit_event_loop() + + def event_loop(self): + """Loop reading and handling events until exit_event_loop() is called.""" + # Implementations may override this together with exit_event_loop() to + # implement non-modal event loops in a different way. + self._event_loop(None) + + def _event_loop(self, modal_window): + # Generic modal and non-modal event loop. + # Loop reading and handling events for the given window, or for all + # windows if window is None, until exit_event_loop() is called. + # Enabled application-wide menu items should be selectable in any case. + # If an exception other than Cancel is raised, it should either be + # reported using report_error() or propagated. Implementations may + # override this together with _exit_event_loop() if handling events + # individually is not desirable. + save = self._exit_event_loop_flag + self._exit_event_loop_flag = False + try: + while not self._exit_event_loop_flag: + try: + self.handle_next_event(modal_window) + except Cancel: + pass + finally: + self._exit_event_loop_flag = save + + def exit_event_loop(self): + """Cause the current call to event_loop() or modal_event_loop() + to exit.""" + self._exit_event_loop() + + def _exit_event_loop(self): + # Exit the generic _event_loop implementation. + self._exit_event_loop_flag = True + +# def event_loop_until(self, exit): +# """Loop reading and handling events until exit() returns +# true, _quit_flag is set or an exception other than Cancel +# is raised.""" +# while not exit() and not self._quit_flag: +# try: +# self.handle_next_event() +# except Cancel: +# pass + +# def handle_events(self): +# """Handle events until an exception occurs. Waits for at least one event; +# may handle more, at the discretion of the implementation.""" +# self.handle_next_event() + + def handle_next_event(self, modal_window): + # Wait for the next event to arrive and handle it. Transparently handles + # any internal events such as window updates, etc., and executes any idle + # tasks that become due while waiting for an event. If modal_window is + # not None, restrict interaction to that window (but allow use of enabled + # application-wide menu items). + # + # This only needs to be implemented if the generic _event_loop() is being + # used. + raise UnimplementedMethod(self, "handle_next_event") + + # + # Menu commands + # + + def setup_menus(self, m): + m.new_cmd.enabled = 1 + m.open_cmd.enabled = 1 + m.page_setup_cmd.enabled = 1 + m.quit_cmd.enabled = 1 + + def new_cmd(self): + "Handle the New menu command." + doc = self.make_new_document() + if not doc: + raise UsageError( + "Application.make_document(None) did not return a Document.") + doc.new_contents() + self.make_window(doc) + + def open_cmd(self): + "Handle the Open... menu command." + from FileDialogs import request_old_file + dir = self.get_default_open_directory() + fileref = request_old_file(default_dir = dir, + file_types = self._open_file_types) + if fileref: + self.open_fileref(fileref) + else: + raise Cancel + + def get_default_open_directory(self): + """Called by the default implementation of open_cmd() to find an initial + directory for request_old_file(). Should return a DirRef or FileRef, or + None if there is no preferred location. By default it returns the last + directory in which a document was opened or saved during this session, + if any.""" + return self._last_directory + + def page_setup_cmd(self): + present_page_setup_dialog(self.page_setup) + + def quit_cmd(self): + """Handle the Quit menu command.""" + while self._documents: + self._documents[0].close_cmd() + windows = self._windows + while windows: + window = windows[-1] + window.destroy() + assert not (windows and windows[-1] is window), \ + "%r failed to remove itself from application on destruction" % window + self._quit() + + # + # Opening files + # + + def process_args(self, args): + """Process command line arguments. Called by run() when the application + is starting up.""" + if not args: + self.open_app() + else: + for arg in args: + if os.path.exists(arg): + arg = os.path.abspath(arg) + self.open_path(arg) + + def open_app(self): + """Called by run() when the application is opened with no arguments.""" + pass + + def open_path(self, path): + """Open document specified by a pathname. Called for each command line + argument when the application is starting up.""" + self.open_fileref(FileRef(path = path)) + + def open_fileref(self, fileref): + """Open document specified by a FileRef.""" + doc = self.make_file_document(fileref) + if not doc: + raise ApplicationError("The file '%s' is not recognised by %s." % ( + fileref.name, Globals.application_name)) + doc.set_file(fileref) + try: + doc.read() + except EnvironmentError, e: + raise ApplicationError("Unable to open '%s'." % fileref.name, e) + self.make_window(doc) + + # + # Message dispatching + # + +# def dispatch(self, message, *args): +# target_window = self._find_target_window() +# if target_window: +# target_window.dispatch(message, *args) +# else: +# self.handle(message, *args) + + def dispatch(self, message, *args): + self.target.handle(message, *args) + + def dispatch_menu_command(self, command): + if isinstance(command, tuple): + name, index = command + self.dispatch(name, index) + else: + self.dispatch(command) + + def get_target(self): + # Implementations may override this to locate the target in a + # different way if they choose not to implement the Window.target + # property. Should return self if no other target can be found. + window = self.target_window + if window: + return window.target + else: + return self + + def get_target_window(self): + """Return the window to which messages should be dispatched, or None.""" + raise NotImplementedError + + # + # Abstract + # + + def make_new_document(self): + """Create a new Document object of the appropriate + class in response to a New command.""" + return self.make_document(None) + + def make_file_document(self, fileref): + """Create a new Document object of the appropriate + class for the given FileRef.""" + return self.make_document(fileref) + + def make_document(self, fileref): + """Should create a new Document object of the appropriate + class for the given FileRef, or if FileRef is None, a new + empty Document of the appropriate class for the New command.""" + return None + + def make_window(self, document): + """Should create a Window set up appropriately for viewing + the given Document.""" + raise UnimplementedMethod(self, 'make_window') + + # + # Clipboard + # + + def query_clipboard(self): + "Tests whether the clipboard contains any data." + return not not self._clipboard + + def get_clipboard(self): + return self._clipboard + + def set_clipboard(self, x): + self._clipboard = x + + # + # Window list management + # + + def _add_window(self, window): + if window not in self._windows: + self._windows.append(window) + + def _remove_window(self, window): + if window in self._windows: + self._windows.remove(window) + + # + # Document list management + # + + def _add_document(self, doc): + if doc not in self._documents: + self._documents.append(doc) + + def _remove_document(self, doc): + if doc in self._documents: + self._documents.remove(doc) + + # + # Exception reporting + # + + def report_error(self): + """Display an appropriate error message for the most recent + exception caught.""" + try: + raise + except Cancel: + pass + except ApplicationError, e: + from GUI.Alerts import stop_alert + stop_alert(str(e)) + except: + self.report_exception() + + def report_exception(self): + """Display an alert box describing the most recent exception, and + giving the options Continue, Traceback or Abort. Traceback displays + a traceback and continues; Abort raises SystemExit.""" + try: + exc_type, exc_val, exc_tb = sys.exc_info() + exc_desc = "%s: %s" % (exc_type.__name__, exc_val) + self.print_traceback(exc_desc, exc_tb) + from GUI.Alerts import alert3 + message = "Sorry, something went wrong." + result = alert3('stop', "%s\n\n%s" % (message, exc_desc), + "Continue", "Abort", "Traceback", + default = 1, cancel = None, width = 450, lines = 5) + if result == 1: # Continue + return + elif result == -1: # Traceback + self.display_traceback(exc_desc, exc_tb) + return + else: # Abort + raise SystemExit + except (KeyboardInterrupt, SystemExit): + os._exit(1) + except: + print >>sys.stderr, "---------- Exception while reporting exception ----------" + traceback.print_exc() + print >>sys.stderr, "------------------ Original exception -------------------" + traceback.print_exception(exc_type, exc_val, exc_tb) + #os._exit(1) + + def display_traceback(self, exc_desc, exc_tb): + """Display an exception description and traceback. + TODO: display this in a scrolling window.""" + self.print_traceback(exc_desc, exc_tb) + + def print_traceback(self, exc_desc, exc_tb): + """Print exception description and traceback to standard error.""" + import traceback + sys.stderr.write("\nTraceback (most recent call last):\n") + traceback.print_tb(exc_tb) + sys.stderr.write("%s\n\n" % exc_desc) + + # + # Other + # + + def zero_windows_allowed(self): + """Platforms should implement this to return false if there + must be at least one window open at all times. Returning false + here forces the Quit command to be used instead of Close when + there is only one window open.""" + # TODO: Move this somewhere more global. + raise UnimplementedMethod(self, 'zero_windows_allowed') + + def _perform_menu_setup(self, menus = None): + """Given a list of Menu objects, perform menu setup processing + and update associated platform menus ready for popping up or + pulling down.""" + if menus is None: + menus = self._effective_menus() + menu_state = MenuState(menus) + menu_state.reset() + self._dispatch_menu_setup(menu_state) + for menu in menus: + menu._update_platform_menu() + + def _dispatch_menu_setup(self, menu_state): + self.dispatch('_setup_menus', menu_state) + + def _effective_menus(self): + """Return a list of the menus in effect for the currently active + window, including both application-wide and window-specific menus, + in an appropriate order according to platform conventions.""" + window = self.target_window + return self._effective_menus_for_window(window) + + def _effective_menus_for_window(self, window): + """Return a list of the menus in effect for the specified + window, including both application-wide and window-specific menus, + in an appropriate order according to platform conventions.""" + menus = self.menus + if window: + menus = menus + window.menus + regular_menus = [] + special_menus = [] + for menu in menus: + if menu.special: + special_menus.insert(0, menu) + else: + regular_menus.append(menu) + return regular_menus + special_menus + +# def _may_close_a_window(self): +# # On implementations where at least one window is needed in order to +# # interact with the application, check whether closing a window would +# # leave no more visible windows. +# if self.zero_windows_allowed(): +# return True +# count = 0 +# for window in self.windows: +# if window.visible: +# count += 1 +# if count >= 2: +# return True +# return False + + def _check_for_no_windows(self): + # On implementations where at least one window is needed in order to + # interact with the application, check whether there are no more visible + # windows and take appropriate action. + if not self.zero_windows_allowed(): + for window in self.windows: + if window.visible: + return + self.no_visible_windows() + + def no_visible_windows(self): + """On platforms that require a window in order to interact with the + application, this is called when there are no more visible windows. + The default action is to close the application; subclasses may override + it to take some other action, such as creating a new window.""" + self.quit_cmd() diff --git a/GUI/Generic/GBaseAlerts.py b/GUI/Generic/GBaseAlerts.py new file mode 100644 index 0000000..36840da --- /dev/null +++ b/GUI/Generic/GBaseAlerts.py @@ -0,0 +1,117 @@ +# +# Python GUI - Alert base class - Generic +# + +import textwrap +from GUI import ModalDialog +from GUI import Label + +class BaseAlert(ModalDialog): + + _wrapwidth = 50 + _minimum_width = 200 + _left_margin = 24 + _right_margin = 24 + _top_margin = 14 + _bottom_margin = 20 + _icon_spacing = 16 + _label_button_spacing = 20 + _default_width = 380 + _default_lines = 3 + + yes_button = None + no_button = None + other_button = None + + def __init__(self, kind, prompt, width = None, lines = None, + button_labels = None, default = None, cancel = None): + #if width is None: + # width = self._default_width + #if lines is None: + # lines = self._default_lines + ModalDialog.__init__(self, style = 'alert') + self.label = Label(text = self._wrap(prompt), lines = lines) + if self.label.width < self._minimum_width: + self.label.width = self._minimum_width + self._create_buttons(*button_labels) + #self.default_button = self._find_button(default) + #self.cancel_button = self._find_button(cancel) + self._layout(kind) + + def _layout(self, kind): + icon_width, icon_height = self._layout_icon(kind) + label_left = self._left_margin + if icon_width: + label_left += icon_width + self._icon_spacing + if self.label.height < icon_height: + self.label.height = icon_height + self.place(self.label, + left = label_left, + top = self._top_margin)# + icon_height/4) + #_wrap_text(self.label, self._default_width - label_left - self._right_margin) + self._layout_buttons() + self.shrink_wrap(padding = (self._right_margin, self._bottom_margin)) + + def _layout_icon(self, kind): + # Place icon for the given alert kind, if any, and return its size. + # If there is no icon, return (0, 0). + return (0, 0) + + def _wrap(self, text): + width = self._wrapwidth + return "\n\n".join( + [textwrap.fill(para, width) + for para in text.split("\n\n")]) + + def _find_button(self, value): + #print "BaseAlert._find_button:", value ### + if value == 1: + result = self.yes_button + elif value == 0: + result = self.no_button + elif value == -1: + result = self.other_button + else: + result = None + #print "BaseAlert._find_button: result =", result ### + return result + + def yes(self): + self.dismiss(1) + + def no(self): + self.dismiss(0) + + def other(self): + self.dismiss(-1) + +#def _wrap_text(label, label_width): +# hard_lines = [text.split() +# for text in label.text.split("\n")] +# words = hard_lines[0] +# for hard_line in hard_lines[1:]: +# words.append("\n") +# words.extend(hard_line) +# font = label.font +# space_width = font.width(" ") +# lines = [] +# line = [] +# line_width = 0 +# for word in words: +# word_width = font.width(word) +# if word == "\n" or (line_width > 0 +# and line_width + space_width + word_width > label_width): +# lines.append(line) +# line = [] +# line_width = 0 +# if word <> "\n": +# line.append(word) +# if line_width > 0: +# line_width += space_width +# line_width += word_width +# if line: +# lines.append(line) +# label.text = "\n".join([" ".join(line) for line in lines]) + + + diff --git a/GUI/Generic/GButtons.py b/GUI/Generic/GButtons.py new file mode 100644 index 0000000..7b24bcd --- /dev/null +++ b/GUI/Generic/GButtons.py @@ -0,0 +1,23 @@ +# +# Python GUI - Buttons - Generic +# + +from GUI.Properties import overridable_property +from GUI.Actions import Action +from GUI import Control + +class Button(Control, Action): + """ A pushbutton control.""" + + style = overridable_property('style', + "One of 'normal', 'default', 'cancel'") + + def activate(self): + """Highlight the button momentarily and then perform its action.""" + self.flash() + self.do_action() + + def flash(self): + """Highlight the button momentarily as though it had been clicked, + without performing the action.""" + raise NotImplementedError diff --git a/GUI/Generic/GCanvasPaths.py b/GUI/Generic/GCanvasPaths.py new file mode 100644 index 0000000..8055411 --- /dev/null +++ b/GUI/Generic/GCanvasPaths.py @@ -0,0 +1,55 @@ +# +# Python GUI - Canvas Paths - Generic +# + +class CanvasPaths: + # Mixin class providing generic implementations of + # canvas path construction operators. + + def __init__(self): + self.newpath() + + def newpath(self): + self._path = [] + self._current_subpath = None + self._current_point = (0, 0) + + def moveto(self, x, y): + self._current_subpath = None + self._current_point = self._coords(x, y) + + def rmoveto(self, dx, dy): + x, y = self._current_point + self.moveto(x + dx, y + dy) + + def lineto(self, x, y): + subpath = self._current_subpath + if subpath is None: + subpath = [self._current_point] + self._path.append(subpath) + self._current_subpath = subpath + p = self._coords(x, y) + subpath.append(p) + self._current_point = p + + def rlineto(self, dx, dy): + x, y = self._current_point + self.lineto(x + dx, y + dy) + + def closepath(self): + subpath = self._current_subpath + if subpath: + subpath.append(subpath[0]) + self._current_subpath = None + + def get_current_point(self): + return self._current_point + + # Implementations may set _coords to one of the following + + def _int_coords(self, x, y): + return int(round(x)), int(round(y)) + + def _float_coords(self, x, y): + return x, y + diff --git a/GUI/Generic/GCanvases.py b/GUI/Generic/GCanvases.py new file mode 100644 index 0000000..a1dbab0 --- /dev/null +++ b/GUI/Generic/GCanvases.py @@ -0,0 +1,282 @@ +# +# Python GUI - Drawing - Generic +# + +from GUI.StdColors import black, white +from GUI.StdFonts import application_font +from GUI.Properties import Properties, overridable_property + +class Canvas(Properties): + + _default_forecolor = black + _default_backcolor = white + _printing = False + + pencolor = overridable_property('pencolor', "Current color for stroking paths.") + fillcolor = overridable_property('fillcolor', "Current color for filling paths.") + textcolor = overridable_property('textcolor', "Current color for drawint text.") + forecolor = overridable_property('forecolor', "Sets pen, fill and text colors to the same color.") + backcolor = overridable_property('backcolor', "Current color for erasing regions.") + pensize = overridable_property('pensize', "Width of pen for framing and stroking.") + font = overridable_property('font', "Font for drawing text.") + current_point = overridable_property('current_point', "The current point, or None.") + printing = overridable_property('printing', "True if drawing destination is a non-display device.") + + #forecolor = overridable_property('forecolor', "Sets both pencolor and fillcolor.") + + def __init__(self): + self.newpath() + + def get_printing(self): + return self._printing + + def initgraphics(self): + self.set_forecolor(self._default_forecolor) + self.set_backcolor(self._default_backcolor) + self.set_pensize(1) + self.set_font(application_font) + + def set_forecolor(self, c): + self.pencolor = c + self.fillcolor = c + self.textcolor = c + + def rmoveto(self, dx, dy): + x0, y0 = self._current_point() + self.moveto(x0 + dx, y0 + dy) + + def rlineto(self, dx, dy): + x0, y0 = self.current_point + self.lineto(x0 + dx, y0 + dy) + + def curve(self, sp, cp1, cp2, ep): + self.moveto(sp) + self.curveto(cp1, cp2, ep) + + def rcurveto(self, cp1, cp2, ep): + x0, y0 = self.current_point + x1, y1 = cp1 + x2, y2 = cp2 + x3, y3 = ep + self.curveto( + (x0 + x1, y0 + y1), + (x0 + x2, y0 + y2), + (x0 + x3, y0 + y3)) + + def fill_stroke(self): + self.fill() + self.stroke() + + # Rectangles + + def _pen_inset_rect(self, rect): + l, t, r, b = rect + p = 0.5 * self.pensize + return (l + p, t + p, r - p, b - p) + + def rect(self, rect): + l, t, r, b = rect + self.moveto(l, t) + self.lineto(r, t) + self.lineto(r, b) + self.lineto(l, b) + self.closepath() + + def rect_frame(self, rect): + self.rect(self._pen_inset_rect(rect)) + + def fill_rect(self, rect): + self.newpath() + self.rect(rect) + self.fill() + + def stroke_rect(self, rect): + self.newpath() + self.rect(rect) + self.stroke() + + def frame_rect(self, rect): + self.newpath() + self.rect_frame(rect) + self.stroke() + + def fill_stroke_rect(self, rect): + self.rect_path(rect) + self.fill_stroke() + + def fill_frame_rect(self, rect): + self.fill_rect(rect) + self.frame_rect(rect) + + def erase_rect(self, rect): + self.newpath() + self.rect(rect) + self.erase() + + # Ovals + + def oval_frame(self, rect): + self.oval(self._pen_inset_rect(rect)) + + def fill_oval(self, rect): + self.newpath() + self.oval_frame(rect) + self.fill() + + def stroke_oval(self, rect): + self.newpath() + self.oval(rect) + self.stroke() + + def frame_oval(self, rect): + self.newpath() + self.oval_frame(rect) + self.stroke() + + def fill_stroke_oval(self, rect): + self.newpath() + self.oval(rect) + self.fill_stroke() + + def fill_frame_oval(self, rect): + self.fill_oval(rect) + self.frame_oval() + + def erase_oval(self, rect): + self.newpath() + self.oval(rect) + self.erase() + + # Arcs + + def _arc_path(self, c, r, a0, a1): +# x, y = c +# a0r = a0 * deg +# x0 = x + r * cos(a0r) +# y0 = y + r * sin(a0r) + self.newpath() +# self.moveto(x0, y0) + self.arc(c, r, a0, a1) + + def _arc_frame_path(self, c, r, a0, a1): + self._arc_path(c, r - 0.5 * self.pensize, a0, a1) + + def stroke_arc(self, c, r, a0, a1): + self._arc_path(c, r, a0, a1) + self.stroke() + + def frame_arc(self, c, r, a0, a1): + self._arc_frame_path(c, r, a0, a1) + self.stroke() + + # Wedges + + def wedge(self, c, r, a0, a1): + self.moveto(*c) + self.arc(c, r, a0, a1) + self.closepath() + + def fill_wedge(self, c, r, a0, a1): + self.newpath() + self.wedge(c, r, a0, a1) + self.fill() + + def stroke_wedge(self, c, r, a0, a1): + self.newpath() + self.wedge(c, r, a0, a1) + self.stroke() + + def fill_stroke_wedge(self, c, r, a0, a1): + self.newpath() + self.wedge(c, r, a0, a1) + self.fill_stroke() + + def erase_wedge(self, c, r, a0, a1): + self.newpath() + self.wedge(c, r, a0, a1) + self.erase() + + # Polylines + + def lines(self, points): + point_iter = iter(points) + self.moveto(*point_iter.next()) + for p in point_iter: + self.lineto(*p) + + def linesto(self, points): + for p in points: + self.lineto(*p) + + def stroke_lines(self, points): + self.newpath() + self.lines(points) + self.stroke() + + # Polycurves + + def curves(self, points): + self.moveto(*points[0]) + for i in xrange(1, len(points), 3): + self.curveto(*points[i:i+3]) + + def curvesto(self, points): + for i in xrange(0, len(points), 3): + self.curveto(*points[i:i+3]) + + def stroke_curves(self, points): + self.newpath() + self.curves(points) + self.stroke() + + # Polygons + + def poly(self, points): + self.lines(points) + self.closepath() + + def fill_poly(self, points): + self.newpath() + self.poly(points) + self.fill() + + def stroke_poly(self, points): + self.newpath() + self.poly(points) + self.stroke() + + def fill_stroke_poly(self, points): + self.newpath() + self.poly(points) + self.fill_stroke() + + def erase_poly(self, points): + self.newpath() + self.poly(points) + self.erase() + + # Loops + + def loop(self, points): + self.curves(points) + self.closepath() + + def fill_loop(self, points): + self.newpath() + self.loop(points) + self.fill() + + def stroke_loop(self, points): + self.newpath() + self.loop(points) + self.stroke() + + def fill_stroke_loop(self, points): + self.newpath() + self.loop(points) + self.fill_stroke() + + def erase_loop(self, points): + self.newpath() + self.loop(points) + self.erase() diff --git a/GUI/Generic/GCheckBoxes.py b/GUI/Generic/GCheckBoxes.py new file mode 100644 index 0000000..2860744 --- /dev/null +++ b/GUI/Generic/GCheckBoxes.py @@ -0,0 +1,43 @@ +# +# Python GUI - Check boxes - Generic +# + +from GUI.Properties import overridable_property +from GUI import Control +from GUI.Actions import Action + +class CheckBox(Control, Action): + """A CheckBox is a control used to represent a binary choice.""" + + def __init__(self, **kwds): + Control.__init__(self, **kwds) + + on = overridable_property('on', "Boolean value of the check box.") + + auto_toggle = overridable_property('auto_toggle', """If true, + the check box's 'on' property will automatically be toggled + before performing the action, if any.""") + + mixed = overridable_property('mixed', """If true, the check box + is capable of displaying a mixed state.""") + + _auto_toggle = True + _mixed = False + + def get_auto_toggle(self): + return self._auto_toggle + + def set_auto_toggle(self, v): + self._auto_toggle = v + + def get_mixed(self): + return self._mixed + + def set_mixed(self, v): + self._mixed = v + + def get_value(self): + return self.on + + def set_value(self, x): + self.on = x diff --git a/GUI/Generic/GColors.py b/GUI/Generic/GColors.py new file mode 100644 index 0000000..07799c9 --- /dev/null +++ b/GUI/Generic/GColors.py @@ -0,0 +1,46 @@ +# +# Python GUI - Colors - Generic +# + +from GUI.Properties import overridable_property + +class Color(object): + """A drawing color. + + Constructors: + rgb(red, green, blue, alpha = 1.0) + where red, green, blue, alpha are in the range 0.0 to 1.0 + + Properties: + red --> float + green --> float + blue --> float + rgb --> (red, green, blue) + rgba --> (red, green, blue, alpha) + """ + + red = overridable_property('red', "Red component (0.0 to 1.0)") + green = overridable_property('green', "Blue component (0.0 to 1.0)") + blue = overridable_property('blue', "Blue component (0.0 to 1.0)") + alpha = overridable_property('alpha', "Alpha (opacity) component") + rgb = overridable_property('rgb', "Tuple of (red, green, blue) (0.0 to 1.0)") + rgba = overridable_property('rgba', + "Tuple of (red, green, blue, alpha) (0.0 to 1.0)") + + def get_alpha(self): + return 1.0 + + def get_rgb(self): + return (self.red, self.green, self.blue) + + def set_rgb(self, x): + self.red, self.green, self.blue = x + + def get_rgba(self): + return (self.red, self.green, self.blue, self.alpha) + + def set_rgba(self, x): + self.red, self.green, self.blue, self.alpha = x + + def __str__(self): + return "Color(%g,%g,%g,%g)" % self.rgba diff --git a/GUI/Generic/GComponents.py b/GUI/Generic/GComponents.py new file mode 100644 index 0000000..a516955 --- /dev/null +++ b/GUI/Generic/GComponents.py @@ -0,0 +1,477 @@ +# +# Python GUI - Components - Generic +# + +import os +from GUI.Properties import Properties, overridable_property +from GUI import MessageHandler +from GUI.Geometry import add_pt, sub_pt, rect_size, rect_sized, rect_topleft +from GUI import application + +_user_tab_stop = os.environ.get("PYGUI_KEYBOARD_NAVIGATION") or None +# Allow "False", "True", "0", "1" +if _user_tab_stop is not None: + _user_tab_stop = _user_tab_stop.strip().capitalize() + try: + _user_tab_stop = {"False": False, "True": True}[_user_tab_stop] + except KeyError: + try: + _user_tab_stop = int(_user_tab_stop) + except ValueError: + sys.stderr.write("PYGUI_KEYBOARD_NAVIGATION: Unrecognized value %r" + % _user_tab_stop) + _user_tab_stop = None + +class Component(Properties, MessageHandler): + """Component is an abstract class representing a user + interface component.""" + + left = overridable_property('left', "Position of left edge relative to container.") + top = overridable_property('top', "Position of top edge relative to container.") + right = overridable_property('right', "Position of right edge relative to container.") + bottom = overridable_property('bottom', "Position of bottom edge relative to container.") + + x = overridable_property('x', "Horizontal position relative to container.") + y = overridable_property('y', "Vertical position relative to container.") + width = overridable_property('width') + height = overridable_property('height') + + position = overridable_property('position', "Position relative to container.") + size = overridable_property('size') + + bounds = overridable_property('bounds', "Bounding rectangle in container's coordinates.") + + container = overridable_property('container', + "Container which contains this Component. Setting this property has the " + "effect of removing the component from its previous container, if any, " + "and adding it to the new one, if any.") + +# visible = overridable_property('visible', +# "Whether the component is currently shown.") + + tab_stop = overridable_property('tab_stop', + "Whether tab key can navigate into this control.") + + anchor = overridable_property('anchor', "A string of 'ltrb' controlling behaviour when container is resized.") + + border = overridable_property('border', "True if the component should have a border.") + + _is_scrollable = False # Overridden by scrollable subclasses + _generic_tabbing = True # Whether to use generic tab navigation code + _default_tab_stop = False + _user_tab_stop_override = False # Whether user preference overrides _default_tab_stop + _tab_stop = None + + # + # Class variables defined by implementations: + # + # _has_local_coords bool True if component has a local coordinate system + # + + _container = None + _border = False + hmove = 0 + vmove = 0 + hstretch = 0 + vstretch = 0 + + def __init__(self, tab_stop = None, **kwds): + Properties.__init__(self, **kwds) + if tab_stop is None: + tab_stop = self._get_default_tab_stop() + self.tab_stop = tab_stop + + def destroy(self): + self.container = None + + # + # Geometry properties + # + # Default implementations of position and size properties + # in terms of the bounds property. A minimal implementation + # need only implement get_bounds and set_bounds. + # + # It is the implementation's responsibility to call _resized() + # whenever the size of the component changes, either by + # explicit assignment to geometry properties or by the user + # resizing the containing window. It should not be called if + # setting a geometry property does not cause the size to change. + # + + def get_left(self): + return self.position[0] + + def set_left(self, v): + l, t, r, b = self.bounds + self.bounds = (v, t, r, b) + + def get_top(self): + return self.bounds[1] + + def set_top(self, v): + l, t, r, b = self.bounds + self.bounds = (l, v, r, b) + + def get_right(self): + return self.bounds[2] + + def set_right(self, v): + l, t, r, b = self.bounds + self.bounds = (l, t, v, b) + + def get_bottom(self): + return self.bounds[3] + + def set_bottom(self, v): + l, t, r, b = self.bounds + self.bounds = (l, t, r, v) + + def get_x(self): + return self.bounds[0] + + def set_x(self, v): + l, t, r, b = self.bounds + self.bounds = (v, t, v + r - l, b) + + def get_y(self): + return self.bounds[1] + + def set_y(self, v): + l, t, r, b = self.bounds + self.bounds = (l, v, r, v + b - t) + + def get_position(self): + l, t, r, b = self.bounds + return (l, t) + + def set_position(self, (x, y)): + l, t, r, b = self.bounds + self.bounds = (x, y, x + r - l, y + b - t) + + def get_width(self): + l, t, r, b = self.bounds + return r - l + + def set_width(self, v): + l, t, r, b = self.bounds + self.bounds = (l, t, l + v, b) + + def get_height(self): + l, t, r, b = self.bounds + return b - t + + def set_height(self, v): + l, t, r, b = self.bounds + self.bounds = (l, t, r, t + v) + + def get_size(self): + l, t, r, b = self.bounds + return (r - l, b - t) + + def set_size(self, (w, h)): + l, t, r, b = self.bounds + self.bounds = (l, t, l + w, t + h) + + # + # Container management + # + + def get_container(self): + return self._container + + def set_container(self, new_container): + if self._container != new_container: + self._change_container(new_container) + + def _change_container(self, new_container): + old_container = self._container + if old_container: + self._container = None + old_container._remove(self) + if new_container: + self._container = new_container + new_container._add(self) + + # + # Message dispatching + # + + def become_target(self): + """Arrange for this object to be the first to handle messages + dispatched to the containing Window. If the component is not + contained in a Window, the effect is undefined.""" + raise NotImplementedError + + def is_target(self): + """Return true if this is the current target within the containing + Window. If the component is not contained in a Window, the result + is undefined.""" + return self.window and self.window.target is self + + # + # Message handling + # + + def next_handler(self): + return self._container + + # + # Visibility control + # + +# def show(self): +# """Make the Component visible (provided its container is visible).""" +# self.visible = 1 +# +# def hide(self): +# """Make the Component invisible.""" +# self.visible = 0 + + # + # Border + # + + def get_border(self): + return self._border + + def set_border(self, x): + self._border = x + + # + # Resizing + # + + def get_anchor(self): + if self.hmove: + s1 = 'r' + elif self.hstretch: + s1 = 'lr' + else: + s1 = 'l' + if self.vmove: + s2 = 'b' + elif self.vstretch: + s2 = 'tb' + else: + s2 = 't' + return s1 + s2 + + def set_anchor(self, s): + if 'r' in s: + if 'l' in s: + self.hstretch = True + self.hmove = False + else: + self.hstretch = False + self.hmove = True + else: + self.hstretch = False + self.hmove = False + if 'b' in s: + if 't' in s: + self.vstretch = True + self.vmove = False + else: + self.vstretch = False + self.vmove = True + else: + self.vstretch = False + self.vmove = False + + def get_auto_layout(self): + return self._auto_layout + + def set_auto_layout(self, x): + self._auto_layout = x + + def _resized(self, delta): + # Called whenever the size of the component changes for + # any reason. + pass + + def container_resized(self, delta): + """Called whenever the component's container changes size and the + container's auto_layout property is true. The default implementation + repositions and resizes this component according to its resizing + options.""" + dw, dh = delta + left, top, right, bottom = self.bounds + if self.hmove: + left += dw + right += dw + elif self.hstretch: + right += dw + if self.vmove: + top += dh + bottom += dh + elif self.vstretch: + bottom += dh + self.bounds = (left, top, right, bottom) + + # + # Update region maintenance + # + + def invalidate(self): + """Mark the whole Component as needing to be redrawn.""" + self.invalidate_rect(self.viewed_rect()) + +# def invalidate_rect(self, r): +# print "GComponent.invalidate_rect:", self, r ### +# container = self._container +# if container: +# container.invalidate_rect(r) + +# def _invalidate_in_container(self): +# container = self._container +# if container: +# container._invalidate_subcomponent(self) + + # + # Coordinate transformation + # + + def local_to_global(self, p): + p = self.local_to_container(p) + parent = self._container + if parent: + return parent.local_to_global(p) + else: + return p + + def global_to_local(self, p): + parent = self._container + if parent: + p = parent.global_to_local(p) + return self.container_to_local(p) + + def local_to_container(self, p): + if self._has_local_coords: + return add_pt(p, self.local_to_container_offset()) + else: + return p + + def container_to_local(self, p): + if self._has_local_coords: + return sub_pt(p, self.local_to_container_offset()) + else: + return p + + def local_to_container_offset(self): + if self._has_local_coords: + return self.position + else: + return (0, 0) + + def transform_from(self, other, p): + return transform_coords(other, self, p) + + def transform_to(self, other, p): + return transform_coords(self, other, p) + + # + # Placement specification support + # + + def __add__(self, offset): + return (self, offset) + + def __sub__(self, offset): + return (self, -offset) + + # + # Tabbing + # + +# def get_tabbable(self): +# return self._tabbable +# +# def set_tabbable(self, value): +# if self._tabbable <> value: +# self._tabbable = value +# self._invalidate_tab_chain() + + def get_tab_stop(self): + return self._tab_stop + + def set_tab_stop(self, x): + if self._tab_stop <> x: + self._tab_stop = x + self._invalidate_tab_chain() + + def _get_default_tab_stop(self): + if self._user_tab_stop_override: + result = _user_tab_stop + else: + result = None + if result is None: + result = self._default_tab_stop + return result + + def _tab_out(self): + pass + + def _tab_in(self): + self.become_target() + + def _build_tab_chain(self, chain): + if self._tab_stop: + chain.append(self) + + def _invalidate_tab_chain(self): + window = self.window + if window: + window._invalidate_tab_chain() + + def _is_targetable(self): + return True + + # + # Other + # + + window = overridable_property('window', """The Window ultimately containing + this Component, or None.""") + + def get_window(self): + container = self._container + if container: + return container.window + else: + return None + + def reset_blink(self): + application().reset_blink() + + def viewed_rect(self): + """Returns the rectangle in local coordinates that is + currently visible within the component.""" + if self._has_local_coords: + width, height = self.size + return (0, 0, width, height) + else: + return self.bounds + + def broadcast(self, message, *args): + """Traverse the component hierarchy, calling each component's handler for + the given message, if any.""" + method = getattr(self, message, None) + if method: + method(*args) + + def _dispatch_mouse_event(self, event): + self._handle_mouse_event(event) + + def _handle_mouse_event(self, event): + self.handle(event.kind, event) + + +def transform_coords(from_component, to_component, p): + if from_component: + g = from_component.local_to_global(p) + else: + g = p + if to_component: + return to_component.global_to_local(g) + else: + return g diff --git a/GUI/Generic/GContainers.py b/GUI/Generic/GContainers.py new file mode 100644 index 0000000..fb6714f --- /dev/null +++ b/GUI/Generic/GContainers.py @@ -0,0 +1,382 @@ +# +# Python GUI - Containers - Generic +# + +try: + maketrans = str.maketrans +except AttributeError: + from string import maketrans +from GUI.Properties import overridable_property +from GUI.Exceptions import ArgumentError +from GUI.Geometry import pt_in_rect +from GUI import Component + +anchor_to_sticky = maketrans("ltrb", "wnes") + +class Container(Component): + """A Container is a Component that can contain other Components. + The sub-components are clipped to the boundary of their container.""" + + contents = overridable_property('contents', + "List of subcomponents. Do not modify directly.") + + content_width = overridable_property('content_width', "Width of the content area.") + content_height = overridable_property('content_height', "Height of the content area.") + content_size = overridable_property('content_size', "Size of the content area.") + + auto_layout = overridable_property('auto_layout', + "Automatically adjust layout of subcomponents when resized.") + + _auto_layout = True + + # _contents [Component] + + def __init__(self, **kw): + self._contents = [] + Component.__init__(self, **kw) + + def destroy(self): + """Destroy this Container and all of its contents.""" + contents = self._contents + while contents: + comp = contents[-1] + comp.destroy() + assert not contents or contents[-1] is not comp, \ + "%r failed to remove itself from container on destruction" % comp + Component.destroy(self) + + # + # Content area + # + + def get_content_width(self): + return self.content_size[0] + + def set_content_width(self, w): + self.content_size = w, self.content_height + + def get_content_height(self): + return self.content_size[1] + + def set_content_height(self, h): + self.content_size = self.content_width, h + + get_content_size = Component.get_size + set_content_size = Component.set_size + + # + # Subcomponent Management + # + + def get_contents(self): + return self._contents + + def add(self, comp): + """Add the given Component as a subcomponent.""" + if comp: + if isinstance(comp, Component): + comp.container = self + else: + for item in comp: + self.add(item) + + def remove(self, comp): + """Remove subcomponent, if present.""" + if isinstance(comp, Component): + if comp in self._contents: + comp.container = None + else: + for item in comp: + self.remove(item) + + def _add(self, comp): + # Called by comp.set_container() to implement subcomponent addition. + self._contents.append(comp) + self._invalidate_tab_chain() + self.added(comp) + + def _remove(self, comp): + # Called by comp.set_container() to implement subcomponent removal. + self._contents.remove(comp) + self._invalidate_tab_chain() + self.removed(comp) + + def added(self, comp): + """Called after a subcomponent has been added.""" + pass + + def removed(self, comp): + """Called after a subcomponent has been removed.""" + pass + + # + # The infamous 'place' method and friends. + # + + _place_default_spacing = 8 + + def place(self, item, + left = None, right = None, top = None, bottom = None, + sticky = 'nw', scrolling = '', border = None, anchor = None): + """Add a component to the frame with positioning, + resizing and scrolling options. See the manual for details.""" + self._place([item], left = left, right = right, top = top, bottom = bottom, + sticky = sticky, scrolling = scrolling, border = border, anchor = anchor) + + def place_row(self, items, + left = None, right = None, top = None, bottom = None, + sticky = 'nw', scrolling = '', border = None, spacing = None, + anchor = None): + """Add a row of components to the frame with positioning, + resizing and scrolling options. See the manual for details.""" + if left is not None and right is not None: + raise ValueError("Cannot specify both left and right to place_row") + elif left is None and right is not None: + direction = 'left' + items = items[:] + items.reverse() + else: + direction = 'right' + self._place(items, left = left, right = right, top = top, bottom = bottom, + sticky = sticky, scrolling = scrolling, border = border, + direction = direction, spacing = spacing, anchor = anchor) + + def place_column(self, items, + left = None, right = None, top = None, bottom = None, + sticky = 'nw', scrolling = '', border = None, spacing = None, + anchor = None): + """Add a column of components to the frame with positioning, + resizing and scrolling options. See the manual for details.""" + if top is not None and bottom is not None: + raise ValueError("Cannot specify both top and bottom to place_column") + elif top is None and bottom is not None: + direction = 'up' + items = items[:] + items.reverse() + else: + direction = 'down' + self._place(items, left = left, right = right, top = top, bottom = bottom, + sticky = sticky, scrolling = scrolling, border = border, + direction = direction, spacing = spacing, anchor = anchor) + + def _place(self, items, + left = None, + right = None, + top = None, + bottom = None, + sticky = 'nw', + scrolling = '', + direction = 'right', + spacing = None, + border = None, + anchor = None): + + def side(spec, name): + # Process a side specification taking the form of either + # (1) an offset, (2) a reference component, or (3) a + # tuple (component, offset). Returns a tuple (ref, offset) + # where ref is the reference component or None (representing + # the Frame being placed into). Checks that the reference + # component, if any, is directly contained by this Frame. + ref = None + offset = None + if spec is not None: + if isinstance(spec, tuple): + ref, offset = spec + elif isinstance(spec, Component): + ref = spec + offset = 0 + elif isinstance(spec, (int, float)): + offset = spec + else: + raise ArgumentError(self, 'place', name, spec) + if ref is self: + ref = None + elif ref: + con = ref.container + #if con is not self and isinstance(con, ScrollFrame): + # ref = con + # con = ref.container + if con is not self: + raise ValueError("Reference component for place() is not" + " directly contained by the frame being placed into.") + return ref, offset + + if spacing is None: + spacing = self._place_default_spacing + + # Decode the sticky options + if anchor is not None: + sticky = anchor.translate(anchor_to_sticky) + hmove = vmove = hstretch = vstretch = 0 + if 'e' in sticky: + if 'w' in sticky: + hstretch = 1 + else: + hmove = 1 + if 's' in sticky: + if 'n' in sticky: + vstretch = 1 + else: + vmove = 1 + + # Translate the direction argument + try: + dir = {'right':0, 'down':1, 'left':2, 'up':3}[direction] + except KeyError: + raise ArgumentError(self, 'place', 'direction', direction) + + # Unpack the side arguments + left_obj, left_off = side(left, 'left') + right_obj, right_off = side(right, 'right') + top_obj, top_off = side(top, 'top') + bottom_obj, bottom_off = side(bottom, 'bottom') + + # Process the items + #if not isinstance(items, list): + # items = [items] + for item in items: + x, y = item.position + w, h = item.size + # Calculate left edge position + if left_obj: + l = left_obj.left + left_obj.width + left_off + elif left_off is not None: + if left_off < 0: + l = self.width + left_off + else: + l = left_off + else: + l = None + # Calculate top edge position + if top_obj: + t = top_obj.top + top_obj.height + top_off + elif top_off is not None: + if top_off < 0: + t = self.height + top_off + else: + t = top_off + else: + t = None + # Calculate right edge position + if right_obj: + r = right_obj.left + right_off + elif right_off is not None: + if right_off <= 0: + r = self.width + right_off + else: + r = right_off + else: + r = None + # Calculate bottom edge position + if bottom_obj: + b = bottom_obj.top + bottom_off + elif bottom_off is not None: + if bottom_off <= 0: + b = self.height + bottom_off + else: + b = bottom_off + else: + b = None + # Fill in unspecified positions + if l is None: + if r is not None: + l = r - w + else: + l = x + if r is None: + r = l + w + if t is None: + if b is not None: + t = b - h + else: + t = y + if b is None: + b = t + h + if scrolling: + item.scrolling = scrolling + # Position, resize and add the item + item.bounds = (l, t, r, b) + self.add(item) + # Record resizing and border options + item.hmove = hmove + item.vmove = vmove + item.hstretch = hstretch + item.vstretch = vstretch + if border is not None: + item.border = border + # Step to the next item + if dir == 0: + left_obj = item + left_off = spacing + elif dir == 1: + top_obj = item + top_off = spacing + elif dir == 2: + right_obj = item + right_off = -spacing + else: + bottom_obj = item + bottom_off = -spacing + + # + # Resizing + # + + def _resized(self, delta): + if self._auto_layout: + self.resized(delta) + + def resized(self, delta): + for c in self._contents: + c.container_resized(delta) + + def resize(self, auto_layout = False, **kwds): + """Change the geometry of the component, with control over whether + the layout of subcomponents is updated. The default is not to do so. + Keyword arguments to this method may be any of the properties + affecting position and size (i.e. left, top, right, bottom, x, y, + width, height, position, size, bounds).""" + old_auto_layout = self.auto_layout + try: + self.auto_layout = auto_layout + self.set(**kwds) + finally: + self.auto_layout = old_auto_layout + + # + # Tabbing + # + + def _build_tab_chain(self, chain): + Component._build_tab_chain(self, chain) + for c in self._contents: + c._build_tab_chain(chain) + + # + # Other + # + + def shrink_wrap(self, padding = None): + """Adjust the size of the component so that it neatly encloses its + contents. If padding is specified, it specifies the amount of space + to leave at right and bottom, otherwise the minimum distance from the + left and top sides to the nearest components is used.""" + contents = self.contents + if not contents: + return + if padding: + hpad, vpad = padding + else: + hpad = min([item.left for item in contents]) + vpad = min([item.top for item in contents]) + rights = [item.right for item in contents] + bottoms = [item.bottom for item in contents] + self.resize(size = (max(rights) + hpad, max(bottoms) + vpad)) + + def broadcast(self, message, *args): + """Traverse the component hierarchy, calling each component's handler for + the given message, if any.""" + Component.broadcast(self, message, *args) + for comp in self._contents: + comp.broadcast(message, *args) diff --git a/GUI/Generic/GControls.py b/GUI/Generic/GControls.py new file mode 100644 index 0000000..47f0a81 --- /dev/null +++ b/GUI/Generic/GControls.py @@ -0,0 +1,46 @@ +# +# Python GUI - Controls - Generic +# + +from GUI.Properties import overridable_property +from GUI import Component + +class Control(Component): + """Abstract base class for components such as buttons, check + boxes and text entry boxes.""" + + title = overridable_property('title', "Title of the control.") + value = overridable_property('value', "Value of the control.") + enabled = overridable_property('enabled', "True if user can manipulate the control.") + font = overridable_property('font') + color = overridable_property('color') + just = overridable_property('just', "Justification ('left', 'center' or 'right').") + lines = overridable_property('lines', + "Height of the control measured in lines of the current font.") + tab_stop = overridable_property('tab_stop', + "Whether tab key can navigate into this control.") + + _vertical_padding = 0 # Extra height to add when setting 'lines' property + _default_tab_stop = True + _user_tab_stop_override = True + + def __init__(self, font = None, lines = None, **kwds): + Component.__init__(self, **kwds) + # If font and lines are both specified, must set font first. + if font: + self.font = font + if lines is not None: + self.lines = lines + + def get_lines(self): + return int(round((self.height - self._vertical_padding) / self.font.line_height)) + + def set_lines(self, num_lines): + self.height = self._calc_height(self.font, num_lines) + + def _calc_height(self, font, num_lines = 1): + return num_lines * font.line_height + self._vertical_padding + + def _is_targetable(self): + return self.enabled + \ No newline at end of file diff --git a/GUI/Generic/GCursors.py b/GUI/Generic/GCursors.py new file mode 100644 index 0000000..a5982f0 --- /dev/null +++ b/GUI/Generic/GCursors.py @@ -0,0 +1,55 @@ +#-------------------------------------------------------------------------- +# +# Python GUI - Cursors - Generic +# +#-------------------------------------------------------------------------- + +from GUI.Properties import Properties +from GUI.Resources import lookup_resource, find_resource, get_resource +from GUI import Image + +def _hotspot_for_resource(resource_name): + path = lookup_resource(resource_name, "hot") + if path: + f = open(path, "rU") + xs, ys = f.readline().split() + return int(xs), int(ys) + else: + return None + +class Cursor(Properties): + """A Cursor is an image representing the mouse pointer. + + Constructors: + Cursor(resource_name, hotspot) + Cursor(image, hotspot) + """ + + def from_resource(cls, name, hotspot = None, **kwds): + def load(path): + image = Image.from_resource(name, **kwds) + return cls(image, hotspot or _hotspot_for_resource(name)) + return get_resource(load, name) + + from_resource = classmethod(from_resource) + + def __init__(self, spec, hotspot = None): + """Construct a Cursor from a resource or Image and a hotspot point. + The hotspot defaults to the centre of the image.""" + if isinstance(spec, basestring): + self._init_from_resource(spec, hotspot) + else: + self._init_from_image(spec, hotspot) + + def _init_from_resource(self, resource_name, hotspot): + image = Image(file = find_resource(resource_name)) + if not hotspot: + hotspot = _hotspot_for_resource(resource_name) + self._init_from_image(image, hotspot) + + def _init_from_image(self, image, hotspot): + if not hotspot: + width, height = image.size + hotspot = (width // 2, height // 2) + self._init_from_image_and_hotspot(image, hotspot) + diff --git a/GUI/Generic/GDialogs.py b/GUI/Generic/GDialogs.py new file mode 100644 index 0000000..ab9116e --- /dev/null +++ b/GUI/Generic/GDialogs.py @@ -0,0 +1,77 @@ +# +# Python GUI - Dialogs - Generic +# + +from GUI import Globals +from GUI.Properties import overridable_property +from GUI.Actions import ActionBase, action_property +from GUI import Window + +class Dialog(Window, ActionBase): + + _default_keys = "\r" + _cancel_keys = "\x1b" + +# default_button = overridable_property('default_button', +# "Button to be activated by the default key.") +# +# cancel_button = overridable_property('cancel_button', +# "Button to be activated by the cancel key.") +# +# _default_button = None +# _cancel_button = None + + default_action = action_property('default_action', + "Action to perform when Return or Enter is pressed.") + + cancel_action = action_property('cancel_action', + "Action to perform when Escape is pressed.") + + _default_action = 'ok' + _cancel_action ='cancel' + + def __init__(self, style = 'nonmodal_dialog', + closable = 0, zoomable = 0, resizable = 0, **kwds): + if 'title' not in kwds: + kwds['title'] = Globals.application_name + Window.__init__(self, style = style, + closable = closable, zoomable = zoomable, resizable = resizable, + **kwds) + +# def get_default_button(self): +# return self._default_button +# +# def set_default_button(self, button): +# self._default_button = button +# if button: +# button.style = 'default' +# +# def get_cancel_button(self): +# return self._cancel_button +# +# def set_cancel_button(self, button): +# self._cancel_button = button +# if button: +# button.style = 'cancel' + + def key_down(self, event): + #print "GDialog.key_down:", repr(event.char) ### + c = event.char + if c: + if c in self._default_keys: + self.do_default_action() + return + elif c in self._cancel_keys: + self.do_cancel_action() + return + Window.key_down(self, event) + + def do_default_action(self): + self.do_named_action('default_action') + + def do_cancel_action(self): + self.do_named_action('cancel_action') + +# def _activate_button(self, button): +# if button: +# button.activate() diff --git a/GUI/Generic/GDrawableContainers.py b/GUI/Generic/GDrawableContainers.py new file mode 100644 index 0000000..f060b18 --- /dev/null +++ b/GUI/Generic/GDrawableContainers.py @@ -0,0 +1,62 @@ +#-------------------------------------------------------------------- +# +# PyGUI - DrawableContainer - Generic +# +#-------------------------------------------------------------------- + +from GUI.Geometry import rect_sized +from GUI import Container +from GUI import ViewBase +from GUI.Printing import Printable + +default_size = (100, 100) + +class DrawableContainer(ViewBase, Container, Printable): + + # + # Construction and destruction + # + + def __init__(self, **kwds): + Container.__init__(self, **kwds) + ViewBase.__init__(self) + + def destroy(self): + ViewBase.destroy(self) + Container.destroy(self) + + def setup_menus(self, m): + ViewBase.setup_menus(self, m) + Container.setup_menus(self, m) + + def viewed_rect(self): + """Return the rectangle in local coordinates bounding the currently + visible part of the extent.""" + return rect_sized((0, 0), self.size) + + def with_canvas(self, proc): + """Call the procedure with a canvas suitable for drawing in this + view. The canvas is only valid for the duration of the call, and + should not be retained beyond it.""" + raise NotImplementedError + + def update(self): + """Redraw invalidated regions immediately, without waiting for a + return to the event loop.""" + raise NotImplementedError + + def get_print_extent(self): + return self.content_size + + def _draw_background(self, canvas, clip_rect): + return clip_rect + + # + # Callbacks + # + + def draw(self, canvas, rect): + """Called when the view needs to be drawn. The rect is the bounding + rectangle of the region needing to be drawn. The default implementation + does nothing.""" + pass diff --git a/GUI/Generic/GEditCmdHandlers.py b/GUI/Generic/GEditCmdHandlers.py new file mode 100644 index 0000000..eac1bbe --- /dev/null +++ b/GUI/Generic/GEditCmdHandlers.py @@ -0,0 +1,28 @@ +# +# PyGUI - Edit command handling - Generic +# + +from GUI import application + +class EditCmdHandler(object): + # Mixin for objects that implement the standard editing commands. + + _may_be_password = False + + def setup_menus(self, m): + selbeg, selend = self.selection + anysel = selbeg < selend + anyscrap = application().query_clipboard() + passwd = self._may_be_password and self.password + m.cut_cmd.enabled = anysel and not passwd + m.copy_cmd.enabled = anysel and not passwd + m.paste_cmd.enabled = anyscrap + m.clear_cmd.enabled = anysel + m.select_all_cmd.enabled = True + + def select_all_cmd(self): + self.select_all() + + def select_all(self): + self.selection = (0, self.get_text_length()) + diff --git a/GUI/Generic/GEvents.py b/GUI/Generic/GEvents.py new file mode 100644 index 0000000..d4522ea --- /dev/null +++ b/GUI/Generic/GEvents.py @@ -0,0 +1,71 @@ +# +# Python GUI - Events - Generic +# + +class Event(object): + + """An input event. + + Attributes: + + kind Type of event. One of 'mouse_down', 'mouse_up', 'key_down', + 'key_up'. + + global_position Position of mouse in screen coordinates at the time of the event. + + position For mouse events, position in local coordinates of the View that + was the target of this event. Undefined for other event types. + + time Time of event, in platform-dependent units. + + button Button identifier for mouse down/up events. + + num_clicks Number of consecutive clicks within double-click time. + + char For key events, an ASCII character. Undefined for other event types. + + key For non-printing keys, a value identifying the key. Undefined for other event types. + + auto True if key-down event is an autorepeat (not supported on all platforms). + + Platform-independent modifiers (boolean): + + shift The Shift key. + control The Control key. + option The additional modifier key. + extend_contig The contiguous selection extension modifier key. + extend_noncontig The noncontiguous selection extension modifier key. + """ + + kind = None + global_position = None + position = None + time = None + button = None + num_clicks = 0 + char = None + key = None + auto = False + shift = False + control = False + option = False + extend_contig = False + extend_noncontig = False + delta = (0, 0) + _keycode = 0 # Platform-dependent key code + _originator = None # Component to which originally delivered by platform + _not_handled = False # Reached default event method of originating component + + def position_in(self, view): + """Return the position of this event in the coordinate system + of the specified view.""" + return view.global_to_local(self.global_position) + + def __str__(self): + return "" \ + % (self.kind, self.global_position, self.position, self.time, + self.num_clicks, self.char, self.key, self.shift, self.control, + self.option, self.extend_contig, self.extend_noncontig, self.auto, + self._platform_modifiers_str()) diff --git a/GUI/Generic/GFiles.py b/GUI/Generic/GFiles.py new file mode 100644 index 0000000..585d9af --- /dev/null +++ b/GUI/Generic/GFiles.py @@ -0,0 +1,193 @@ +# +# Python GUI - File references and types - Generic +# +# Classes for dealing with file references and file types +# in as platform-independent a manner as possible. +# +# In this view of things, a file reference consists +# of two parts: +# +# 1) A directory reference, whose nature is +# platform-dependent, +# +# 2) A name. +# + +import os +from GUI.Properties import Properties, overridable_property + +class FileRef(Properties): + """A FileRef represents a file system object in a platform-independent way. + It consists of two parts, a directory specification and the name of an + object within that directory. The directory specification always refers + to an existing directory, but the named object may or may not exist. + + Constructors: + FileRef(dir = DirRef or path, name = string) + FileRef(path = string) + """ + + dir = overridable_property('dir', "DirRef representing the parent directory.") + name = overridable_property('name', "Name of the object within the parent directory.") + path = overridable_property('path', "Full pathname of the object.") + + _dir = None # DirRef representing the parent directory + _name = None # Name, including type suffix if any + + # + # Constructor + # + + def __init__(self, dir = None, name = None, path = None): + if dir and name and not path: + if not isinstance(dir, DirRef): + dir = DirRef(dir) + elif path and not (dir or name): + dirpath, name = os.path.split(path) + dir = DirRef(path = dirpath) + else: + raise TypeError("Invalid argument combination to FileRef constructor") + self._dir = dir + self._name = name + + # + # Properties + # + + def get_dir(self): + return self._dir + + def get_name(self): + "Return the name of the file." + return self._name + + def get_path(self): + return os.path.join(self._dir.path, self._name) + + # + # Methods + # + + def open(self, mode, file_type = None): + """Open as a file with the given mode and return a file object. On + platforms which have file-type metadata (e.g. Macintosh), if the + mode contains 'w' and a file_type is specified, the newly-created + file will be given the specified type.""" + f = open(self.path, mode) + if "w" in mode and file_type: + self._set_type(file_type) + return f + + def mkdir(self): + """Create a directory with the name and parent directory specified + by this FileRef. Returns a DirRef for the created directory.""" + return DirRef(os.mkdir(self.path)) + + def _set_type(self, file_type): + # Platforms which have file-type metadata (e.g. Macintosh) use this + # to set the type of a file. + pass + + def __str__(self): + return "FileRef(%r,%r)" % (self.dir.path, self.name) + +#------------------------------------------------------------------------- + +class DirRef(Properties): + """A DirRef is an object representing a directory in the + file system. Its representation is completely platform + dependent. + + Constructor: + DirRef(path = string) + """ + + _path = None + + path = overridable_property('path', "Full pathname of the directory.") + + def __init__(self, path): + self._path = path + + def get_path(self): + return self._path + + def __str__(self): + return "DirRef(%r)" % self.path + +#------------------------------------------------------------------------- + +class FileType(Properties): + """A FileType is a multi-platform representation of a file type.""" + + _name = None + _suffix = None + _mac_creator = None + _mac_type = None + _mac_force_suffix = True + + name = overridable_property('name', "Human-readable description of the file type") + suffix = overridable_property('suffix', "Filename suffix (without dot)") + mac_creator = overridable_property('mac_creator', "Macintosh 4-character creator code") + mac_type = overridable_property('mac_type', "Macintosh 4-character type code") + mac_force_suffix = overridable_property('mac_force_suffix', "Enforce filename suffix on MacOSX") + + def get_name(self): + return self._name + + def set_name(self, x): + self._name = x + + def get_suffix(self): + return self._suffix + + def set_suffix(self, x): + self._suffix = x + + def get_mac_creator(self): + return self._mac_creator + + def set_mac_creator(self, x): + self._mac_creator = x + + def get_mac_type(self): + return self._mac_type + + def set_mac_type(self, x): + self._mac_type = x + + def get_mac_force_suffix(self): + return self._mac_force_suffix + + def set_mac_force_suffix(self, x): + self._mac_force_suffix = x + + def _matches(self, name, mac_type): + # Return true if the given name or type code matches that of + # this file type. + this_mac_type = self._mac_type + this_suffix = self._suffix + if this_mac_type and mac_type == this_mac_type: + return True + # Allow generic text files to match typeless files for MacOSX + if not this_suffix and this_mac_type == "TEXT" and mac_type == "\0\0\0\0": + return True + if this_suffix and _matches_suffix(name, this_suffix): + return True + return False + + def _add_suffix(self, name): + # Force the given name to have the appropriate suffix for this file + # type. Platforms which have other means of representing file types + # (e.g. Macintosh) may override this. + suffix = self._suffix + if suffix and not _matches_suffix(name, suffix): + name = "%s.%s" % (name, suffix) + return name + +#------------------------------------------------------------------------- + +def _matches_suffix(name, suffix): + # Test case-insensitively whether the given filename has + # the given suffix. + return name.lower().endswith("." + suffix.lower()) diff --git a/GUI/Generic/GFonts.py b/GUI/Generic/GFonts.py new file mode 100644 index 0000000..80ccc29 --- /dev/null +++ b/GUI/Generic/GFonts.py @@ -0,0 +1,93 @@ +# +# Python GUI - Fonts - Generic +# + +import sys +from GUI.Properties import overridable_property + +class Font(object): + """A Font object represents a set of characters of a particular + typeface, style and size. Font objects are immutable. + + Constructors: + Font(family, size, style) + family = family name + size = size in points + style = a list of 'bold', 'italic' + + Properties: + family --> string + size --> number + style --> ['bold', 'italic'] + ascent --> number + descent --> number + leading --> number + height --> number + cap_height --> number + x_height --> number + line_height --> number + """ + + family = overridable_property('family', "Family name ('Times', 'Helvetica', etc.)") + size = overridable_property('size', "Size in points") + style = overridable_property('style', "A list of 'bold', 'italic'") + ascent = overridable_property('ascent', "Distance from baseline to top of highest character") + descent = overridable_property('descent', "Distance from baseline to bottom of lowest character") + height = overridable_property('height', "Sum of ascent and descent") + cap_height = overridable_property('cap_height', "Height above baseline of capital letters") + x_height = overridable_property('x_height', "Height above baseline of lowercase letters without ascenders") + leading = overridable_property('leading', "Recommended extra space between lines") + line_height = overridable_property('line_height', "Recommended distance between baselines") + + def get_cap_height(self): + # Approximation for platforms not supporting this + return self.ascent + + def get_x_height(self): + # Approximation for platforms not supporting this + return self.ascent - self.descent + + def get_leading(self): + return self.line_height - self.height + + def but(self, family = None, size = None, style = None, + style_includes = None, style_excludes = None): + """Return a new Font that is the same as this one except for the + specified characteristics.""" + if not family: + family = self.family + if not size: + size = self.size + if style is None: + style = self.style + style = style[:] + if style_includes: + for item in style_includes: + style.append(item) + if style_excludes: + for item in style_excludes: + if item in style: + style.remove(item) + return self.__class__(family, size, style) + + def width(self, s, start = 0, end = None): + """width(s [,start [,end ]]) + The width of the specified part of the given string in this font.""" + if start or end is not None: + if end is None: + end = len(s) + s = s[start:end] + return self._width(s) + + def _width(self, s): + raise NotImplementedError + + def x_to_pos(self, s, x): + """Given a number of pixels measured from the left of + the given string, returns the nearest inter-character position. + Returns 0 if x is negative, and len(string) if x is beyond the + right end of the string.""" + raise NotImplementedError + + def __str__(self): + return "Font(%r,%g,%s)" % (self.family, self.size, self.style) diff --git a/GUI/Generic/GFrames.py b/GUI/Generic/GFrames.py new file mode 100644 index 0000000..503bb6d --- /dev/null +++ b/GUI/Generic/GFrames.py @@ -0,0 +1,10 @@ +# +# Python GUI - Frames - Generic +# + +from GUI import Container + +class Frame(Container): + """A Frame is a general-purpose instantiable subclass of Container.""" + + _default_size = (100, 100) diff --git a/GUI/Generic/GGLConfig.py b/GUI/Generic/GGLConfig.py new file mode 100644 index 0000000..3a92da2 --- /dev/null +++ b/GUI/Generic/GGLConfig.py @@ -0,0 +1,162 @@ +# +# PyGUI - OpenGL Pixel Formats - Generic +# + +from GUI.Properties import Properties, overridable_property + +class GLConfig(Properties): + """Class holding the attributes of an OpenGL context configuration.""" + + # NOTE: When adding a property here, also add it to + # _pixel_format_attribute_names below. + + double_buffer = overridable_property("double_buffer", "True if context is to be double-buffered.") + alpha = overridable_property("alpha", "True if there is to be an alpha channel.") + color_size = overridable_property("color_size", "Number of bits per colour buffer component.") + alpha_size = overridable_property("alpha_size", "Number of bits per alpha channel component.") + stereo = overridable_property("stereo", "True if stereoscopic context is required.") + aux_buffers = overridable_property("aux_buffers", "Number of auxiliary colour buffers to allocate.") + depth_buffer = overridable_property("depth_buffer", "True if a depth buffer is required.") + depth_size = overridable_property("depth_size", "Number of bits per depth buffer element.") + stencil_buffer = overridable_property("stencil_buffer", "True if a stencil buffer is required.") + stencil_size = overridable_property("stencil_size", "Number of bits per stencil buffer element.") + accum_buffer = overridable_property("accum_buffer", "True if an accumulation buffer is required.") + accum_size = overridable_property("accum_size", "Number of bits per accumulation buffer component.") + multisample = overridable_property("multisample", "True if a multisampled context is required.") + samples_per_pixel = overridable_property("samples_per_pixel", "Number of samples per multisampled pixel.") + + _double_buffer = True + _alpha = True + _color_size = 8 + _alpha_size = 8 + _stereo = False + _aux_buffers = 0 + _depth_buffer = True + _depth_size = 32 + _stencil_buffer = False + _stencil_size = 8 + _accum_buffer = False + _accum_size = 8 + _multisample = False + _samples_per_pixel = 4 + + _pixel_format_attribute_names = ( + 'double_buffer', 'alpha', 'color_size', 'alpha_size', + 'stereo', 'aux_buffers', 'depth_buffer', 'depth_size', + 'stencil_buffer', 'stencil_size', 'accum_buffer', 'accum_size', + 'multisample', 'samples_per_pixel', + ) + + def _from_args(cls, config, kwds): + # Extract pixel format arguments from arguments of GLView.__init__ + # or GLPixmap.__init__ and return a GLConfig. Used keyword + # arguments are removed from kwds. + pf_kwds = {} + for name in cls._pixel_format_attribute_names: + if name in kwds: + pf_kwds[name] = kwds.pop(name) + if config and pf_kwds: + raise TypeError("Explicit config cannot be used with other configuration keyword arguments") + if not config: + config = cls(**pf_kwds) + return config + + _from_args = classmethod(_from_args) + + def get_double_buffer(self): + return self._double_buffer + + def set_double_buffer(self, x): + self._double_buffer = x + + def get_alpha(self): + return self._alpha + + def set_alpha(self, x): + self._alpha = x + + def get_color_size(self): + return self._color_size + + def set_color_size(self, x): + self._color_size = x + + def get_alpha_size(self): + return self._alpha_size + + def set_alpha_size(self, x): + self._alpha_size = x + + def get_stereo(self): + return self._stereo + + def set_stereo(self, x): + self._stereo = x + + def get_aux_buffers(self): + return self._aux_buffers + + def set_aux_buffers(self, x): + self._aux_buffers = x + + def get_depth_buffer(self): + return self._depth_buffer + + def set_depth_buffer(self, x): + self._depth_buffer = x + + def get_depth_size(self): + return self._depth_size + + def set_depth_size(self, x): + self._depth_size = x + + def get_stencil_buffer(self): + return self._stencil_buffer + + def set_stencil_buffer(self, x): + self._stencil_buffer = x + + def get_stencil_size(self): + return self._stencil_size + + def set_stencil_size(self, x): + self._stencil_size = x + + def get_accum_buffer(self): + return self._accum_buffer + + def set_accum_buffer(self, x): + self._accum_buffer = x + + def get_accum_size(self): + return self._accum_size + + def set_accum_size(self, x): + self._accum_size = x + + def get_multisample(self): + return self._multisample + + def set_multisample(self, x): + self._multisample = x + + def get_samples_per_pixel(self): + return self._samples_per_pixel + + def set_samples_per_pixel(self, x): + self._samples_per_pixel = x + + def supported(self): + """Determine whether the combination of attributes requested by this configuration + can be satisfied. If successful, a new GLConfig object is returned whose + attributes reflect those actually allocated. Otherwise, a GLConfigError is + raised.""" + raise NotImplementedError + +#------------------------------------------------------------------------------ + +class GLConfigError(ValueError): + + def __init__(self, msg = "OpenGL configuration not available"): + ValueError.__init__(self, msg) diff --git a/GUI/Generic/GGLContexts.py b/GUI/Generic/GGLContexts.py new file mode 100644 index 0000000..82ed632 --- /dev/null +++ b/GUI/Generic/GGLContexts.py @@ -0,0 +1,72 @@ +# +# PyGUI - OpenGL Contexts - Generic +# + +from GUI.Properties import overridable_property +from GUI.GLShareGroups import ShareGroup + +_current_share_group = None + +class GLContext(object): + """Abstract base class for objects having an OpenGL context.""" + # + # _share_group ShareGroup + # + + share_group = overridable_property('share_group', + "ShareGroup to which this context should belong, or None.") + + def __init__(self, share_group): + if not share_group: + share_group = ShareGroup() + self._share_group = share_group + if share_group: + share_group._add(self) + + def destroy(self): + pass + + def init_context(self): + """This method is called once after the associated OpenGL context + is created. When called, this object's OpenGL context is the current + context and the viewport is set to (0, 0, width, height). This method + may be used to establish any desired initial OpenGL state.""" + pass + + def get_share_group(self): + return self._share_group + + def _get_shared_context(self): + """Return another arbitrarily-chosen member of the share group of this + context, or None if this context has no share group or there are no + other members.""" + return self._share_group._some_member(exclude = self) + + def with_context(self, proc, flush = False): + """The proc should be a callable object of no arguments. Calls + the proc with the associated OpenGL context as the current context. + If flush is true, after calling proc, a glFlush followed by a + buffer flush or swap is performed as appropriate.""" + self._with_context(proc, flush) + + def _with_context(self, proc, flush): + # Subclasses override this to implement with_context. + # Should call _with_share_group(proc). + # Signature can be changed if with_context is overridden to match. + raise NotImplementedError + + def _with_share_group(self, proc): + global _current_share_group + old_share_group = _current_share_group + _current_share_group = self._share_group + try: + proc() + finally: + _current_share_group = old_share_group + + +def current_share_group(): + group = _current_share_group + if not group: + raise ValueError("No current PyGUI OpenGL context") + return group diff --git a/GUI/Generic/GGLPixelFormats.py b/GUI/Generic/GGLPixelFormats.py new file mode 100644 index 0000000..1f20cc8 --- /dev/null +++ b/GUI/Generic/GGLPixelFormats.py @@ -0,0 +1,163 @@ +# +# PyGUI - OpenGL Pixel Formats - Generic +# + +from GUI.Properties import Properties, overridable_property + +class GLPixelFormat(Properties): + """Class holding the attributes of an OpenGL pixel format.""" + + # NOTE: When adding a property here, also add it to + # _pixel_format_attribute_names below. + + double_buffer = overridable_property("double_buffer", "True if context is to be double-buffered.") + alpha = overridable_property("alpha", "True if there is to be an alpha channel.") + color_size = overridable_property("color_size", "Number of bits per colour buffer component.") + alpha_size = overridable_property("alpha_size", "Number of bits per alpha channel component.") + stereo = overridable_property("stereo", "True if stereoscopic context is required.") + aux_buffers = overridable_property("aux_buffers", "Number of auxiliary colour buffers to allocate.") + depth_buffer = overridable_property("depth_buffer", "True if a depth buffer is required.") + depth_size = overridable_property("depth_size", "Number of bits per depth buffer element.") + stencil_buffer = overridable_property("stencil_buffer", "True if a stencil buffer is required.") + stencil_size = overridable_property("stencil_size", "Number of bits per stencil buffer element.") + accum_buffer = overridable_property("accum_buffer", "True if an accumulation buffer is required.") + accum_size = overridable_property("accum_size", "Number of bits per accumulation buffer component.") + multisample = overridable_property("multisample", "True if a multisampled context is required.") + samples_per_pixel = overridable_property("samples_per_pixel", "Number of samples per multisampled pixel.") + + _double_buffer = True + _alpha = True + _color_size = 8 + _alpha_size = 8 + _stereo = False + _aux_buffers = 0 + _depth_buffer = True + _depth_size = 32 + _stencil_buffer = False + _stencil_size = 8 + _accum_buffer = False + _accum_size = 8 + _multisample = False + _samples_per_pixel = False + + _pixel_format_attribute_names = ( + 'double_buffer', 'alpha', 'color_size', 'alpha_size', + 'stereo', 'aux_buffers', 'depth_buffer', 'depth_size', + 'stencil_buffer', 'stencil_size', 'accum_buffer', 'accum_size', + 'multisample', 'samples_per_pixel', + ) + + def _from_args(cls, pixel_format, kwds): + # Extract pixel format arguments from arguments of GLView.__init__ + # or GLPixmap.__init__ and return a GLPixelFormat. Used keyword + # arguments are removed from kwds. + pf_kwds = {} + for name in cls._pixel_format_attribute_names: + if name in kwds: + pf_kwds[name] = kwds.pop(name) + if pixel_format and pf_kwds: + raise TypeError("Explicit pixel_format cannot be used with other pixel format keyword arguments") + if not pixel_format: + pixel_format = cls(**pf_kwds) + return pixel_format + + _from_args = classmethod(_from_args) + + def get_double_buffer(self): + return self._double_buffer + + def set_double_buffer(self, x): + self._double_buffer = x + + def get_alpha(self): + return self._alpha + + def set_alpha(self, x): + self._alpha = x + + def get_color_size(self): + return self._color_size + + def set_color_size(self, x): + self._color_size = x + + def get_alpha_size(self): + return self._alpha_size + + def set_alpha_size(self, x): + self._alpha_size = x + + def get_stereo(self): + return self._stereo + + def set_stereo(self, x): + self._stereo = x + + def get_aux_buffers(self): + return self._aux_buffers + + def set_aux_buffers(self, x): + self._aux_buffers = x + + def get_depth_buffer(self): + return self._depth_buffer + + def set_depth_buffer(self, x): + self._depth_buffer = x + + def get_depth_size(self): + return self._depth_size + + def set_depth_size(self, x): + self._depth_size = x + + def get_stencil_buffer(self): + return self._stencil_buffer + + def set_stencil_buffer(self, x): + self._stencil_buffer = x + + def get_stencil_size(self): + return self._stencil_size + + def set_stencil_size(self, x): + self._stencil_size = x + + def get_accum_buffer(self): + return self._accum_buffer + + def set_accum_buffer(self, x): + self._accum_buffer = x + + def get_accum_size(self): + return self._accum_size + + def set_accum_size(self, x): + self._accum_size = x + + def get_multisample(self): + return self._multisample + + def set_multisample(self, x): + self._multisample = x + + def get_samples_per_pixel(self): + return self._samples_per_pixel + + def set_samples_per_pixel(self, x): + self._samples_per_pixel = x + + def supported(self): + """Determine whether the combination of attributes requested by this pixel format + can be satisfied. If successful, a new GLPixelFormat object is returned whose + attributes reflect those actually allocated. Otherwise, a GLPixelFormatError is + raised.""" + raise NotImplementedError + +#------------------------------------------------------------------------------ + +class GLPixelFormatError(ValueError): + + def __init__(self): + ValueError.__init__(self, + "OpenGL pixel format attribute request cannot be satisfied") diff --git a/GUI/Generic/GGLPixmaps.py b/GUI/Generic/GGLPixmaps.py new file mode 100644 index 0000000..8f07090 --- /dev/null +++ b/GUI/Generic/GGLPixmaps.py @@ -0,0 +1,23 @@ +# +# PyGUI - OpenGL Pixmap - Generic +# + +from OpenGL.GL import glViewport +from GUI import ImageBase +from GUI.GLContexts import GLContext + +class GLPixmap(ImageBase, GLContext): + """An offscreen OpenGL drawing area. + + Constructors: + GLPixmap(width, height, share = None, config_attr = value...) + GLPixmap(width, height, config, share = None) + """ + + def destroy(self): + GLContext.destroy(self) + + def _init_context(self): + width, height = self.size + glViewport(0, 0, int(width), int(height)) + self.init_context() diff --git a/GUI/Generic/GGLTextures.py b/GUI/Generic/GGLTextures.py new file mode 100644 index 0000000..0a56f85 --- /dev/null +++ b/GUI/Generic/GGLTextures.py @@ -0,0 +1,122 @@ +# +# PyGUI - OpenGL Textures - Generic +# + +from weakref import WeakKeyDictionary +from OpenGL.GL import glGenTextures, glBindTexture, glDeleteTextures, \ + glTexImage2D, GL_TEXTURE_2D, GL_RGBA +from OpenGL.GLU import gluBuild2DMipmaps +from GUI.GGLContexts import current_share_group +from GUI.GLDisplayLists import call_when_not_compiling_display_list + +#---------------------------------------------------------------------- + +class TextureIdMap(WeakKeyDictionary): + + def __del__(self): + #print "GL.TextureIdMap.__del__:", self ### + def free_texture(): + glDeleteTextures([gl_id]) + for share_group, gl_id in self.items(): + context = share_group._some_member() + if context: + #print "...freeing texture id", gl_id, "for", share_group, "using", context ### + context.with_context(free_texture) + +#---------------------------------------------------------------------- + +class Texture(object): + """This class encapsulates an OpenGL texture and maintains a + representation of it for each OpenGL context with which it is used. + Allocation and maintentance of texture numbers is handled automatically. + + Constructor: + Texture(texture_type) + where texture_type is the appropriate GL constant for the type + of texture (GL_TEXTURE_2D etc.) + """ + # + # _gl_type int GL_TEXTURE_2D, etc. + # _gl_id ShareGroup -> int Mapping from OpenGL share group to texture number + + def __init__(self, texture_type): + self._gl_type = texture_type + self._gl_id = TextureIdMap() + + def deallocate(self): + """Deallocate any OpenGL resources that have been allocated for this + texture in any context.""" + self._gl_id.__del__() + + def bind(self): + """Makes this texture the current texture for the current context + by calling glBindTexture. If this texture has not previously been + used with the current context, do_setup() is called to allocate + and initialise a representation of the texture.""" + gl_id = self.gl_id() + glBindTexture(self._gl_type, gl_id) + + def gl_id(self): + """Returns the OpenGL texture number corresponding to this texture + in the current context. May trigger allocation of a new texture and + a call to do_setup(). Does not bind the texture, unless a new texture + is allocated, in which case the current texture binding may be changed + as a side effect.""" + share_group = current_share_group() + gl_id = self._gl_id.get(share_group) + if gl_id is None: + gl_id = glGenTextures(1) + #print "GLTexture: assigned id %d for %s in share group %s" % ( + # gl_id, self, share_group) ### + self._gl_id[share_group] = gl_id + call_when_not_compiling_display_list(lambda: self._setup(gl_id)) + return gl_id + + def _setup(self, gl_id): + glBindTexture(self._gl_type, gl_id) + self.do_setup() + + def do_setup(self): + """Subclasses should override this to make the necessary OpenGL + calls to initialise the texture, assuming that glBindTexture has + already been called.""" + raise NotImplementedError + + def gl_tex_image_2d(self, image, target = GL_TEXTURE_2D, internal_format = GL_RGBA, + border = False, with_mipmaps = False): + """Load the currently bound texture with data from an image, with automatic + scaling to power-of-2 size and optional mipmap generation.""" + border = bool(border) + if border and with_mipmaps: + raise ValueError("Bordered texture cannot have mipmaps") + b2 = 2 * border + width, height = image.size + twidth = pow2up(width - b2) + b2 + theight = pow2up(height - b2) + b2 + #print "GUI.GGLTextures.Texture.gl_tex_image_2d: before scaling: size =", (width, height) ### + if width <> twidth or height <> theight: + #print "GUI.GGLTextures.Texture.gl_tex_image_2d: scaling image to size", (twidth, theight) ### + from Pixmaps import Pixmap + image2 = Pixmap(twidth, theight) + def scale(canvas): + image.draw(canvas, (0, 0, width, height), (0, 0, twidth, theight)) + image2.with_canvas(scale) + image = image2 + format, type, data = self._gl_get_texture_data(image) + if with_mipmaps: + #print "GUI.GGLTextures.Texture.gl_tex_image_2d: loading mipmaps" ### + gluBuild2DMipmaps(target, internal_format, twidth, theight, + format, type, data) + else: + #print "GUI.GGLTextures.Texture.gl_tex_image_2d: loading texture" ### + glTexImage2D(target, 0, internal_format, twidth, theight, border, + format, type, data) + +#---------------------------------------------------------------------- + +def pow2up(size): + # Round size up to a power of 2 + psize = 1 + while psize < size: + psize <<= 1 + return psize diff --git a/GUI/Generic/GGLViews.py b/GUI/Generic/GGLViews.py new file mode 100644 index 0000000..368970d --- /dev/null +++ b/GUI/Generic/GGLViews.py @@ -0,0 +1,73 @@ +# +# PyGUI - OpenGL View - Generic +# + +from OpenGL.GL import glViewport, glMatrixMode, glLoadIdentity, \ + GL_PROJECTION, GL_MODELVIEW +from GUI import Component +from GUI import ViewBase +from GUI.GLContexts import GLContext + +class GLError(StandardError): + pass + +class GLView(ViewBase, Component, GLContext): + """A GLView is a Component providing an OpenGL 3D display area. + + Constructors: + GLView(config_attr = value..., share = None) + GLView(config, share = None) + """ + + _default_size = (100, 100) + + def __init__(self, **kwds): + Component.__init__(self, **kwds) + ViewBase.__init__(self) + + def destroy(self): + ViewBase.destroy(self) + Component.destroy(self) + + def _render(self): + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + self.render() + + def render(self): + """This method is called when the contents of the view needs to + be redrawn, with the view's OpenGL context as the current context. + The modelview matrix has been selected as the current matrix and + set to an identity matrix. After calling this method, buffers will + be automatically swapped or drawing flushed as appropriate.""" + pass + + def viewport_changed(self): + """This method is called when the view's size has changed, with + the view's OpenGL context as the current context, and the OpenGL + viewport set to (0, 0, width, height). The default implementation + loads an identity projection matrix and calls init_projection().""" + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + self.init_projection() + + def init_projection(self): + """This method is called to establish the projection whenever the + viewport changes. The projection matrix has been selected as the + current matrix and set to an identity matrix.""" + pass + + def update(self): + """Redraws the contents of the view immediately, without waiting + for a return to the event loop.""" + self.with_context(self.render, flush = True) + + def _init_context(self): + self.init_context() + self._update_viewport() + + def _update_viewport(self): + width, height = self.size + glViewport(0, 0, int(width), int(height)) + self.viewport_changed() + diff --git a/GUI/Generic/GGeometry.py b/GUI/Generic/GGeometry.py new file mode 100644 index 0000000..0a5bb8a --- /dev/null +++ b/GUI/Generic/GGeometry.py @@ -0,0 +1,73 @@ +# +# Python GUI - Point and rectangle utilities - Generic +# + +def add_pt((x1, y1), (x2, y2)): + return (x1 + x2), (y1 + y2) + +def sub_pt((x1, y1), (x2, y2)): + return (x1 - x2), (y1 - y2) + +def rect_sized((l, t), (w, h)): + return (l, t, l + w, t + h) + +def rect_left(r): + return r[0] + +def rect_top(r): + return r[1] + +def rect_right(r): + return r[2] + +def rect_bottom(r): + return r[3] + +def rect_width(r): + return r[2] - r[0] + +def rect_height(r): + return r[3] - r[1] + +def rect_topleft(r): + return r[:2] + +def rect_botright(r): + return r[2:] + +def rect_center((l, t, r, b)): + return ((l + r) // 2, (t + b) // 2) + +def rect_size((l, t, r, b)): + return (r - l, b - t) + +def union_rect((l1, t1, r1, b1), (l2, t2, r2, b2)): + return (min(l1, l2), min(t1, t2), max(r1, r2), max(b1, b2)) + +def sect_rect((l1, t1, r1, b1), (l2, t2, r2, b2)): + return (max(l1, l2), max(t1, t2), min(r1, r2), min(b1, b2)) + +def inset_rect((l, t, r, b), (dx, dy)): + return (l + dx, t + dy, r - dx, b - dy) + +def offset_rect((l, t, r, b), (dx, dy)): + return (l + dx, t + dy, r + dx, b + dy) + +def offset_rect_neg((l, t, r, b), (dx, dy)): + return (l - dx, t - dy, r - dx, b - dy) + +def empty_rect((l, t, r, b)): + return r <= l or b <= t + +def pt_in_rect((x, y), (l, t, r, b)): + return l <= x < r and t <= y < b + +def rects_intersect((l1, t1, r1, b1), (l2, t2, r2, b2)): + return l1 < r2 and l2 < r1 and t1 < b2 and t2 < b1 + +def rect_with_center((l, t, r, b), (x, y)): + w = r - l + h = b - t + rl = x - w // 2 + rt = y - h // 2 + return (rl, rt, rl + w, rt + h) diff --git a/GUI/Generic/GImageBases.py b/GUI/Generic/GImageBases.py new file mode 100644 index 0000000..5762234 --- /dev/null +++ b/GUI/Generic/GImageBases.py @@ -0,0 +1,26 @@ +# +# Python GUI - Image Base - Generic +# + +from GUI.Properties import Properties, overridable_property +from GUI.Geometry import rect_sized + +class ImageBase(Properties): + """Abstract base class for Image, Pixmap and GLPixmap.""" + + width = overridable_property('width', "Width of the image in pixels (read only).") + height = overridable_property('height', "Height of the image in pixels (read only).") + size = overridable_property('size', "Size of the image in pixels (read only).") + bounds = overridable_property('bounds', "Bounding rectangle of the image in pixels (read only).") + + def get_size(self): + return (self.width, self.height) + + def get_bounds(self): + return rect_sized((0, 0), self.size) + + def draw(self, canvas, src_rect, dst_rect): + """Draw the part of the image specified by src_rect on the given canvas, + scaled to fit within dst_rect.""" + raise NotImplementedError + diff --git a/GUI/Generic/GImages.py b/GUI/Generic/GImages.py new file mode 100644 index 0000000..21fb9bb --- /dev/null +++ b/GUI/Generic/GImages.py @@ -0,0 +1,24 @@ +# +# Python GUI - Images - Generic +# + +from GUI.Files import FileRef +from GUI.Resources import get_resource +from GUI import ImageBase + +class Image(ImageBase): + """Class Image represents an RGB or RGBA image. + + Constructors: + Image(file = FileRef or pathname) + """ + + def from_resource(cls, name, **kwds): + return get_resource(cls, name, **kwds) + + from_resource = classmethod(from_resource) + + def __init__(self, file): + if isinstance(file, FileRef): + file = file.path + self._init_from_file(file) diff --git a/GUI/Generic/GLDisplayLists.py b/GUI/Generic/GLDisplayLists.py new file mode 100644 index 0000000..e6ed682 --- /dev/null +++ b/GUI/Generic/GLDisplayLists.py @@ -0,0 +1,107 @@ +# +# PyGUI - OpenGL Display Lists - Generic +# + +from weakref import WeakKeyDictionary +from OpenGL.GL import glGenLists, glNewList, glEndList, glCallList, \ + glDeleteLists, GL_COMPILE +from GUI.Properties import Properties, overridable_property +from GUI.GGLContexts import current_share_group + +#---------------------------------------------------------------------- + +class DisplayListIdMap(WeakKeyDictionary): + + def __del__(self): + # Delete any display lists that have been allocated for this map. + #print "GL.DisplayListIdMap.__del__:", self ### + def free_display_list(): + glDeleteLists(gl_id, 1) + for share_group, gl_id in self.items(): + context = share_group._some_member() + if context: + #print "...freeing display list id", gl_id, "for", share_group, "using", context ### + context.with_context(free_display_list) + +#---------------------------------------------------------------------- + +class DisplayList(Properties): + """This class encapsulates an OpenGL display list and maintains a + representation of it for each OpenGL context with which it is used. + Allocation and maintentance of display list numbers is handled + automatically.""" + # + # _gl_id ShareGroup -> int Mapping from OpenGL share group to + # display list number + + setup = overridable_property('setup', + """Function to set up the display list by making the necessary + OpenGL calls, excluding glNewList and glEndList.""") + + def __init__(self, setup = None): + self._gl_id = DisplayListIdMap() + self._setup = setup + + def deallocate(self): + """Deallocate any OpenGL resources that have been allocated for this + display list in any context.""" + self._gl_id.__del__() + + def get_setup(self): + return self._setup + + def set_setup(self, x): + self._setup = x + + def call(self): + """Calls the display list using glCallList(). If this display list + has not previously been used with the current context, allocates + a display list number and arranges for do_setup() to be called + to compile a representation of the display list.""" + share_group = current_share_group() + gl_id = self._gl_id.get(share_group) + if gl_id is None: + gl_id = glGenLists(1) + #print "GLDisplayList: assigned id %d for %s in share group %s" % ( + # gl_id, self, share_group) ### + self._gl_id[share_group] = gl_id + call_when_not_compiling_display_list(lambda: self._compile(gl_id)) + glCallList(gl_id) + + def _compile(self, gl_id): + global compiling_display_list + compiling_display_list = True + glNewList(gl_id, GL_COMPILE) + try: + self.do_setup() + finally: + glEndList() + compiling_display_list = False + + def do_setup(self): + """Make all the necessary OpenGL calls to compile the display list, + except for glNewList() and glEndList() which will be called automatically + before and after. The default implementation calls the 'setup' property.""" + setup = self._setup + if setup: + setup() + else: + raise NotImplementedError( + "No setup function or do_setup method for GL.DisplayList") + + +compiling_display_list = False +pending_functions = [] + +def call_when_not_compiling_display_list(func): + #print "GLDisplayLists: entering call_when_not_compiling_display_list" ### + if compiling_display_list: + #print "GLDisplayLists: deferring", func ### + pending_functions.append(func) + else: + #print "GLDisplayLists: immediately calling", func ### + func() + while pending_functions: + #print "GLDisplayLists: calling deferred", func ### + pending_functions.pop()() + #print "GLDisplayLists: exiting call_when_not_compiling_display_list" ### diff --git a/GUI/Generic/GLShareGroups.py b/GUI/Generic/GLShareGroups.py new file mode 100644 index 0000000..8bb2cfc --- /dev/null +++ b/GUI/Generic/GLShareGroups.py @@ -0,0 +1,29 @@ +# +# PyGUI - OpenGL Context Sharing - Generic +# + +from weakref import WeakKeyDictionary + +class ShareGroup(object): + """Object representing a shared texture and display list + namespace for OpenGL contexts.""" + + def __init__(self): + self.contexts = WeakKeyDictionary() + + def __contains__(self, context): + "Test whether a GLView or GLPixmap is a member of this share group." + return context in self.contexts + + def __iter__(self): + "Return an iterator over the members of this share group." + return iter(self.contexts) + + def _add(self, context): + self.contexts[context] = 1 + + def _some_member(self, exclude = None): + for member in self.contexts: + if member is not exclude: + return member + return None diff --git a/GUI/Generic/GLabels.py b/GUI/Generic/GLabels.py new file mode 100644 index 0000000..db84870 --- /dev/null +++ b/GUI/Generic/GLabels.py @@ -0,0 +1,14 @@ +# +# Python GUI - Labels - Generic +# + +from GUI.Properties import overridable_property +from GUI import Control + +class Label(Control): + """A piece of static text for labelling items in a window.""" + + _default_tab_stop = False + + text = overridable_property('text') + diff --git a/GUI/Generic/GListButtons.py b/GUI/Generic/GListButtons.py new file mode 100644 index 0000000..a811401 --- /dev/null +++ b/GUI/Generic/GListButtons.py @@ -0,0 +1,66 @@ +#-------------------------------------------------------------- +# +# PyGUI - Pop-up list control - Generic +# +#-------------------------------------------------------------- + +from GUI.Properties import overridable_property +from GUI.Actions import Action +from GUI import Control, application + +class ListButton(Control, Action): + """A button that displays a value and provides a pop-up or + pull-down list of choices.""" + + titles = overridable_property('titles', + "List of item title strings") + + values = overridable_property('values', + "List of values corresponding to tiles, or None to use item index as value") + + def _extract_initial_items(self, kwds): + titles = kwds.pop('titles', None) or [] + values = kwds.pop('values', None) + return titles, values + + def get_titles(self): + return self._titles + + def set_titles(self, x): + self._titles = x + self._update_items() + + def get_values(self): + return self._values + + def set_values(self, x): + self._values = x + + def get_value(self): + i = self._get_selected_index() + if i >= 0: + values = self.values + if values: + return values[i] + else: + return i + + def set_value(self, value): + values = self.values + if values: + try: + i = values.index(value) + except ValueError: + i = -1 + else: + if value is None: + i = -1 + else: + i = value + self._set_selected_index(i) + + def do_action(self): + try: + Action.do_action(self) + except: + application().report_error() diff --git a/GUI/Generic/GMenus.py b/GUI/Generic/GMenus.py new file mode 100644 index 0000000..13fd7ef --- /dev/null +++ b/GUI/Generic/GMenus.py @@ -0,0 +1,335 @@ +#---------------------------------------------------------------------- +# +# Python GUI - Menus - Generic +# +#---------------------------------------------------------------------- + +from GUI import Globals +from GUI.Properties import Properties, overridable_property + +#---------------------------------------------------------------------- + +def search_list_for_key(items, char, shift, option): + for i in xrange(len(items)-1, -1, -1): + result = items[i]._search_for_key(char, shift, option) + if result: + return result + +#---------------------------------------------------------------------- + +class Menu(Properties): + """Pull-down or pop-up menu class. + + Menu(title, item_descriptors) + constructs a menu with the given title and items. Each + item_descriptor is of the form + + "-" + + for a separator, + + ("text/key", 'command_name') + + for a single menu item, or + + (["text/key", ...], 'command_name') + + for an indexed item group. An indexed group is a group + of items sharing the same command name and distinguished + by an integer index. Items can be added to and removed + from the group dynamically, to implement e.g. a font + menu or windows menu. + + The "key" part of the item descriptor (which is optional) + specifies the keyboard equivalent. It should consist of + a single character together with the following optional + modifiers: + + ^ representing the Shift key + @ representing the Alt or Option key + """ + + title = overridable_property('title', "Title string appearing in menu bar") + special = overridable_property('special', "Menu appears at right end of menu bar") + + _flat_items = None + + def __init__(self, title, items, special = False, substitutions = {}, **kwds): + self._title = title + self._items = [] + self._special = special + Properties.__init__(self, **kwds) + self.extend(items, substitutions) + + def get_title(self): + return self._title + + def get_special(self): + return self._special + + def item_with_command(self, cmd): + for item in self._items: + if item._command_name == cmd: + return item + return None + + def append(self, item, substitutions = {}): + items = self._items + item = self._make_item(item, substitutions) + if not (items and isinstance(item, MenuSeparator) + and isinstance(items[-1], MenuSeparator)): + items.append(item) + + def extend(self, items, substitutions = {}): + for item in items: + self.append(item, substitutions) + + def _make_item(self, item, substitutions): + if isinstance(item, MenuItem): + return item + elif item == "-": + return _menu_separator + else: + (text, cmd) = item + if isinstance(text, basestring): + return SingleMenuItem(text, cmd, substitutions) + else: + return MenuItemGroup(text, cmd) + + def _command_and_args_for_item(self, item_num): + i = 1 + for item in self._items: + n = item._num_subitems() + if item_num < i + n: + return item._command_and_args_for_subitem(item_num - i) + i += n + return '', () + + def _update_platform_menu(self): + # Called during menu setup after items have been enabled/checked. + # Generic implementation rebuilds the whole menu from scratch. + # Implementations may override this to be more elegant. + self._rebuild_platform_menu() + + def _rebuild_platform_menu(self): + self._clear_platform_menu() + for item in self._items: + item._add_to_platform_menu(self) + + def _search_for_key(self, char, shift, option): + return search_list_for_key(self._items, char, shift, option) + + def _get_flat_items(self): + flat = self._flat_items + if flat is None: + flat = [] + for item in self._items: + item._collect_flat_items(flat) + self._flat_items = flat + return flat + + def _get_flat_item(self, i): + return self._get_flat_items()[i] + +#---------------------------------------------------------------------- + +class MenuItem(Properties): + # Internal class representing a menu item, group or separator. + # + # _command_name string Internal command name + + def _num_subitems(self): + return 1 + + def _split_text(self, text): + # Split menu text into label and key combination. + if "/" in text: + return text.split("/") + else: + return text, "" + + def _name(self): + return self._label.replace("", Globals.application_name) + + def _collect_flat_items(self, result): + result.append(self) + +#---------------------------------------------------------------------- + +class MenuSeparator(MenuItem): + # Internal class representing a menu separator. + + _command_name = '' + + def _add_to_platform_menu(self, menu): + menu._add_separator_to_platform_menu() + + def _search_for_key(self, char, shift, option): + pass + +#---------------------------------------------------------------------- + +class SingleMenuItem(MenuItem): + """Class representing a menu item. + + Properties: + enabled boolean + checked boolean + """ + + enabled = 0 + checked = 0 + _key = None + _shift = 0 + _option = 0 + #_index = None + + def __init__(self, text, cmd, substitutions = {}): + label1, keycomb1 = self._split_text(text) + label2, keycomb2 = self._split_text(substitutions.get(cmd, "")) + self._label = label2 or label1 + keycomb = keycomb2 or keycomb1 + for c in keycomb: + if c == '^': + self._shift = 1 + elif c == '@': + self._option = 1 + else: + self._key = c.upper() + self._command_name = cmd + + def __str__(self): + return "" % ( + self._label, self._key, self._shift, self._option, self.enabled) + + def _add_to_platform_menu(self, menu): + menu._add_item_to_platform_menu(self, self._name(), self._command_name) + + def _command_and_args_for_subitem(self, i): + return self._command_name, () + + def _search_for_key(self, char, shift, option): + if self._matches_key(char, shift, option): + return self._command_name + + def _matches_key(self, char, shift, option): + return self._key == char and self._shift == shift \ + and self._option == option and self.enabled + +#---------------------------------------------------------------------- + +class MenuItemGroup(MenuItem): + """Class representing a menu item group. + + Properties: + enabled <- boolean Assigning to these changes the corresponding + checked <- boolean property of all the group's items. + + Operators: + group[index] -> MenuItem + + Methods: + set_items(["text/key", ...]) + Replaces all the items in the group by the specified items. + """ + + enabled = overridable_property('enabled') + checked = overridable_property('checked') + + def __init__(self, text_list, cmd): + self.set_items(text_list) + self._command_name = cmd + + def _num_subitems(self): + return len(self._items) + + def _command_and_args_for_subitem(self, i): + return self._command_name, (i,) + + def get_enabled(self): + raise AttributeError("'enabled' property of MenuItemGroup is write-only") + + def set_enabled(self, state): + for item in self._items: + item.enabled = state + + def get_checked(self): + raise AttributeError("'checked' property of MenuItemGroup is write-only") + + def set_checked(self, state): + for item in self._items: + item.checked = state + + def __getitem__(self, index): + return self._items[index] + + def set_items(self, text_list): + self._items = [SingleMenuItem(text, '') for text in text_list] + + def _add_to_platform_menu(self, menu): + #for item in self._items: + # item._add_to_platform_menu(menu) + cmd = self._command_name + for index, item in enumerate(self._items): + menu._add_item_to_platform_menu(item, item._name(), cmd, index) + + def _search_for_key(self, char, shift, option): + items = self._items + for i in xrange(len(items)-1, -1, -1): + if items[i]._matches_key(char, shift, option): + return (self._command_name, i) + + def _collect_flat_items(self, result): + for item in self._items: + item._collect_flat_items(result) + +#---------------------------------------------------------------------- + +_menu_separator = MenuSeparator() +_dummy_menu_item = SingleMenuItem("", '') + +#---------------------------------------------------------------------- + +class MenuState: + """A MenuState object is used to enable/disable and check/uncheck + menu items, and to add or remove items of indexed groups, + during the menu setup phase of menu command handling. + + Each single menu item or item group appears as an attribute of + the MenuState object, with the command name as the attribute name, + allowing operations such as + + menu_state.copy_cmd.enabled = 1 + menu_state.font_cmd[current_font].checked = 1 + + The command name may also be used as a mapping index. + + Operators: + menu_state.command_name -> MenuItem + menu_state['command_name'] -> MenuItem + """ + + def __init__(self, menu_list): + mapping = {} + for menu in menu_list: + for item in menu._items: + cmd = item._command_name + if cmd: + mapping[cmd] = item + self._mapping = mapping + + def __getattr__(self, name): + try: + return self._mapping[name] + except KeyError: + if name.startswith("__"): + raise AttributeError, name + return _dummy_menu_item + + __getitem__ = __getattr__ + + def reset(self): + """Disable and uncheck all items.""" + for item in self._mapping.values(): + item.enabled = 0 + item.checked = None diff --git a/GUI/Generic/GMouseTrackers.py b/GUI/Generic/GMouseTrackers.py new file mode 100644 index 0000000..6644dba --- /dev/null +++ b/GUI/Generic/GMouseTrackers.py @@ -0,0 +1,27 @@ +# +# Python GUI - Mouse trackers - Generic +# + +from GUI import application + +class MouseTracker(object): + """Iterator used to track movements of the mouse following a mouse_down + event in a Views. Each call to the next() method returns a mouse_drag + event, except for the last one, which returns a mouse_up event.""" + + def __init__(self, view): + self._view = view + self._finished = 0 + + def __iter__(self): + return self + + def next(self): + if not self._finished: + event = self._next_mouse_event() + event.position = event.position_in(self._view) + if event.kind == 'mouse_up': + self._finished = 1 + return event + else: + raise StopIteration diff --git a/GUI/Generic/GPixmaps.py b/GUI/Generic/GPixmaps.py new file mode 100644 index 0000000..af13c33 --- /dev/null +++ b/GUI/Generic/GPixmaps.py @@ -0,0 +1,20 @@ +# +# Python GUI - Pixmap - Generic +# + +from GUI import ImageBase + +class Pixmap(ImageBase): + """A Pixmap is an offscreen area that can be used both as a + destination for drawing and a source of image data for drawing + in a View or another Pixmap. + + Constructor: + Pixmap(width, height) + """ + + def with_canvas(self, proc): + """Call the given procedure with a canvas suitable for drawing on + this Pixmap. The canvas is valid only for the duration of the call, + and should not be retained beyond it.""" + raise NotImplementedError diff --git a/GUI/Generic/GPrinting.py b/GUI/Generic/GPrinting.py new file mode 100644 index 0000000..28b8ea2 --- /dev/null +++ b/GUI/Generic/GPrinting.py @@ -0,0 +1,167 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Printing - Generic +# +#------------------------------------------------------------------------------ + +from __future__ import division +from math import ceil +import cPickle as pickle +from GUI.Properties import overridable_property +from GUI import application + +class PageSetup(object): + """Holder of information specified by the "Page Setup" command.""" + + paper_name = overridable_property('paper_name') + paper_width = overridable_property('paper_width') + paper_height = overridable_property('paper_height') + left_margin = overridable_property('left_margin') + top_margin = overridable_property('top_margin') + right_margin = overridable_property('right_margin') + bottom_margin = overridable_property('bottom_margin') + orientation = overridable_property('orientation') + + paper_size = overridable_property('paper_size') + margins = overridable_property('margins') + page_width = overridable_property('page_width') + page_height = overridable_property('page_height') + page_size = overridable_property('page_size') + page_rect = overridable_property('page_rect') + printable_rect = overridable_property('printable_rect') # May not work + printer_name = overridable_property('printer_name') + + _pickle_attributes = ['paper_name', 'paper_size', 'margins', + 'printer_name', 'orientation'] + + def __getstate__(self): + state = {} + for name in self._pickle_attributes: + state[name] = getattr(self, name) + return state + + def __setstate__(self, state): + for name, value in state.iteritems(): + setattr(self, name, value) + + def from_string(s): + """Restore a pickled PageSetup object from a string.""" + return pickle.loads(s) + + from_string = staticmethod(from_string) + + def to_string(self): + """Pickle the PageSetup object and return it as a string.""" + return pickle.dumps(self, 2) + + def get_paper_size(self): + return self.paper_width, self.paper_height + + def set_paper_size(self, x): + self.paper_width, self.paper_height = x + + def get_margins(self): + return self.left_margin, self.top_margin, self.right_margin, self.bottom_margin + + def set_margins(self, x): + self.left_margin, self.top_margin, self.right_margin, self.bottom_margin = x + + def get_page_width(self): + return self.paper_width - self.left_margin - self.right_margin + + def get_page_height(self): + return self.paper_height - self.top_margin - self.bottom_margin + + def get_page_size(self): + return (self.page_width, self.page_height) + + def get_page_rect(self): + lm, tm, rm, bm = self.margins + pw, ph = self.paper_size + return (lm, tm, pw - rm, ph - bm) + +#------------------------------------------------------------------------------ + +class Printable(object): + """Mixin class for components implementing the "Print" command.""" + + printable = overridable_property('printable', "Whether this component should handle the 'Print' command.") + page_setup = overridable_property('page_setup', "The PageSetup object to use for printing.") + print_title = overridable_property('print_title', "Title for print job.") + + _printable = True + + def get_printable(self): + return self._printable + + def set_printable(self, x): + self._printable = x + + def get_print_title(self): + window = self.window + if window: + return window.title + else: + return "" + + def get_page_setup(self): + result = None + model = getattr(self, 'model', None) + if model: + result = getattr(model, 'page_setup', None) + if not result: + result = application().page_setup + return result + + def setup_menus(self, m): + if self.printable: + m.print_cmd.enabled = True + + def print_cmd(self): + if self.printable: + page_setup = self.page_setup + if page_setup: + self.print_view(page_setup) + else: + self.pass_to_next_handler('print_cmd') + +#------------------------------------------------------------------------------ + +class Paginator(object): + """Used internally. A generic pagination algorithm for printing.""" + + def __init__(self, view, page_setup): + self.view = view + extent_width, extent_height = view.get_print_extent() + #paper_width, paper_height = page_setup.paper_size + #lm, tm, rm, bm = page_setup.margins + #page_width = int(paper_width - lm - rm) + #page_height = int(paper_height - tm - bm) + page_width, page_height = page_setup.page_size + if page_width <= 0 or page_height <= 0: + from AlertFunctions import stop_alert + stop_alert("Margins are too large for the page size.") + return + self.extent_rect = (0, 0, extent_width, extent_height) + self.page_width = page_width + self.page_height = page_height + self.left_margin = page_setup.left_margin + self.top_margin = page_setup.top_margin + self.pages_wide = int(ceil(extent_width / page_width)) + self.pages_high = int(ceil(extent_height / page_height)) + self.num_pages = self.pages_wide * self.pages_high + + def draw_page(self, canvas, page_num): + row, col = divmod(page_num, self.pages_wide) + view_left = col * self.page_width + view_top = row * self.page_height + view_right = view_left + self.page_width + view_bottom = view_top + self.page_height + view_rect = (view_left, view_top, view_right, view_bottom) + dx = self.left_margin - view_left + dy = self.top_margin - view_top + canvas.translate(dx, dy) + canvas.rectclip(self.extent_rect) + canvas.rectclip(view_rect) + canvas._printing = True + self.view.draw(canvas, view_rect) diff --git a/GUI/Generic/GRadioButtons.py b/GUI/Generic/GRadioButtons.py new file mode 100644 index 0000000..952f6d0 --- /dev/null +++ b/GUI/Generic/GRadioButtons.py @@ -0,0 +1,52 @@ +# +# Python GUI - Radio buttons - Generic +# + +from GUI.Properties import overridable_property +from GUI import Control + +class RadioButton(Control): + """RadioButtons are used in groups to represent a 1-of-N + choice. A group of RadioButtons is coordinated by a + RadioGroup object. The 'group' property indicates the + RadioGroup to which it belongs, and the 'value' property + is the value to which the RadioGroup's value is set + when this RadioButton is selected.""" + + group = overridable_property('group', """The RadioGroup to + which this radio button belongs.""") + + value = overridable_property('value', """The value to which + the associated radio group's 'value' property should be + set when this radio button is selected.""") + + _group = None + _value = None + + # + # Properties + # + + def get_group(self): + return self._group + + def set_group(self, new_group): + old_group = self._group + if new_group is not old_group: + if old_group: + old_group._remove_item(self) + self._group = new_group + if new_group: + new_group._add_item(self) + + def get_value(self): + return self._value + + def set_value(self, new_value): + old_value = self._value + if new_value != old_value: + self._value = new_value + self._value_changed() + + def _value_changed(self): + raise NotImplementedError diff --git a/GUI/Generic/GRadioGroups.py b/GUI/Generic/GRadioGroups.py new file mode 100644 index 0000000..a6fd792 --- /dev/null +++ b/GUI/Generic/GRadioGroups.py @@ -0,0 +1,87 @@ +# +# Python GUI - Radio groups - Generic +# + +from GUI.Properties import Properties, overridable_property +from GUI.Actions import Action + +class RadioGroup(Properties, Action): + """A RadioGroup coordinates a group of RadioButtons. + It has a 'value' property which is equal to the value + of the currently selected RadioButton. It may be given + an action procedure to execute when its value changes. + + Operations: + iter(group) + Returns an iterator over the items of the group. + """ + + value = overridable_property('value', """The value of the currently + selected radio button.""") + + _items = None + _value = None + + def __init__(self, items = [], **kwds): + Properties.__init__(self, **kwds) + self._items = [] + self.add_items(items) + + # + # Operations + # + + def __iter__(self): + return iter(self._items) + + # + # Properties + # + + def get_value(self): + return self._value + + def set_value(self, x): + if self._value <> x: + self._value = x + self._value_changed() + self.do_action() + + # + # Adding and removing items + # + + def add_items(self, items): + "Add a sequence of RadioButtons to this group." + for item in items: + self.add_item(item) + + def add_item(self, item): + "Add a RadioButton to this group." + item.group = self + + def remove_items(self, items): + "Remove a sequence of RadioButtons from this group." + for item in items: + item.group = None + + def remove_item(self, item): + "Remove a RadioButton from this group." + item.group = None + + def _add_item(self, item): + self._items.append(item) + self._item_added(item) + + def _remove_item(self, item): + self._items.remove(item) + self._item_removed(item) + + def _item_added(self, item): + raise NotImplementedError + + def _item_removed(self, item): + raise NotImplementedError + + def _value_changed(self): + raise NotImplementedError diff --git a/GUI/Generic/GScrollableViews.py b/GUI/Generic/GScrollableViews.py new file mode 100644 index 0000000..a23f442 --- /dev/null +++ b/GUI/Generic/GScrollableViews.py @@ -0,0 +1,168 @@ +# +# Python GUI - Scrollable Views - Generic +# + +from GUI.Geometry import rect_sized, add_pt, sub_pt +from GUI.Properties import overridable_property +from GUI.Geometry import sect_rect +from GUI import DrawableContainer + +default_extent = (300, 300) +default_line_scroll_amount = (16, 16) +default_scrolling = 'hv' + +class ScrollableView(DrawableContainer): + """A ScrollableView is a 2D drawing area having its own coordinate + system and clipping area, with support for scrolling.""" + + scrolling = overridable_property('scrolling', + "String containing 'h' for horizontal and 'v' for vertical scrolling.") + + hscrolling = overridable_property('hscrolling', + "True if horizontal scrolling is enabled.") + + vscrolling = overridable_property('vscrolling', + "True if vertical scrolling is enabled.") + + extent = overridable_property('extent', + "Size of scrollable area in local coordinates.") + + scroll_offset = overridable_property('scroll_offset', + "Current scrolling position.") + + line_scroll_amount = overridable_property('line_scroll_amount', + "Tuple specifying horizontal and vertical line scrolling increments.") + + background_color = overridable_property('background_color', + "Color with which to fill areas outside the extent, or None") + + #scroll_bars = overridable_property('scroll_bars', + # "Attached ScrollBar instances.") + # + ## _scroll_bars [ScrollBar] + + def set(self, **kwds): + if 'scrolling' in kwds: + self.scrolling = kwds.pop('scrolling') + DrawableContainer.set(self, **kwds) + + def get_scrolling(self): + chars = [] + if self.hscrolling: + chars.append('h') + if self.vscrolling: + chars.append('v') + return ''.join(chars) + + def set_scrolling(self, value): + self.hscrolling = 'h' in value + self.vscrolling = 'v' in value + + def viewed_rect(self): + """Return the rectangle in local coordinates bounding the currently + visible part of the extent.""" + return rect_sized(self.scroll_offset, self.size) + + def get_print_extent(self): + return self.extent + + def get_background_color(self): + return self._background_color + + def set_background_color(self, x): + self._background_color = x + self.invalidate() + + # + # Coordinate transformation + # + + def local_to_container_offset(self): + return sub_pt(self.position, self.scroll_offset) + + # + # Scrolling + # + + def h_line_scroll_amount(self): + """Return the horizontal line scroll increment.""" + return self.line_scroll_amount[0] + + def v_line_scroll_amount(self): + """Return the vertical line scroll increment.""" + return self.line_scroll_amount[1] + + def h_page_scroll_amount(self): + """Return the horizontal page scroll increment.""" + return self.width - self.h_line_scroll_amount() + + def v_page_scroll_amount(self): + """Return the vertical page scroll increment.""" + return self.height - self.v_line_scroll_amount() + + def scroll_by(self, dx, dy): + """Scroll by the given amount horizontally and vertically.""" + self.scroll_offset = add_pt(self.scroll_offset, (dx, dy)) + + def scroll_line_left(self): + """Called by horizontal scroll bar to scroll left by one line.""" + self.scroll_by(-self.h_line_scroll_amount(), 0) + + def scroll_line_right(self): + """Called by horizontal scroll bar to scroll right by one line.""" + self.scroll_by(self.h_line_scroll_amount(), 0) + + def scroll_line_up(self): + """Called by vertical scroll bar to scroll up by one line.""" + self.scroll_by(0, -self.v_line_scroll_amount()) + + def scroll_line_down(self): + """Called by vertical scroll bar to scroll down by one line.""" + self.scroll_by(0, self.v_line_scroll_amount()) + + def scroll_page_left(self): + """Called by horizontal scroll bar to scroll left by one page.""" + self.scroll_by(-self.h_page_scroll_amount(), 0) + + def scroll_page_right(self): + """Called by horizontal scroll bar to scroll right by one page.""" + self.scroll_by(self.h_page_scroll_amount(), 0) + + def scroll_page_up(self): + """Called by vertical scroll bar to scroll up by one page.""" + self.scroll_by(0, -self.v_page_scroll_amount()) + + def scroll_page_down(self): + """Called by vertical scroll bar to scroll down by one page.""" + self.scroll_by(0, self.v_page_scroll_amount()) + + # + # Background drawing + # + + def _draw_background(self, canvas, clip_rect): + # If the view has a background color, uses it to fill the parts of the + # clip_rect that are outside the view's extent and returns the remaining + # rectangle. Otherwise, returns the clip_rect unchanged. + color = self._background_color + if color: + vl, vt, vr, vb = clip_rect + ew, eh = self.extent + if vr > ew or vb > eh: + #if getattr(self, "_debug_bg", False): ### + # print "ScrollableView: old backcolor =", canvas.backcolor ### + canvas.gsave() + canvas.backcolor = color + if ew < vr: + #if getattr(self, "_debug_bg", False): ### + # print "ScrollableView: erasing", (ew, vt, vr, vb) ### + canvas.erase_rect((ew, vt, vr, vb)) + if eh < vb: + if getattr(self, "_debug_bg", False): ### + print "ScrollableView: erasing", (vl, eh, ew, vb) ### + canvas.erase_rect((vl, eh, ew, vb)) + canvas.grestore() + #if getattr(self, "_debug_bg", False): ### + # print "ScrollableView: restored backcolor =", canvas.backcolor ### + return sect_rect(clip_rect, (0, 0, ew, eh)) + return clip_rect diff --git a/GUI/Generic/GSliders.py b/GUI/Generic/GSliders.py new file mode 100644 index 0000000..a1a9822 --- /dev/null +++ b/GUI/Generic/GSliders.py @@ -0,0 +1,32 @@ +# +# Python GUI - Slider - Generic +# + +from GUI.Properties import overridable_property +from GUI.Actions import Action +from GUI import Control + +class Slider(Control, Action): + """A control for entering a value by moving a knob along a scale. + + Constructor: + Slider(orient) + where orient = 'h' for horizontal or 'v' for vertical. + """ + + _default_length = 100 + + value = overridable_property('value', "The current value of the control") + min_value = overridable_property('min_value', "Minimum value of the control") + max_value = overridable_property('max_value', "Maximum value of the control") + range = overridable_property('range', "Tuple (min_value, max_value)") + ticks = overridable_property('ticks', "Number of tick marks") + discrete = overridable_property('discrete', "Whether to constrain value to ticks") + live = overridable_property('live', "Whether to invoke action continuously while dragging") + + def get_range(self): + return (self.min_value, self.max_value) + + def set_range(self, x): + self.min_value = x[0] + self.max_value = x[1] diff --git a/GUI/Generic/GStdMenus.py b/GUI/Generic/GStdMenus.py new file mode 100644 index 0000000..005984a --- /dev/null +++ b/GUI/Generic/GStdMenus.py @@ -0,0 +1,117 @@ +#------------------------------------------------------------------------------- +# +# PyGUI - Standard Menus - Generic +# +#------------------------------------------------------------------------------- + +from GUI.Compatibility import set +from GUI import Menu +from GUI import MenuList + +#------------------------------------------------------------------------------- + +class CommandSet(set): + """A set of menu command names. + + Constructors: + CommandSet(string) + CommandSet(sequence of strings) + + Operations: + string in CommandSet + CommandSet + x + CommmandSet - x + x + CommandSet + x - CommandSet + CommandSet += x + CommandSet -= x + where x is a CommandSet, a string or a sequence of strings + """ + + def __init__(self, arg = None): + if arg: + if isinstance(arg, basestring): + arg = [arg] + set.__init__(self, arg) + + def __or__(self, other): + return set.__or__(self, as_command_set(other)) + + __ror__ = __add__ = __radd__ = __or__ + + def __ior__(self, other): + return set.__ior__(self, as_command_set(other)) + + __iadd__ = __ior__ + + def __sub__(self, other): + return set.__sub__(self, as_command_set(other)) + + def __rsub__(self, other): + return as_command_set(other) - self + + def __isub__(self, other): + return set.__isub__(self, as_command_set(other)) + +#------------------------------------------------------------------------------- + +def as_command_set(x): + if not isinstance(x, CommandSet): + if isinstance(x, basestring): + x = [x] + x = CommandSet(x) + return x + +def filter_menu_items(items, include): + result = [] + sep = False + for item in items: + if item == "-": + sep = True + elif item[1] in include: + if sep: + result.append("-") + sep = False + result.append(item) + return result + +def build_menus(spec_list, substitutions = {}, include = None, exclude = None): + if include is None: + include = sum(default_includes) + include = include + sum(always_include) + if exclude is not None: + include = include - exclude + menus = [] + for title, items, special in spec_list: + items = filter_menu_items(items, include) + if items: + menus.append(Menu(title, items, special = special, substitutions = substitutions)) + return MenuList(menus) + +#------------------------------------------------------------------------------- + +fundamental_cmds = CommandSet(['quit_cmd']) +help_cmds = CommandSet(['about_cmd', 'help_cmd']) +pref_cmds = CommandSet(['preferences_cmd']) +file_cmds = CommandSet(['new_cmd', 'open_cmd', 'close_cmd', 'save_cmd', 'save_as_cmd', 'revert_cmd']) +print_cmds = CommandSet(['page_setup_cmd', 'print_cmd']) +edit_cmds = CommandSet(['undo_cmd', 'redo_cmd', 'cut_cmd', 'copy_cmd', 'paste_cmd', 'clear_cmd', 'select_all_cmd']) + +always_include = [fundamental_cmds, edit_cmds] +default_includes = [help_cmds, pref_cmds, file_cmds, print_cmds] + +#------------------------------------------------------------------------------- + +if __name__ == "__main__": + s1 = CommandSet('a') + print "s1 =", s1 + s2 = CommandSet(['a', 'b']) + print "s2 =", s2 + s3 = s2 + 'c' + print "s3 =", s3 + s4 = 'd' + s3 + print "s4 =", s4 + s5 = s4 - 'b' + print "s5 =", s5 + s6 = ['a', 'b', 'c', 'd', 'e', 'f'] - s5 + print "s6 =", s6 diff --git a/GUI/Generic/GTasks.py b/GUI/Generic/GTasks.py new file mode 100644 index 0000000..2582c77 --- /dev/null +++ b/GUI/Generic/GTasks.py @@ -0,0 +1,37 @@ +# +# PyGUI - Tasks - Generic +# + +from GUI.Properties import Properties, overridable_property + +class Task(Properties): + """A Task represents an action to be performed after a specified + time interval, either once or repeatedly. + + Constructor: + Task(proc, interval, repeat = False, start = True) + Creates a task to call the given proc, which should be + a callable object of no arguments, after the specified + interval in seconds from the time the task is scheduled. + If repeat is true, the task will be automatically re-scheduled + each time the proc is called. If start is true, the task will be + automatically scheduled upon creation; otherwise the start() + method must be called to schedule the task. + """ + + interval = overridable_property('interval', "Time in seconds between firings") + repeat = overridable_property('repeat', "Whether to fire repeatedly or once only") + + def __del__(self): + self.stop() + + scheduled = overridable_property('scheduled', + "True if the task is currently scheduled. Read-only.") + + def start(self): + """Schedule the task if it is not already scheduled.""" + raise NotImplementedError("GUI.Task.start") + + def stop(self): + """Unschedules the task if it is currently scheduled.""" + raise NotImplementedError("GUI.Task.stop") diff --git a/GUI/Generic/GTextEditors.py b/GUI/Generic/GTextEditors.py new file mode 100644 index 0000000..58ec145 --- /dev/null +++ b/GUI/Generic/GTextEditors.py @@ -0,0 +1,34 @@ +# +# Python GUI - Text Editor - Generic +# + +from GUI.Properties import overridable_property +from GUI import Component +from GUI import EditCmdHandler +from GUI.Printing import Printable + +class TextEditor(Component, EditCmdHandler, Printable): + """A component for editing substantial amounts of text. The text is + kept internally to the component and cannot be shared between views.""" + + text = overridable_property('text', "The contents as a string.") + text_length = overridable_property('text_length', "Number of characters in the text.") + selection = overridable_property('selection', "Range of text selected.") + font = overridable_property('font') + tab_spacing = overridable_property('tab_spacing', "Distance between tab stops") + + def setup_menus(self, m): + Component.setup_menus(self, m) + EditCmdHandler.setup_menus(self, m) + Printable.setup_menus(self, m) + + def key_down(self, e): + if e.key == 'enter': + self.pass_to_next_handler('key_down', e) + else: + Component.key_down(self, e) + + def print_view(self, page_setup): + from TextEditorPrinting import TextEditorPrintView + view = TextEditorPrintView(self, page_setup) + view.print_view(page_setup) diff --git a/GUI/Generic/GTextFields.py b/GUI/Generic/GTextFields.py new file mode 100644 index 0000000..8314920 --- /dev/null +++ b/GUI/Generic/GTextFields.py @@ -0,0 +1,76 @@ +# +# Python GUI - Text fields - Generic +# + +from GUI.Properties import overridable_property +from GUI.Actions import ActionBase, action_property +from GUI import application +from GUI import Control +from GUI import EditCmdHandler + +class TextField(Control, ActionBase, EditCmdHandler): + """A control for entering and editing small amounts of text.""" + + text = overridable_property('text') + selection = overridable_property('selection', "Range of text selected.") + multiline = overridable_property('multiline', "Multiple text lines allowed.") + password = overridable_property('password', "Display characters obfuscated.") + enter_action = action_property('enter_action', "Action to be performed " + "when the Return or Enter key is pressed.") + escape_action = action_property('escape_action', "Action to be performed " + "when the Escape key is pressed.") + + _may_be_password = True + + #_tabbable = True + _default_tab_stop = True + _user_tab_stop_override = False + _enter_action = 'do_default_action' + _escape_action = 'do_cancel_action' + + _intercept_tab_key = True + + def __init__(self, **kwds): + self._multiline = kwds.pop('multiline') + Control.__init__(self, **kwds) + + def get_multiline(self): + return self._multiline + + def key_down(self, event): + #print "GTextField.key_down for", self ### + c = event.char + if c == '\r': + if event.key == 'enter' or not self._multiline: + self.do_enter_action() + return + if c == '\x1b': + self.do_escape_action() + return + if c == '\t': + if self._intercept_tab_key: + self.pass_event_to_next_handler(event) + return + Control.key_down(self, event) + + def setup_menus(self, m): + Control.setup_menus(self, m) + EditCmdHandler.setup_menus(self, m) + + def do_enter_action(self): + self.do_named_action('enter_action') + + def do_escape_action(self): + self.do_named_action('escape_action') + + def get_text_length(self): + # Implementations can override this if they have a more + # efficient way of getting the text length. + return len(self.text) + + def get_value(self): + return self.text + + def set_value(self, x): + self.text = x + diff --git a/GUI/Generic/GUtils.py b/GUI/Generic/GUtils.py new file mode 100644 index 0000000..768eab7 --- /dev/null +++ b/GUI/Generic/GUtils.py @@ -0,0 +1,14 @@ +#-------------------------------------------------------------------------- +# +# PyGUI - Utilities - Generic +# +#-------------------------------------------------------------------------- + +def splitdict(src, *names, **defaults): + result = {} + for name in names: + if name in src: + result[name] = src.pop(name) + for name, default in defaults.iteritems(): + result[name] = src.pop(name, default) + return result diff --git a/GUI/Generic/GViewBases.py b/GUI/Generic/GViewBases.py new file mode 100644 index 0000000..0274840 --- /dev/null +++ b/GUI/Generic/GViewBases.py @@ -0,0 +1,135 @@ +# +# Python GUI - View Base - Generic +# + +from GUI.Properties import overridable_property + +class ViewBase(object): + """ViewBase is an abstract base class for user-defined views. + It provides facilities for handling mouse and keyboard events + and associating the view with one or more models, and default + behaviour for responding to changes in the models.""" + + models = overridable_property('models', + "List of Models being observed. Do not modify directly.") + + model = overridable_property('model', + "Convenience property for views which observe only one Model.") + + cursor = overridable_property('cursor', + "The cursor to display over the view.") + + # _models [Model] + + _cursor = None + + def __init__(self): + self._models = [] + + def destroy(self): + #print "GViewBase.destroy:", self ### + for m in self._models[:]: + #print "GViewBase.destroy: removing model", m ### + self.remove_model(m) + + def setup_menus(self, m): + pass + + # + # Getting properties + # + + def get_model(self): + models = self._models + if models: + return self._models[0] + else: + return None + + def get_models(self): + return self._models + + # + # Setting properties + # + + def set_model(self, new_model): + models = self._models + if not (len(models) == 1 and models[0] == new_model): + for old_model in models[:]: + self.remove_model(old_model) + if new_model: + self.add_model(new_model) + + # + # Model association + # + + def add_model(self, model): + """Add the given Model to the set of models being observed.""" + if model not in self._models: + self._models.append(model) + model.add_view(self) + self.model_added(model) + + def remove_model(self, model): + """Remove the given Model from the set of models being observed.""" + if model in self._models: + self._models.remove(model) + model.remove_view(self) + self.model_removed(model) + + def model_added(self, model): + """Called after a model has been added to the view.""" + pass + + def model_removed(self, model): + """Called after a model has been removed from the view.""" + pass + + # + # Input event handling + # + + def track_mouse(self): + """Following a mouse_down event, returns an iterator which can be used + to track the movements of the mouse until the mouse is released. + Each call to the iterator's next() method returns a mouse_drag + event, except for the last one, which returns a mouse_up event.""" + raise NotImplementedError + + def targeted(self): + """Called when the component becomes the target within its Window.""" + pass + + def untargeted(self): + """Called when the component ceases to be the target within its Window.""" + pass + + # + # Cursors + # + + def get_cursor(self, x): + return self._cursor + + def set_cursor(self, x): + self._cursor = x + self._cursor_changed() + + # + # Callbacks + # + + def model_changed(self, model, *args, **kwds): + """Default method called by the attached Model's notify_views + method. Default is to invalidate the whole view.""" + self.invalidate() + + def model_destroyed(self, model): + """Called when an attached model is destroyed. Default is to + destroy the window containing this view.""" + win = self.window + if win: + win.destroy() + diff --git a/GUI/Generic/GViews.py b/GUI/Generic/GViews.py new file mode 100644 index 0000000..87e136c --- /dev/null +++ b/GUI/Generic/GViews.py @@ -0,0 +1,14 @@ +# +# Python GUI - Views - Generic +# + +from GUI.Properties import overridable_property +from GUI.Geometry import add_pt, sub_pt, rect_sized +from GUI import DrawableContainer + +class View(DrawableContainer): + """A View is a 2D drawing area having its own coordinate + system and clipping area.""" + + _default_size = (100, 100) + diff --git a/GUI/Generic/GWindows.py b/GUI/Generic/GWindows.py new file mode 100644 index 0000000..cf03f79 --- /dev/null +++ b/GUI/Generic/GWindows.py @@ -0,0 +1,257 @@ +# +# Python GUI - Windows - Generic +# + +import Exceptions +from GUI.Properties import overridable_property +from GUI import Container +from GUI import application + +class Window(Container): + """Top-level Container.""" + + menus = overridable_property('menus', "Menus to be available when this window is active.") + document = overridable_property('document', "Document with which this window is associated.") + title = overridable_property('title', "Title of the window.") + auto_position = overridable_property('auto_position', "Whether to position automatically when first shown.") + target = overridable_property('target', "Current target for key events and menu messages.") + tab_chain = overridable_property('tab_chain', "List of subcomponents in tabbing order.") + visible = overridable_property('visible', "Whether the window is currently shown.") + + keeps_document_open = True + + _default_width = 200 + _default_height = 200 + _modal_styles = ('modal_dialog', 'alert') + _dialog_styles = ('nonmodal_dialog', 'modal_dialog', 'alert') + + _menus = [] + _document = None + _closable = 0 + _auto_position = True + _tab_chain = None + + def __init__(self, style = 'standard', closable = None, **kwds): + if closable is None: + raise Exceptions.InternalError( + "'closable' parameter unspecified in call to generic Window.__init__") + Container.__init__(self, **kwds) + self._style = style + self._dialog_style = style.find('dialog') >= 0 + self._closable = closable + application()._add_window(self) + + def destroy(self): + """Detaches the window from document and application and removes it + from the screen.""" + self.set_document(None) + application()._remove_window(self) + Container.destroy(self) + + # + # Message handling + # + + def next_handler(self): + if self._document: + return self._document + else: + return application() + + def dispatch(self, message, *args): + self.target.handle(message, *args) + + # + # Menus + # + + def get_menus(self): + return self._menus + + def set_menus(self, x): + self._menus = x + + # + # Document association + # + + def get_document(self): + return self._document + + def set_document(self, x): + if self._document != x: + if self._document: + self._document._windows.remove(self) + self._document = x + if self._document: + self._document._windows.append(self) + self.update_title() + + # + # Title + # + + def update_title(self): + """Update the window's title after a change in its document's title.""" + doc = self._document + if doc: + self.set_title(doc.title) + + # + # Showing and Positioning + # + + def get_auto_position(self): + return self._auto_position + + def set_auto_position(self, v): + self._auto_position = v + + def center(self): + """Position the window in the centre of the screen.""" + print "GWindow.center" ### + sl, st, sr, sb = self._screen_rect() + w, h = self.size + l = (sr - sl - w) // 2 + t = (sb - st - h) // 2 + self.position = (l, t) + + def centre(self): + self.center() + + def show(self): + if self._auto_position: + if self._style == 'standard': + self._stagger() + else: + self.center() + self._auto_position = False + self._show() + + def _stagger(self): + pass + + def _show(self): + self.visible = True + + def hide(self): + self.visible = False + + # + # Menu commands + # + + def setup_menus(self, m): + Container.setup_menus(self, m) + app = application() + if self._closable: + m.close_cmd.enabled = 1 + + def close_cmd(self): + """If this window is the only window belonging to a document + whose keeps_document_open attribute is true, then close the + document, else destroy the window.""" +# app = application() +# if not app._may_close_a_window(): +# #print "GWindow.close_cmd: Quitting the application" ### +# app.quit_cmd() +# else: + doc = self._document + n = 0 + if doc: + for win in doc._windows: + if win is not self and win.keeps_document_open: + n += 1 + if doc and n == 0: + doc.close_cmd() + else: + self.destroy() + + # + # Tabbing + # + + def get_tab_chain(self): + chain = self._tab_chain + if chain is None: + chain = [] + self._build_tab_chain(chain) + self._tab_chain = chain + #print "Window.get_tab_chain:", chain ### + return chain + + def _invalidate_tab_chain(self): + self._tab_chain = None + + def _tab_to_next(self): + self._tab_to(1) + + def _tab_to_prev(self): + self._tab_to(-1) + + def _tab_to(self, direction): + #print "GWindow._tab_to:", direction ### + chain = self.tab_chain + if chain: + old_target = application().target + new_target = None + n = len(chain) + try: + i = chain.index(old_target) + except ValueError: + if direction > 0: + i = -1 + else: + i = n + k = n + while k: + k -= 1 + i = (i + direction) % n + comp = chain[i] + if comp._is_targetable(): + new_target = comp + break + if new_target: + if old_target: + old_target._tab_out() + new_target._tab_in() + + def key_down(self, event): + #print "GWindow.key_down:", event ### + if self._generic_tabbing and event.char == '\t': + #print "GWindow.key_down: doing generic tabbing" ### + if event.shift: + self._tab_to_prev() + else: + self._tab_to_next() + else: + Container.key_down(self, event) + +# def _default_key_event(self, event): +# #print "GWindow._default_key_event" ### +# self.pass_event_to_next_handler(event) + + # + # Other + # + + def get_window(self): + return self + + def first_dispatcher(self): + return self + + def _document_needs_saving(self, state): + pass + + def modal_event_loop(self): + """Loop reading and handling events for the given window until + exit_event_loop() is called. Interaction with other windows is prevented + (although enabled application-wide menu commands can be used).""" + # Implementations can override this together with exit_modal_event_loop() + # to implement modal event loops in a different way. + application()._event_loop(self) + + def exit_modal_event_loop(self): + # Cause the current call to modal_event_loop() to exit. + application()._exit_event_loop() diff --git a/GUI/Generic/Globals.py b/GUI/Generic/Globals.py new file mode 100644 index 0000000..e822339 --- /dev/null +++ b/GUI/Generic/Globals.py @@ -0,0 +1,26 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Generic - Global variables and functions +# +#-------------------------------------------------------------------- + +import os, sys + +_main_file_name = os.path.basename(sys.argv[0]) +application_name = os.path.splitext(_main_file_name)[0] + +_application = None + +def application(): + """Returns the global Application object. Creates a default one if needed.""" + global _application + print _application + if not _application: + from GUI import Application + _application = Application() + return _application + +def run(): + """Runs the application, retaining control until the application is quit.""" + application().run() + diff --git a/GUI/Generic/Grid.py b/GUI/Generic/Grid.py new file mode 100644 index 0000000..67bdb92 --- /dev/null +++ b/GUI/Generic/Grid.py @@ -0,0 +1,73 @@ +#--------------------------------------------------------------------------- +# +# PyGUI - Grid layout component - Generic +# +#--------------------------------------------------------------------------- + +from LayoutUtils import equalize_components +from GUI import Frame, export + +class Grid(Frame): + + def __init__(self, items, row_spacing = 5, column_spacing = 10, + align = 'l', equalize = '', expand_row = None, expand_column = None, + padding = (0, 0), **kwds): + Frame.__init__(self) + hpad, vpad = padding + num_rows = len(items) + num_cols = max([len(row) for row in items]) + col_widths = [0] * num_cols + row_heights = [0] * num_rows + for i, row in enumerate(items): + for j, item in enumerate(row): + if item: + row_heights[i] = max(row_heights[i], item.height) + col_widths[j] = max(col_widths[j], item.width) + tot_width = 0 + row_top = 0 + row_gap = 0 + vanchor = 't' + for i, row in enumerate(items): + row_height = row_heights[i] + row_top += row_gap + col_left = 0 + col_gap = 0 + hanchor = 'l' + if i == expand_row: + vanchor = 'tb' + for j, item in enumerate(row): + col_width = col_widths[j] + col_left += col_gap + if item: + if 'l' in align: + x = 0 + elif 'r' in align: + x = col_width - item.width + else: + x = (col_width - item.width) // 2 + if 't' in align: + y = 0 + elif 'b' in align: + y = row_height - item.height + else: + y = (row_height - item.height) // 2 + item.position = (hpad + col_left + x, vpad + row_top + y) + if j == expand_column: + item.anchor = 'lr' + vanchor + else: + item.anchor = hanchor + vanchor + self.add(item) + if j == expand_column: + hanchor = 'r' + col_left += col_width + col_gap = column_spacing + tot_width = max(tot_width, col_left) + if i == expand_row: + vanchor = 'b' + row_top += row_height + row_gap = row_spacing + tot_height = row_top + self.size = (tot_width + 2 * hpad, tot_height + 2 * vpad) + self.set(**kwds) + +export(Grid) diff --git a/GUI/Generic/GridView.py b/GUI/Generic/GridView.py new file mode 100644 index 0000000..c201f90 --- /dev/null +++ b/GUI/Generic/GridView.py @@ -0,0 +1,116 @@ +#-------------------------------------------------------------------------- +# +# PyGUI - Grid View - Generic +# +#-------------------------------------------------------------------------- + +from GUI import export +from GUI.Properties import overridable_property +from GUI import ScrollableView, rgb + +class GridView(ScrollableView): + """A ScrollableView consisting of a grid of equal-sized cells.""" + + num_columns = overridable_property('num_columns', + "Width of the view in columns") + + num_rows = overridable_property('num_rows', + "Height of the view in rows") + + cell_size = overridable_property('cell_size', + "Size of each cell") + + backcolor = overridable_property('backcolor', + "Background fill colour") + + _cell_size = (32, 32) + _num_rows = 0 + _num_columns = 0 + _backcolor = rgb(1, 1, 1, 1) + + def __init__(self, num_rows, num_columns, cell_size, **kwds): + ScrollableView.__init__(self) + self._num_rows = num_rows + self._num_columns = num_columns + self._cell_size = cell_size + self._update_extent() + self.set(**kwds) + + def get_cell_size(self): + return self._cell_size + + def set_cell_size(self, x): + self._cell_size = x + self._update_extent() + + def get_num_rows(self): + return self._num_rows + + def set_num_rows(self, x): + self._num_rows = x + self._update_extent() + + def get_num_columns(self): + return self._num_columns + + def set_num_columns(self, x): + self._num_columns = x + self._update_extent() + + def _update_extent(self): + cw, ch = self._cell_size + nr = self._num_rows + nc = self._num_columns + self.extent = (cw * nc, ch * nr) + + def cell_rect(self, row, col): + w, h = self._cell_size + l = col * w + t = row * h + return (l, t, l + w, t + h) + + def get_backcolor(self): + return self._backcolor + + def set_backcolor(self, x): + self._backcolor = x + + def cell_containing_point(self, p): + x, y = p + cw, ch = self.cell_size + return (int(y // ch), int(x // cw)) + + def draw(self, canvas, update_rect): + canvas.backcolor = self.backcolor + canvas.erase_rect(update_rect) + ul, ut, ur, ub = update_rect + nr = self._num_rows + nc = self._num_columns + cw, ch = self.cell_size + row0 = max(0, int(ut // ch)) + row1 = min(nr, int(ub // ch) + 1) + col0 = max(0, int(ul // cw)) + col1 = min(nc, int(ur // cw) + 1) + row_range = xrange(row0, row1) + col_range = xrange(col0, col1) + for row in row_range: + for col in col_range: + rect = self.cell_rect(row, col) + self.draw_cell(canvas, row, col, rect) + + def draw_cell(self, canvas, row, col, rect): + """Should draw the cell at the given row and colum inside the given rect.""" + pass + + def mouse_down(self, event): + row, col = self.cell_containing_point(event.position) + nr = self._num_rows + nc = self._num_columns + if 0 <= row < nr and 0 <= col < nc: + self.click_cell(row, col, event) + + def click_cell(self, row, col, event): + """Called when a mouse_down event has occured in the indicated cell.""" + pass + +export(GridView) diff --git a/GUI/Generic/LayoutUtils.py b/GUI/Generic/LayoutUtils.py new file mode 100644 index 0000000..6b49662 --- /dev/null +++ b/GUI/Generic/LayoutUtils.py @@ -0,0 +1,18 @@ +#--------------------------------------------------------------------------- +# +# PyGUI - Utilities for use by layout components - Generic +# +#--------------------------------------------------------------------------- + +def equalize_components(items, flags): + if items: + if 'w' in flags: + width = max([item.width for item in items if item]) + for item in items: + if item: + item.width = width + if 'h' in flags: + height = max([item.height for item in items if item]) + for item in items: + if item: + item.height = height diff --git a/GUI/Generic/MenuList.py b/GUI/Generic/MenuList.py new file mode 100644 index 0000000..16892c3 --- /dev/null +++ b/GUI/Generic/MenuList.py @@ -0,0 +1,28 @@ +# +# Python GUI - Menu Lists - Generic +# + +from GUI import export + +class MenuList(list): + """A MenuList is a sequence of Menus with methods for finding + menus and menu items by command.""" + + def menu_with_command(self, cmd): + """Returns the menu containing the given command, or None + if there is no such menu in the list.""" + for menu in self: + if menu.item_with_command(cmd): + return menu + return None + + def item_with_command(self, cmd): + """Returns the menu item having the given command, or None + if there is no such item.""" + for menu in self: + item = menu.item_with_command(cmd) + if item: + return item + return None + +export(MenuList) diff --git a/GUI/Generic/MessageHandler.py b/GUI/Generic/MessageHandler.py new file mode 100644 index 0000000..2407b90 --- /dev/null +++ b/GUI/Generic/MessageHandler.py @@ -0,0 +1,140 @@ +# +# Python GUI - Message handlers - Generic +# + +from GUI import export + +class MessageHandler(object): + """A MessageHandler is an object which can form part of the + message handling hierarchy. This hierarchy is used to handle + keyboard events, menu commands, and action messages generated + by buttons or other components. + + At any given moment, one of the application's windows is the + 'target window' for messages. Within the target window, some + component is designated as the 'target object', or just 'target'. + + Messages are initially delivered to the target object, and + passed up the hierarchy using the handle() method. At each step, + if the object has a method with the same name as the message, it + is called with the message's arguments. Otherwise the message is + passed on to the object determined by the next_handler() method. + Usually this is the object's container, but need not be. + + The become_target() method is used to make a component the target + within its window. The targeted() and untargeted() methods are + called to notify a component when it has become or ceased to be + the target. The is_target() method can be used to test whether a + component is currently the target.""" + + #----- Event handling ----- + + def handle_event_here(self, event): + """Send an event message to this object, ignoring the event if + there is no method to handle it.""" + self.handle_here(event.kind, event) + + def handle_event(self, event): + """Send an event message up the message path until a method + is found to handle it.""" + self.handle(event.kind, event) + + #----- Message handling ----- + + def handle_here(self, message, *args): + """If there is a method with the same name as the message, call + it with the given args. Otherwise, ignore the message.""" + method = getattr(self, message, None) + if method: + method(*args) + + def handle(self, message, *args): + """If there is a method with the same name as the message, call + it with the given args. Otherwise, pass the message up to the + next handler.""" + #print "MessageHandler: handling", message, "for", self ### + method = getattr(self, message, None) + if method: + #print "MessageHandler: calling method from", method.im_func.func_code.co_filename ### + method(*args) + else: + #print "MessageHandler: passing to next handler" ### + self.pass_to_next_handler(message, *args) + + def pass_event_to_next_handler(self, event): + """Pass the given event on to the next handler, if any.""" + self.pass_to_next_handler(event.kind, event) + + def pass_to_next_handler(self, message, *args): + """Pass the given message on to the next handler, if any.""" + next = self.next_handler() + if next: + next.handle(message, *args) + + def next_handler(self): + """Return the object, if any, to which messages not handled + by this object should be passed on.""" + return None + + #----- Default handlers and callbacks ----- + + def _setup_menus(self, m): + self.pass_to_next_handler('_setup_menus', m) + #print "MessageHandler._setup_menus: calling setup_menus for", self ### + self.setup_menus(m) + + def setup_menus(self, m): + """Called before a menu is pulled down, to allow the Component to + enable menu commands that it responds to.""" + pass + + _pass_key_events_to_platform = False + + def _default_key_event(self, event): + #print "MessageHandler._default_key_event for", self ### + #print "...originator =", event._originator ### + if event._originator is self and self._pass_key_events_to_platform: + #print "...passing to platform" ### + event._not_handled = True + else: + self.pass_event_to_next_handler(event) + + def _default_mouse_event(self, event): + event._not_handled = True + + def _event_custom_handled(self, event): + # Give custom event handlers of this component a chance to handle + # the event. If it reaches a default event method of this component, + # the event is not passed to the next handler and false is returned. + # If it is handled by an overridden method or explicitly passed to + # the next handler, true is returned. + event._originator = self + self.handle_event(event) + return not event._not_handled + + def key_down(self, event): + #print "MessageHandler.key_down for", self ### + self._default_key_event(event) + + def key_up(self, event): + self._default_key_event(event) + + def mouse_down(self, event): + self._default_mouse_event(event) + + def mouse_drag(self, event): + self._default_mouse_event(event) + + def mouse_up(self, event): + self._default_mouse_event(event) + + def mouse_move(self, event): + self._default_mouse_event(event) + + def mouse_enter(self, event): + self._default_mouse_event(event) + + def mouse_leave(self, event): + self._default_mouse_event(event) + +export(MessageHandler) diff --git a/GUI/Generic/ModalDialog.py b/GUI/Generic/ModalDialog.py new file mode 100644 index 0000000..c0d7922 --- /dev/null +++ b/GUI/Generic/ModalDialog.py @@ -0,0 +1,42 @@ +# +# Python GUI - Modal Dialogs - Generic +# + +from GUI import application, export +from GUI import Dialog + +class ModalDialog(Dialog): + + def __init__(self, style = 'modal_dialog', **kwds): + Dialog.__init__(self, style = style, **kwds) + + def present(self): + self._result = None + self._dismissed = 0 + self.show() + app = application() + try: + while not self._dismissed: + self.modal_event_loop() + finally: + self.hide() + return self._result + + def dismiss(self, result = 0): + self._result = result + self._dismissed = 1 + self.exit_modal_event_loop() + + def close_cmd(self): + self.dismiss() + + def next_handler(self): + return None + + def ok(self): + self.dismiss(True) + + def cancel(self): + self.dismiss(False) + +export(ModalDialog) diff --git a/GUI/Generic/Model.py b/GUI/Generic/Model.py new file mode 100644 index 0000000..ff69691 --- /dev/null +++ b/GUI/Generic/Model.py @@ -0,0 +1,126 @@ +# +# Python GUI - Models - Generic +# + +import weakref +from GUI import export +from GUI.Properties import Properties, overridable_property + +# List of views for a model is kept separately so that models +# can be pickled without fear of accidentally trying to pickle +# the user interface. + +_model_views = weakref.WeakKeyDictionary() # {Model: [object]} + +class Model(Properties): + """A Model represents an application object which can appear in a View. + Each Model can have any number of Views attached to it. When a Model is + changed, it should notify all of its Views so that they can update + themselves. + + The 'parent' attribute of a Model is treated specially when pickling. + If it refers to an object having a 'pickle_as_parent_model' attribute + whose value is false, the 'parent' attribute is not pickled. This allows + a Model to have a Document as a parent without the Document being pickled + along with the Model. + """ + + views = overridable_property('views', + "List of objects observing this model. Do not modify directly.") + + parent = None # Model + + def __init__(self, parent = None, **kwds): + Properties.__init__(self, **kwds) + if parent: + self.parent = parent + + def destroy(self): + """All views currently observing this model are removed, and their + 'model_destroyed' methods, if any, are called with the model as + an argument.""" + for view in self.views[:]: + self.remove_view(view) + self._call_if_present(view, 'model_destroyed', self) + + # + # Properties + # + + def get_views(self): + views = _model_views.get(self) + if views is None: + views = [] + _model_views[self] = views + return views + + # + # Pickling behaviour + # + + def __getstate__(self): + state = self.__dict__ + parent = self.parent + if not getattr(parent, 'pickle_as_parent_model', True): + state = state.copy() + del state['parent'] + return state + + def __setstate__(self, state): + self.__dict__.update(state) + + # + # Adding and removing views + # + + def add_view(self, view): + """Add the given object as an observer of this model. The view will + typically be a View subclass, but need not be. If the view is not + already an observer of this model and defines an 'add_model' method, + this method is called with the model as an argument.""" + views = self.views + if view not in views: + views.append(view) + self._call_if_present(view, 'add_model', self) + + def remove_view(self, view): + """If the given object is currently an observer of this model, it + is removed, and if it defines a 'remove_model' method, this method + is called with the model as an argument.""" + views = self.views + if view in views: + views.remove(view) + self._call_if_present(view, 'remove_model', self) + + # + # View notification + # + + def notify_views(self, message = 'model_changed', *args, **kwds): + """For each observer, if the observer defines a method with the name of the + message, call it with the given arguments. Otherwise, if it defines a + method called 'model_changed', call it with no arguments. Otherwise, + do nothing for that observer.""" + for view in self.views: + if not self._call_if_present(view, message, self, *args, **kwds): + self._call_if_present(view, 'model_changed', self) + + def _call_if_present(self, obj, method_name, *args, **kwds): + method = getattr(obj, method_name, None) + if method: + method(*args, **kwds) + return 1 + else: + return 0 + + # + # Marking as changed + # + + def changed(self): + "Mark the containing Document as changed." + parent = self.parent + if parent: + parent.changed() + +export(Model) diff --git a/GUI/Generic/PaletteView.py b/GUI/Generic/PaletteView.py new file mode 100644 index 0000000..9387afa --- /dev/null +++ b/GUI/Generic/PaletteView.py @@ -0,0 +1,144 @@ +#-------------------------------------------------------------------------- +# +# PyGUI - Palette View - Generic +# +#-------------------------------------------------------------------------- + +from GUI import export +from GUI.Properties import overridable_property +from GUI import StdColors, GridView +from GUI.GUtils import splitdict + +class PaletteView(GridView): + """A GridView whose cells are identified by a linear index from + left to right and top to bottom. Also provides support for + highlighting one or more selected cells.""" + + num_items = overridable_property('num_items', + "Total number of items") + + items_per_row = overridable_property('items_per_row', + "Number of items displayed in one row") + + highlight_style = overridable_property('highlight_style', + "Style of selection highlighting") + + highlight_color = overridable_property('highlight_color', + "Color of selection highlighting") + + highlight_thickness = overridable_property('highlight_thickness', + "Width of selection highlighting for 'frame' highlight mode") + + _highlight_style = 'fill' + _highlight_color = StdColors.selection_backcolor + _highlight_thickness = 4 + + def __init__(self, num_items, items_per_row, cell_size, + scrolling = '', **kwds): + base_kwds = splitdict(kwds, 'border', scrolling = '') + GridView.__init__(self, num_rows = 0, num_columns = 0, + cell_size = cell_size, **base_kwds) + self._num_items = num_items + self._items_per_row = items_per_row + self._update_num_rows_and_columns() + ew, eh = self.extent + if not self.hscrolling: + self.content_width = ew + if not self.vscrolling: + self.content_height = eh + self.set(**kwds) + + def get_num_items(self): + return self._num_items + + def set_num_items(self, n): + self._num_items = n + self._update_num_rows_and_columns() + + def get_items_per_row(self): + return self.num_columns + + def set_items_per_row(self, n): + self._items_per_row = n + self._update_num_rows_and_columns() + + def _update_num_rows_and_columns(self): + nc = self._items_per_row + nr = (self._num_items + nc - 1) // nc + self._num_columns = nc + self._num_rows = nr + self._update_extent() + + def get_highlight_style(self): + return self._highlight_style + + def set_highlight_style(self, x): + self._highlight_style = x + + def get_highlight_color(self): + return self._highlight_color + + def set_highlight_color(self, x): + self._highlight_color = x + + def get_highlight_color(self): + return self._highlight_color + + def set_highlight_color(self, x): + self._highlight_color = x + + def item_no_of_cell(self, row, col): + i = row * self._items_per_row + col + if 0 <= i < self._num_items: + return i + + def cell_of_item_no(self, item_no): + if 0 <= item_no < self._num_items: + return divmod(item_no, self._items_per_row) + + def item_rect(self, item_no): + cell = self.cell_of_item_no(item_no) + if cell: + return self.cell_rect(*cell) + + def draw_cell(self, canvas, row, col, rect): + i = self.item_no_of_cell(row, col) + if i is not None: + highlight = self.item_is_selected(i) + self.draw_item_and_highlight(canvas, i, rect, highlight) + + def draw_item_and_highlight(self, canvas, item_no, rect, highlight): + """Draw the specified item, with selection highlighting if highlight + is true.""" + if highlight: + style = self.highlight_style + if style: + canvas.gsave() + if style == 'fill': + canvas.fillcolor = self.highlight_color + canvas.fill_rect(rect) + else: + canvas.pencolor = self.highlight_color + canvas.pensize = self.highlight_thickness + canvas.frame_rect(rect) + canvas.grestore() + self.draw_item(canvas, item_no, rect) + + def draw_item(self, canvas, item_no, rect): + """Should draw the specified item in the given rect.""" + pass + + def click_cell(self, row, col, event): + i = self.item_no_of_cell(row, col) + if i is not None: + self.click_item(i, event) + + def click_item(self, item_no, event): + """Called when a mouse_down event has occurred in the indicated item.""" + pass + + def item_is_selected(self, item_no): + """Should return true if the indicated item is to be drawn highlighted.""" + return False + +export(PaletteView) diff --git a/GUI/Generic/Picture.py b/GUI/Generic/Picture.py new file mode 100644 index 0000000..2a58b65 --- /dev/null +++ b/GUI/Generic/Picture.py @@ -0,0 +1,37 @@ +# +# Python GUI - Picture class - Generic +# + +from GUI import export +from GUI.Properties import overridable_property +from GUI import View + +class Picture(View): + + image = overridable_property('image', "The image to display") + + _image = None + + def __init__(self, image = None, file = None, **kwds): + if file: + from Images import Image + image = Image(file) + View.__init__(self, **kwds) + if image: + self.size = image.size + self._image = image + + def get_image(self): + return self._image + + def set_image(self, x): + self._image = x + self.invalidate() + + def draw(self, canvas, rect): + image = self._image + if image: + w, h = self.size + image.draw(canvas, image.bounds, (0, 0, w, h)) + +export(Picture) diff --git a/GUI/Generic/Properties.py b/GUI/Generic/Properties.py new file mode 100644 index 0000000..5fa0811 --- /dev/null +++ b/GUI/Generic/Properties.py @@ -0,0 +1,42 @@ +#------------------------------------------------------------------------------ +# +# Python GUI - Properties - Generic +# +#------------------------------------------------------------------------------ + +class Properties(object): + """ + This class implements the standard interface for initialising + properties using keyword arguments. + """ + + def __init__(self, **kw): + "Properties(name=value, ...) passes the given arguments to the set() method." + self.set(**kw) + + def set(self, **kw): + """set(name=value, ...) sets property values according to the given + keyword arguments. Will only set attributes for which a descriptor exists.""" + cls = self.__class__ + for name, value in kw.iteritems(): + try: + s = getattr(cls, name).__set__ + except AttributeError: + raise TypeError("%s object has no writable property %r" % ( + self.__class__.__name__, name)) + s(self, value) + +#------------------------------------------------------------------------------ + +def overridable_property(name, doc = None): + """Creates a property which calls methods get_xxx and set_xxx of + the underlying object to get and set the property value, so that + the property's behaviour may be easily overridden by subclasses.""" + + getter_name = intern('get_' + name) + setter_name = intern('set_' + name) + return property( + lambda self: getattr(self, getter_name)(), + lambda self, value: getattr(self, setter_name)(value), + None, + doc) diff --git a/GUI/Generic/Resources.py b/GUI/Generic/Resources.py new file mode 100644 index 0000000..91d570d --- /dev/null +++ b/GUI/Generic/Resources.py @@ -0,0 +1,88 @@ +# +# PyGUI - Resources - Generic +# + +import os + +resource_path = [] +resource_cache = {} + +class ResourceNotFoundError(ValueError): + + def __init__(self, name, type, path): + name = _append_type(name, type) + ValueError.__init__(self, "Resource %r not found in %s" % (name, path)) + +def _append_type(name, type): + if type: + name = "%s.%s" % (os.path.splitext(name)[0], type) + return name + +def _add_directory_path(dir, up = 0): + # Add the given directory to the resource path if it exists. + dir = os.path.abspath(dir) + while up > 0: + dir = os.path.dirname(dir) + up -= 1 + resdir = os.path.join(dir, "Resources") + #print "GUI.Resources: Checking for directory", repr(resdir) ### + if os.path.isdir(resdir): + resource_path.insert(0, resdir) + +def _add_file_path(file, up = 0): + # Add the directory containing the given file to the resource path. + #print "GUI.Resources: Adding path for file", repr(file) ### + dir = os.path.dirname(os.path.abspath(file)) + _add_directory_path(dir, up) + +def _add_module_path(module, up = 0): + # Add the directory containing the given module to the resource path. + if hasattr(module, '__file__'): + _add_file_path(module.__file__, up) + +def lookup_resource(name, type = None): + """ + Return the full pathname of a resource given its relative name + using '/' as a directory separator. If a type is specified, any + dot-suffix on the name is replaced with '.type'. Returns None if + no matching file is found on the resource search path. + """ + name = _append_type(name, type) + relpath = os.path.join(*name.split("/")) + for dir in resource_path: + path = os.path.join(dir, relpath) + if os.path.exists(path): + return path + return None + +def find_resource(name, type = None): + """ + Returns the full pathname of a resource as per lookup_resource(), but + raises ResourceNotFoundError if the resource is not found. + """ + path = lookup_resource(name, type) + if not path: + raise ResourceNotFoundError(name, type, resource_path) + return path + +def get_resource(loader, name, type = None, default = None, **kwds): + """ + Find a resource and load it using the specified loader function. + The loader is called as: loader(path, **kwds) where path is the full + pathname of the resource. The loaded resource is cached, and subsequent + calls referencing the same resource will return the cached value. + If the resource is not found, the specified default is returned if any, + otherwise ResourceNotFoundError is raised. + """ + path = lookup_resource(name, type) + if path: + result = resource_cache.get(path) + if result is None: + result = loader(path, **kwds) + resource_cache[path] = result + else: + if default is not None: + result = default + else: + raise ResourceNotFoundError(name, type, resource_path) + return result diff --git a/GUI/Generic/Row.py b/GUI/Generic/Row.py new file mode 100644 index 0000000..25b6fc3 --- /dev/null +++ b/GUI/Generic/Row.py @@ -0,0 +1,51 @@ +#--------------------------------------------------------------------------- +# +# PyGUI - Row layout component - Generic +# +#--------------------------------------------------------------------------- + +from LayoutUtils import equalize_components +from GUI import Frame, export + +class Row(Frame): + + def __init__(self, items, spacing = 10, align = 'c', equalize = '', + expand = None, padding = (0, 0), **kwds): + Frame.__init__(self) + hpad, vpad = padding + if expand is not None and not isinstance(expand, int): + expand = items.index(expand) + equalize_components(items, equalize) + height = 0 + for item in items: + if item: + height = max(height, item.height) + x = hpad + gap = 0 + hanchor = 'l' + vanchor = align + for i, item in enumerate(items): + x += gap; + if item: + if 't' in align: + y = 0 + if 'b' in align: + item.height = height + elif align == 'b': + y = height - item.height + else: + y = (height - item.height) // 2 + item.position = (x, y + vpad) + if i == expand: + item.anchor = 'lr' + vanchor + else: + item.anchor = hanchor + vanchor + x += item.width; + if i == expand: + hanchor = 'r' + gap = spacing + self.size = (x + hpad, height + 2 * vpad) + self.add(items) + self.set(**kwds) + +export(Row) diff --git a/GUI/Generic/StdButtons.py b/GUI/Generic/StdButtons.py new file mode 100644 index 0000000..f448461 --- /dev/null +++ b/GUI/Generic/StdButtons.py @@ -0,0 +1,19 @@ +# +# Python GUI - Standard Buttons +# + +from GUI import Button + +class DefaultButton(Button): + + def __init__(self, title = "OK", **kwds): + kwds.setdefault('style', 'default') + kwds.setdefault('action', 'do_default_action') + Button.__init__(self, title = title, **kwds) + +class CancelButton(Button): + + def __init__(self, title = "Cancel", **kwds): + kwds.setdefault('style', 'cancel') + kwds.setdefault('action', 'do_cancel_action') + Button.__init__(self, title = title, **kwds) diff --git a/GUI/Generic/StdColors.py b/GUI/Generic/StdColors.py new file mode 100644 index 0000000..f7701b4 --- /dev/null +++ b/GUI/Generic/StdColors.py @@ -0,0 +1,18 @@ +# +# Python GUI - Standard Colors - Generic +# + +from GUI.Colors import rgb, selection_forecolor, selection_backcolor + +black = rgb(0, 0, 0) +dark_grey = rgb(0.25, 0.25, 0.25) +grey = rgb(0.5, 0.5, 0.5) +light_grey = rgb(0.75, 0.75, 0.75) +white = rgb(1, 1, 1) +red = rgb(1, 0, 0) +green = rgb(0, 1, 0) +blue = rgb(0, 0, 1) +yellow = rgb(1, 1, 0) +cyan = rgb(0, 1, 1) +magenta = rgb(1, 0, 1) +clear = rgb(0, 0, 0, 0) diff --git a/GUI/Generic/TextEditorPrinting.py b/GUI/Generic/TextEditorPrinting.py new file mode 100644 index 0000000..d3afe19 --- /dev/null +++ b/GUI/Generic/TextEditorPrinting.py @@ -0,0 +1,68 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - TextEditor Printing - Generic +# +#------------------------------------------------------------------------------ + +import re +from GUI import View + +class TextEditorPrintView(View): + + def __init__(self, base_view, page_setup): + print "TextEditorPrintView:" ### + print "...paper_size =", page_setup.paper_size ### + print "...margins =", page_setup.margins ### + print "...page_size =", page_setup.page_size ### + View.__init__(self) + self.base_view = base_view + self.width = page_setup.page_width + self.page_height = page_setup.page_height + self.lay_out_text() + lines_per_page = int(page_setup.page_height / base_view.font.line_height) + self.lines_per_page = lines_per_page + num_lines = len(self.lines) + self.num_pages = (num_lines + lines_per_page - 1) // lines_per_page + self.height = self.num_pages * self.page_height + + def lay_out_text(self): + base_view = self.base_view + font = base_view.font + space_width = font.width(" ") + tab_spacing = base_view.tab_spacing + page_width = self.width + pat = re.compile(r"[ \t]|[^ \t]+") + lines = [] + line = [] + x = 0 + for text_line in base_view.text.splitlines(): + for match in pat.finditer(text_line): + item = match.group() + if item == " ": + item_width = space_width + item = "" + elif item == "\t": + item_width = tab_spacing - x % tab_spacing + item = "" + else: + item_width = font.width(item) + if x + item_width > page_width and x > 0: + lines.append(line); line = []; x = 0 + line.append((x, item)) + x += item_width + lines.append(line); line = []; x = 0 + self.lines = lines + + def draw(self, canvas, page_rect): + l, t, r, b = page_rect + page_no = int(t / self.page_height) + n = self.lines_per_page + i = page_no * n + font = self.base_view.font + y = t + font.ascent + dy = font.line_height + for line in self.lines[i : i + n]: + for x, item in line: + canvas.moveto(x, y) + canvas.show_text(item) + y += dy diff --git a/GUI/Gtk/AlertClasses.py b/GUI/Gtk/AlertClasses.py new file mode 100644 index 0000000..806fb0d --- /dev/null +++ b/GUI/Gtk/AlertClasses.py @@ -0,0 +1,5 @@ +# +# Python GUI - Alerts - Gtk +# + +from GUI.GAlertClasses import Alert, Alert2, Alert3 diff --git a/GUI/Gtk/Application.py b/GUI/Gtk/Application.py new file mode 100644 index 0000000..2fb46d2 --- /dev/null +++ b/GUI/Gtk/Application.py @@ -0,0 +1,143 @@ +# +# Python GUI - Application class - Gtk +# + +import sys +import gtk +from GUI import export +from GUI import application +from GUI.GApplications import Application as GApplication + +class Application(GApplication): + + _in_gtk_main = 0 + + def run(self): + GApplication.run(self) + + def set_menus(self, menu_list): + GApplication.set_menus(self, menu_list) + for window in self._windows: + window._gtk_update_menubar() + +# def handle_events(self): +# #print "Application.handle_events: entering gtk.main" ### +# _call_with_excepthook(gtk.main, gtk.main_quit) +# #print "Application.handle_events: returned from gtk.main" ### + + def handle_next_event(self, modal_window = None): + _call_with_excepthook(gtk.main_iteration) + self._check_for_no_windows() + +# def _quit(self): +# self._quit_flag = True +# gtk.main_quit() + +# def _exit_event_loop(self): +# gtk.main_quit() + + def get_target_window(self): + for window in self._windows: + if window._gtk_outer_widget.has_focus: + return window + return None + + def zero_windows_allowed(self): + return 0 + + def query_clipboard(self): + return _gtk_clipboard.available() + + def get_clipboard(self): + return _gtk_clipboard.get() + + def set_clipboard(self, data): + _gtk_clipboard.set(data) + +#------------------------------------------------------------------------------ + +class GtkClipboard(gtk.Window): + + data = "" + + def __init__(self): + gtk.Window.__init__(self) + self.realize() + self.connect('selection_get', self.selection_get_signalled) + self.connect('selection_received', self.selection_received_signalled) + self.selection_add_target("CLIPBOARD", "STRING", 0) + + def selection_get_signalled(self, w, selection_data, info, time_stamp): + #print "Clipboard.selection_get_signalled" ### + selection_data.set_text(self.data, len(self.data)) + + def selection_received_signalled(self, w, selection_data, info): + #print "Clipboard.selection_received_signalled:", selection_data ### + type = str(selection_data.type) + if type == "STRING": + data = selection_data.get_text() + elif type == "ATOM": + data = selection_data.get_targets() + else: + #print "Clipboard.selection_received_signalled: Unknown type: %r" % type + data = None + #print "...data =", repr(data) ### + self.received_data = data + + def request(self, target, default): + # Get the contents of the clipboard. + #print "Clipboard.request:", target ### + self.received_data = -1 + self.selection_convert("CLIPBOARD", target) + while self.received_data == -1: + gtk.main_iteration() + data = self.received_data + self.received_data = None + if data is None: + data = default + #print "Clipboard.request ->", repr(data) ### + return data + + def available(self): + targets = self.request("TARGETS", ()) + #print "Clipboard.available: targets =", repr(targets) ### + return "STRING" in map(str, targets) + + def get(self): + # Get the contents of the clipboard. + text = self.request("STRING", "") + #print "Clipboard.get ->", repr(text) ### + return text + + def set(self, text): + # Put the given text on the clipboard. + #print "Clipboard.set:", text ### + self.data = text + result = self.selection_owner_set("CLIPBOARD") + #print "...result =", result ### + +#------------------------------------------------------------------------------ + +_gtk_clipboard = GtkClipboard() + +#------------------------------------------------------------------------------ + +def _call_with_excepthook(proc, breakout = None): + # This function arranges for exceptions to be propagated + # across calls to the Gtk event loop functions. + exc_info = [] + def excepthook(*args): + exc_info[:] = args + if breakout: + breakout() + old_excepthook = sys.excepthook + try: + sys.excepthook = excepthook + proc() + finally: + sys.excepthook = old_excepthook + if exc_info: + #print "_call_with_excepthook: raising", exc_info ### + raise exc_info[0], exc_info[1], exc_info[2] + +export(Application) diff --git a/GUI/Gtk/BaseAlert.py b/GUI/Gtk/BaseAlert.py new file mode 100644 index 0000000..d845ef8 --- /dev/null +++ b/GUI/Gtk/BaseAlert.py @@ -0,0 +1,27 @@ +# +# Python GUI - Alert base class - Gtk +# + +import gtk +from GUI import export +from GUI.GBaseAlerts import BaseAlert as GBaseAlert + +_kind_to_gtk_stock_id = { + 'stop': gtk.STOCK_DIALOG_ERROR, + 'caution': gtk.STOCK_DIALOG_WARNING, + 'note': gtk.STOCK_DIALOG_INFO, + 'query': gtk.STOCK_DIALOG_QUESTION, +} + +class BaseAlert(GBaseAlert): + + def _layout_icon(self, kind): + gtk_stock_id = _kind_to_gtk_stock_id[kind] + gtk_icon = gtk.image_new_from_stock(gtk_stock_id, gtk.ICON_SIZE_DIALOG) + gtk_icon.show() + icon_size = gtk_icon.size_request() + icon_width, icon_height = icon_size + self._gtk_inner_widget.put(gtk_icon, self._left_margin, self._top_margin) + return icon_size + +export(BaseAlert) diff --git a/GUI/Gtk/BaseFileDialogs.py b/GUI/Gtk/BaseFileDialogs.py new file mode 100644 index 0000000..fef6f86 --- /dev/null +++ b/GUI/Gtk/BaseFileDialogs.py @@ -0,0 +1,214 @@ +# +# Python GUI - File selection dialogs - Gtk +# + +import os +import gtk +from GUI.Files import FileRef +from GUI.Alerts import confirm +from GUI import application + +#------------------------------------------------------------------ + +class _FileDialog(gtk.FileChooserDialog): + + def __init__(self, ok_label, **kwds): + gtk.FileChooserDialog.__init__(self, **kwds) + self.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) + self.add_button(ok_label, gtk.RESPONSE_ACCEPT) + self.connect('response', self.response) + self.set_default_size(600, 600) + self.set_position(gtk.WIN_POS_CENTER) + + def add_file_type(self, file_type): + suffix = file_type.suffix + if suffix: + filter = gtk.FileFilter() + name = file_type.name + if name: + filter.set_name(name) + filter.add_pattern("*.%s" % suffix) + self.add_filter(filter) + + def present_modally(self): + return self.run() == gtk.RESPONSE_ACCEPT + + def response(self, _, id): + #print "_FileDialog.response:", id ### + if id == gtk.RESPONSE_ACCEPT: + if not self.check(): + self.stop_emission('response') + + def check(self): + return True + +#------------------------------------------------------------------ + +class _SaveFileDialog(_FileDialog): + + def check(self): + path = self.get_filename() + #print "_SaveFileDialog.ok: checking path %r" % path ### + #if path is None: + # return False + if not os.path.exists(path): + return True + else: + result = confirm("Replace existing '%s'?" % os.path.basename(path), + "Cancel", "Replace", cancel = None) + return result == 0 + +#------------------------------------------------------------------ + +def _request_old(prompt, default_dir, file_types, dir, multiple): + + if prompt.endswith(":"): + prompt = prompt[:-1] + if dir: + action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER + else: + action = gtk.FILE_CHOOSER_ACTION_OPEN + dlog = _FileDialog(title = prompt, action = action, + ok_label = gtk.STOCK_OPEN) + dlog.set_select_multiple(multiple) + if file_types: + for file_type in file_types: + dlog.add_file_type(file_type) + if default_dir: + dlog.set_current_folder(default_dir.path) + if dlog.present_modally(): + if multiple: + result = [FileRef(path = path) for path in dlog.get_filenames()] + else: + result = FileRef(path = dlog.get_filename()) + else: + result = None + dlog.destroy() + return result + +#------------------------------------------------------------------ + +#def request_old_file(prompt = "Open File", default_dir = None, +# file_types = None, multiple = False): +# """Present a dialog for selecting an existing file or set of files. +# Returns a FileRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _FileDialog(title = prompt, ok_label = gtk.STOCK_OPEN) +# dlog.set_select_multiple(multiple) +# if file_types: +# for file_type in file_types: +# dlog.add_file_type(file_type) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if dlog.present_modally(): +# if multiple: +# result = [FileRef(path = path) for path in dlog.get_filenames()] +# else: +# result = FileRef(path = dlog.get_filename()) +# else: +# result = None +# dlog.destroy() +# return result + +#------------------------------------------------------------------ + +#def request_old_directory(prompt = "Choose Folder", default_dir = None, +# multiple = False): +# """Present a dialog for selecting an existing directory or set of directories. +# Returns a DirRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _FileDialog(title = prompt, ok_label = gtk.STOCK_OPEN, +# action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) +# dlog.set_select_multiple(multiple) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if dlog.present_modally(): +# if multiple: +# result = [DirRef(path = path) for path in dlog.get_filenames()] +# else: +# result = DirRef(path = dlog.get_filename()) +# else: +# result = None +# dlog.destroy() +# return result + +#------------------------------------------------------------------ + +def _request_new(prompt, default_dir, default_name, file_type, dir): + if dir: + action = gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER + else: + action = gtk.FILE_CHOOSER_ACTION_SAVE + if prompt.endswith(":"): + prompt = prompt[:-1] + dlog = _SaveFileDialog(title = prompt, action = action, + ok_label = gtk.STOCK_SAVE) + if file_type: + dlog.add_file_type(file_type) + if default_dir: + dlog.set_current_folder(default_dir.path) + if default_name: + dlog.set_current_name(default_name) + if dlog.present_modally(): + path = dlog.get_filename() + if file_type: + path = file_type._add_suffix(path) + result = FileRef(path = path) + else: + result = None + dlog.destroy() + return result + +#------------------------------------------------------------------ + +#def request_new_file(prompt = "Save File", default_dir = None, +# default_name = "", file_type = None): +# """Present a dialog requesting a name and location for a new file. +# Returns a FileRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _SaveFileDialog(title = prompt, ok_label = gtk.STOCK_SAVE, +# action = gtk.FILE_CHOOSER_ACTION_SAVE) +# if file_type: +# dlog.add_file_type(file_type) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if default_name: +# dlog.set_current_name(default_name) +# if dlog.present_modally(): +# path = dlog.get_filename() +# if file_type: +# path = file_type._add_suffix(path) +# result = FileRef(path = path) +# else: +# result = None +# dlog.destroy() +# return result + +#------------------------------------------------------------------ + +#def request_new_directory(prompt = "Create Folder", default_dir = None, +# default_name = ""): +# """Present a dialog requesting a name and location for a new directory. +# Returns a FileRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _SaveFileDialog(title = prompt, ok_label = gtk.STOCK_SAVE, +# action = gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if default_name: +# dlog.set_current_name(default_name) +# if dlog.present_modally(): +# path = dlog.get_filename() +# result = FileRef(path = path) +# else: +# result = None +# dlog.destroy() +# return result diff --git a/GUI/Gtk/Button.py b/GUI/Gtk/Button.py new file mode 100644 index 0000000..2c0f62e --- /dev/null +++ b/GUI/Gtk/Button.py @@ -0,0 +1,100 @@ +# +# Python GUI - Buttons - Gtk version +# + +import gtk +from GUI import export +from GUI.StdFonts import system_font +from GUI.GButtons import Button as GButton + +_gtk_extra_hpad = 5 # Amount to add to default width at each end +_gtk_icon_spacing = 2 # Space to leave between icon and label + +class Button(GButton): + + _gtk_icon = None # Icon, when we have one + _style = 'normal' # or 'default' or 'cancel' + + def __init__(self, title = "Button", #style = 'normal', + font = system_font, **kwds): + gtk_label = gtk.Label(title) + gtk_box = gtk.HBox(spacing = _gtk_icon_spacing) + gtk_box.pack_end(gtk_label, fill = True, expand = True) + gtk_alignment = gtk.Alignment(0.5, 0.5, 0.0, 0.0) + hp = _gtk_extra_hpad + gtk_alignment.set_padding(0, 0, hp, hp) + gtk_alignment.add(gtk_box) + gtk_button = gtk.Button() + gtk_button.add(gtk_alignment) + gtk_button.set_focus_on_click(False) + gtk_button.show_all() + w, h = font.text_size(title) + w2 = w + 2 * _gtk_button_hpad + _gtk_icon_width + _gtk_icon_spacing + h2 = max(h + 2 * _gtk_button_vpad, _gtk_default_button_height) + gtk_button.set_size_request(int(round(w2)), int(round(h2))) + self._gtk_box = gtk_box + self._gtk_alignment = gtk_alignment + self._gtk_connect(gtk_button, 'clicked', self._gtk_clicked_signal) + GButton.__init__(self, _gtk_outer = gtk_button, _gtk_title = gtk_label, + font = font, **kwds) + + def _gtk_get_alignment(self): + return self._gtk_alignment.get_property('xalign') + + def _gtk_set_alignment(self, fraction, just): + self._gtk_alignment.set_property('xalign', fraction) + self._gtk_title_widget.set_justify(just) + + def get_style(self): + return self._style + + def set_style(self, new_style): + if self._style <> new_style: + if new_style == 'default': + self._gtk_add_icon(gtk.STOCK_OK) + elif new_style == 'cancel': + self._gtk_add_icon(gtk.STOCK_CANCEL) + else: + self._gtk_remove_icon() + self._style = new_style + + def _gtk_add_icon(self, gtk_stock_id): + gtk_icon = gtk.image_new_from_stock(gtk_stock_id, gtk.ICON_SIZE_BUTTON) + gtk_icon.show() + self._gtk_box.pack_start(gtk_icon) + self._gtk_icon = gtk_icon + + def _gtk_remove_icon(self): + gtk_icon = self._gtk_icon + if gtk_icon: + gtk_icon.destroy() + self._gtk_icon = None + + def activate(self): + """Highlight the button momentarily and then perform its action.""" + self._gtk_outer_widget.activate() + + def _gtk_clicked_signal(self): + self.do_action() + + +def _calc_size_constants(): + global _gtk_icon_width, _gtk_default_button_height + global _gtk_button_hpad, _gtk_button_vpad + gtk_icon = gtk.image_new_from_stock(gtk.STOCK_OK, gtk.ICON_SIZE_BUTTON) + gtk_button = gtk.Button() + gtk_button.add(gtk_icon) + gtk_button.show_all() + icon_width, icon_height = gtk_icon.size_request() + butn_width, butn_height = gtk_button.size_request() + _gtk_icon_width = icon_width + _gtk_default_button_height = butn_height + _gtk_button_hpad = (butn_width - icon_width) / 2 + _gtk_extra_hpad + _gtk_button_vpad = (butn_height - icon_height) / 2 + gtk_button.destroy() + +_calc_size_constants() +del _calc_size_constants + +export(Button) + diff --git a/GUI/Gtk/Canvas.py b/GUI/Gtk/Canvas.py new file mode 100644 index 0000000..f83f3ec --- /dev/null +++ b/GUI/Gtk/Canvas.py @@ -0,0 +1,255 @@ +#-------------------------------------------------------------------- +# +# Python GUI - Canvas - Gtk +# +#-------------------------------------------------------------------- + +from math import sin, cos, pi, floor +from cairo import OPERATOR_OVER, OPERATOR_SOURCE, FILL_RULE_EVEN_ODD +from GUI import export +from GUI.Geometry import sect_rect +from GUI.StdFonts import application_font +from GUI.StdColors import black, white +from GUI.GCanvases import Canvas as GCanvas +from GUI.GCanvasPaths import CanvasPaths as GCanvasPaths + +deg = pi / 180 +twopi = 2 * pi + +#-------------------------------------------------------------------- + +class GState(object): + + pencolor = black + fillcolor = black + textcolor = black + backcolor = white + pensize = 1 + font = application_font + + def __init__(self, clone = None): + if clone: + self.__dict__.update(clone.__dict__) + +#-------------------------------------------------------------------- + +class Canvas(GCanvas, GCanvasPaths): + + def _from_gdk_drawable(cls, gdk_drawable): + return cls(gdk_drawable.cairo_create()) + + _from_gdk_drawable = classmethod(_from_gdk_drawable) + + def _from_cairo_context(cls, ctx): + return cls(ctx) + + _from_cairo_context = classmethod(_from_cairo_context) + + def __init__(self, ctx): + ctx.set_fill_rule(FILL_RULE_EVEN_ODD) + self._gtk_ctx = ctx + self._gstack = [] + self._state = GState() + GCanvas.__init__(self) + GCanvasPaths.__init__(self) + + def get_pencolor(self): + return self._state.pencolor + + def set_pencolor(self, c): + self._state.pencolor = c + + def get_fillcolor(self): + return self._state.fillcolor + + def set_fillcolor(self, c): + self._state.fillcolor = c + + def get_textcolor(self): + return self._state.textcolor + + def set_textcolor(self, c): + self._state.textcolor = c + + def get_backcolor(self): + return self._state.backcolor + + def set_backcolor(self, c): + self._state.backcolor = c + + def get_pensize(self): + return self._state.pensize + + def set_pensize(self, d): + self._state.pensize = d + + def get_font(self): + return self._state.font + + def set_font(self, f): + self._state.font = f + + def get_current_point(self): + return self._gtk_ctx.get_current_point() + + def rectclip(self, r): + l, t, r, b = r + ctx = self._gtk_ctx + ctx.new_path() + ctx.rectangle(l, t, r - l, b - t) + ctx.clip() + + def gsave(self): + old_state = self._state + self._gstack.append(old_state) + self._state = GState(old_state) + self._gtk_ctx.save() + + def grestore(self): + self._state = self._gstack.pop() + self._gtk_ctx.restore() + + def newpath(self): + self._gtk_ctx.new_path() + + def moveto(self, x, y): + self._gtk_ctx.move_to(x, y) + + def rmoveto(self, x, y): + self._gtk_ctx.rel_move_to(x, y) + + def lineto(self, x, y): + self._gtk_ctx.line_to(x, y) + + def rlineto(self, x, y): + self._gtk_ctx.rel_line_to(x, y) + + def curveto(self, p1, p2, p3): + self._gtk_ctx.curve_to(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) + + def rcurveto(self, p1, p2, p3): + self._gtk_ctx.rel_curve_to(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) + + def arc(self, c, r, a0, a1): + self._gtk_ctx.arc(c[0], c[1], r, a0 * deg, a1 * deg) + + def closepath(self): + ctx = self._gtk_ctx + ctx.close_path() + ctx.new_sub_path() + + def clip(self): + self._gtk_ctx.clip_preserve() + + def stroke(self): + state = self._state + ctx = self._gtk_ctx + #ctx.set_source_rgba(*state.pencolor._rgba) + ctx.set_source_color(state.pencolor._gdk_color) + ctx.set_line_width(state.pensize) + ctx.stroke_preserve() + + def fill(self): + ctx = self._gtk_ctx + #ctx.set_source_rgba(*self._state.fillcolor._rgba) + ctx.set_source_color(self._state.fillcolor._gdk_color) + ctx.fill_preserve() + + def erase(self): + ctx = self._gtk_ctx + #ctx.set_source_rgba(*self._state.backcolor._rgba) + ctx.set_source_color(self._state.backcolor._gdk_color) + ctx.set_operator(OPERATOR_SOURCE) + ctx.fill_preserve() + ctx.set_operator(OPERATOR_OVER) + + def show_text(self, text): + font = self._state.font + layout = font._get_pango_layout(text, True) + dx = layout.get_pixel_size()[0] + dy = font.ascent + ctx = self._gtk_ctx + #ctx.set_source_rgba(*self._state.textcolor._rgba) + ctx.set_source_color(self._state.textcolor._gdk_color) + ctx.rel_move_to(0, -dy) + ctx.show_layout(layout) + ctx.rel_move_to(dx, dy) + + def rect(self, rect): + l, t, r, b = rect + self._gtk_ctx.rectangle(l, t, r - l, b - t) + + def oval(self, rect): + l, t, r, b = rect + a = 0.5 * (r - l) + b = 0.5 * (b - t) + ctx = self._gtk_ctx + ctx.new_sub_path() + ctx.save() + ctx.translate(l + a, t + b) + ctx.scale(a, b) + ctx.arc(0, 0, 1, 0, twopi) + ctx.close_path() + ctx.restore() + + def translate(self, dx, dy): + self._gtk_ctx.translate(dx, dy) + return + + def rotate(self, degrees): + self._gtk_ctx.rotate(degrees*pi/180) + return + + def scale(self, xscale, yscale): + self._gtk_ctx.scale(xscale, yscale) + return + +# def _coords(self, x, y): +# x0, y0 = self._origin +# return int(round(x0 + x)), int(round(y0 + y)) + +# def _coords(self, x, y): +# return int(round(x)), int(round(y)) + +# def _rect_coords(self, (l, t, r, b)): +# x0, y0 = self._origin +# l = int(round(x0 + l)) +# t = int(round(y0 + t)) +# r = int(round(x0 + r)) +# b = int(round(y0 + b)) +# return l, t, r - l, b - t + +# def _rect_coords(self, (l, t, r, b)): +# l = int(round(l)) +# t = int(round(t)) +# r = int(round(r)) +# b = int(round(b)) +# return l, t, r - l, b - t + +# def _frame_coords(self, r): +# l, t, w, h = self._rect_coords(r) +# p = self._gdk_gc.line_width +# d = p // 2 +# return ( +# int(floor(l + d)), +# int(floor(t + d)), +# int(floor(w - p)), +# int(floor(h - p))) + +#def _gdk_angles(start_angle, end_angle): +# arc_angle = (end_angle - start_angle) % 360 +# start = int(round(start_angle * 64)) +# arc = int(round((arc_angle) * 64)) +# return -start, -arc + +#def _arc_rect((cx, cy), r): +# return (cx - r, cy - r, cx + r, cy + r) + +#def _arc_endpoint(center, r, a): +# cx, cy = center +# ar = a * deg +# x = int(round(cx + r * cos(ar))) +# y = int(round(cy + r * sin(ar))) +# return x, y + +export(Canvas) diff --git a/GUI/Gtk/CheckBox.py b/GUI/Gtk/CheckBox.py new file mode 100644 index 0000000..ef3a74b --- /dev/null +++ b/GUI/Gtk/CheckBox.py @@ -0,0 +1,56 @@ +# +# Python GUI - Check boxes - Gtk +# + +import gtk +from GUI import export +from GUI.GCheckBoxes import CheckBox as GCheckBox + +class CheckBox(GCheckBox): + + def __init__(self, title = "New Control", **kwds): + gtk_checkbox = gtk.CheckButton(title) + gtk_checkbox.show() + self._gtk_connect(gtk_checkbox, 'clicked', self._gtk_clicked_signal) + self._gtk_inhibit_action = 0 + GCheckBox.__init__(self, _gtk_outer = gtk_checkbox, **kwds) + + def get_on(self): + gtk_checkbox = self._gtk_outer_widget + if gtk_checkbox.get_inconsistent(): + return 'mixed' + else: + return gtk_checkbox.get_active() + + def set_on(self, state): + mixed = state == 'mixed' + if mixed: + if not self._mixed: + raise ValueError("CheckBox state cannot be 'mixed'") + active = False + else: + active = bool(state) + save = self._gtk_inhibit_action + self._gtk_inhibit_action = 1 + try: + gtk_checkbox = self._gtk_outer_widget + gtk_checkbox.set_active(active) + gtk_checkbox.set_inconsistent(mixed) + finally: + self._gtk_inhibit_action = save + + def _gtk_clicked_signal(self): + gtk_checkbox = self._gtk_outer_widget + if not self._gtk_inhibit_action: + if self._auto_toggle: + gtk_checkbox.set_inconsistent(False) + else: + save = self._gtk_inhibit_action + self._gtk_inhibit_action = 1 + try: + gtk_checkbox.set_active(not gtk_checkbox.get_active()) + finally: + self._gtk_inhibit_action = save + self.do_action() + +export(CheckBox) diff --git a/GUI/Gtk/Color.py b/GUI/Gtk/Color.py new file mode 100644 index 0000000..39b15e3 --- /dev/null +++ b/GUI/Gtk/Color.py @@ -0,0 +1,41 @@ +# +# Python GUI - Colors - Gtk +# + +from gtk import gdk +from GUI import export +from GUI.GColors import Color as GColor + +class Color(GColor): + + _alpha = 1.0 + + def _from_gdk_color(cls, _gdk_color): + c = cls.__new__(cls) + c._gdk_color = _gdk_color + return c + + _from_gdk_color = classmethod(_from_gdk_color) + + def __init__(self, red, green, blue, alpha = 1.0): + self._rgba = (red, green, blue, alpha) + gdk_color = gdk.Color() + gdk_color.red = int(red * 65535) + gdk_color.green = int(green * 65535) + gdk_color.blue = int(blue * 65535) + self._gdk_color = gdk_color + self._alpha = alpha + + def get_red(self): + return self._gdk_color.red / 65535.0 + + def get_green(self): + return self._gdk_color.green / 65535.0 + + def get_blue(self): + return self._gdk_color.blue / 65535.0 + + def get_alpha(self): + return self._alpha + +export(Color) diff --git a/GUI/Gtk/Colors.py b/GUI/Gtk/Colors.py new file mode 100644 index 0000000..9fa29dd --- /dev/null +++ b/GUI/Gtk/Colors.py @@ -0,0 +1,13 @@ +# +# Python GUI - Color constants and functions - Gtk +# + +from gtk import Style +from GUI import Color + +rgb = Color + +s = Style() +selection_forecolor = Color._from_gdk_color(s.fg[3]) +selection_backcolor = Color._from_gdk_color(s.bg[3]) + diff --git a/GUI/Gtk/Component.py b/GUI/Gtk/Component.py new file mode 100644 index 0000000..36e76b8 --- /dev/null +++ b/GUI/Gtk/Component.py @@ -0,0 +1,252 @@ +# +# Python GUI - Components - Gtk +# + +import gtk +from gtk import gdk +from GUI import export +from GUI import Event +from GUI.Geometry import sub_pt +from GUI.GComponents import Component as GComponent + +_gdk_events_of_interest = ( + gdk.POINTER_MOTION_MASK | + gdk.BUTTON_MOTION_MASK | + gdk.BUTTON_PRESS_MASK | + gdk.BUTTON_RELEASE_MASK | + gdk.KEY_PRESS_MASK | + gdk.KEY_RELEASE_MASK | + gdk.ENTER_NOTIFY_MASK | + gdk.LEAVE_NOTIFY_MASK | + 0 +) + +_gtk_widget_to_component = {} +_gtk_last_keyval_down = None + +#------------------------------------------------------------------------------ + +class Component(GComponent): + + _pass_key_events_to_platform = True + + def _gtk_find_component(gtk_widget): + while gtk_widget: + component = _gtk_widget_to_component.get(gtk_widget) + if component: + return component + gtk_widget = gtk_widget.get_parent() + return None + + _gtk_find_component = staticmethod(_gtk_find_component) + + def __init__(self, _gtk_outer, _gtk_inner = None, + _gtk_focus = None, _gtk_input = None, **kwds): + self._position = (0, 0) + self._size = _gtk_outer.size_request() + _gtk_inner = _gtk_inner or _gtk_outer + self._gtk_outer_widget = _gtk_outer + self._gtk_inner_widget = _gtk_inner + self._gtk_focus_widget = _gtk_focus + _gtk_widget_to_component[_gtk_outer] = self + self._gtk_connect_input_events(_gtk_input or _gtk_inner) + if _gtk_focus: + _gtk_focus.set_property('can-focus', True) + self._gtk_connect(_gtk_focus, 'focus-in-event', self._gtk_focus_in) + GComponent.__init__(self, **kwds) + + def destroy(self): + gtk_widget = self._gtk_outer_widget + if gtk_widget in _gtk_widget_to_component: + del _gtk_widget_to_component[gtk_widget] + GComponent.destroy(self) + + # + # Properties + # + + def set_width(self, v): + w, h = self.size + self.size = (v, h) + + def set_height(self, v): + w, h = self.size + self.size = (w, v) + + def get_position(self): + return self._position + + def set_position(self, v): + self._position = v + widget = self._gtk_outer_widget + parent = widget.get_parent() + if parent: + parent.move(widget, *v) + + def get_size(self): + return self._size + + def set_size(self, new_size): + w0, h0 = self._size + w1, h1 = new_size + self._gtk_outer_widget.set_size_request(max(int(w1), 1), max(int(h1), 1)) + self._size = new_size + if w0 != w1 or h0 != h1: + self._resized((w1 - w0, h1 - h0)) + + def get_bounds(self): + x, y = self._position + w, h = self.size + return (x, y, x + w, y + h) + + def set_bounds(self, (l, t, r, b)): + self.position = (l, t) + self.size = (r - l, b - t) + +# def get_visible(self): +# return self._gtk_outer_widget.get_property('visible') +# +# def set_visible(self, v): +# self._gtk_outer_widget.set_property('visible', v) + + # + # Message dispatching + # + + def become_target(self): + gtk_focus = self._gtk_focus_widget + if gtk_focus: + gtk_focus.grab_focus() + else: + raise ValueError("%r cannot be targeted" % self) + +# def current_target(self): +# """Find the current target object within the Window containing +# this component. If the component is not contained in a Window, +# the result is undefined.""" +# target = _gtk_find_component(self._gtk_outer_widget.get_focus()) +# if not target: +# target = self.window +# return target + + def is_target(self): + """Return true if this is the current target within the containing + Window. If the component is not contained in a Window, the result + is undefined.""" + gtk_focus = self._gtk_focus_widget + if gtk_focus: + return gtk_focus.get_property('has-focus') + else: + return False + + # + # Internal + # + + def _gtk_connect(self, gtk_widget, signal, handler): + def catch(widget, *args): + try: + handler(*args) + except: + _gtk_exception_in_signal_handler() + gtk_widget.connect(signal, lambda widget, *args: handler(*args)) + + def _gtk_connect_after(self, gtk_widget, signal, handler): + def catch(widget, *args): + try: + handler(*args) + except: + _gtk_exception_in_signal_handler() + gtk_widget.connect_after(signal, lambda widget, *args: handler(*args)) + + def _gtk_focus_in(self, gtk_event): + window = self.window + if window: + old_target = window._target + window._target = self + if old_target and old_target is not self: + old_target._untargeted() + self._targeted() + + def _targeted(self): + pass + + def _untargeted(self): + pass + + def _gtk_connect_input_events(self, gtk_widget): + self._last_mouse_down_time = 0 + self._click_count = 0 + gtk_widget.add_events(_gdk_events_of_interest) + self._gtk_connect(gtk_widget, 'button-press-event', + self._gtk_button_press_event_signal) + self._gtk_connect(gtk_widget, 'motion-notify-event', + self._gtk_motion_notify_event_signal) + self._gtk_connect(gtk_widget, 'button-release-event', + self._gtk_button_release_event_signal) + self._gtk_connect(gtk_widget, 'enter-notify-event', + self._gtk_enter_leave_event_signal) + self._gtk_connect(gtk_widget, 'leave-notify-event', + self._gtk_enter_leave_event_signal) + self._gtk_connect(gtk_widget, 'key-press-event', + self._handle_gtk_key_event) + self._gtk_connect(gtk_widget, 'key-release-event', + self._handle_gtk_key_event) + + def _gtk_button_press_event_signal(self, gtk_event): + if gtk_event.type == gdk.BUTTON_PRESS: # don't want 2BUTTON or 3BUTTON + event = Event._from_gtk_mouse_event(gtk_event) + last_time = self._last_mouse_down_time + this_time = event.time + num_clicks = self._click_count + if this_time - last_time <= 0.25: + num_clicks += 1 + else: + num_clicks = 1 + event.num_clicks = num_clicks + self._click_count = num_clicks + self._last_mouse_down_time = this_time + #print "Component._gtk_button_press_event_signal:" ### + #print event ### + return self._event_custom_handled(event) + + def _gtk_motion_notify_event_signal(self, gtk_event): + event = Event._from_gtk_mouse_event(gtk_event) + self._mouse_event = event + return self._event_custom_handled(event) + + def _gtk_button_release_event_signal(self, gtk_event): + event = Event._from_gtk_mouse_event(gtk_event) + self._mouse_event = event + return self._event_custom_handled(event) + + def _gtk_enter_leave_event_signal(self, gtk_event): + #print "Component._gtk_enter_leave_event_signal:" ### + event = Event._from_gtk_mouse_event(gtk_event) + return self._event_custom_handled(event) + + def _handle_gtk_key_event(self, gtk_event): + """Convert a Gtk key-press or key-release event into an Event + object and pass it up the message path.""" + #print "Component._handle_gtk_key_event for", self ### + global _gtk_last_keyval_down + if Event._gtk_key_event_of_interest(gtk_event): + event = Event._from_gtk_key_event(gtk_event) + if event.kind == 'key_down': + this_keyval = gtk_event.keyval + if _gtk_last_keyval_down == this_keyval: + event.auto = 1 + _gtk_last_keyval_down = this_keyval + else: + _gtk_last_keyval_down = None + #if event.kind == 'key_down': ### + # print event ### + return self._event_custom_handled(event) + +#------------------------------------------------------------------------------ + +def _gtk_exception_in_signal_handler(): + print >>sys.stderr, "---------- Exception in gtk signal handler ----------" + traceback.print_exc() + +export(Component) diff --git a/GUI/Gtk/Container.py b/GUI/Gtk/Container.py new file mode 100644 index 0000000..eac37ed --- /dev/null +++ b/GUI/Gtk/Container.py @@ -0,0 +1,23 @@ +# +# Python GUI - Containers - Gtk version +# + +from gtk import gdk +from GUI import export +from GUI.Geometry import inset_rect +from GUI.GContainers import Container as GContainer + +class Container(GContainer): + # Subclasses must set the inner widget to be a Fixed or Layout + # widget. + + def _add(self, comp): + GContainer._add(self, comp) + x, y = comp._position + self._gtk_inner_widget.put(comp._gtk_outer_widget, int(x), int(y)) + + def _remove(self, comp): + self._gtk_inner_widget.remove(comp._gtk_outer_widget) + GContainer._remove(self, comp) + +export(Container) diff --git a/GUI/Gtk/Control.py b/GUI/Gtk/Control.py new file mode 100644 index 0000000..7d1a2f3 --- /dev/null +++ b/GUI/Gtk/Control.py @@ -0,0 +1,83 @@ +# +# Python GUI - Controls - Gtk +# + +import gtk +from GUI import export +from GUI.Enumerations import EnumMap +from GUI import Color +from GUI import Font +from GUI.GControls import Control as GControl + +_justs = ['left', 'center', 'right'] + +_just_to_gtk_alignment = EnumMap("justification", + left = (0.0, gtk.JUSTIFY_LEFT), + centre = (0.5, gtk.JUSTIFY_CENTER), + center = (0.5, gtk.JUSTIFY_CENTER), + right = (1.0, gtk.JUSTIFY_RIGHT), +) + +class Control(GControl): + # A component which encapsulates a Gtk control widget. + + _font = None + + def __init__(self, _gtk_outer = None, _gtk_title = None, **kwds): + self._gtk_title_widget = _gtk_title or _gtk_outer + GControl.__init__(self, _gtk_outer = _gtk_outer, + _gtk_focus = kwds.pop('_gtk_focus', _gtk_outer), + **kwds) + + def get_title(self): + return self._gtk_title_widget.get_label() + + def set_title(self, v): + self._gtk_title_widget.set_label(v) + + def get_enabled(self): + #return self._gtk_outer_widget.get_sensitive() + return self._gtk_outer_widget.get_property('sensitive') + + def set_enabled(self, v): + self._gtk_outer_widget.set_sensitive(v) + + def get_color(self): + gdk_color = self._gtk_title_widget.style.fg[gtk.STATE_NORMAL] + return Color._from_gdk_color(gdk_color) + + def set_color(self, v): + self._gtk_title_widget.modify_fg(gtk.STATE_NORMAL, v._gdk_color) + + def get_font(self): + font = self._font + if not font: + font = Font._from_pango_description(self._gtk_title_widget.style.font_desc) + return font + + def set_font(self, f): + self._font = f + gtk_title = self._gtk_title_widget + gtk_title.modify_font(f._pango_description) + gtk_title.queue_resize() + + def get_just(self): + h = self._gtk_get_alignment() + return _justs[int(round(2.0 * h))] + + def set_just(self, v): + fraction, just = _just_to_gtk_alignment[v] + self._gtk_set_alignment(fraction, just) + + def set_lines(self, num_lines): + line_height = self.font.text_size("X")[1] + #print "Control.set_lines: line_height =", line_height ### + self.height = num_lines * line_height + self._vertical_padding + + def _gtk_get_alignment(self): + raise NotImplementedError + + def _gtk_set_alignment(self, h): + raise NotImplementedError + +export(Control) diff --git a/GUI/Gtk/Cursor.py b/GUI/Gtk/Cursor.py new file mode 100644 index 0000000..cfee65f --- /dev/null +++ b/GUI/Gtk/Cursor.py @@ -0,0 +1,35 @@ +# +# Python GUI - Cursors - Gtk +# + +from gtk import gdk +from GUI import export +from GUI.GCursors import Cursor as GCursor + +class Cursor(GCursor): + # + # _gtk_cursor gtk.gdk.Cursor + + def _from_gtk_std_cursor(cls, id): + cursor = cls.__new__(cls) + cursor._gtk_cursor = gdk.Cursor(id) + return cursor + + _from_gtk_std_cursor = classmethod(_from_gtk_std_cursor) + + def _from_nothing(cls): + cursor = cls.__new__(cls) + pixmap = gdk.Pixmap(None, 1, 1, 1) + color = gdk.Color() + cursor._gtk_cursor = gdk.Cursor(pixmap, pixmap, color, color, 0, 0) + return cursor + + _from_nothing = classmethod(_from_nothing) + + def _init_from_image_and_hotspot(self, image, hotspot): + #print "Cursor._init_from_image_and_hotspot:", image, hotspot ### + x, y = hotspot + gdk_display = gdk.display_get_default() + self._gtk_cursor = gdk.Cursor(gdk_display, image._gdk_pixbuf, x, y) + +export(Cursor) diff --git a/GUI/Gtk/Dialog.py b/GUI/Gtk/Dialog.py new file mode 100644 index 0000000..46ea57d --- /dev/null +++ b/GUI/Gtk/Dialog.py @@ -0,0 +1,13 @@ +# +# Python GUI - Dialogs - Gtk +# + +from GUI import export +from GUI.GDialogs import Dialog as GDialog + +class Dialog(GDialog): + + _default_keys = ['\r'] + _cancel_keys = ['\x1b'] + +export(Dialog) diff --git a/GUI/Gtk/DrawableContainer.py b/GUI/Gtk/DrawableContainer.py new file mode 100644 index 0000000..ca9893b --- /dev/null +++ b/GUI/Gtk/DrawableContainer.py @@ -0,0 +1,72 @@ +# +# Python GUI - DrawableViews - Gtk +# + +import os, traceback +from math import floor, ceil +import gtk +from gtk import gdk +from GUI import export +from GUI import Canvas, Event, rgb +from GUI.StdColors import grey +from GUI.GDrawableContainers import DrawableContainer as GDrawableContainer + +class DrawableContainer(GDrawableContainer): + + _background_color = grey + + def __init__(self, _gtk_outer = None, **kwds): + gtk_layout = gtk.Layout() + gtk_layout.add_events(gdk.EXPOSURE_MASK) + gtk_layout.show() + self._gtk_connect(gtk_layout, 'expose-event', + self._gtk_expose_event_signal) + if _gtk_outer: + _gtk_outer.add(gtk_layout) + else: + _gtk_outer = gtk_layout + GDrawableContainer.__init__(self, _gtk_outer = _gtk_outer, _gtk_inner = gtk_layout, + _gtk_focus = gtk_layout, _gtk_input = gtk_layout) + self.set(**kwds) + + # + # Other methods + # + + def with_canvas(self, proc): + hadj, vadj = self._gtk_adjustments() + clip = rect_sized((hadj.value, vadj.value), self.size) + canvas = Canvas._from_gdk_drawable(self._gtk_inner_widget.bin_window) + proc(canvas) + + def invalidate_rect(self, (l, t, r, b)): + gdk_window = self._gtk_inner_widget.bin_window + if gdk_window: + gdk_rect = (int(floor(l)), int(floor(t)), + int(ceil(r - l)), int(ceil(b - t))) + #print "View.invalidate_rect: gdk_rect =", gdk_rect ### + gdk_window.invalidate_rect(gdk_rect, 0) + + def update(self): + gdk_window = self._gtk_inner_widget.bin_window + gdk_window.process_updates() + + # + # Internal + # + + def _gtk_expose_event_signal(self, gtk_event): + try: + #print "View._gtk_expose_event_signal:", self ### + l, t, w, h = gtk_event.area + clip = (l, t, l + w, t + h) + #print "...clip =", clip ### + gtk_layout = self._gtk_inner_widget + canvas = Canvas._from_gdk_drawable(gtk_layout.bin_window) + update = self._draw_background(canvas, clip) + self.draw(canvas, update) + except: + print "------------------ Exception while drawing ------------------" + traceback.print_exc() + +export(DrawableContainer) diff --git a/GUI/Gtk/EditCmdHandler.py b/GUI/Gtk/EditCmdHandler.py new file mode 100644 index 0000000..d068331 --- /dev/null +++ b/GUI/Gtk/EditCmdHandler.py @@ -0,0 +1,8 @@ +# +# PyGUI - Edit command handling - Gtk +# + +from GUI import export +from GUI.GEditCmdHandlers import EditCmdHandler + +export(EditCmdHandler) diff --git a/GUI/Gtk/Event.py b/GUI/Gtk/Event.py new file mode 100644 index 0000000..bbbf74e --- /dev/null +++ b/GUI/Gtk/Event.py @@ -0,0 +1,147 @@ +# +# Python GUI - Events - Gtk +# + +from gtk import gdk +from GUI import export +from GUI.GEvents import Event as GEvent + +_gdk_button_mask = ( + gdk.BUTTON1_MASK | + gdk.BUTTON2_MASK | + gdk.BUTTON3_MASK | + gdk.BUTTON4_MASK | + gdk.BUTTON5_MASK +) + +_gdk_event_type_to_kind = { + gdk.BUTTON_PRESS: 'mouse_down', + gdk._2BUTTON_PRESS: 'mouse_down', + gdk._3BUTTON_PRESS: 'mouse_down', + gdk.MOTION_NOTIFY: 'mouse_move', + gdk.BUTTON_RELEASE: 'mouse_up', + gdk.KEY_PRESS: 'key_down', + gdk.KEY_RELEASE: 'key_up', + gdk.ENTER_NOTIFY: 'mouse_enter', + gdk.LEAVE_NOTIFY: 'mouse_leave', +} + +_gtk_button_to_button = { + 1: 'left', + 2: 'middle', + 3: 'right', + 4: 'fourth', + 5: 'fifth', +} + +_gdk_keyval_to_keyname = { + 0xFF50: 'home', + 0xFF51: 'left_arrow', + 0xFF52: 'up_arrow', + 0xFF53: 'right_arrow', + 0xFF54: 'down_arrow', + 0xFF55: 'page_up', + 0xFF56: 'page_down', + 0xFF57: 'end', + #0xFF6A: 'help', + 0xFF6A: 'insert', + 0xFF0D: 'return', + 0xFF8D: 'enter', + 0xFFBE: 'f1', + 0xFFBF: 'f2', + 0xFFC0: 'f3', + 0xFFC1: 'f4', + 0xFFC2: 'f5', + 0xFFC3: 'f6', + 0xFFC4: 'f7', + 0xFFC5: 'f8', + 0xFFC6: 'f9', + 0xFFC7: 'f10', + 0xFFC8: 'f11', + 0xFFC9: 'f12', + 0xFFCA: 'f13', + 0xFFCB: 'f14', + 0xFFCC: 'f15', + 0xFFFF: 'delete', +} + +class Event(GEvent): + """Platform-dependent modifiers (boolean): + mod1 The X11 MOD1 key. + """ + + button = None + position = None + global_position = None + num_clicks = 0 + char = None + key = None + auto = 0 + + def _gtk_key_event_of_interest(gtk_event): + keyval = gtk_event.keyval + return (keyval <= 0xFF + or 0xFF00 <= keyval <= 0xFF1F + or 0xFF80 <= keyval <= 0xFFBD + or keyval == 0xFE20 # shift-tab + or keyval == 0xFFFF + or keyval in _gdk_keyval_to_keyname) + + _gtk_key_event_of_interest = staticmethod(_gtk_key_event_of_interest) + + def _from_gtk_key_event(cls, gtk_event): + event = cls.__new__(cls) + event._set_from_gtk_event(gtk_event) + keyval = gtk_event.keyval + event._keycode = keyval + #print "Event._from_gtk_key_event: keyval = 0x%04X" % keyval ### + key = _gdk_keyval_to_keyname.get(keyval, "") + if keyval == 0xFFFF: # GDK_Delete + char = "\x7F" + elif keyval == 0xFF8D: + char = "\r" + elif keyval == 0xFE20: # shift-tab + char = "\t" + elif keyval <= 0xFF1F: + if event.control: + char = chr(keyval & 0x1F) + else: + char = chr(keyval & 0x7F) + else: + char = "" + if not key and "\x20" <= char <= "\x7e": + key = char + event.char = char + event.key = key + event.unichars = unicode(char) + return event + + _from_gtk_key_event = classmethod(_from_gtk_key_event) + + def _from_gtk_mouse_event(cls, gtk_event): + event = cls.__new__(cls) + event._set_from_gtk_event(gtk_event) + if event.kind in ('mouse_down', 'mouse_up'): + event.button = _gtk_button_to_button[gtk_event.button] + event.position = (gtk_event.x, gtk_event.y) + event.global_position = (gtk_event.x_root, gtk_event.y_root) + return event + + _from_gtk_mouse_event = classmethod(_from_gtk_mouse_event) + + def _set_from_gtk_event(self, gtk_event): + typ = gtk_event.type + state = gtk_event.state + if typ == gdk.MOTION_NOTIFY and state & _gdk_button_mask: + self.kind = 'mouse_drag' + else: + self.kind = _gdk_event_type_to_kind[gtk_event.type] + self.time = gtk_event.time / 1000.0 + self.shift = self.extend_contig = (state & gdk.SHIFT_MASK) <> 0 + self.control = self.extend_noncontig = (state & gdk.CONTROL_MASK) <> 0 + self.option = self.mod1 = (state & gdk.MOD1_MASK) <> 0 + + def _platform_modifiers_str(self): + return " mod1:%s" % (self.mod1) + +export(Event) diff --git a/GUI/Gtk/Files.py b/GUI/Gtk/Files.py new file mode 100644 index 0000000..2807408 --- /dev/null +++ b/GUI/Gtk/Files.py @@ -0,0 +1,5 @@ +# +# Python GUI - File references and types - Gtk +# + +from GUI.GFiles import * diff --git a/GUI/Gtk/Font.py b/GUI/Gtk/Font.py new file mode 100644 index 0000000..e9c3f7d --- /dev/null +++ b/GUI/Gtk/Font.py @@ -0,0 +1,155 @@ +# +# Python GUI - Fonts - Gtk +# + +from __future__ import division + +import sys +import pango, gtk +from gtk import gdk +from GUI import export +from GUI.GFonts import Font as GFont + +class Font(GFont): + + #_gdk_font = None + _pango_font = None + _pango_metrics = None + _pango_layout = None + +# def _from_gdk_font(cls, gdk_font): +# font = cls.__new__(cls) +# font._gdk_font = gdk_font +# return font +# +# _from_gdk_font = classmethod(_from_gdk_font) + + def _from_pango_description(cls, pango_description): + font = cls.__new__(cls) + font._pango_description = pango_description + return font + + _from_pango_description = classmethod(_from_pango_description) + + def __init__(self, family, size = 12, style = []): + if 'italic' in style: + pango_style = pango.STYLE_ITALIC + else: + pango_style = pango.STYLE_NORMAL + if 'bold' in style: + pango_weight = pango.WEIGHT_BOLD + else: + pango_weight = pango.WEIGHT_NORMAL + jigger = _find_size_correction_factor(family, pango_style, pango_weight) + pfd = pango.FontDescription() + pfd.set_family(family) + pfd.set_size(int(round(jigger * size * pango.SCALE))) + pfd.set_style(pango_style) + pfd.set_weight(pango_weight) + self._pango_description = pfd + + def get_family(self): + return self._pango_description.get_family() + + def get_size(self): + return self._pango_description.get_size() / pango.SCALE + + def get_style(self): + style = [] + pfd = self._pango_description + if pfd.get_weight() > pango.WEIGHT_NORMAL: + style.append('bold') + if pfd.get_style() <> pango.STYLE_NORMAL: + style.append('italic') + return style + + def get_ascent(self): + self._get_pango_metrics() + result = self._ascent + return result + + def get_descent(self): + self._get_pango_metrics() + return self._descent + + def get_height(self): + self._get_pango_metrics() + return self._ascent + self._descent + + def get_line_height(self): + return self.get_height() + + def _get_pango_metrics(self): + #print "Font._get_pango_metrics: enter" ### + pfm = self._pango_metrics + if not pfm: + pf = self._get_pango_font() + pfm = pf.get_metrics() + self._pango_metrics = pfm + self._ascent = pfm.get_ascent() / pango.SCALE + self._descent = pfm.get_descent() / pango.SCALE + return pfm + + def _get_pango_font(self): + pf = self._pango_font + if not pf: + pf = _pango_context.load_font(self._pango_description) + if not pf: + raise ValueError("Unable to load Pango font for %s" % self) + self._pango_font = pf + return pf + + def width(self, s, start = 0, end = sys.maxint): + layout = self._get_pango_layout(s[start:end], True) + return layout.get_pixel_size()[0] + + def text_size(self, text): + layout = self._get_pango_layout(text, False) + return layout.get_pixel_size() + #w, h = layout.get_size() + #u = pango.SCALE + #return (w / u, h / u) + + def x_to_pos(self, s, x): + layout = self._get_pango_layout(s, True) + return pango_layout.xy_to_index(x, 0) + + def _get_pango_layout(self, text, single_paragraph_mode): + layout = self._pango_layout + if not layout: + layout = pango.Layout(_pango_context) + layout.set_font_description(self._pango_description) + self._pango_layout = layout + layout.set_single_paragraph_mode(single_paragraph_mode) + layout.set_text(text) + return layout + + +_pango_context = gtk.Label().create_pango_context() + +_jigger_cache = {} + +def _find_size_correction_factor(family, pango_style, pango_weight): + # Unlike the rest of the world, Pango seems to consider the point + # size of a font to only include the ascent. So we first ask for + # a 1-point font, find the ratio of its ascent to its descent, + # and use that to adjust the size requested by the user. + key = (family, pango_style, pango_weight) + result = _jigger_cache.get(key) + if result is None: + pd = pango.FontDescription() + pd.set_family(family) + pd.set_size(pango.SCALE) + pd.set_style(pango_style) + pd.set_weight(pango_weight) + pf = _pango_context.load_font(pd) + pm = pf.get_metrics() + a = pm.get_ascent() + d = pm.get_descent() + result = a / (a + d) + #print "Jigger factor for font:", family, pango_style, pango_weight ### + #print "ascent =", a, "descent =", d, "factor =", result ### + _jigger_cache[key] = result + return result + +export(Font) diff --git a/GUI/Gtk/Frame.py b/GUI/Gtk/Frame.py new file mode 100644 index 0000000..2723a15 --- /dev/null +++ b/GUI/Gtk/Frame.py @@ -0,0 +1,16 @@ +# +# Python GUI - Frames - Gtk +# + +from gtk import Fixed +from GUI import export +from GUI.GFrames import Frame as GFrame + +class Frame(GFrame): + + def __init__(self, **kwds): + gtk_widget = Fixed() + gtk_widget.show() + GFrame.__init__(self, _gtk_outer = gtk_widget) + +export(Frame) diff --git a/GUI/Gtk/GL.py b/GUI/Gtk/GL.py new file mode 100644 index 0000000..df93e8e --- /dev/null +++ b/GUI/Gtk/GL.py @@ -0,0 +1,212 @@ +# +# PyGUI - OpenGL View - Gtk/GtkGLExt +# + +try: + from gtk import gdkgl, gtkgl + from OpenGL.GL import glViewport +except ImportError, e: + raise ImportError("OpenGL support is not available (%s)" % e) + +from GUI.GGLConfig import GLConfig as GGLConfig, GLConfigError +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI import ImageBase +from GUI.GtkPixmaps import GtkPixmap +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture +from GUI.GLDisplayLists import DisplayList + +#------------------------------------------------------------------------------ + +def gtk_find_config_default(attr, mode_bit): + try: + cfg = gdkgl.Config(mode = mode_bit) + value = cfg.get_attrib(attr)[0] + except gdkgl.NoMatches: + value = 0 + print "default for attr", attr, "=", value + return value + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + + _alpha = False + _color_size = 1 + _alpha_size = 1 + _depth_size = 1 + _stencil_size = 1 + _accum_size = 1 + + def _gtk_get_config(self): + csize = self._color_size + asize = 0 + dsize = 0 + ssize = 0 + acsize = 0 + aasize = 0 + if self._alpha: + asize = self._alpha_size + if self._depth_buffer: + dsize = self._depth_size + if self._stencil_buffer: + ssize = self._stencil_size + if self._accum_buffer: + acsize = self._accum_size + if self._alpha: + aasize = acsize + attrs = [ + gdkgl.RGBA, + gdkgl.RED_SIZE, csize, + gdkgl.GREEN_SIZE, csize, + gdkgl.BLUE_SIZE, csize, + gdkgl.ALPHA_SIZE, asize, + gdkgl.AUX_BUFFERS, self._aux_buffers, + gdkgl.DEPTH_SIZE, dsize, + gdkgl.STENCIL_SIZE, ssize, + gdkgl.ACCUM_RED_SIZE, acsize, + gdkgl.ACCUM_GREEN_SIZE, acsize, + gdkgl.ACCUM_BLUE_SIZE, acsize, + gdkgl.ACCUM_ALPHA_SIZE, aasize, + ] + if self._double_buffer: + attrs += [gdkgl.DOUBLEBUFFER] + if self._stereo: + attrs += [gdkgl.STEREO] + if self._multisample: + attrs += [ + gdkgl.SAMPLE_BUFFERS, 1, + gdkgl.SAMPLES, self._samples_per_pixel + ] + result = self._gdkgl_config(attrs) + if not result and self._double_buffer: + attrs.remove(gdkgl.DOUBLEBUFFER) + result = self._gdkgl_config(attrs) + if not result: + raise GLConfigError + return result + + def _gdkgl_config(self, attrs): + try: + return gdkgl.Config(attrib_list = attrs) + except gdkgl.NoMatches: + return None + + def _gtk_set_config(self, gtk_config): + def attr(key): + return gtk_config.get_attrib(key)[0] + self._color_size = attr(gdkgl.RED_SIZE) + self._alpha_size = attr(gdkgl.ALPHA_SIZE) + self._alpha = gtk_config.has_alpha() + self._double_buffer = gtk_config.is_double_buffered() + self._stereo = gtk_config.is_stereo() + self._aux_buffers = attr(gdkgl.AUX_BUFFERS) + self._depth_size = attr(gdkgl.DEPTH_SIZE) + self._depth_buffer = gtk_config.has_depth_buffer() + self._stencil_size = attr(gdkgl.STENCIL_SIZE) + self._stencil_buffer = gtk_config.has_stencil_buffer() + self._accum_size = attr(gdkgl.ACCUM_RED_SIZE) + self._accum_buffer = gtk_config.has_accum_buffer() + self._multisample = attr(gdkgl.SAMPLE_BUFFERS) <> 0 + self._samples_per_pixel = attr(gdkgl.SAMPLES) + + def supported(self, mode = 'both'): + try: + gtk_config = self._gtk_get_config() + pf = GLConfig.__new__(GLConfig) + pf._gtk_set_config(gtk_config) + return pf + except GLConfigError: + return None + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + + _first_expose = 0 + + def __init__(self, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + GLContext.__init__(self, share_group, pf, kwds) + gtk_share = self._gtk_get_share() + area = gtkgl.DrawingArea(glconfig = self._gl_config, share_list = gtk_share, + render_type = gdkgl.RGBA_TYPE) + area.show() + self._gtk_connect_after(area, "realize", self._gtk_realize_signal) + self._gtk_connect(area, "expose-event", self._gtk_expose_event_signal) + GGLView.__init__(self, _gtk_outer = area, _gtk_input = area, + _gtk_focus = area) + self.set(**kwds) + + def _resized(self, delta): + self.with_context(self._update_viewport) + + def _gtk_get_gl_context(self): + if not self._gl_context: + self._gtk_inner_widget.realize() + return self._gl_context + + def _gtk_realize_signal(self): + #print "GLView._gtk_realize_signal" ### + area = self._gtk_inner_widget + self._gl_drawable = area.get_gl_drawable() + self._gl_context = area.get_gl_context() + self.with_context(self.init_context) + + def _gtk_expose_event_signal(self, gtk_event): + #print "GLView._gtk_expose_event_signal" ### + if not self._first_expose: + self.with_context(self._update_viewport) + self._first_expose = 1 + try: + self.with_context(self._render, flush = True) + except: + import sys, traceback + sys.stderr.write("\n<<<<<<<<<< Exception while rendering a GLView\n") + traceback.print_exc() + sys.stderr.write(">>>>>>>>>>\n\n") + + def invalidate(self): + gtk_window = self._gtk_outer_widget.window + if gtk_window: + width, height = self.size + gtk_window.invalidate_rect((0, 0, width, height), 0) + +#------------------------------------------------------------------------------ + +class GLPixmap(GtkPixmap, GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + GLContext.__init__(self, share_group, pf, kwds) + gl_config = pf._gtk_get_config() + self._gl_config = gl_config +# if share: +# gtk_share = share.shared_context._gtk_get_gl_context() +# else: +# gtk_share = None + gtk_share = self._gtk_get_share() + GtkPixmap.__init__(self, width, height) + gdk_pixmap = self._gdk_pixmap + gdkgl.ext(gdk_pixmap) + self._gl_drawable = gdk_pixmap.set_gl_capability(glconfig = gl_config) + print "GLPixmap: self._gl_drawable =", self._gl_drawable ### + self._gl_context = gdkgl.Context( + self._gl_drawable, + direct = False, + share_list = gtk_share, + render_type = gdkgl.RGBA_TYPE + ) + print "GLPixmap: self._gl_context =", self._gl_context ### + ImageBase.__init__(self, **kwds) + self.with_context(self._init_context) + print "GLPixmap: initialised context" ### + +# def _init_context(self): +# width, height = self.size +# glViewport(0, 0, int(width), int(height)) +# print "GLPixmap: Set viewport to", width, height ### +# self.init_context() + + \ No newline at end of file diff --git a/GUI/Gtk/GLContexts.py b/GUI/Gtk/GLContexts.py new file mode 100644 index 0000000..1765565 --- /dev/null +++ b/GUI/Gtk/GLContexts.py @@ -0,0 +1,46 @@ +# +# PyGUI - GL Context - Gtk +# + +from GUI.GGLContexts import GLContext as GGLContext + +try: + from OpenGL.GL import glFlush +except ImportError, e: + raise ImportError("OpenGL support is not available (%s)" % e) + +class GLContext(GGLContext): + + _gl_drawable = None + _gl_context = None + + def __init__(self, share_group, config, kwds): + GGLContext.__init__(self, share_group) + self._gl_config = config._gtk_get_config() + + def _gtk_get_share(self): + shared_context = self._get_shared_context() + if shared_context: + return shared_context._gtk_get_gl_context() + else: + return None + + def _with_context(self, proc, flush): + drawable = self._gl_drawable + if drawable: + if not drawable.gl_begin(self._gl_context): + raise ValueError( + "Unable to make %s the current OpenGL context (gl_begin failed)" % self) + try: + self._with_share_group(proc) + if flush: + if drawable.is_double_buffered(): + #print "GLContext.with_context: swapping buffers" ### + drawable.swap_buffers() + else: + #print "GLContext.with_context: flushing" ### + glFlush() + finally: + drawable.gl_end() + #return result + diff --git a/GUI/Gtk/GLTextures.py b/GUI/Gtk/GLTextures.py new file mode 100644 index 0000000..d1fb55e --- /dev/null +++ b/GUI/Gtk/GLTextures.py @@ -0,0 +1,10 @@ +# +# PyGUI - OpenGL Textures - Gtk +# + +from GUI.GGLTextures import Texture as GTexture + +class Texture(GTexture): + + def _gl_get_texture_data(self, image): + raise NotImplementedError("Loading texture from image not yet implemented for Gtk") diff --git a/GUI/Gtk/Geometry.py b/GUI/Gtk/Geometry.py new file mode 100644 index 0000000..2ebbd95 --- /dev/null +++ b/GUI/Gtk/Geometry.py @@ -0,0 +1,5 @@ +# +# Python GUI - Points and Rectangles - Gtk +# + +from GUI.GGeometry import * diff --git a/GUI/Gtk/GtkImageScaling.py b/GUI/Gtk/GtkImageScaling.py new file mode 100644 index 0000000..119ddb5 --- /dev/null +++ b/GUI/Gtk/GtkImageScaling.py @@ -0,0 +1,19 @@ +# +# Python GUI - Image scaling utilities - Gtk +# + +from gtk import gdk + +def gtk_scale_pixbuf(src_pixbuf, sx, sy, sw, sh, dw, dh): + """Return a new pixbuf containing the specified part of + the given pixbuf scaled to the specified size.""" + dst_pixbuf = gdk.Pixbuf( + src_pixbuf.get_colorspace(), src_pixbuf.get_has_alpha(), + src_pixbuf.get_bits_per_sample(), dw, dh) + xscale = float(dw) / sw + yscale = float(dh) / sh + xoffset = - xscale * sx + yoffset = - yscale * sy + src_pixbuf.scale(dst_pixbuf, 0, 0, dw, dh, + xoffset, yoffset, xscale, yscale, gdk.INTERP_BILINEAR) + return dst_pixbuf diff --git a/GUI/Gtk/GtkPixmaps.py b/GUI/Gtk/GtkPixmaps.py new file mode 100644 index 0000000..e7908e5 --- /dev/null +++ b/GUI/Gtk/GtkPixmaps.py @@ -0,0 +1,34 @@ +# +# Python GUI - Gtk - Common pixmap code +# + +from gtk import gdk +from GUI.StdColors import clear +from GUI.GtkImageScaling import gtk_scale_pixbuf +from GUI import Canvas + +class GtkPixmap: + + def __init__(self, width, height): + gdk_root = gdk.get_default_root_window() + self._gdk_pixmap = gdk.Pixmap(gdk_root, width, height) + #ctx = self._gdk_pixmap.cairo_create() + #self._gtk_surface = ctx.get_target() + + def _gtk_set_source(self, ctx, x, y): + ctx.set_source_pixmap(self._gdk_pixmap, x, y) + + def get_width(self): + return self._gdk_pixmap.get_size()[0] + + def get_height(self): + return self._gdk_pixmap.get_size()[1] + + def get_size(self): + return self._gdk_pixmap.get_size() + + def with_canvas(self, proc): + canvas = Canvas._from_gdk_drawable(self._gdk_pixmap) + canvas.backcolor = clear + proc(canvas) + diff --git a/GUI/Gtk/Image.py b/GUI/Gtk/Image.py new file mode 100644 index 0000000..1bd4017 --- /dev/null +++ b/GUI/Gtk/Image.py @@ -0,0 +1,33 @@ +# +# Python GUI - Images - Gtk +# + +from __future__ import division +from array import array +import cairo +from gtk import gdk +from GUI import export +from GUI.GImages import Image as GImage + +class Image(GImage): + + def _init_from_file(self, file): + self._gdk_pixbuf = gdk.pixbuf_new_from_file(file) + + def _from_gdk_pixbuf(cls, gdk_pixbuf): + self = cls.__new__(cls) + self._gdk_pixbuf = gdk_pixbuf + return self + + _from_gdk_pixbuf = classmethod(_from_gdk_pixbuf) + + def _gtk_set_source(self, ctx, x, y): + ctx.set_source_pixbuf(self._gdk_pixbuf, x, y) + + def get_width(self): + return self._gdk_pixbuf.get_width() + + def get_height(self): + return self._gdk_pixbuf.get_height() + +export(Image) diff --git a/GUI/Gtk/ImageBase.py b/GUI/Gtk/ImageBase.py new file mode 100644 index 0000000..873c51f --- /dev/null +++ b/GUI/Gtk/ImageBase.py @@ -0,0 +1,35 @@ +# +# PyGUI - Image Base - Gtk +# + +from __future__ import division +from GUI import export +from GUI.GImageBases import ImageBase as GImageBase + +class ImageBase(GImageBase): + +# def get_width(self): +# return self._gtk_surface.get_width() +# +# def get_height(self): +# return self._gtk_surface.get_height() + + def draw(self, canvas, src_rect, dst_rect): + sx, sy, sr, sb = src_rect + dx, dy, dr, db = dst_rect + sw = sr - sx + sh = sb - sy + dw = dr - dx + dh = db - dy + ctx = canvas._gtk_ctx + ctx.save() + ctx.translate(dx, dy) + ctx.new_path() + ctx.rectangle(0, 0, dw, dh) + ctx.clip() + ctx.scale(dw / sw, dh / sh) + self._gtk_set_source(canvas._gtk_ctx, -sx, -sy) + ctx.paint() + ctx.restore() + +export(ImageBase) diff --git a/GUI/Gtk/Label.py b/GUI/Gtk/Label.py new file mode 100644 index 0000000..9699177 --- /dev/null +++ b/GUI/Gtk/Label.py @@ -0,0 +1,36 @@ +# +# Python GUI - Labels - Gtk +# + +import gtk +from GUI import export +from GUI.StdFonts import system_font +from GUI.GLabels import Label as GLabel + +class Label(GLabel): + + _vertical_padding = 6 + + def __init__(self, text = "New Label", font = system_font, **kwds): + width, height = font.text_size(text) + gtk_label = gtk.Label(text) + gtk_label.set_alignment(0.0, 0.5) + gtk_label.set_size_request(width, height + self._vertical_padding) + gtk_label.show() + GLabel.__init__(self, _gtk_outer = gtk_label, font = font, **kwds) + + def get_text(self): + return self._gtk_outer_widget.get_text() + + def set_text(self, text): + self._gtk_outer_widget.set_text(text) + + def _gtk_get_alignment(self): + return self._gtk_outer_widget.get_alignment()[0] + + def _gtk_set_alignment(self, fraction, just): + gtk_label = self._gtk_outer_widget + gtk_label.set_alignment(fraction, 0.0) + gtk_label.set_justify(just) + +export(Label) diff --git a/GUI/Gtk/ListButton.py b/GUI/Gtk/ListButton.py new file mode 100644 index 0000000..7d3d75c --- /dev/null +++ b/GUI/Gtk/ListButton.py @@ -0,0 +1,50 @@ +#-------------------------------------------------------------- +# +# PyGUI - Pop-up list control - Gtk +# +#-------------------------------------------------------------- + +import gtk +from GUI import export +from GUI.GListButtons import ListButton as GListButton + +class ListButton(GListButton): + + _gtk_suppress_action = False + + def __init__(self, **kwds): + titles, values = self._extract_initial_items(kwds) + self._titles = titles + self._values = values + gtk_widget = gtk.combo_box_new_text() + gtk_widget.connect('changed', self._gtk_changed_signalled) + gtk_widget.set_property('focus_on_click', False) + gtk_widget.show() + self._gtk_update_items(gtk_widget) + GListButton.__init__(self, _gtk_outer = gtk_widget, **kwds) + + def _update_items(self): + self._gtk_update_items(self._gtk_outer_widget) + + def _gtk_update_items(self, gtk_widget): + self._gtk_suppress_action = True + n = gtk_widget.get_model().iter_n_children(None) + for i in xrange(n - 1, -1, -1): + gtk_widget.remove_text(i) + for title in self._titles: + gtk_widget.append_text(title) + self._gtk_suppress_action = False + + def _get_selected_index(self): + return self._gtk_outer_widget.get_active() + + def _set_selected_index(self, i): + self._gtk_suppress_action = True + self._gtk_outer_widget.set_active(i) + self._gtk_suppress_action = False + + def _gtk_changed_signalled(self, _): + if not self._gtk_suppress_action: + self.do_action() + +export(ListButton) diff --git a/GUI/Gtk/Menu.py b/GUI/Gtk/Menu.py new file mode 100644 index 0000000..05b9e65 --- /dev/null +++ b/GUI/Gtk/Menu.py @@ -0,0 +1,66 @@ +# +# Python GUI - Menus - Gtk version +# + +import gtk +from gtk import gdk +from GUI import export +from GUI.Globals import application +from GUI.GMenus import Menu as GMenu, MenuItem + +def _report_accel_changed_(*args): + print "Menus: accel_changed:", args + +class Menu(GMenu): + + def __init__(self, title, items, **kwds): + GMenu.__init__(self, title, items, **kwds) + self._gtk_menu = gtk.Menu() + self._gtk_accel_group = gtk.AccelGroup() + #self._gtk_accel_group.connect('accel_changed', _report_accel_changed_) ### + + def _clear_platform_menu(self): + gtk_menu = self._gtk_menu + for gtk_item in gtk_menu.get_children(): + gtk_item.destroy() + + def _add_separator_to_platform_menu(self): + gtk_item = gtk.MenuItem() + gtk_item.set_sensitive(0) + gtk_separator = gtk.HSeparator() + gtk_item.add(gtk_separator) + self._gtk_add_item(gtk_item) + + def _gtk_add_item(self, gtk_item): + gtk_item.show_all() + self._gtk_menu.append(gtk_item) + + def _add_item_to_platform_menu(self, item, name, command = None, index = None): + checked = item.checked + if checked is None: + gtk_item = gtk.MenuItem(name) + else: + gtk_item = gtk.CheckMenuItem(name) + self._gtk_add_item(gtk_item) + if not item.enabled: + gtk_item.set_sensitive(0) + if checked: + gtk_item.set_active(1) + if command: + app = application() + if index is not None: + action = lambda widget: app.dispatch(command, index) + else: + action = lambda widget: app.dispatch(command) + gtk_item.connect('activate', action) + key = item._key + if key: + gtk_modifiers = gdk.CONTROL_MASK + if item._shift: + gtk_modifiers |= gdk.SHIFT_MASK + if item._option: + gtk_modifiers |= gdk.MOD1_MASK + gtk_item.add_accelerator('activate', self._gtk_accel_group, + ord(key), gtk_modifiers, gtk.ACCEL_VISIBLE) + +export(Menu) diff --git a/GUI/Gtk/Numerical.py b/GUI/Gtk/Numerical.py new file mode 100644 index 0000000..8cec11b --- /dev/null +++ b/GUI/Gtk/Numerical.py @@ -0,0 +1,37 @@ +#-------------------------------------------------------------- +# +# PyGUI - NumPy interface - Gtk +# +#-------------------------------------------------------------- + +from gtk import gdk +from GUI import Image + +def image_from_ndarray(array, format, size = None): + """ + Creates an Image from a numpy ndarray object. The format + may be 'RGB' or 'RGBA'. If a size is specified, the array + will be implicitly reshaped to that size, otherwise the size + is inferred from the first two dimensions of the array. + """ + if array.itemsize <> 1: + raise ValueError("Color component size must be 1 byte") + if size is None: + shape = array.shape + if len(shape) <> 3: + raise ValueError("Array has wrong number of dimensions") + width, height, pixel_size = shape + if pixel_size <> len(format): + raise ValueError("Last dimension of array does not match format") + else: + width, height = size + pixel_size = len(format) + data_size = array.size + if data_size <> width * height * pixel_size: + raise ValueError("Array has wrong shape for specified size and format") + alpha = pixel_size == 4 + gdk_pixbuf = gdk.pixbuf_new_from_data(array, gdk.COLORSPACE_RGB, alpha, + 8, width, height, width * pixel_size) + image = Image._from_gdk_pixbuf(gdk_pixbuf) + #image._data = array ### + return image diff --git a/GUI/Gtk/PIL.py b/GUI/Gtk/PIL.py new file mode 100644 index 0000000..2c09c3e --- /dev/null +++ b/GUI/Gtk/PIL.py @@ -0,0 +1,27 @@ +#-------------------------------------------------------------- +# +# PyGUI - PIL interface - Gtk +# +#-------------------------------------------------------------- + +from gtk import gdk +from gtk.gdk import COLORSPACE_RGB +from GUI import Image + +def image_from_pil_image(pil_image): + """Creates an Image from a Python Imaging Library (PIL) + Image object.""" + mode = pil_image.mode + w, h = pil_image.size + data = pil_image.tostring() + if mode == "RGB": + bps = 3; alpha = False + elif mode == "RGBA": + bps = 4; alpha = True + else: + raise ValueError("Unsupported PIL image mode '%s'" % mode) + bpr = w * bps + image = Image.__new__(Image) + image._gdk_pixbuf = gdk.pixbuf_new_from_data(data, COLORSPACE_RGB, + alpha, 8, w, h, bpr) + return image diff --git a/GUI/Gtk/Pixmap.py b/GUI/Gtk/Pixmap.py new file mode 100644 index 0000000..1a0c452 --- /dev/null +++ b/GUI/Gtk/Pixmap.py @@ -0,0 +1,15 @@ +# +# Python GUI - Pixmap - Gtk +# + +from gtk import gdk +from GUI import export +from GUI.GtkPixmaps import GtkPixmap +from GUI.GPixmaps import Pixmap as GPixmap + +class Pixmap(GtkPixmap, GPixmap): + + def __init__(self, width, height): + GtkPixmap.__init__(self, width, height) + +export(Pixmap) diff --git a/GUI/Gtk/Printing.py b/GUI/Gtk/Printing.py new file mode 100644 index 0000000..5ee9da3 --- /dev/null +++ b/GUI/Gtk/Printing.py @@ -0,0 +1,188 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Printing - Gtk +# +#------------------------------------------------------------------------------ + +import gtk, gtkunixprint +from gtk import UNIT_POINTS +from GUI import Canvas +from GUI.GPrinting import PageSetup as GPageSetup, Printable as GPrintable, \ + Paginator + +gtk_paper_names = [ + gtk.PAPER_NAME_A3, + gtk.PAPER_NAME_A4, + gtk.PAPER_NAME_A5, + gtk.PAPER_NAME_B5, + gtk.PAPER_NAME_LETTER, + gtk.PAPER_NAME_EXECUTIVE, + gtk.PAPER_NAME_LEGAL, +] + +gtk_paper_formats = {} + +gtk_print_settings = gtk.PrintSettings() + +def init_gtk_paper_formats(): + for gtk_name in gtk_paper_names: + display_name = gtk.PaperSize(gtk_name).get_display_name() + gtk_paper_formats[display_name] = gtk_name + +init_gtk_paper_formats() + +def gtk_default_page_setup(): + pset = gtk.PageSetup() + pset.set_paper_size(gtk.PaperSize()) + return pset + +def get_gtk_state(gtk_page_setup): + state = {} + state['orientation'] = gtk_page_setup.get_orientation() + state['paper_size'] = gtk_page_setup.get_paper_size().get_name() + state['top_margin'] = gtk_page_setup.get_top_margin(UNIT_POINTS) + state['bottom_margin'] = gtk_page_setup.get_bottom_margin(UNIT_POINTS) + state['left_margin'] = gtk_page_setup.get_left_margin(UNIT_POINTS) + state['right_margin'] = gtk_page_setup.get_right_margin(UNIT_POINTS) + return state + +def set_gtk_state(gtk_page_setup, state): + gtk_page_setup.set_orientation(state['orientation']) + gtk_page_setup.set_paper_size(gtk.PaperSize(state['paper_size'])) + gtk_page_setup.set_top_margin(state['top_margin'], UNIT_POINTS) + gtk_page_setup.set_bottom_margin(state['bottom_margin'], UNIT_POINTS) + gtk_page_setup.set_left_margin(state['left_margin'], UNIT_POINTS) + gtk_page_setup.set_right_margin(state['right_margin'], UNIT_POINTS) + +#------------------------------------------------------------------------------ + +class PageSetup(GPageSetup): + + _printer_name = "" + _left_margin = 36 + _top_margin = 36 + _right_margin = 36 + _bottom_margin = 36 + + def __init__(self): + self._gtk_page_setup = gtk_default_page_setup() + + def __getstate__(self): + state = GPageSetup.__getstate__(self) + state['_gtk_page_setup'] = get_gtk_state(self._gtk_page_setup) + return state + + def __setstate__(self, state): + gtk_setup = gtk_default_page_setup() + self._gtk_page_setup = gtk_setup + gtk_state = state.pop('_gtk_page_setup', None) + if gtk_state: + set_gtk_state(gtk_setup, gtk_state) + self.margins = state['margins'] + self.printer_name = state['printer_name'] + else: + GPageSetup.__setstate__(state) + + def get_paper_name(self): + return self._gtk_page_setup.get_paper_size().get_display_name() + + def set_paper_name(self, x): + psize = gtk.PaperSize(gtk_paper_formats.get(x) or x) + self._gtk_page_setup.set_paper_size(psize) + + def get_paper_width(self): + return self._gtk_page_setup.get_paper_width(UNIT_POINTS) + + def set_paper_width(self, x): + self._gtk_page_setup.set_paper_width(x, UNIT_POINTS) + + def get_paper_height(self): + return self._gtk_page_setup.get_paper_height(UNIT_POINTS) + + def set_paper_height(self, x): + self._gtk_page_setup.set_paper_height(x, UNIT_POINTS) + + def get_orientation(self): + o = self._gtk_page_setup.get_orientation() + if o in (gtk.PAGE_ORIENTATION_LANDSCAPE, + gtk.PAGE_ORIENTATION_REVERSE_LANDSCAPE): + return 'landscape' + else: + return 'portrait' + + def set_orientation(self, x): + if x == 'landscape': + o = gtk.PAGE_ORIENTATION_LANDSCAPE + else: + o = gtk.PAGE_ORIENTATION_PORTRAIT + self._gtk_page_setup.set_orientation(o) + + def get_left_margin(self): + return self._left_margin + + def get_top_margin(self): + return self._top_margin + + def get_right_margin(self): + return self._right_margin + + def get_bottom_margin(self): + return self._bottom_margin + + def set_left_margin(self, x): + self._left_margin = x + + def set_top_margin(self, x): + self._top_margin = x + + def set_right_margin(self, x): + self._right_margin = x + + def set_bottom_margin(self, x): + self._bottom_margin = x + + def get_printer_name(self): + return self._printer_name + + def set_printer_name(self, x): + self._printer_name = x + +#------------------------------------------------------------------------------ + +class Printable(GPrintable): + + def print_view(self, page_setup, prompt = True): + global gtk_print_settings + paginator = Paginator(self, page_setup) + + def draw_page(_, gtk_print_context, page_num): + cairo_context = gtk_print_context.get_cairo_context() + canvas = Canvas._from_cairo_context(cairo_context) + paginator.draw_page(canvas, page_num) + + gtk_op = gtk.PrintOperation() + gtk_op.set_print_settings(gtk_print_settings) + gtk_op.set_default_page_setup(page_setup._gtk_page_setup) + gtk_op.set_n_pages(paginator.num_pages) + gtk_op.set_use_full_page(True) + gtk_op.set_unit(UNIT_POINTS) + gtk_op.connect('draw-page', draw_page) + if prompt: + action = gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG + else: + action = gtk.PRINT_OPERATION_ACTION_PRINT + result = gtk_op.run(action) + if result == gtk.PRINT_OPERATION_RESULT_APPLY: + gtk_print_settings = gtk_op.get_print_settings() + +#------------------------------------------------------------------------------ + +def present_page_setup_dialog(page_setup): + old_setup = page_setup._gtk_page_setup + ps = gtk.PrintSettings() + new_setup = gtk.print_run_page_setup_dialog(None, old_setup, ps) + if get_gtk_state(old_setup) <> get_gtk_state(new_setup): + page_setup._gtk_page_setup = new_setup + return True + else: + return False diff --git a/GUI/Gtk/RadioButton.py b/GUI/Gtk/RadioButton.py new file mode 100644 index 0000000..3db37a1 --- /dev/null +++ b/GUI/Gtk/RadioButton.py @@ -0,0 +1,38 @@ +# +# Python GUI - Radio buttons - Gtk +# + +import gtk +from GUI import export +from GUI.GRadioButtons import RadioButton as GRadioButton + +class RadioButton(GRadioButton): + + def __init__(self, title = "New Control", **kwds): + gtk_radiobutton = gtk.RadioButton(None, title) + gtk_radiobutton.show() + self._gtk_connect(gtk_radiobutton, 'toggled', self._gtk_toggled_signal) + GRadioButton.__init__(self, _gtk_outer = gtk_radiobutton, **kwds) + + def _value_changed(self): + group = self._group + if group: + if self._value == group._value: + self._turn_on() + else: + group._turn_all_off() + + def _turn_on(self): + self._gtk_outer_widget.set_active(1) + + def _is_on(self): + return self._gtk_outer_widget.get_active() + + def _gtk_toggled_signal(self): + if self._is_on(): + group = self._group + if group and group._value <> self._value: + group._value = self._value + group.do_action() + +export(RadioButton) diff --git a/GUI/Gtk/RadioGroup.py b/GUI/Gtk/RadioGroup.py new file mode 100644 index 0000000..92b0e80 --- /dev/null +++ b/GUI/Gtk/RadioGroup.py @@ -0,0 +1,37 @@ +# +# Python GUI - Radio groups - Gtk +# + +import gtk +from GUI import export +from GUI.GRadioGroups import RadioGroup as GRadioGroup + +class RadioGroup(GRadioGroup): + + def __init__(self, items = [], **kwds): + self._gtk_dummy_radiobutton = gtk.RadioButton() + GRadioGroup.__init__(self, items, **kwds) + + def _item_added(self, item): + old_value = self._value + item._gtk_outer_widget.set_group(self._gtk_dummy_radiobutton) + self.value = old_value + + def _item_removed(self, item): + item._gtk_outer_widget.set_group(None) + if item._value == self._value: + self._value = None + self._turn_all_off() + + def _value_changed(self): + new_value = self._value + for item in self._items: + if item._value == new_value: + item._turn_on() + return + self._turn_all_off() + + def _turn_all_off(self): + self._gtk_dummy_radiobutton.set_active(1) + +export(RadioGroup) diff --git a/GUI/Gtk/Scrollable.py b/GUI/Gtk/Scrollable.py new file mode 100644 index 0000000..dccb3dd --- /dev/null +++ b/GUI/Gtk/Scrollable.py @@ -0,0 +1,31 @@ +# +# Python GUI - Common code for scrollable components - Gtk +# + +import gtk +from GUI import export +from GUI import Globals + +gtk_scroll_policies = [gtk.POLICY_NEVER, gtk.POLICY_ALWAYS] + + +class Scrollable(object): + + gtk_scrollbar_breadth = gtk.VScrollbar().size_request()[0] + 3 + s = gtk.ScrolledWindow().get_style() + gtk_border_thickness = (s.xthickness, s.ythickness) + del s + + def get_hscrolling(self): + return self._gtk_outer_widget.get_property('hscrollbar-policy') <> gtk.POLICY_NEVER + + def set_hscrolling(self, value): + self._gtk_outer_widget.set_property('hscrollbar-policy', gtk_scroll_policies[value]) + + def get_vscrolling(self): + return self._gtk_outer_widget.get_property('vscrollbar-policy') <> gtk.POLICY_NEVER + + def set_vscrolling(self, value): + self._gtk_outer_widget.set_property('vscrollbar-policy', gtk_scroll_policies[value]) + +export(Scrollable) diff --git a/GUI/Gtk/ScrollableView.py b/GUI/Gtk/ScrollableView.py new file mode 100644 index 0000000..8712d62 --- /dev/null +++ b/GUI/Gtk/ScrollableView.py @@ -0,0 +1,104 @@ +# +# Python GUI - Scrollable Views - Gtk +# + +import gtk +from GUI import export +from GUI import Scrollable +from GUI.GScrollableViews import ScrollableView as GScrollableView, \ + default_extent, default_line_scroll_amount, default_scrolling + +class ScrollableView(GScrollableView, Scrollable): + + def __init__(self, extent = default_extent, + line_scroll_amount = default_line_scroll_amount, + scrolling = default_scrolling, + **kwds): + gtk_scrolled_window = gtk.ScrolledWindow() + gtk_scrolled_window.show() + GScrollableView.__init__(self, _gtk_outer = gtk_scrolled_window, + extent = extent, line_scroll_amount = line_scroll_amount, + scrolling = scrolling) + self.set(**kwds) + + # + # Properties + # + + def get_border(self): + return self._gtk_outer_widget.get_shadow_type() <> gtk.SHADOW_NONE + + def set_border(self, x): + if x: + s = gtk.SHADOW_IN + else: + s = gtk.SHADOW_NONE + self._gtk_outer_widget.set_shadow_type(s) + + def get_content_width(self): + w = self._size[0] + if self.hscrolling: + w -= self.gtk_scrollbar_breadth + if self.border: + w -= 2 * self.gtk_border_thickness[0] + return w + + def get_content_height(self): + h = self._size[1] + if self.vscrolling: + h -= self.gtk_scrollbar_breadth + if self.border: + h -= 2 * self.gtk_border_thickness[1] + return h + + def get_content_size(self): + return self.content_width, self.content_height + + def set_content_size(self, size): + w, h = size + d = self.gtk_scrollbar_breadth + if self.hscrolling: + w += d + if self.vscrolling: + h += d + if self.border: + b = self.gtk_border_thickness + w += 2 * b[0] + h += 2 * b[1] + self.size = (w, h) + + def get_extent(self): + return self._gtk_inner_widget.get_size() + + def set_extent(self, (w, h)): + self._gtk_inner_widget.set_size(int(round(w)), int(round(h))) + + def get_scroll_offset(self): + hadj, vadj = self._gtk_adjustments() + return int(hadj.value), int(vadj.value) + + def set_scroll_offset(self, (x, y)): + hadj, vadj = self._gtk_adjustments() + hadj.set_value(min(float(x), hadj.upper - hadj.page_size)) + vadj.set_value(min(float(y), vadj.upper - vadj.page_size)) + + def get_line_scroll_amount(self): + hadj, vadj = self._gtk_adjustments() + return hadj.step_increment, vadj.step_increment + + def set_line_scroll_amount(self, (dx, dy)): + hadj, vadj = self._gtk_adjustments() + hadj.step_increment = float(dx) # Amazingly, ints are not + vadj.step_increment = float(dy) # acceptable here. + + # + # Internal + # + + def _gtk_adjustments(self): + gtk_widget = self._gtk_inner_widget + hadj = gtk_widget.get_hadjustment() + vadj = gtk_widget.get_vadjustment() + return hadj, vadj + +export(ScrollableView) diff --git a/GUI/Gtk/Slider.py b/GUI/Gtk/Slider.py new file mode 100644 index 0000000..015d177 --- /dev/null +++ b/GUI/Gtk/Slider.py @@ -0,0 +1,157 @@ +# +# Python GUI - Slider - Gtk +# + +import gtk +from GUI import export +from GUI.GSliders import Slider as GSlider + +class Slider(GSlider): + + _gtk_tick_length = 8 + _gtk_tick_inset = 18 + + def __init__(self, orient = 'h', ticks = 0, **kwds): + self._orient = orient + self._ticks = ticks + self._discrete = False + self._live = True + self._gtk_ticks = None + length = 100 + gtk_adjustment = gtk.Adjustment(upper = 1.0) + xs = 0.0 + ys = 0.0 + if orient == 'h': + gtk_scale = gtk.HScale(gtk_adjustment) + gtk_scale.set_size_request(length, -1) + gtk_box = gtk.VBox() + xs = 1.0 + elif orient == 'v': + gtk_scale = gtk.VScale(gtk_adjustment) + gtk_scale.set_size_request(-1, length) + gtk_box = gtk.HBox() + ys = 1.0 + else: + raise ValueError("Invalid orientation, should be 'h' or 'v'") + gtk_scale.set_draw_value(False) + self._gtk_scale = gtk_scale + gtk_box.pack_start(gtk_scale) + self._gtk_box = gtk_box + if ticks: + self._gtk_create_ticks() + gtk_alignment = gtk.Alignment(xalign = 0.5, yalign = 0.5, + xscale = xs, yscale = ys) + gtk_alignment.add(gtk_box) + gtk_alignment.show_all() + self._gtk_connect(gtk_adjustment, 'value-changed', self._gtk_value_changed) + self._gtk_connect(gtk_scale, 'change-value', self._gtk_change_value) + self._gtk_connect(gtk_scale, 'button-release-event', self._gtk_button_release) + self._gtk_scale = gtk_scale + self._gtk_adjustment = gtk_adjustment + self._gtk_enable_action = True + GSlider.__init__(self, _gtk_outer = gtk_alignment, **kwds) + + def get_min_value(self): + return self._min_value + + def set_min_value(self, x): + self._gtk_adjustment.lower = x + + def get_max_value(self): + return self._max_value + + def set_max_value(self, x): + self._gtk_adjustment.upper = x + + def get_value(self): + return self._gtk_adjustment.value + + def set_value(self, x): + self._gtk_enable_action = False + self._gtk_adjustment.value = x + self._gtk_enable_action = True + + def get_ticks(self): + return self._ticks + + def set_ticks(self, x): + self._ticks = x + if x: + self._gtk_create_ticks() + else: + self._gtk_destroy_ticks() + + def get_discrete(self): + return self._discrete + + def set_discrete(self, x): + self._discrete = x + + def get_live(self): + return self._live + + def set_live(self, x): + self._live = x + + def _gtk_create_ticks(self): + if not self._gtk_ticks: + gtk_ticks = gtk.DrawingArea() + length = self._gtk_tick_length + if self._orient == 'h': + gtk_ticks.set_size_request(-1, length) + else: + gtk_ticks.set_size_request(length, -1) + self._gtk_ticks = gtk_ticks + self._gtk_connect(gtk_ticks, 'expose-event', self._gtk_draw_ticks) + self._gtk_box.pack_start(gtk_ticks) + + def _gtk_destroy_ticks(self): + gtk_ticks = self._gtk_ticks + if gtk_ticks: + gtk_ticks.destroy() + self._gtk_ticks = None + + def _gtk_draw_ticks(self, event): + gtk_ticks = self._gtk_ticks + gdk_win = gtk_ticks.window + gtk_style = gtk_ticks.style + orient = self._orient + steps = self._ticks - 1 + _, _, w, h = gtk_ticks.allocation + u0 = self._gtk_tick_inset + v0 = 0 + if orient == 'h': + draw_line = gtk_style.paint_vline + u1 = w - u0 + v1 = h + else: + draw_line = gtk_style.paint_hline + u1 = h - u0 + v1 = w + state = gtk.STATE_NORMAL + for i in xrange(steps + 1): + u = u0 + i * (u1 - u0) / steps + draw_line(gdk_win, state, None, gtk_ticks, "", v0, v1, u) + + def _gtk_value_changed(self): + if self._live and self._gtk_enable_action: + self.do_action() + + def _gtk_change_value(self, event_type, value): + gtk_adjustment = self._gtk_adjustment + vmin = gtk_adjustment.lower + vmax = gtk_adjustment.upper + value = min(max(vmin, value), vmax) + if self._discrete: + steps = self._ticks - 1 + if steps > 0: + q = round(steps * (value - vmin) / (vmax - vmin)) + value = vmin + q * (vmax - vmin) / steps + if gtk_adjustment.value <> value: + gtk_adjustment.value = value + return True + + def _gtk_button_release(self, gtk_event): + self.do_action() + +export(Slider) diff --git a/GUI/Gtk/StdCursors.py b/GUI/Gtk/StdCursors.py new file mode 100644 index 0000000..be943e9 --- /dev/null +++ b/GUI/Gtk/StdCursors.py @@ -0,0 +1,30 @@ +# +# Python GUI - Standard Cursors - Gtk +# + +from gtk import gdk +from GUI import Cursor + +__all__ = [ + 'arrow', + 'ibeam', + 'crosshair', + 'fist', + 'hand', + 'finger', + 'invisible', +] + +arrow = Cursor._from_gtk_std_cursor(gdk.LEFT_PTR) +ibeam = Cursor._from_gtk_std_cursor(gdk.XTERM) +crosshair = Cursor._from_gtk_std_cursor(gdk.TCROSS) +fist = Cursor("cursors/fist.tiff") +hand = Cursor("cursors/hand.tiff") +finger = Cursor("cursors/finger.tiff") +invisible = Cursor._from_nothing() + +del gdk +del Cursor + +def empty_cursor(): + return invisible diff --git a/GUI/Gtk/StdFonts.py b/GUI/Gtk/StdFonts.py new file mode 100644 index 0000000..9281638 --- /dev/null +++ b/GUI/Gtk/StdFonts.py @@ -0,0 +1,9 @@ +# +# Python GUI - Standard Fonts - Gtk +# + +import gtk +from GUI import Font + +system_font = Font._from_pango_description(gtk.Label().style.font_desc) +application_font = Font._from_pango_description(gtk.Entry().style.font_desc) diff --git a/GUI/Gtk/StdMenus.py b/GUI/Gtk/StdMenus.py new file mode 100644 index 0000000..c60f653 --- /dev/null +++ b/GUI/Gtk/StdMenus.py @@ -0,0 +1,51 @@ +# +# Python GUI - Standard Menus - Gtk +# + +from GUI.GStdMenus import build_menus, \ + fundamental_cmds, help_cmds, pref_cmds, file_cmds, print_cmds, edit_cmds + +_file_menu_items = [ + ("New/N", 'new_cmd'), + ("Open.../O", 'open_cmd'), + ("Close/W", 'close_cmd'), + "-", + ("Save/S", 'save_cmd'), + ("Save As...", 'save_as_cmd'), + ("Revert", 'revert_cmd'), + "-", + ("Page Setup...", 'page_setup_cmd'), + ("Print.../P", 'print_cmd'), + "-", + ("Quit/Q", 'quit_cmd'), +] + +_edit_menu_items = [ + ("Undo/Z", 'undo_cmd'), + ("Redo/^Z", 'redo_cmd'), + "-", + ("Cut/X", 'cut_cmd'), + ("Copy/C", 'copy_cmd'), + ("Paste/V", 'paste_cmd'), + ("Clear", 'clear_cmd'), + "-", + ("Select All/A", 'select_all_cmd'), + "-", + ("Preferences...", 'preferences_cmd'), +] + +_help_menu_items = [ + ("About ", 'about_cmd'), +] + +#------------------------------------------------------------------------------ + +def basic_menus(substitutions = {}, include = None, exclude = None): + return build_menus([ + ("File", _file_menu_items, False), + ("Edit", _edit_menu_items, False), + ("Help", _help_menu_items, True), + ], + substitutions = substitutions, + include = include, + exclude = exclude) diff --git a/GUI/Gtk/Task.py b/GUI/Gtk/Task.py new file mode 100644 index 0000000..ae2a791 --- /dev/null +++ b/GUI/Gtk/Task.py @@ -0,0 +1,46 @@ +# +# PyGUI - Tasks - Gtk +# + +import gobject +from GUI import export +from GUI.GTasks import Task as GTask + +class Task(GTask): + + def __init__(self, proc, interval, repeat = 0, start = 1): + self._proc = proc + self._gtk_interval = int(interval * 1000) + self._repeat = repeat + self._gtk_timeout_id = None + if start: + self.start() + + def get_scheduled(self): + return self._gtk_timeout_id is not None + + def get_interval(self): + return self._gtk_interval / 1000.0 + + def get_repeat(self): + return self._repeat + + def start(self): + if self._gtk_timeout_id is None: + self._gtk_timeout_id = gobject.timeout_add(self._gtk_interval, + self._gtk_fire) + + def stop(self): + id = self._gtk_timeout_id + if id is not None: + gobject.source_remove(id) + self._gtk_timeout_id = None + + def _gtk_fire(self): + self._proc() + if self._repeat: + return 1 + else: + self._gtk_timeout_id = None + +export(Task) diff --git a/GUI/Gtk/TextEditor.py b/GUI/Gtk/TextEditor.py new file mode 100644 index 0000000..5a82cad --- /dev/null +++ b/GUI/Gtk/TextEditor.py @@ -0,0 +1,122 @@ +# +# Python GUI - Text Editor - Gtk +# + +import pango, gtk +from GUI import export +from GUI import application +from GUI import Scrollable +from GUI import Font +from GUI.GTextEditors import TextEditor as GTextEditor + +class TextEditor(GTextEditor, Scrollable): + + _font = None + + def __init__(self, scrolling = 'hv', **kwds): + gtk_sw = gtk.ScrolledWindow() + gtk_sw.show() + gtk_tv = gtk.TextView() + gtk_tv.show() + gtk_sw.add(gtk_tv) + gtk_tb = gtk.TextBuffer() + self._gtk_textbuffer = gtk_tb + gtk_tv.set_buffer(self._gtk_textbuffer) + tag = gtk.TextTag() + tabs = pango.TabArray(1, True) + tabs.set_tab(0, pango.TAB_LEFT, 28) + tag.set_property('tabs', tabs) + tag.set_property('tabs-set', True) + self._gtk_tag = tag + gtk_tb.get_tag_table().add(tag) + GTextEditor.__init__(self, _gtk_outer = gtk_sw, _gtk_inner = gtk_tv, + _gtk_focus = gtk_tv, **kwds) + self.set_hscrolling('h' in scrolling) + self.set_vscrolling('v' in scrolling) + if 'h' not in scrolling: + gtk_tv.set_wrap_mode(gtk.WRAP_WORD) + self._gtk_apply_tag() + + def _gtk_get_sel_iters(self): + gtk_textbuffer = self._gtk_textbuffer + sel_iters = gtk_textbuffer.get_selection_bounds() + if not sel_iters: + insert_mark = gtk_textbuffer.get_insert() + insert_iter = gtk_textbuffer.get_iter_at_mark(insert_mark) + sel_iters = (insert_iter, insert_iter) + return sel_iters + + def _gtk_apply_tag(self): + tb = self._gtk_textbuffer + tb.apply_tag(self._gtk_tag, tb.get_start_iter(), tb.get_end_iter()) + + def get_selection(self): + tb = self._gtk_textbuffer + bounds = tb.get_selection_bounds() + if bounds: + return (bounds[0].get_offset(), bounds[1].get_offset()) + else: + i = tb.get_property('cursor-position') + return (i, i) + + def set_selection(self, value): + tb = self._gtk_textbuffer + start = tb.get_iter_at_offset(value[0]) + end = tb.get_iter_at_offset(value[1]) + tb.select_range(start, end) + + def get_text(self): + tb = self._gtk_textbuffer + start = tb.get_start_iter() + end = tb.get_end_iter() + return tb.get_slice(start, end) + + def set_text(self, text): + self._gtk_textbuffer.set_text(text) + self._gtk_apply_tag() + + def get_text_length(self): + return self._gtk_textbuffer.get_end_iter().get_offset() + + def get_font(self): + font = self._font + if not font: + font = Font._from_pango_description(self._gtk_inner_widget.style.font_desc) + return font + + def set_font(self, f): + self._font = f + tv = self._gtk_inner_widget + tv.modify_font(f._pango_description) + + def get_tab_spacing(self): + tabs = self._gtk_tag.get_property('tabs') + return tabs.get_tab(0)[1] + + def set_tab_spacing(self, x): + tabs = pango.TabArray(1, True) + tabs.set_tab(0, pango.TAB_LEFT, x) + self._gtk_tag.set_property('tabs', tabs) + + def cut_cmd(self): + self.copy_cmd() + self.clear_cmd() + + def copy_cmd(self): + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + text = gtk_textbuffer.get_text(start_iter, end_iter, 1) + if text: + application().set_clipboard(text) + + def paste_cmd(self): + text = application().get_clipboard() + self.clear_cmd() + self._gtk_textbuffer.insert_at_cursor(text) + + def clear_cmd(self): + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + gtk_textbuffer.delete(start_iter, end_iter) + +export(TextEditor) diff --git a/GUI/Gtk/TextField.py b/GUI/Gtk/TextField.py new file mode 100644 index 0000000..8ad6bc4 --- /dev/null +++ b/GUI/Gtk/TextField.py @@ -0,0 +1,188 @@ +# +# Python GUI - Text fields - Gtk +# + +import gtk +from GUI import export +from GUI.Properties import overridable_property +from GUI import application +from GUI.StdFonts import application_font +from GUI.GTextFields import TextField as GTextField + +gtk_margins = (2, 2, 0, 0) + +class TextField(GTextField): + + _pass_key_events_to_platform = True + + _multiline = 0 + + def __init__(self, font = application_font, lines = 1, + multiline = 0, password = 0, **kwds): + self._multiline = multiline + lm, tm, rm, bm = gtk_margins + if multiline: + gtk_textbuffer = gtk.TextBuffer() + gtk_textview = gtk.TextView(gtk_textbuffer) + #gtk_textview.set_accepts_tab(False) #^%$#^%$!!! moves the focus itself. + #self._gtk_connect(gtk_textview, 'key-press-event', self._gtk_key_press_event) + gtk_alignment = gtk.Alignment(0.5, 0.5, 1.0, 1.0) + gtk_alignment.set_padding(tm, bm, lm, rm) + gtk_alignment.add(gtk_textview) + gtk_box = gtk.EventBox() + gtk_box.add(gtk_alignment) + gtk_box.modify_bg(gtk.STATE_NORMAL, + gtk_textview.style.base[gtk.STATE_NORMAL]) + gtk_frame = gtk.Frame() + gtk_frame.set_shadow_type(gtk.SHADOW_IN) + gtk_frame.add(gtk_box) + self._gtk_textbuffer = gtk_textbuffer + gtk_text_widget = gtk_textview + gtk_outer = gtk_frame + else: + gtk_entry = gtk.Entry() + #self._gtk_connect(gtk_entry, 'key-press-event', self._gtk_key_press_event) + self._gtk_entry = gtk_entry + gtk_text_widget = gtk_entry + gtk_outer = gtk_entry + self._font = font + gtk_text_widget.modify_font(font._pango_description) + self._vertical_padding = tm + 2 * gtk_outer.style.ythickness + bm + height = self._vertical_padding + lines * font.text_size("X")[1] + gtk_outer.set_size_request(-1, height) + self._password = password + if password: + if not multiline: + self._gtk_entry.set_visibility(0) + self._gtk_entry.set_invisible_char("*") + else: + raise ValueError("The password option is not supported for multiline" + " TextFields on this platform") + gtk_outer.show_all() + GTextField.__init__(self, + _gtk_outer = gtk_outer, + _gtk_title = gtk_text_widget, + _gtk_focus = gtk_text_widget, + _gtk_input = gtk_text_widget, + multiline = multiline, **kwds) + + def get_text(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start = gtk_textbuffer.get_start_iter() + end = gtk_textbuffer.get_end_iter() + return self._gtk_textbuffer.get_text(start, end, 1) + else: + return self._gtk_entry.get_text() + + def set_text(self, text): + if self._multiline: + self._gtk_textbuffer.set_text(text) + else: + self._gtk_entry.set_text(text) + + def get_selection(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + start = start_iter.get_offset() + end = end_iter.get_offset() + sel = (start, end) + else: + sel = self._gtk_get_sel_positions() + return sel + + def _gtk_get_sel_iters(self): + gtk_textbuffer = self._gtk_textbuffer + sel_iters = gtk_textbuffer.get_selection_bounds() + if not sel_iters: + insert_mark = gtk_textbuffer.get_insert() + insert_iter = gtk_textbuffer.get_iter_at_mark(insert_mark) + sel_iters = (insert_iter, insert_iter) + return sel_iters + + def _gtk_get_sel_positions(self): + gtk_entry = self._gtk_entry + sel = gtk_entry.get_selection_bounds() + if not sel: + pos = gtk_entry.get_position() + sel = (pos, pos) + return sel + + def _set_selection(self, start, end): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter = gtk_textbuffer.get_iter_at_offset(start) + end_iter = gtk_textbuffer.get_iter_at_offset(end) + gtk_textbuffer.select_range(start_iter, end_iter) + else: + self._gtk_entry.select_region(start, end) + + def set_selection(self, (start, end)): + self._set_selection(start, end) + self.become_target() + + def get_multiline(self): + return self._multiline + + def get_password(self): + return self._password + + def _select_all(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start = gtk_textbuffer.get_start_iter() + end = gtk_textbuffer.get_end_iter() + gtk_textbuffer.select_range(start, end) + else: + self._gtk_entry.select_region(0, -1) + + def select_all(self): + self._select_all() + self.become_target() + + def cut_cmd(self): + self.copy_cmd() + self.clear_cmd() + + def copy_cmd(self): + if self._password: + return + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + text = gtk_textbuffer.get_text(start_iter, end_iter, 1) + else: + start, end = self._gtk_get_sel_positions() + text = self._gtk_entry.get_chars(start, end) + if text: + application().set_clipboard(text) + + def paste_cmd(self): + text = application().get_clipboard() + self.clear_cmd() + if self._multiline: + self._gtk_textbuffer.insert_at_cursor(text) + else: + gtk_entry = self._gtk_entry + pos = gtk_entry.get_position() + gtk_entry.insert_text(text, pos) + gtk_entry.set_position(pos + len(text)) + + def clear_cmd(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + gtk_textbuffer.delete(start_iter, end_iter) + else: + start, end = self._gtk_get_sel_positions() + self._gtk_entry.delete_text(start, end) + + def _untargeted(self): + self._set_selection(0, 0) + + def _tab_in(self): + self._select_all() + GTextField._tab_in(self) + +export(TextField) diff --git a/GUI/Gtk/View.py b/GUI/Gtk/View.py new file mode 100644 index 0000000..b507db7 --- /dev/null +++ b/GUI/Gtk/View.py @@ -0,0 +1,8 @@ +# +# Python GUI - Views - Gtk +# + +from GUI import export +from GUI.GViews import View + +export(View) diff --git a/GUI/Gtk/ViewBase.py b/GUI/Gtk/ViewBase.py new file mode 100644 index 0000000..8cea9e2 --- /dev/null +++ b/GUI/Gtk/ViewBase.py @@ -0,0 +1,45 @@ +# +# Python GUI - View Base - Gtk +# + +import gtk +from GUI import export +from GUI.GViewBases import ViewBase as GViewBase + +class ViewBase(GViewBase): + + def __init__(self, **kwds): + GViewBase.__init__(self, **kwds) + self._gtk_connect(self._gtk_inner_widget, 'realize', self._gtk_realize) + + def track_mouse(self): + finished = 0 + while not finished: + self._mouse_event = None + while not self._mouse_event: + gtk.main_iteration() + event = self._mouse_event + if event.kind == 'mouse_up': + finished = 1 + yield event + + def _cursor_changed(self): + gtk_widget = self._gtk_inner_widget + gdk_window = gtk_widget.window + if gdk_window: + cursor = self._cursor + if cursor: + gdk_window.set_cursor(self._cursor._gtk_cursor) + else: + gdk_window.set_cursor(None) + + def _gtk_realize(self): + self._cursor_changed() + + def _targeted(self): + self.targeted() + + def _untargeted(self): + self.untargeted() + +export(ViewBase) diff --git a/GUI/Gtk/Window.py b/GUI/Gtk/Window.py new file mode 100644 index 0000000..15da4ff --- /dev/null +++ b/GUI/Gtk/Window.py @@ -0,0 +1,263 @@ +# +# Python GUI - Windows - Gtk version +# + +import sys +import gtk +from gtk import gdk +from GUI import export +from GUI import export +from GUI.GGeometry import sub_pt +from GUI import Component +from GUI import Container +from GUI import application +from GUI.GWindows import Window as GWindow + +_default_width = 200 +_default_height = 200 + +_modal_styles = ('modal_dialog', 'alert') +_dialog_styles = ('nonmodal_dialog', 'modal_dialog', 'alert') + +class Window(GWindow): + + #_pass_key_events_to_platform = False + + _size = (_default_width, _default_height) + _gtk_menubar = None + _need_menubar_update = 0 + _target = None + + def __init__(self, style = 'standard', title = "New Window", + movable = 1, closable = 1, hidable = None, resizable = 1, + zoomable = 1, **kwds): + self._all_menus = [] + modal = style in _modal_styles + if hidable is None: + hidable = not modal + self._resizable = resizable + gtk_win = gtk.Window(gtk.WINDOW_TOPLEVEL) + gtk_win.set_gravity(gdk.GRAVITY_STATIC) + gtk_win.set_decorated(style <> 'fullscreen' + and (movable or closable or hidable or zoomable)) + gtk_win.set_resizable(resizable) + gtk_win.set_modal(style in _modal_styles) + gtk_content = gtk.Layout() + gtk_content.show() + if style in _dialog_styles: + gtk_win.set_type_hint(gdk.WINDOW_TYPE_HINT_DIALOG) + gtk_win.add(gtk_content) + else: + self._gtk_create_menubar() + gtk_box = gtk.VBox() + gtk_box.show() + gtk_box.pack_start(self._gtk_menubar, expand = 0, fill = 0) + gtk_box.pack_end(gtk_content, expand = 1, fill = 1) + gtk_win.add(gtk_box) + self._need_menubar_update = 1 + self._gtk_connect(gtk_win, 'configure-event', self._gtk_configure_event) + self._gtk_connect(gtk_win, 'key-press-event', self._gtk_key_press_event) + self._gtk_connect(gtk_win, 'delete-event', self._gtk_delete_event) + GWindow.__init__(self, _gtk_outer = gtk_win, _gtk_inner = gtk_content, + _gtk_focus = gtk_content, _gtk_input = gtk_content, + style = style, title = title, closable = closable) + if style == 'fullscreen': + size = (gdk.screen_width(), gdk.screen_height()) + else: + size = (_default_width, _default_height) + self.set_size(size) + self.set(**kwds) + self.become_target() + + def _gtk_create_menubar(self): + gtk_menubar = gtk.MenuBar() + gtk_dummy_item = gtk.MenuItem("") + gtk_menubar.append(gtk_dummy_item) + gtk_menubar.show_all() + h = gtk_menubar.size_request()[1] + gtk_menubar.set_size_request(-1, h) + gtk_dummy_item.remove_submenu() + self._gtk_menubar = gtk_menubar + self._gtk_connect(gtk_menubar, 'button-press-event', + self._gtk_menubar_button_press_event) + + def destroy(self): + self.hide() + GWindow.destroy(self) + + def set_menus(self, x): + GWindow.set_menus(self, x) + self._need_menubar_update = 1 + if self.visible: + self._gtk_update_menubar() + + def get_title(self): + return self._gtk_outer_widget.get_title() + + def set_title(self, new_title): + self._gtk_outer_widget.set_title(new_title) + + def set_position(self, v): + self._position = v + self._gtk_outer_widget.move(*v) + + def set_size(self, new_size): + w, h = new_size + if self._resizable: + h += self._gtk_menubar_height() + gtk_resize = self._gtk_outer_widget.resize + else: + gtk_resize = self._gtk_inner_widget.set_size_request + gtk_resize(max(w, 1), max(h, 1)) + self._size = new_size + + def _gtk_configure_event(self, gtk_event): + gtk_win = self._gtk_outer_widget + self._position = gtk_win.get_position() + #self._update_size(gtk_win.get_size()) + w, h = gtk_win.get_size() + #w, h = self._gtk_inner_widget.get_size() + #w, h = self._gtk_inner_widget.size_request() + old_size = self._size + new_size = (w, h - self._gtk_menubar_height()) + #new_size = (w, h) + #print "Window._gtk_configure_event:", old_size, "->", new_size ### + self._size = new_size + if old_size <> new_size: + self._resized(sub_pt(new_size, old_size)) + + def get_visible(self): + return self._gtk_outer_widget.get_property('visible') + + def set_visible(self, new_v): + old_v = self.visible + self._gtk_outer_widget.set_property('visible', new_v) + if new_v and not old_v and self._need_menubar_update: + self._gtk_update_menubar() + + def _show(self): + self.set_visible(1) + self._gtk_outer_widget.present() + +# def key_down(self, event): +# if event.char == '\t': +# if event.shift: +# self._tab_to_prev() +# else: +# self._tab_to_next() +# else: +# self.pass_to_next_handler('key_down', event) + + def get_target(self): + target = Component._gtk_find_component(self._gtk_outer_widget.get_focus()) + return target or self + + def _screen_rect(self): + w = gdk.screen_width() + h = gdk.screen_height() + return (0, 0, w, h) + + def _gtk_menubar_height(self): + mb = self._gtk_menubar + if mb: + h = mb.size_request()[1] + else: + h = 0 + #print "Window._gtk_menubar_height -->", h ### + return h + + def _gtk_delete_event(self, event): + try: + self.close_cmd() + except: + sys.excepthook(*sys.exc_info()) + return 1 + + def _gtk_update_menubar(self): + # + # Update the contents of the menubar after either the application + # menu list or this window's menu list has changed. We only add + # the menu titles at this stage; the menus themselves are attached + # during menu setup. We also attach the accel groups associated + # with the new menus. + # + # Things would be simpler if we could attach the menus here, + # but attempting to share menus between menubar items provokes + # a warning from Gtk, even though it otherwise appears to work. + # + gtk_menubar = self._gtk_menubar + gtk_window = self._gtk_outer_widget + # Remove old accel groups + for menu in self._all_menus: + gtk_window.remove_accel_group(menu._gtk_accel_group) + # Detach any existing menus and remove old menu titles + if gtk_menubar: + for gtk_menubar_item in gtk_menubar.get_children(): + gtk_menubar_item.remove_submenu() + gtk_menubar_item.destroy() + # Install new menu list + #all_menus = application().menus + self.menus + all_menus = application()._effective_menus_for_window(self) + self._all_menus = all_menus + # Create new menu titles and attach accel groups + for menu in all_menus: + if gtk_menubar: + gtk_menubar_item = gtk.MenuItem(menu._title) + gtk_menubar_item.show() + gtk_menubar.append(gtk_menubar_item) + gtk_window.add_accel_group(menu._gtk_accel_group) + self._need_menubar_update = 0 + + def _gtk_menubar_button_press_event(self, event): + # A button press has occurred in the menu bar. Before pulling + # down the menu, perform menu setup and attach the menus to + # the menubar items. + self._gtk_menu_setup() + for (gtk_menubar_item, menu) in \ + zip(self._gtk_menubar.get_children(), self._all_menus): + gtk_menu = menu._gtk_menu + attached_widget = gtk_menu.get_attach_widget() + if attached_widget and attached_widget is not gtk_menubar_item: + attached_widget.remove_submenu() + gtk_menubar_item.set_submenu(gtk_menu) + + def _gtk_key_press_event(self, gtk_event): + # Intercept key presses with the Control key down and update + # menus, in case this is a keyboard equivalent for a menu command. + if gtk_event.state & gdk.CONTROL_MASK: + #print "Window._gtk_key_press_event: doing menu setup" + self._gtk_menu_setup() + # It appears that GtkWindow caches accelerators, and updates + # the cache in an idle task after accelerators change. This + # would be too late for us, so we force it to be done now. + self._gtk_outer_widget.emit("keys-changed") + #print "Window._gtk_key_press_event: done menu setup" + + def _gtk_menu_setup(self): + application()._perform_menu_setup(self._all_menus) + + def _default_key_event(self, event): + self.pass_event_to_next_handler(event) + if event._originator is self: + event._not_handled = True + + def dispatch(self, message, *args): + self.target.handle(message, *args) + + +_gtk_menubar_height = None + +def _gtk_find_menubar_height(): + global _gtk_menubar_height + if _gtk_menubar_height is None: + print "Windows: Finding menubar height" + item = gtk.MenuItem("X") + bar = gtk.MenuBar() + bar.append(item) + bar.show_all() + w, h = bar.size_request() + _gtk_menubar_height = h + print "...done" + return _gtk_menubar_height + +export(Window) diff --git a/GUI/GtkGI/AlertClasses.py b/GUI/GtkGI/AlertClasses.py new file mode 100644 index 0000000..806fb0d --- /dev/null +++ b/GUI/GtkGI/AlertClasses.py @@ -0,0 +1,5 @@ +# +# Python GUI - Alerts - Gtk +# + +from GUI.GAlertClasses import Alert, Alert2, Alert3 diff --git a/GUI/GtkGI/Applications.py b/GUI/GtkGI/Applications.py new file mode 100644 index 0000000..1e0c9d4 --- /dev/null +++ b/GUI/GtkGI/Applications.py @@ -0,0 +1,81 @@ +# +# Python GUI - Application class - Gtk +# + +import sys +from gi.repository import Gtk, Gdk +from GUI.Globals import application +from GUI.GApplications import Application as GApplication + +class Application(GApplication): + + _in_gtk_main = 0 + + def run(self): + GApplication.run(self) + + def set_menus(self, menu_list): + GApplication.set_menus(self, menu_list) + for window in self._windows: + window._gtk_update_menubar() + +# def handle_events(self): +# #print "Application.handle_events: entering Gtk.main" ### +# _call_with_excepthook(Gtk.main, Gtk.main_quit) +# #print "Application.handle_events: returned from Gtk.main" ### + + def handle_next_event(self, modal_window = None): + _call_with_excepthook(Gtk.main_iteration) + +# def _quit(self): +# self._quit_flag = True +# Gtk.main_quit() + +# def _exit_event_loop(self): +# Gtk.main_quit() + + def get_target_window(self): + for window in self._windows: + if window._gtk_outer_widget.has_toplevel_focus(): + return window + return None + + def zero_windows_allowed(self): + return 0 + + def query_clipboard(self): + return _gtk_clipboard.wait_is_text_available() + + def get_clipboard(self): + return _gtk_clipboard.wait_for_text() + + def set_clipboard(self, data): + _gtk_clipboard.set_text(data, len(data)) + +#------------------------------------------------------------------------------ + +CLIPBOARD = Gdk.atom_intern("CLIPBOARD", False) + +_gtk_clipboard = Gtk.Clipboard.get(CLIPBOARD) + +#------------------------------------------------------------------------------ + +def _call_with_excepthook(proc, breakout = None): + # This function arranges for exceptions to be propagated + # across calls to the Gtk event loop functions. + exc_info = [] + def excepthook(*args): + exc_info[:] = args + if breakout: + breakout() + old_excepthook = sys.excepthook + try: + sys.excepthook = excepthook + proc() + finally: + sys.excepthook = old_excepthook + if exc_info: + #print "_call_with_excepthook: raising", exc_info ### + raise exc_info[0], exc_info[1], exc_info[2] + + \ No newline at end of file diff --git a/GUI/GtkGI/BaseAlerts.py b/GUI/GtkGI/BaseAlerts.py new file mode 100644 index 0000000..6846267 --- /dev/null +++ b/GUI/GtkGI/BaseAlerts.py @@ -0,0 +1,24 @@ +# +# Python GUI - Alert base class - Gtk +# + +from gi.repository import Gtk +from GUI.GBaseAlerts import BaseAlert as GBaseAlert + +_kind_to_gtk_stock_id = { + 'stop': Gtk.STOCK_DIALOG_ERROR, + 'caution': Gtk.STOCK_DIALOG_WARNING, + 'note': Gtk.STOCK_DIALOG_INFO, + 'query': Gtk.STOCK_DIALOG_QUESTION, +} + +class BaseAlert(GBaseAlert): + + def _layout_icon(self, kind): + gtk_stock_id = _kind_to_gtk_stock_id[kind] + gtk_icon = Gtk.Image.new_from_stock(gtk_stock_id, Gtk.IconSize.DIALOG) + gtk_icon.show() + icon_size = gtk_icon.size_request() + self._gtk_inner_widget.put(gtk_icon, self._left_margin, self._top_margin) + return icon_size.width, icon_size.height + diff --git a/GUI/GtkGI/BaseFileDialogs.py b/GUI/GtkGI/BaseFileDialogs.py new file mode 100644 index 0000000..d8d4f52 --- /dev/null +++ b/GUI/GtkGI/BaseFileDialogs.py @@ -0,0 +1,164 @@ +# +# Python GUI - File selection dialogs - Gtk +# + +import os +from gi.repository import Gtk +from GUI.Files import FileRef +from GUI.AlertFunctions import confirm +from GUI.Applications import application + +#------------------------------------------------------------------ + +class _FileDialog(Gtk.FileChooserDialog): + + def __init__(self, ok_label, **kwds): + Gtk.FileChooserDialog.__init__(self, **kwds) + self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT) + self.add_button(ok_label, Gtk.ResponseType.ACCEPT) + self.connect('response', self.response) + self.set_default_size(600, 600) + self.set_position(Gtk.WindowPosition.CENTER) + + def add_file_type(self, file_type): + suffix = file_type.suffix + if suffix: + filter = Gtk.FileFilter() + name = file_type.name + if name: + filter.set_name(name) + filter.add_pattern("*.%s" % suffix) + self.add_filter(filter) + + def present_modally(self): + return self.run() == Gtk.ResponseType.ACCEPT + + def response(self, _, id): + #print "_FileDialog.response:", id ### + if id == Gtk.ResponseType.ACCEPT: + if not self.check(): + self.stop_emission('response') + + def check(self): + return True + +#------------------------------------------------------------------ + +class _SaveFileDialog(_FileDialog): + + def check(self): + path = self.get_filename() + print "_SaveFileDialog.ok: checking path %r" % path ### + #if path is None: + # return False + if not os.path.exists(path): + return True + else: + result = confirm("Replace existing '%s'?" % os.path.basename(path), + "Cancel", "Replace", cancel = None) + return result == 0 + +#------------------------------------------------------------------ + +def _request_old(prompt, default_dir, file_types, dir, multiple): + + if prompt.endswith(":"): + prompt = prompt[:-1] + if dir: + action = Gtk.FileChooserAction.SELECT_FOLDER + else: + action = Gtk.FileChooserAction.OPEN + dlog = _FileDialog(title = prompt, action = action, + ok_label = Gtk.STOCK_OPEN) + dlog.set_select_multiple(multiple) + if file_types: + for file_type in file_types: + dlog.add_file_type(file_type) + if default_dir: + dlog.set_current_folder(default_dir.path) + if dlog.present_modally(): + if multiple: + result = [FileRef(path = path) for path in dlog.get_filenames()] + else: + result = FileRef(path = dlog.get_filename()) + else: + result = None + dlog.destroy() + return result + +#------------------------------------------------------------------ + +def _request_new(prompt, default_dir, default_name, file_type, dir): +# if dir: +# action = Gtk.FileChooserAction.CREATE_FOLDER +# else: + action = Gtk.FileChooserAction.SAVE + if prompt.endswith(":"): + prompt = prompt[:-1] + dlog = _SaveFileDialog(title = prompt, action = action, + ok_label = Gtk.STOCK_SAVE) + if file_type: + dlog.add_file_type(file_type) + if default_dir: + dlog.set_current_folder(default_dir.path) + if default_name: + dlog.set_current_name(default_name) + if dlog.present_modally(): + path = dlog.get_filename() + if file_type: + path = file_type._add_suffix(path) + result = FileRef(path = path) + else: + result = None + dlog.destroy() + return result + +#------------------------------------------------------------------ + +#def request_new_file(prompt = "Save File", default_dir = None, +# default_name = "", file_type = None): +# """Present a dialog requesting a name and location for a new file. +# Returns a FileRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _SaveFileDialog(title = prompt, ok_label = Gtk.STOCK_SAVE, +# action = Gtk.FileChooserAction.SAVE) +# if file_type: +# dlog.add_file_type(file_type) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if default_name: +# dlog.set_current_name(default_name) +# if dlog.present_modally(): +# path = dlog.get_filename() +# if file_type: +# path = file_type._add_suffix(path) +# result = FileRef(path = path) +# else: +# result = None +# dlog.destroy() +# return result + +#------------------------------------------------------------------ + +#def request_new_directory(prompt = "Create Folder", default_dir = None, +# default_name = ""): +# """Present a dialog requesting a name and location for a new directory. +# Returns a FileRef, or None if cancelled.""" +# +# if prompt.endswith(":"): +# prompt = prompt[:-1] +# dlog = _SaveFileDialog(title = prompt, ok_label = Gtk.STOCK_SAVE, +# action = Gtk.FileChooserAction.CREATE_FOLDER) +# if default_dir: +# dlog.set_current_folder(default_dir.path) +# if default_name: +# dlog.set_current_name(default_name) +# if dlog.present_modally(): +# path = dlog.get_filename() +# result = FileRef(path = path) +# else: +# result = None +# dlog.destroy() +# return result diff --git a/GUI/GtkGI/Buttons.py b/GUI/GtkGI/Buttons.py new file mode 100644 index 0000000..7318d2a --- /dev/null +++ b/GUI/GtkGI/Buttons.py @@ -0,0 +1,97 @@ +# +# Python GUI - Buttons - Gtk version +# + +from gi.repository import Gtk +from GUI.StdFonts import system_font +from GUI.GButtons import Button as GButton + +_gtk_extra_hpad = 5 # Amount to add to default width at each end +_gtk_icon_spacing = 2 # Space to leave between icon and label + +class Button(GButton): + + _gtk_icon = None # Icon, when we have one + _style = 'normal' # or 'default' or 'cancel' + + def __init__(self, title = "Button", #style = 'normal', + font = system_font, **kwds): + gtk_label = Gtk.Label(label=title) + gtk_box = Gtk.HBox(spacing = _gtk_icon_spacing) + gtk_box.pack_end(gtk_label, True, True, 0) + gtk_alignment = Gtk.Alignment.new(0.5, 0.5, 0.0, 0.0) + hp = _gtk_extra_hpad + gtk_alignment.set_padding(0, 0, hp, hp) + gtk_alignment.add(gtk_box) + gtk_button = Gtk.Button() + gtk_button.add(gtk_alignment) + gtk_button.set_focus_on_click(False) + gtk_button.show_all() + w, h = font.text_size(title) + w2 = w + 2 * _gtk_button_hpad + _gtk_icon_width + _gtk_icon_spacing + h2 = max(h + 2 * _gtk_button_vpad, _gtk_default_button_height) + gtk_button.set_size_request(w2, h2) + self._gtk_box = gtk_box + self._gtk_alignment = gtk_alignment + self._gtk_connect(gtk_button, 'clicked', self._gtk_clicked_signal) + GButton.__init__(self, _gtk_outer = gtk_button, _gtk_title = gtk_label, + font = font, **kwds) + + def _gtk_get_alignment(self): + return self._gtk_alignment.get_property('xalign') + + def _gtk_set_alignment(self, fraction, just): + self._gtk_alignment.set_property('xalign', fraction) + self._gtk_title_widget.set_justify(just) + + def get_style(self): + return self._style + + def set_style(self, new_style): + if self._style <> new_style: + if new_style == 'default': + self._gtk_add_icon(Gtk.STOCK_OK) + elif new_style == 'cancel': + self._gtk_add_icon(Gtk.STOCK_CANCEL) + else: + self._gtk_remove_icon() + self._style = new_style + + def _gtk_add_icon(self, gtk_stock_id): + gtk_icon = Gtk.Image.new_from_stock(gtk_stock_id, Gtk.IconSize.BUTTON) + gtk_icon.show() + self._gtk_box.pack_start(gtk_icon, True, True, 0) + self._gtk_icon = gtk_icon + + def _gtk_remove_icon(self): + gtk_icon = self._gtk_icon + if gtk_icon: + gtk_icon.destroy() + self._gtk_icon = None + + def activate(self): + """Highlight the button momentarily and then perform its action.""" + self._gtk_outer_widget.activate() + + def _gtk_clicked_signal(self): + self.do_action() + + +def _calc_size_constants(): + global _gtk_icon_width, _gtk_default_button_height + global _gtk_button_hpad, _gtk_button_vpad + gtk_icon = Gtk.Image.new_from_stock(Gtk.STOCK_OK, Gtk.IconSize.BUTTON) + gtk_button = Gtk.Button() + gtk_button.add(gtk_icon) + gtk_button.show_all() + icon = gtk_icon.size_request() + butn = gtk_button.size_request() + _gtk_icon_width = icon.width + _gtk_default_button_height = butn.height + _gtk_button_hpad = (butn.width - icon.width) / 2 + _gtk_extra_hpad + _gtk_button_vpad = (butn.height - icon.height) / 2 + gtk_button.destroy() + +_calc_size_constants() +del _calc_size_constants + diff --git a/GUI/GtkGI/Canvases.py b/GUI/GtkGI/Canvases.py new file mode 100644 index 0000000..5f5d59e --- /dev/null +++ b/GUI/GtkGI/Canvases.py @@ -0,0 +1,253 @@ +#-------------------------------------------------------------------- +# +# Python GUI - Canvas - Gtk +# +#-------------------------------------------------------------------- + +from math import sin, cos, pi, floor +from gi.repository import cairo, PangoCairo +print "GUI.Canvases (GtkGI): TODO: Import cairo from gi.repository" +from cairo import OPERATOR_OVER, OPERATOR_SOURCE, FILL_RULE_EVEN_ODD +from GUI.Geometry import sect_rect +from GUI.StdFonts import application_font +from GUI.StdColors import black, white +from GUI.GCanvases import Canvas as GCanvas +from GUI.GCanvasPaths import CanvasPaths as GCanvasPaths + +deg = pi / 180 +twopi = 2 * pi + +show_layout = PangoCairo.show_layout + +#-------------------------------------------------------------------- + +class GState(object): + + pencolor = black + fillcolor = black + textcolor = black + backcolor = white + pensize = 1 + font = application_font + + def __init__(self, clone = None): + if clone: + self.__dict__.update(clone.__dict__) + +#-------------------------------------------------------------------- + +class Canvas(GCanvas, GCanvasPaths): + + def _from_gdk_drawable(cls, gdk_drawable): + return cls(gdk_drawable.cairo_create()) + + _from_gdk_drawable = classmethod(_from_gdk_drawable) + + def _from_cairo_context(cls, ctx): + return cls(ctx) + + _from_cairo_context = classmethod(_from_cairo_context) + + def __init__(self, ctx): + ctx.set_fill_rule(FILL_RULE_EVEN_ODD) + self._gtk_ctx = ctx + self._gstack = [] + self._state = GState() + GCanvas.__init__(self) + GCanvasPaths.__init__(self) + + def get_pencolor(self): + return self._state.pencolor + + def set_pencolor(self, c): + self._state.pencolor = c + + def get_fillcolor(self): + return self._state.fillcolor + + def set_fillcolor(self, c): + self._state.fillcolor = c + + def get_textcolor(self): + return self._state.textcolor + + def set_textcolor(self, c): + self._state.textcolor = c + + def get_backcolor(self): + return self._state.backcolor + + def set_backcolor(self, c): + self._state.backcolor = c + + def get_pensize(self): + return self._state.pensize + + def set_pensize(self, d): + self._state.pensize = d + + def get_font(self): + return self._state.font + + def set_font(self, f): + self._state.font = f + + def get_current_point(self): + return self._gtk_ctx.get_current_point() + + def rectclip(self, r): + l, t, r, b = r + ctx = self._gtk_ctx + ctx.new_path() + ctx.rectangle(l, t, r - l, b - t) + ctx.clip() + + def gsave(self): + old_state = self._state + self._gstack.append(old_state) + old_state = GState(old_state) + self._gtk_ctx.save() + + def grestore(self): + self._state = self._gstack.pop() + self._gtk_ctx.restore() + + def newpath(self): + self._gtk_ctx.new_path() + + def moveto(self, x, y): + self._gtk_ctx.move_to(x, y) + + def rmoveto(self, x, y): + self._gtk_ctx.rel_move_to(x, y) + + def lineto(self, x, y): + self._gtk_ctx.line_to(x, y) + + def rlineto(self, x, y): + self._gtk_ctx.rel_line_to(x, y) + + def curveto(self, p1, p2, p3): + self._gtk_ctx.curve_to(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) + + def rcurveto(self, p1, p2, p3): + self._gtk_ctx.rel_curve_to(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) + + def arc(self, c, r, a0, a1): + self._gtk_ctx.arc(c[0], c[1], r, a0 * deg, a1 * deg) + + def closepath(self): + ctx = self._gtk_ctx + ctx.close_path() + ctx.new_sub_path() + + def clip(self): + self._gtk_ctx.clip_preserve() + + def stroke(self): + state = self._state + ctx = self._gtk_ctx + ctx.set_source_rgba(*state.pencolor._rgba) + #ctx.set_source_color(state.pencolor._gdk_color) + ctx.set_line_width(state.pensize) + ctx.stroke_preserve() + + def fill(self): + ctx = self._gtk_ctx + ctx.set_source_rgba(*self._state.fillcolor._rgba) + #ctx.set_source_color(self._state.fillcolor._gdk_color) + ctx.fill_preserve() + + def erase(self): + ctx = self._gtk_ctx + ctx.set_source_rgba(*self._state.backcolor._rgba) + #ctx.set_source_color(self._state.backcolor._gdk_color) + ctx.set_operator(OPERATOR_SOURCE) + ctx.fill_preserve() + ctx.set_operator(OPERATOR_OVER) + + def show_text(self, text): + font = self._state.font + layout = font._get_pango_layout(text, True) + dx = layout.get_pixel_size()[0] + dy = font.ascent + ctx = self._gtk_ctx + ctx.set_source_rgba(*self._state.textcolor._rgba) + #ctx.set_source_color(self._state.textcolor._gdk_color) + ctx.rel_move_to(0, -dy) + show_layout(ctx, layout) + ctx.rel_move_to(dx, dy) + + def rect(self, rect): + l, t, r, b = rect + self._gtk_ctx.rectangle(l, t, r - l, b - t) + + def oval(self, rect): + l, t, r, b = rect + a = 0.5 * (r - l) + b = 0.5 * (b - t) + ctx = self._gtk_ctx + ctx.new_sub_path() + ctx.save() + ctx.translate(l + a, t + b) + ctx.scale(a, b) + ctx.arc(0, 0, 1, 0, twopi) + ctx.close_path() + ctx.restore() + + def translate(self, dx, dy): + self._gtk_ctx.translate(dx, dy) + + def rotate(self, degrees): + self._gtk_ctx.rotate(degrees*math.pi/180) + + def scale(self, xscale, yscale): + self._gtk_ctx.scale(xscale, yscale) + +# def _coords(self, x, y): +# x0, y0 = self._origin +# return int(round(x0 + x)), int(round(y0 + y)) + +# def _coords(self, x, y): +# return int(round(x)), int(round(y)) + +# def _rect_coords(self, (l, t, r, b)): +# x0, y0 = self._origin +# l = int(round(x0 + l)) +# t = int(round(y0 + t)) +# r = int(round(x0 + r)) +# b = int(round(y0 + b)) +# return l, t, r - l, b - t + +# def _rect_coords(self, (l, t, r, b)): +# l = int(round(l)) +# t = int(round(t)) +# r = int(round(r)) +# b = int(round(b)) +# return l, t, r - l, b - t + +# def _frame_coords(self, r): +# l, t, w, h = self._rect_coords(r) +# p = self._gdk_gc.line_width +# d = p // 2 +# return ( +# int(floor(l + d)), +# int(floor(t + d)), +# int(floor(w - p)), +# int(floor(h - p))) + +#def _gdk_angles(start_angle, end_angle): +# arc_angle = (end_angle - start_angle) % 360 +# start = int(round(start_angle * 64)) +# arc = int(round((arc_angle) * 64)) +# return -start, -arc + +#def _arc_rect((cx, cy), r): +# return (cx - r, cy - r, cx + r, cy + r) + +#def _arc_endpoint(center, r, a): +# cx, cy = center +# ar = a * deg +# x = int(round(cx + r * cos(ar))) +# y = int(round(cy + r * sin(ar))) +# return x, y diff --git a/GUI/GtkGI/CheckBoxes.py b/GUI/GtkGI/CheckBoxes.py new file mode 100644 index 0000000..6a3e1d9 --- /dev/null +++ b/GUI/GtkGI/CheckBoxes.py @@ -0,0 +1,53 @@ +# +# Python GUI - Check boxes - Gtk +# + +from gi.repository import Gtk +from GUI.GCheckBoxes import CheckBox as GCheckBox + +class CheckBox(GCheckBox): + + def __init__(self, title = "New Control", **kwds): + gtk_checkbox = Gtk.CheckButton(title) + gtk_checkbox.show() + self._gtk_connect(gtk_checkbox, 'clicked', self._gtk_clicked_signal) + self._gtk_inhibit_action = 0 + GCheckBox.__init__(self, _gtk_outer = gtk_checkbox, **kwds) + + def get_on(self): + gtk_checkbox = self._gtk_outer_widget + if gtk_checkbox.get_inconsistent(): + return 'mixed' + else: + return gtk_checkbox.get_active() + + def set_on(self, state): + mixed = state == 'mixed' + if mixed: + if not self._mixed: + raise ValueError("CheckBox state cannot be 'mixed'") + active = False + else: + active = bool(state) + save = self._gtk_inhibit_action + self._gtk_inhibit_action = 1 + try: + gtk_checkbox = self._gtk_outer_widget + gtk_checkbox.set_active(active) + gtk_checkbox.set_inconsistent(mixed) + finally: + self._gtk_inhibit_action = save + + def _gtk_clicked_signal(self): + gtk_checkbox = self._gtk_outer_widget + if not self._gtk_inhibit_action: + if self._auto_toggle: + gtk_checkbox.set_inconsistent(False) + else: + save = self._gtk_inhibit_action + self._gtk_inhibit_action = 1 + try: + gtk_checkbox.set_active(not gtk_checkbox.get_active()) + finally: + self._gtk_inhibit_action = save + self.do_action() diff --git a/GUI/GtkGI/Colors.py b/GUI/GtkGI/Colors.py new file mode 100644 index 0000000..6059f6f --- /dev/null +++ b/GUI/GtkGI/Colors.py @@ -0,0 +1,52 @@ +# +# Python GUI - Colors - Gtk +# + +from gi.repository import Gdk +from gi.repository.Gtk import Style +from GUI.GColors import Color as GColor + +class Color(GColor): + + _alpha = 1.0 + + def _from_gdk_color(cls, _gdk_color): + c = cls.__new__(cls) + c._gdk_color = _gdk_color + return c + + _from_gdk_color = classmethod(_from_gdk_color) + + def __init__(self, red, green, blue, alpha = 1.0): + self._rgba = (red, green, blue, alpha) + self._gdk_rgba = Gdk.RGBA(red, green, blue, alpha) + gdk_color = Gdk.Color( + int(red * 65535), + int(green * 65535), + int(blue * 65535)) + self._gdk_color = gdk_color + self._alpha = alpha + + def get_red(self): + return self._gdk_color.red / 65535.0 + + def get_green(self): + return self._gdk_color.green / 65535.0 + + def get_blue(self): + return self._gdk_color.blue / 65535.0 + + def get_alpha(self): + return self._alpha + + +rgb = Color + +s = Style() +selection_forecolor = Color._from_gdk_color(s.fg[3]) +selection_backcolor = Color._from_gdk_color(s.bg[3]) + +#selection_forecolor = rgb(1, 1, 1) +#selection_backcolor = rgb(0, 0, 0) + +#s = GtkStyleContext() diff --git a/GUI/GtkGI/Components.py b/GUI/GtkGI/Components.py new file mode 100644 index 0000000..d93fa8d --- /dev/null +++ b/GUI/GtkGI/Components.py @@ -0,0 +1,249 @@ +# +# Python GUI - Components - Gtk +# + +from gi.repository import Gtk +from gi.repository import Gdk +import GUI.Globals +from GUI.Geometry import sub_pt +from GUI.GComponents import Component as GComponent +from GUI.Events import Event, _gtk_key_event_of_interest + +_gdk_events_of_interest = ( + Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.BUTTON_MOTION_MASK | + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.KEY_PRESS_MASK | + Gdk.EventMask.KEY_RELEASE_MASK | + Gdk.EventMask.ENTER_NOTIFY_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + 0 +) + +_gtk_widget_to_component = {} +_gtk_last_keyval_down = None + +#------------------------------------------------------------------------------ + +class Component(GComponent): + + _pass_key_events_to_platform = True + + def __init__(self, _gtk_outer, _gtk_inner = None, + _gtk_focus = None, _gtk_input = None, **kwds): + self._position = (0, 0) + req = _gtk_outer.size_request() + self._size = (req.width, req.height) + _gtk_inner = _gtk_inner or _gtk_outer + self._gtk_outer_widget = _gtk_outer + self._gtk_inner_widget = _gtk_inner + self._gtk_focus_widget = _gtk_focus + _gtk_widget_to_component[_gtk_outer] = self + self._gtk_connect_input_events(_gtk_input or _gtk_inner) + if _gtk_focus: + _gtk_focus.set_property('can-focus', True) + self._gtk_connect(_gtk_focus, 'focus-in-event', self._gtk_focus_in) + GComponent.__init__(self, **kwds) + + def destroy(self): + gtk_widget = self._gtk_outer_widget + if gtk_widget in _gtk_widget_to_component: + del _gtk_widget_to_component[gtk_widget] + GComponent.destroy(self) + + # + # Properties + # + + def set_width(self, v): + w, h = self.size + self.size = (v, h) + + def set_height(self, v): + w, h = self.size + self.size = (w, v) + + def get_position(self): + return self._position + + def set_position(self, v): + self._position = v + widget = self._gtk_outer_widget + parent = widget.get_parent() + if parent: + parent.move(widget, *v) + + def get_size(self): + return self._size + + def set_size(self, new_size): + w0, h0 = self._size + w1, h1 = new_size + self._gtk_outer_widget.set_size_request(max(int(w1), 1), max(int(h1), 1)) + self._size = new_size + if w0 != w1 or h0 != h1: + self._resized((w1 - w0, h1 - h0)) + + def get_bounds(self): + x, y = self._position + w, h = self.size + return (x, y, x + w, y + h) + + def set_bounds(self, (l, t, r, b)): + self.position = (l, t) + self.size = (r - l, b - t) + +# def get_visible(self): +# return self._gtk_outer_widget.get_property('visible') +# +# def set_visible(self, v): +# self._gtk_outer_widget.set_property('visible', v) + + # + # Message dispatching + # + + def become_target(self): + gtk_focus = self._gtk_focus_widget + if gtk_focus: + gtk_focus.grab_focus() + else: + raise ValueError("%r cannot be targeted" % self) + +# def current_target(self): +# """Find the current target object within the Window containing +# this component. If the component is not contained in a Window, +# the result is undefined.""" +# target = _gtk_find_component(self._gtk_outer_widget.get_focus()) +# if not target: +# target = self.window +# return target + + def is_target(self): + """Return true if this is the current target within the containing + Window. If the component is not contained in a Window, the result + is undefined.""" + gtk_focus = self._gtk_focus_widget + if gtk_focus: + return gtk_focus.get_property('has-focus') + else: + return False + + # + # Internal + # + + def _gtk_connect(self, gtk_widget, signal, handler): + def catch(widget, *args): + try: + handler(*args) + except: + _gtk_exception_in_signal_handler() + gtk_widget.connect(signal, lambda widget, *args: handler(*args)) + + def _gtk_connect_after(self, gtk_widget, signal, handler): + def catch(widget, *args): + try: + handler(*args) + except: + _gtk_exception_in_signal_handler() + gtk_widget.connect_after(signal, lambda widget, *args: handler(*args)) + + def _gtk_focus_in(self, gtk_event): + window = self.window + if window: + old_target = window._target + window._target = self + if old_target and old_target is not self: + old_target._untargeted() + self._targeted() + + def _targeted(self): + pass + + def _untargeted(self): + pass + + def _gtk_connect_input_events(self, gtk_widget): + self._last_mouse_down_time = 0 + self._click_count = 0 + gtk_widget.add_events(_gdk_events_of_interest) + self._gtk_connect(gtk_widget, 'button-press-event', + self._gtk_button_press_event_signal) + self._gtk_connect(gtk_widget, 'motion-notify-event', + self._gtk_motion_notify_event_signal) + self._gtk_connect(gtk_widget, 'button-release-event', + self._gtk_button_release_event_signal) + self._gtk_connect(gtk_widget, 'enter-notify-event', + self._gtk_enter_leave_event_signal) + self._gtk_connect(gtk_widget, 'leave-notify-event', + self._gtk_enter_leave_event_signal) + self._gtk_connect(gtk_widget, 'key-press-event', + self._handle_gtk_key_event) + self._gtk_connect(gtk_widget, 'key-release-event', + self._handle_gtk_key_event) + + def _gtk_button_press_event_signal(self, gtk_event): + if gtk_event.type == Gdk.EventType.BUTTON_PRESS: # don't want 2BUTTON or 3BUTTON + event = Event._from_gtk_mouse_event(gtk_event) + last_time = self._last_mouse_down_time + this_time = event.time + num_clicks = self._click_count + if this_time - last_time <= 0.25: + num_clicks += 1 + else: + num_clicks = 1 + event.num_clicks = num_clicks + self._click_count = num_clicks + self._last_mouse_down_time = this_time + #print "Component._gtk_button_press_event_signal:" ### + #print event ### + return self._event_custom_handled(event) + + def _gtk_motion_notify_event_signal(self, gtk_event): + event = Event._from_gtk_mouse_event(gtk_event) + self._mouse_event = event + return self._event_custom_handled(event) + + def _gtk_button_release_event_signal(self, gtk_event): + event = Event._from_gtk_mouse_event(gtk_event) + self._mouse_event = event + return self._event_custom_handled(event) + + def _gtk_enter_leave_event_signal(self, gtk_event): + #print "Component._gtk_enter_leave_event_signal:" ### + event = Event._from_gtk_mouse_event(gtk_event) + return self._event_custom_handled(event) + + def _handle_gtk_key_event(self, gtk_event): + """Convert a Gtk key-press or key-release event into an Event + object and pass it up the message path.""" + #print "Component._handle_gtk_key_event for", self ### + global _gtk_last_keyval_down + if _gtk_key_event_of_interest(gtk_event): + event = Event._from_gtk_key_event(gtk_event) + if event.kind == 'key_down': + this_keyval = gtk_event.keyval + if _gtk_last_keyval_down == this_keyval: + event.auto = 1 + _gtk_last_keyval_down = this_keyval + else: + _gtk_last_keyval_down = None + #if event.kind == 'key_down': ### + # print event ### + return self._event_custom_handled(event) + +#------------------------------------------------------------------------------ + +def _gtk_find_component(gtk_widget): + while gtk_widget: + component = _gtk_widget_to_component.get(gtk_widget) + if component: + return component + gtk_widget = gtk_widget.get_parent() + return None + +def _gtk_exception_in_signal_handler(): + print >>sys.stderr, "---------- Exception in gtk signal handler ----------" + traceback.print_exc() diff --git a/GUI/GtkGI/Containers.py b/GUI/GtkGI/Containers.py new file mode 100644 index 0000000..5cdd631 --- /dev/null +++ b/GUI/GtkGI/Containers.py @@ -0,0 +1,20 @@ +# +# Python GUI - Containers - Gtk version +# + +from gi.repository import Gdk +from GUI.Geometry import inset_rect +from GUI.GContainers import Container as GContainer + +class Container(GContainer): + # Subclasses must set the inner widget to be a Fixed or Layout + # widget. + + def _add(self, comp): + GContainer._add(self, comp) + x, y = comp._position + self._gtk_inner_widget.put(comp._gtk_outer_widget, int(x), int(y)) + + def _remove(self, comp): + self._gtk_inner_widget.remove(comp._gtk_outer_widget) + GContainer._remove(self, comp) diff --git a/GUI/GtkGI/Controls.py b/GUI/GtkGI/Controls.py new file mode 100644 index 0000000..7e877d5 --- /dev/null +++ b/GUI/GtkGI/Controls.py @@ -0,0 +1,89 @@ +# +# Python GUI - Controls - Gtk +# + +from gi.repository import Gtk +from GUI.Enumerations import EnumMap +from GUI.Colors import Color +from GUI.Fonts import Font +from GUI.GControls import Control as GControl + +_justs = ['left', 'center', 'right'] + +_just_to_gtk_alignment = EnumMap("justification", + left = (0.0, Gtk.Justification.LEFT), + centre = (0.5, Gtk.Justification.CENTER), + center = (0.5, Gtk.Justification.CENTER), + right = (1.0, Gtk.Justification.RIGHT), +) + +class Control(GControl): + # A component which encapsulates a Gtk control widget. + + _font = None + _color = None + + def __init__(self, _gtk_outer = None, _gtk_title = None, **kwds): + self._gtk_title_widget = _gtk_title or _gtk_outer + GControl.__init__(self, _gtk_outer = _gtk_outer, + _gtk_focus = kwds.pop('_gtk_focus', _gtk_outer), + **kwds) + + def get_title(self): + return self._gtk_title_widget.get_label() + + def set_title(self, v): + self._gtk_title_widget.set_label(v) + + def get_enabled(self): + #return self._gtk_outer_widget.get_sensitive() + return self._gtk_outer_widget.get_property('sensitive') + + def set_enabled(self, v): + self._gtk_outer_widget.set_sensitive(v) + + def get_color(self): + return self._color +# gdk_color = self._gtk_title_widget.get_style().fg[Gtk.StateType.NORMAL] +# return Color._from_gdk_color(gdk_color) + + def set_color(self, v): + #self._gtk_title_widget.modify_fg(Gtk.StateType.NORMAL, v._gdk_color) + self._color = v + self._gtk_title_widget.override_color(Gtk.StateType.NORMAL, v._gdk_rgba) + + def get_font(self): + font = self._font + if not font: + font = Font._from_pango_description(self._gtk_title_widget.style.font_desc) + return font + + def set_font(self, f): + self._font = f + gtk_title = self._gtk_title_widget +# print "Control.set_font: gtk_title =", gtk_title ### +# pd = f._pango_description ### +# print "...family =", pd.get_family() ### +# print "...size =", pd.get_size() ### + #gtk_title.modify_font(f._pango_description) + gtk_title.override_font(f._pango_description) + gtk_title.queue_resize() + + def get_just(self): + h = self._gtk_get_alignment() + return _justs[int(round(2.0 * h))] + + def set_just(self, v): + fraction, just = _just_to_gtk_alignment[v] + self._gtk_set_alignment(fraction, just) + + def set_lines(self, num_lines): + line_height = self.font.text_size("X")[1] + #print "Control.set_lines: line_height =", line_height ### + self.height = num_lines * line_height + self._vertical_padding + + def _gtk_get_alignment(self): + raise NotImplementedError + + def _gtk_set_alignment(self, h): + raise NotImplementedError diff --git a/GUI/GtkGI/Cursors.py b/GUI/GtkGI/Cursors.py new file mode 100644 index 0000000..0d35082 --- /dev/null +++ b/GUI/GtkGI/Cursors.py @@ -0,0 +1,34 @@ +# +# Python GUI - Cursors - Gtk +# + +from gi.repository import Gdk +from GUI.GCursors import Cursor as GCursor + +class Cursor(GCursor): + # + # _gtk_cursor Gdk.Cursor + + def _from_gtk_std_cursor(cls, id): + cursor = cls.__new__(cls) + cursor._gtk_cursor = Gdk.Cursor.new(id) + return cursor + + _from_gtk_std_cursor = classmethod(_from_gtk_std_cursor) + + def _from_nothing(cls): +# cursor = cls.__new__(cls) +# pixmap = GdkPixmap.Pixmap(None, 1, 1, 1) +# color = Gdk.Color() +# cursor._gtk_cursor = Gdk.Cursor.new(pixmap, pixmap, color, color, 0, 0) +# return cursor + return cls._from_gtk_std_cursor(Gdk.CursorType.BLANK_CURSOR) + + _from_nothing = classmethod(_from_nothing) + + def _init_from_image_and_hotspot(self, image, hotspot): + #print "Cursor._init_from_image_and_hotspot:", image, hotspot ### + x, y = hotspot + gdk_display = Gdk.Display.get_default() + self._gtk_cursor = Gdk.Cursor.new_from_pixbuf(gdk_display, + image._gdk_pixbuf, x, y) diff --git a/GUI/GtkGI/Dialogs.py b/GUI/GtkGI/Dialogs.py new file mode 100644 index 0000000..7d665b2 --- /dev/null +++ b/GUI/GtkGI/Dialogs.py @@ -0,0 +1,10 @@ +# +# Python GUI - Dialogs - Gtk +# + +from GUI.GDialogs import Dialog as GDialog + +class Dialog(GDialog): + + _default_keys = ['\r'] + _cancel_keys = ['\x1b'] diff --git a/GUI/GtkGI/DrawableContainers.py b/GUI/GtkGI/DrawableContainers.py new file mode 100644 index 0000000..fa66b80 --- /dev/null +++ b/GUI/GtkGI/DrawableContainers.py @@ -0,0 +1,69 @@ +# +# Python GUI - DrawableViews - Gtk +# + +import os, traceback +from math import floor, ceil +from gi.repository import Gtk, Gdk, cairo +from GUI.Canvases import Canvas +from GUI.Events import Event +from GUI.GDrawableContainers import DrawableContainer as GDrawableContainer + +class DrawableContainer(GDrawableContainer): + + #_extent_origin = (0, 0) + + def __init__(self, _gtk_outer = None, **kwds): + gtk_layout = Gtk.Layout() + gtk_layout.add_events(Gdk.EventMask.EXPOSURE_MASK) + gtk_layout.show() + self._gtk_connect(gtk_layout, 'draw', self._gtk_draw_signal) + if _gtk_outer: + _gtk_outer.add(gtk_layout) + else: + _gtk_outer = gtk_layout + GDrawableContainer.__init__(self, + _gtk_outer = _gtk_outer, _gtk_inner = gtk_layout, + _gtk_focus = gtk_layout, _gtk_input = gtk_layout) + self.set(**kwds) + + # + # Other methods + # + + def with_canvas(self, proc): + hadj, vadj = self._gtk_adjustments() + clip = rect_sized((hadj.value, vadj.value), self.size) +# canvas = Canvas._from_gdk_drawable(self._gtk_inner_widget.bin_window) + context = Gdk.cairo_create(self._gtk_inner_widget.get_bin_window()) + self._gtk_prepare_cairo_context(context) + canvas = Canvas._from_cairo_context(context) + proc(canvas) + + def invalidate_rect(self, (l, t, r, b)): + x = int(floor(l)) + y = int(floor(t)) + w = int(ceil(r - l)) + h = int(ceil(b - t)) + self._gtk_inner_widget.queue_draw_area(x, y, w, h) + + def update(self): + gdk_window = self._gtk_inner_widget.bin_window + gdk_window.process_updates() + + # + # Internal + # + + def _gtk_draw_signal(self, context): + try: + self._gtk_prepare_cairo_context(context) + clip = context.clip_extents() + canvas = Canvas._from_cairo_context(context) + self.draw(canvas, clip) + except: + print "------------------ Exception while drawing ------------------" + traceback.print_exc() + + def _gtk_prepare_cairo_context(self, context): + pass diff --git a/GUI/GtkGI/EditCmdHandlers.py b/GUI/GtkGI/EditCmdHandlers.py new file mode 100644 index 0000000..3891c01 --- /dev/null +++ b/GUI/GtkGI/EditCmdHandlers.py @@ -0,0 +1,5 @@ +# +# PyGUI - Edit command handling - Gtk +# + +from GUI.GEditCmdHandlers import EditCmdHandler diff --git a/GUI/GtkGI/Events.py b/GUI/GtkGI/Events.py new file mode 100644 index 0000000..349629d --- /dev/null +++ b/GUI/GtkGI/Events.py @@ -0,0 +1,161 @@ +# +# Python GUI - Events - Gtk +# + +from gi.repository import Gdk +from GUI.GEvents import Event as GEvent + +MOTION_NOTIFY = Gdk.EventType.MOTION_NOTIFY + +SHIFT_MASK = Gdk.ModifierType.SHIFT_MASK +CONTROL_MASK = Gdk.ModifierType.CONTROL_MASK +MOD1_MASK = Gdk.ModifierType.MOD1_MASK +MOD2_MASK = Gdk.ModifierType.MOD2_MASK +MOD3_MASK = Gdk.ModifierType.MOD3_MASK +MOD4_MASK = Gdk.ModifierType.MOD4_MASK +MOD5_MASK = Gdk.ModifierType.MOD5_MASK +BIT13_MASK = 0x2000 +SUPER_MASK = Gdk.ModifierType.SUPER_MASK +HYPER_MASK = Gdk.ModifierType.HYPER_MASK +META_MASK = Gdk.ModifierType.META_MASK + +_gdk_button_mask = ( + Gdk.ModifierType.BUTTON1_MASK | + Gdk.ModifierType.BUTTON2_MASK | + Gdk.ModifierType.BUTTON3_MASK | + Gdk.ModifierType.BUTTON4_MASK | + Gdk.ModifierType.BUTTON5_MASK +) + +_gdk_event_type_to_kind = { + Gdk.EventType.BUTTON_PRESS: 'mouse_down', + Gdk.EventType._2BUTTON_PRESS: 'mouse_down', + Gdk.EventType._3BUTTON_PRESS: 'mouse_down', + Gdk.EventType.MOTION_NOTIFY: 'mouse_move', + Gdk.EventType.BUTTON_RELEASE: 'mouse_up', + Gdk.EventType.KEY_PRESS: 'key_down', + Gdk.EventType.KEY_RELEASE: 'key_up', + Gdk.EventType.ENTER_NOTIFY: 'mouse_enter', + Gdk.EventType.LEAVE_NOTIFY: 'mouse_leave', +} + +_gtk_button_to_button = { + 1: 'left', + 2: 'middle', + 3: 'right', + 4: 'fourth', + 5: 'fifth', +} + +_gdk_keyval_to_keyname = { + 0xFF50: 'home', + 0xFF51: 'left_arrow', + 0xFF52: 'up_arrow', + 0xFF53: 'right_arrow', + 0xFF54: 'down_arrow', + 0xFF55: 'page_up', + 0xFF56: 'page_down', + 0xFF57: 'end', + #0xFF6A: 'help', + 0xFF6A: 'insert', + 0xFF8D: 'enter', + 0xFFBE: 'f1', + 0xFFBF: 'f2', + 0xFFC0: 'f3', + 0xFFC1: 'f4', + 0xFFC2: 'f5', + 0xFFC3: 'f6', + 0xFFC4: 'f7', + 0xFFC5: 'f8', + 0xFFC6: 'f9', + 0xFFC7: 'f10', + 0xFFC8: 'f11', + 0xFFC9: 'f12', + 0xFFCA: 'f13', + 0xFFCB: 'f14', + 0xFFCC: 'f15', + 0xFFFF: 'delete', +} + +def _gtk_key_event_of_interest(gtk_event): + keyval = gtk_event.keyval + return (keyval <= 0xFF + or 0xFF00 <= keyval <= 0xFF1F + or 0xFF80 <= keyval <= 0xFFBD + or keyval == 0xFE20 # shift-tab + or keyval == 0xFFFF + or keyval in _gdk_keyval_to_keyname) + +class Event(GEvent): + """Platform-dependent modifiers (boolean): + mod1 The X11 MOD1 key. + """ + + button = None + position = None + global_position = None + num_clicks = 0 + char = None + key = None + auto = 0 + + def _from_gtk_key_event(cls, gtk_event): + event = cls.__new__(cls) + event._set_from_gtk_event(gtk_event) + keyval = gtk_event.keyval + #print "Event._from_gtk_key_event: keyval = 0x%04X" % keyval ### + event.key = _gdk_keyval_to_keyname.get(keyval, "") + if keyval == 0xFFFF: # GDK_Delete + event.char = chr(0x7F) + elif keyval == 0xFF8D: + event.char = "\r" + elif keyval == 0xFE20: # shift-tab + event.char = "\t" + elif keyval <= 0xFF1F: + if event.control: + event.char = chr(keyval & 0x1F) + else: + event.char = chr(keyval & 0x7F) + else: + event.char = "" + return event + + _from_gtk_key_event = classmethod(_from_gtk_key_event) + + def _from_gtk_mouse_event(cls, gtk_event): + event = cls.__new__(cls) + event._set_from_gtk_event(gtk_event) + if event.kind in ('mouse_down', 'mouse_up'): + event.button = _gtk_button_to_button[gtk_event.button] + event.position = (gtk_event.x, gtk_event.y) + event.global_position = (gtk_event.x_root, gtk_event.y_root) + return event + + _from_gtk_mouse_event = classmethod(_from_gtk_mouse_event) + + def _set_from_gtk_event(self, gtk_event): + typ = gtk_event.type + state = gtk_event.get_state() + #print "Event: gtk state = 0x%x" % state ### + if typ == MOTION_NOTIFY and state & _gdk_button_mask: + self.kind = 'mouse_drag' + else: + self.kind = _gdk_event_type_to_kind[gtk_event.type] + self.time = gtk_event.time / 1000.0 + self.shift = self.extend_contig = (state & SHIFT_MASK) <> 0 + self.control = self.extend_noncontig = (state & CONTROL_MASK) <> 0 + self.mod1 = (state & MOD1_MASK) <> 0 + self.mod2 = (state & MOD2_MASK) <> 0 + self.mod3 = (state & MOD3_MASK) <> 0 + self.mod4 = (state & MOD4_MASK) <> 0 + self.mod5 = (state & MOD5_MASK) <> 0 + self.super = (state & SUPER_MASK) <> 0 + self.hyper = (state & HYPER_MASK) <> 0 + self.meta = (state & META_MASK) <> 0 + bit13 = (state & BIT13_MASK) <> 0 # X server on MacOSX maps Option to this + self.option = self.mod1 or bit13 + + def _platform_modifiers_str(self): + return " mod1:%s mod2:%s mod3:%s mod4:%s mod5:%s super:%s hyper:%s meta:%s" % ( + self.mod1, self.mod2, self.mod3, self.mod4, self.mod5, + self.super, self.hyper, self.meta) diff --git a/GUI/GtkGI/Files.py b/GUI/GtkGI/Files.py new file mode 100644 index 0000000..2807408 --- /dev/null +++ b/GUI/GtkGI/Files.py @@ -0,0 +1,5 @@ +# +# Python GUI - File references and types - Gtk +# + +from GUI.GFiles import * diff --git a/GUI/GtkGI/Fonts.py b/GUI/GtkGI/Fonts.py new file mode 100644 index 0000000..b554514 --- /dev/null +++ b/GUI/GtkGI/Fonts.py @@ -0,0 +1,122 @@ +# +# Python GUI - Fonts - Gtk +# + +import sys +from gi.repository import Pango +from gi.repository import Gtk +from gi.repository import Gdk +from GUI.GFonts import Font as GFont + +class Font(GFont): + + #_gdk_font = None + _pango_font = None + _pango_metrics = None + _pango_layout = None + +# def _from_gdk_font(cls, gdk_font): +# font = cls.__new__(cls) +# font._gdk_font = gdk_font +# return font +# +# _from_gdk_font = classmethod(_from_gdk_font) + + def _from_pango_description(cls, pango_description): + font = cls.__new__(cls) + font._pango_description = pango_description + return font + + _from_pango_description = classmethod(_from_pango_description) + + def __init__(self, family, size = 12, style = []): + if 'italic' in style: + pango_style = Pango.Style.ITALIC + else: + pango_style = Pango.Style.NORMAL + if 'bold' in style: + pango_weight = Pango.Weight.BOLD + else: + pango_weight = Pango.Weight.NORMAL + pfd = Pango.FontDescription() + pfd.set_family(family) + pfd.set_size(int(round(size * Pango.SCALE))) + pfd.set_style(pango_style) + pfd.set_weight(pango_weight) + self._pango_description = pfd + + def get_family(self): + return self._pango_description.get_family() + + def get_size(self): + return self._pango_description.get_size() / Pango.SCALE + + def get_style(self): + style = [] + pfd = self._pango_description + if pfd.get_weight() > Pango.Weight.NORMAL: + style.append('bold') + if pfd.get_style() <> Pango.Style.NORMAL: + style.append('italic') + return style + + def get_ascent(self): + self._get_pango_metrics() + result = self._ascent + return result + + def get_descent(self): + self._get_pango_metrics() + return self._descent + + def get_height(self): + self._get_pango_metrics() + return self._ascent + self._descent + + def get_line_height(self): + return self.get_height() + + def _get_pango_metrics(self): + #print "Font._get_pango_metrics: enter" ### + pfm = self._pango_metrics + if not pfm: + pf = self._get_pango_font() + pfm = pf.get_metrics(None) + self._pango_metrics = pfm + self._ascent = pfm.get_ascent() / Pango.SCALE + self._descent = pfm.get_descent() / Pango.SCALE + return pfm + + def _get_pango_font(self): + pf = self._pango_font + if not pf: + pf = _pango_context.load_font(self._pango_description) + if not pf: + raise ValueError("Unable to load Pango font for %s" % self) + self._pango_font = pf + return pf + + def width(self, s, start = 0, end = sys.maxint): + layout = self._get_pango_layout(s[start:end], True) + return layout.get_pixel_size()[0] + + def text_size(self, text): + layout = self._get_pango_layout(text, False) + return layout.get_pixel_size() + + def x_to_pos(self, s, x): + layout = self._get_pango_layout(s, True) + return pango_layout.xy_to_index(x, 0) + + def _get_pango_layout(self, text, single_paragraph_mode): + layout = self._pango_layout + if not layout: + layout = Pango.Layout(_pango_context) + layout.set_font_description(self._pango_description) + self._pango_layout = layout + layout.set_single_paragraph_mode(single_paragraph_mode) + layout.set_text(text, len(text)) + return layout + + +_pango_context = Gtk.Label().create_pango_context() diff --git a/GUI/GtkGI/Frames.py b/GUI/GtkGI/Frames.py new file mode 100644 index 0000000..4a0bc11 --- /dev/null +++ b/GUI/GtkGI/Frames.py @@ -0,0 +1,13 @@ +# +# Python GUI - Frames - Gtk +# + +from gtk import Fixed +from GUI.GFrames import Frame as GFrame + +class Frame(GFrame): + + def __init__(self, **kwds): + gtk_widget = Fixed() + gtk_widget.show() + GFrame.__init__(self, _gtk_outer = gtk_widget) diff --git a/GUI/GtkGI/GL.py b/GUI/GtkGI/GL.py new file mode 100644 index 0000000..0ffc017 --- /dev/null +++ b/GUI/GtkGI/GL.py @@ -0,0 +1,212 @@ +# +# PyGUI - OpenGL View - Gtk/GtkGLExt +# + +try: + from gtk import gdkgl, gtkgl + from OpenGL.GL import glViewport +except ImportError, e: + raise ImportError("OpenGL support is not available (%s)" % e) + +from GUI.GGLConfig import GLConfig as GGLConfig, GLConfigError +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI.ImageBases import ImageBase +from GUI.GtkPixmaps import GtkPixmap +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture +from GUI.GLDisplayLists import DisplayList + +#------------------------------------------------------------------------------ + +def gtk_find_config_default(attr, mode_bit): + try: + cfg = gdkgl.Config(mode = mode_bit) + value = cfg.get_attrib(attr)[0] + except gdkgl.NoMatches: + value = 0 + print "default for attr", attr, "=", value + return value + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + + _alpha = False + _color_size = 1 + _alpha_size = 1 + _depth_size = 1 + _stencil_size = 1 + _accum_size = 1 + + def _gtk_get_config(self): + csize = self._color_size + asize = 0 + dsize = 0 + ssize = 0 + acsize = 0 + aasize = 0 + if self._alpha: + asize = self._alpha_size + if self._depth_buffer: + dsize = self._depth_size + if self._stencil_buffer: + ssize = self._stencil_size + if self._accum_buffer: + acsize = self._accum_size + if self._alpha: + aasize = acsize + attrs = [ + gdkgl.RGBA, + gdkgl.RED_SIZE, csize, + gdkgl.GREEN_SIZE, csize, + gdkgl.BLUE_SIZE, csize, + gdkgl.ALPHA_SIZE, asize, + gdkgl.AUX_BUFFERS, self._aux_buffers, + gdkgl.DEPTH_SIZE, dsize, + gdkgl.STENCIL_SIZE, ssize, + gdkgl.ACCUM_RED_SIZE, acsize, + gdkgl.ACCUM_GREEN_SIZE, acsize, + gdkgl.ACCUM_BLUE_SIZE, acsize, + gdkgl.ACCUM_ALPHA_SIZE, aasize, + ] + if self._double_buffer: + attrs += [gdkgl.DOUBLEBUFFER] + if self._stereo: + attrs += [gdkgl.STEREO] + if self._multisample: + attrs += [ + gdkgl.SAMPLE_BUFFERS, 1, + gdkgl.SAMPLES, self._samples_per_pixel + ] + result = self._gdkgl_config(attrs) + if not result and self._double_buffer: + attrs.remove(gdkgl.DOUBLEBUFFER) + result = self._gdkgl_config(attrs) + if not result: + raise GLConfigError + return result + + def _gdkgl_config(self, attrs): + try: + return gdkgl.Config(attrib_list = attrs) + except gdkgl.NoMatches: + return None + + def _gtk_set_config(self, gtk_config): + def attr(key): + return gtk_config.get_attrib(key)[0] + self._color_size = attr(gdkgl.RED_SIZE) + self._alpha_size = attr(gdkgl.ALPHA_SIZE) + self._alpha = gtk_config.has_alpha() + self._double_buffer = gtk_config.is_double_buffered() + self._stereo = gtk_config.is_stereo() + self._aux_buffers = attr(gdkgl.AUX_BUFFERS) + self._depth_size = attr(gdkgl.DEPTH_SIZE) + self._depth_buffer = gtk_config.has_depth_buffer() + self._stencil_size = attr(gdkgl.STENCIL_SIZE) + self._stencil_buffer = gtk_config.has_stencil_buffer() + self._accum_size = attr(gdkgl.ACCUM_RED_SIZE) + self._accum_buffer = gtk_config.has_accum_buffer() + self._multisample = attr(gdkgl.SAMPLE_BUFFERS) <> 0 + self._samples_per_pixel = attr(gdkgl.SAMPLES) + + def supported(self, mode = 'both'): + try: + gtk_config = self._gtk_get_config() + pf = GLConfig.__new__(GLConfig) + pf._gtk_set_config(gtk_config) + return pf + except GLConfigError: + return None + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + + _first_expose = 0 + + def __init__(self, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + GLContext.__init__(self, share_group, pf, kwds) + gtk_share = self._gtk_get_share() + area = gtkgl.DrawingArea(glconfig = self._gl_config, share_list = gtk_share, + render_type = gdkgl.RGBA_TYPE) + area.show() + self._gtk_connect_after(area, "realize", self._gtk_realize_signal) + self._gtk_connect(area, "expose-event", self._gtk_expose_event_signal) + GGLView.__init__(self, _gtk_outer = area, _gtk_input = area, + _gtk_focus = area) + self.set(**kwds) + + def _resized(self, delta): + self.with_context(self._update_viewport) + + def _gtk_get_gl_context(self): + if not self._gl_context: + self._gtk_inner_widget.realize() + return self._gl_context + + def _gtk_realize_signal(self): + #print "GLView._gtk_realize_signal" ### + area = self._gtk_inner_widget + self._gl_drawable = area.get_gl_drawable() + self._gl_context = area.get_gl_context() + self.with_context(self.init_context) + + def _gtk_expose_event_signal(self, gtk_event): + #print "GLView._gtk_expose_event_signal" ### + if not self._first_expose: + self.with_context(self._update_viewport) + self._first_expose = 1 + try: + self.with_context(self._render, flush = True) + except: + import sys, traceback + sys.stderr.write("\n<<<<<<<<<< Exception while rendering a GLView\n") + traceback.print_exc() + sys.stderr.write(">>>>>>>>>>\n\n") + + def invalidate(self): + gtk_window = self._gtk_outer_widget.window + if gtk_window: + width, height = self.size + gtk_window.invalidate_rect((0, 0, width, height), 0) + +#------------------------------------------------------------------------------ + +class GLPixmap(GtkPixmap, GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + pf = GLConfig._from_args(config, kwds) + GLContext.__init__(self, share_group, pf, kwds) + gl_config = pf._gtk_get_config() + self._gl_config = gl_config +# if share: +# gtk_share = share.shared_context._gtk_get_gl_context() +# else: +# gtk_share = None + gtk_share = self._gtk_get_share() + GtkPixmap.__init__(self, width, height) + gdk_pixmap = self._gdk_pixmap + gdkgl.ext(gdk_pixmap) + self._gl_drawable = gdk_pixmap.set_gl_capability(glconfig = gl_config) + print "GLPixmap: self._gl_drawable =", self._gl_drawable ### + self._gl_context = gdkgl.Context( + self._gl_drawable, + direct = False, + share_list = gtk_share, + render_type = gdkgl.RGBA_TYPE + ) + print "GLPixmap: self._gl_context =", self._gl_context ### + ImageBase.__init__(self, **kwds) + self.with_context(self._init_context) + print "GLPixmap: initialised context" ### + +# def _init_context(self): +# width, height = self.size +# glViewport(0, 0, int(width), int(height)) +# print "GLPixmap: Set viewport to", width, height ### +# self.init_context() + + \ No newline at end of file diff --git a/GUI/GtkGI/GLContexts.py b/GUI/GtkGI/GLContexts.py new file mode 100644 index 0000000..1765565 --- /dev/null +++ b/GUI/GtkGI/GLContexts.py @@ -0,0 +1,46 @@ +# +# PyGUI - GL Context - Gtk +# + +from GUI.GGLContexts import GLContext as GGLContext + +try: + from OpenGL.GL import glFlush +except ImportError, e: + raise ImportError("OpenGL support is not available (%s)" % e) + +class GLContext(GGLContext): + + _gl_drawable = None + _gl_context = None + + def __init__(self, share_group, config, kwds): + GGLContext.__init__(self, share_group) + self._gl_config = config._gtk_get_config() + + def _gtk_get_share(self): + shared_context = self._get_shared_context() + if shared_context: + return shared_context._gtk_get_gl_context() + else: + return None + + def _with_context(self, proc, flush): + drawable = self._gl_drawable + if drawable: + if not drawable.gl_begin(self._gl_context): + raise ValueError( + "Unable to make %s the current OpenGL context (gl_begin failed)" % self) + try: + self._with_share_group(proc) + if flush: + if drawable.is_double_buffered(): + #print "GLContext.with_context: swapping buffers" ### + drawable.swap_buffers() + else: + #print "GLContext.with_context: flushing" ### + glFlush() + finally: + drawable.gl_end() + #return result + diff --git a/GUI/GtkGI/Geometry.py b/GUI/GtkGI/Geometry.py new file mode 100644 index 0000000..2ebbd95 --- /dev/null +++ b/GUI/GtkGI/Geometry.py @@ -0,0 +1,5 @@ +# +# Python GUI - Points and Rectangles - Gtk +# + +from GUI.GGeometry import * diff --git a/GUI/GtkGI/GtkImageScaling.py b/GUI/GtkGI/GtkImageScaling.py new file mode 100644 index 0000000..3900ae3 --- /dev/null +++ b/GUI/GtkGI/GtkImageScaling.py @@ -0,0 +1,19 @@ +# +# Python GUI - Image scaling utilities - Gtk +# + +from gi.repository import Gdk + +def gtk_scale_pixbuf(src_pixbuf, sx, sy, sw, sh, dw, dh): + """Return a new pixbuf containing the specified part of + the given pixbuf scaled to the specified size.""" + dst_pixbuf = GdkPixbuf.Pixbuf( + src_pixbuf.get_colorspace(), src_pixbuf.get_has_alpha(), + src_pixbuf.get_bits_per_sample(), dw, dh) + xscale = float(dw) / sw + yscale = float(dh) / sh + xoffset = - xscale * sx + yoffset = - yscale * sy + src_pixbuf.scale(dst_pixbuf, 0, 0, dw, dh, + xoffset, yoffset, xscale, yscale, GdkPixbuf.InterpType.BILINEAR) + return dst_pixbuf diff --git a/GUI/GtkGI/GtkPixmaps.py b/GUI/GtkGI/GtkPixmaps.py new file mode 100644 index 0000000..dec798b --- /dev/null +++ b/GUI/GtkGI/GtkPixmaps.py @@ -0,0 +1,34 @@ +# +# Python GUI - Gtk - Common pixmap code +# + +from gi.repository import Gdk +from GUI.StdColors import clear +from GUI.GtkImageScaling import gtk_scale_pixbuf +from GUI.Canvases import Canvas + +class GtkPixmap: + + def __init__(self, width, height): + gdk_root = Gdk.get_default_root_window() + self._gdk_pixmap = Gdk.Pixmap(gdk_root, width, height) + #ctx = self._gdk_pixmap.cairo_create() + #self._gtk_surface = ctx.get_target() + + def _gtk_set_source(self, ctx, x, y): + ctx.set_source_pixmap(self._gdk_pixmap, x, y) + + def get_width(self): + return self._gdk_pixmap.get_size()[0] + + def get_height(self): + return self._gdk_pixmap.get_size()[1] + + def get_size(self): + return self._gdk_pixmap.get_size() + + def with_canvas(self, proc): + canvas = Canvas._from_gdk_drawable(self._gdk_pixmap) + canvas.backcolor = clear + proc(canvas) + diff --git a/GUI/GtkGI/GtkUtils.py b/GUI/GtkGI/GtkUtils.py new file mode 100644 index 0000000..39c2b5b --- /dev/null +++ b/GUI/GtkGI/GtkUtils.py @@ -0,0 +1,46 @@ +#----------------------------------------------------------------------------- +# +# Python GUI - Gtk - Utilities +# +#----------------------------------------------------------------------------- + +class GtkFixedSize(object): + # Mixin for Gtk widgets to force them to always request exactly the + # size set using set_size_request(). + + def do_get_preferred_width(self): + w = self.get_size_request()[0] + #print "GtkFixedSize.do_get_preferred_width:", w ### + return w, w + + def do_get_preferred_height(self): + h = self.get_size_request()[1] + #print "GtkFixedSize.do_get_preferred_height:", h ### + return h, h + + def do_get_preferred_height_for_width(self, width): + #print "GtkFixedSize.do_get_preferred_height_for_width:", width ### + return self.do_get_preferred_height() + + def do_get_preferred_width_for_height(self, height): + #print "GtkFixedSize.do_get_preferred_width_for_height:", height ### + return self.do_get_preferred_width() + +#----------------------------------------------------------------------------- + +def mix_in(*src_classes): + # Workaround for do_xxx method overrides not working properly + # with multiple inheritance. + # + # Usage: + # + # class MyClass(Gtk.SomeBaseClass): + # mix_in(Class1, Class2, ...) + # + import sys + frame = sys._getframe(1) + dst_dict = frame.f_locals + for src_class in src_classes: + for name, value in src_class.__dict__.iteritems(): + if name not in dst_dict: + dst_dict[name] = value diff --git a/GUI/GtkGI/ImageBases.py b/GUI/GtkGI/ImageBases.py new file mode 100644 index 0000000..1d19af4 --- /dev/null +++ b/GUI/GtkGI/ImageBases.py @@ -0,0 +1,33 @@ +# +# PyGUI - Image Base - Gtk +# + +from __future__ import division +from GUI.GImageBases import ImageBase as GImageBase + +class ImageBase(GImageBase): + +# def get_width(self): +# return self._gtk_surface.get_width() +# +# def get_height(self): +# return self._gtk_surface.get_height() + + def draw(self, canvas, src_rect, dst_rect): + sx, sy, sr, sb = src_rect + dx, dy, dr, db = dst_rect + sw = sr - sx + sh = sb - sy + dw = dr - dx + dh = db - dy + ctx = canvas._gtk_ctx + ctx.save() + ctx.translate(dx, dy) + ctx.new_path() + ctx.rectangle(0, 0, dw, dh) + ctx.clip() + ctx.scale(dw / sw, dh / sh) + self._gtk_set_source(canvas._gtk_ctx, -sx, -sy) + ctx.paint() + ctx.restore() + diff --git a/GUI/GtkGI/Images.py b/GUI/GtkGI/Images.py new file mode 100644 index 0000000..ccb7396 --- /dev/null +++ b/GUI/GtkGI/Images.py @@ -0,0 +1,23 @@ +# +# Python GUI - Images - Gtk +# + +from __future__ import division +from array import array +import cairo +from gi.repository import Gdk, GdkPixbuf +from GUI.GImages import Image as GImage + +class Image(GImage): + + def _init_from_file(self, file): + self._gdk_pixbuf = GdkPixbuf.Pixbuf.new_from_file(file) + + def _gtk_set_source(self, ctx, x, y): + ctx.set_source_pixbuf(self._gdk_pixbuf, x, y) + + def get_width(self): + return self._gdk_pixbuf.get_width() + + def get_height(self): + return self._gdk_pixbuf.get_height() diff --git a/GUI/GtkGI/Images_xlib.py b/GUI/GtkGI/Images_xlib.py new file mode 100644 index 0000000..b30291f --- /dev/null +++ b/GUI/GtkGI/Images_xlib.py @@ -0,0 +1,36 @@ +# +# Python GUI - Images - Gtk +# + +from gi.repository import Gdk +from GtkImageScaling import gtk_scale_pixbuf +from GImages import Image as GImage + +class Image(GImage): + + def _init_from_file(self, file): + self._gdk_pixbuf = GdkPixbuf.Pixbuf.new_from_file(file) + + def get_width(self): + return self._gdk_pixbuf.get_width() + + def get_height(self): + return self._gdk_pixbuf.get_height() + + def draw(self, canvas, src_rect, dst_rect): + sx, sy, sr, sb = src_rect + dx, dy, dr, db = dst_rect + sw = sr - sx + sh = sb - sy + dw = dr - dx + dh = db - dy + gdk_pixbuf = self._gdk_pixbuf + if sw <> dw or sh <> dh: + gdk_scaled_pixbuf = gtk_scale_pixbuf(gdk_pixbuf, sx, sy, sw, sh, dw, dh) + canvas._gdk_drawable.draw_pixbuf( + canvas._gdk_gc, gdk_scaled_pixbuf, + 0, 0, dx, dy, dw, dh, Gdk.RGB_DITHER_NORMAL, 0, 0) + else: + canvas._gdk_drawable.draw_pixbuf( + canvas._gdk_gc, self._gdk_pixbuf, + sx, sy, dx, dy, sw, sh, Gdk.RGB_DITHER_NORMAL, 0, 0) diff --git a/GUI/GtkGI/Labels.py b/GUI/GtkGI/Labels.py new file mode 100644 index 0000000..e0a2e96 --- /dev/null +++ b/GUI/GtkGI/Labels.py @@ -0,0 +1,33 @@ +# +# Python GUI - Labels - Gtk +# + +from gi.repository import Gtk +from GUI.StdFonts import system_font +from GUI.GLabels import Label as GLabel + +class Label(GLabel): + + _vertical_padding = 6 + + def __init__(self, text = "New Label", font = system_font, **kwds): + width, height = font.text_size(text) + gtk_label = Gtk.Label(label=text) + gtk_label.set_alignment(0.0, 0.5) + gtk_label.set_size_request(width, height + self._vertical_padding) + gtk_label.show() + GLabel.__init__(self, _gtk_outer = gtk_label, font = font, **kwds) + + def get_text(self): + return self._gtk_outer_widget.get_text() + + def set_text(self, text): + self._gtk_outer_widget.set_text(text) + + def _gtk_get_alignment(self): + return self._gtk_outer_widget.get_alignment()[0] + + def _gtk_set_alignment(self, fraction, just): + gtk_label = self._gtk_outer_widget + gtk_label.set_alignment(fraction, 0.0) + gtk_label.set_justify(just) diff --git a/GUI/GtkGI/Menus.py b/GUI/GtkGI/Menus.py new file mode 100644 index 0000000..978345a --- /dev/null +++ b/GUI/GtkGI/Menus.py @@ -0,0 +1,63 @@ +# +# Python GUI - Menus - Gtk version +# + +from gi.repository import Gtk +from gi.repository import Gdk +from GUI.Globals import application +from GUI.GMenus import Menu as GMenu, MenuItem + +def _report_accel_changed_(*args): + print "Menus: accel_changed:", args + +class Menu(GMenu): + + def __init__(self, title, items, **kwds): + GMenu.__init__(self, title, items, **kwds) + self._gtk_menu = Gtk.Menu() + self._gtk_accel_group = Gtk.AccelGroup() + #self._gtk_accel_group.connect('accel_changed', _report_accel_changed_) ### + + def _clear_platform_menu(self): + gtk_menu = self._gtk_menu + for gtk_item in gtk_menu.get_children(): + gtk_item.destroy() + + def _add_separator_to_platform_menu(self): + gtk_item = Gtk.MenuItem() + gtk_item.set_sensitive(0) + gtk_separator = Gtk.HSeparator() + gtk_item.add(gtk_separator) + self._gtk_add_item(gtk_item) + + def _gtk_add_item(self, gtk_item): + gtk_item.show_all() + self._gtk_menu.append(gtk_item) + + def _add_item_to_platform_menu(self, item, name, command = None, index = None): + checked = item.checked + if checked is None: + gtk_item = Gtk.MenuItem.new_with_label(name) + else: + gtk_item = Gtk.CheckMenuItem.new_with_label(name) + self._gtk_add_item(gtk_item) + if not item.enabled: + gtk_item.set_sensitive(0) + if checked: + gtk_item.set_active(1) + if command: + app = application() + if index is not None: + action = lambda widget: app.dispatch(command, index) + else: + action = lambda widget: app.dispatch(command) + gtk_item.connect('activate', action) + key = item._key + if key: + gtk_modifiers = Gdk.ModifierType.CONTROL_MASK + if item._shift: + gtk_modifiers |= Gdk.ModifierType.SHIFT_MASK + if item._option: + gtk_modifiers |= Gdk.ModifierType.MOD1_MASK + gtk_item.add_accelerator('activate', self._gtk_accel_group, + ord(key), gtk_modifiers, Gtk.AccelFlags.VISIBLE) diff --git a/GUI/GtkGI/Pixmaps.py b/GUI/GtkGI/Pixmaps.py new file mode 100644 index 0000000..ed3bbd7 --- /dev/null +++ b/GUI/GtkGI/Pixmaps.py @@ -0,0 +1,12 @@ +# +# Python GUI - Pixmap - Gtk +# + +from gi.repository import Gdk +from GUI.GtkPixmaps import GtkPixmap +from GUI.GPixmaps import Pixmap as GPixmap + +class Pixmap(GtkPixmap, GPixmap): + + def __init__(self, width, height): + GtkPixmap.__init__(self, width, height) diff --git a/GUI/GtkGI/Printing.py b/GUI/GtkGI/Printing.py new file mode 100644 index 0000000..e712cf7 --- /dev/null +++ b/GUI/GtkGI/Printing.py @@ -0,0 +1,190 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Printing - Gtk +# +#------------------------------------------------------------------------------ + +from gi.repository import Gtk +#from gi.repository.Gtk import UNIT_POINTS +from GUI.Canvases import Canvas +from GUI.GPrinting import PageSetup as GPageSetup, Printable as GPrintable, \ + Paginator + +UNIT_POINTS = Gtk.Unit.POINTS + +gtk_paper_names = [ + Gtk.PAPER_NAME_A3, + Gtk.PAPER_NAME_A4, + Gtk.PAPER_NAME_A5, + Gtk.PAPER_NAME_B5, + Gtk.PAPER_NAME_LETTER, + Gtk.PAPER_NAME_EXECUTIVE, + Gtk.PAPER_NAME_LEGAL, +] + +gtk_paper_formats = {} + +gtk_print_settings = Gtk.PrintSettings() + +def init_gtk_paper_formats(): + for gtk_name in gtk_paper_names: + display_name = Gtk.PaperSize.new(gtk_name).get_display_name() + gtk_paper_formats[display_name] = gtk_name + +init_gtk_paper_formats() + +def gtk_default_page_setup(): + pset = Gtk.PageSetup() + pset.set_paper_size(Gtk.PaperSize.new(Gtk.paper_size_get_default())) + return pset + +def get_gtk_state(gtk_page_setup): + state = {} + state['orientation'] = gtk_page_setup.get_orientation() + state['paper_size'] = gtk_page_setup.get_paper_size().get_name() + state['top_margin'] = gtk_page_setup.get_top_margin(UNIT_POINTS) + state['bottom_margin'] = gtk_page_setup.get_bottom_margin(UNIT_POINTS) + state['left_margin'] = gtk_page_setup.get_left_margin(UNIT_POINTS) + state['right_margin'] = gtk_page_setup.get_right_margin(UNIT_POINTS) + return state + +def set_gtk_state(gtk_page_setup, state): + gtk_page_setup.set_orientation(state['orientation']) + gtk_page_setup.set_paper_size(Gtk.PaperSize(state['paper_size'])) + gtk_page_setup.set_top_margin(state['top_margin'], UNIT_POINTS) + gtk_page_setup.set_bottom_margin(state['bottom_margin'], UNIT_POINTS) + gtk_page_setup.set_left_margin(state['left_margin'], UNIT_POINTS) + gtk_page_setup.set_right_margin(state['right_margin'], UNIT_POINTS) + +#------------------------------------------------------------------------------ + +class PageSetup(GPageSetup): + + _printer_name = "" + _left_margin = 36 + _top_margin = 36 + _right_margin = 36 + _bottom_margin = 36 + + def __init__(self): + self._gtk_page_setup = gtk_default_page_setup() + + def __getstate__(self): + state = GPageSetup.__getstate__(self) + state['_gtk_page_setup'] = get_gtk_state(self._gtk_page_setup) + return state + + def __setstate__(self, state): + gtk_setup = gtk_default_page_setup() + self._gtk_page_setup = gtk_setup + gtk_state = state.pop('_gtk_page_setup', None) + if gtk_state: + set_gtk_state(gtk_setup, gtk_state) + self.margins = state['margins'] + self.printer_name = state['printer_name'] + else: + GPageSetup.__setstate__(state) + + def get_paper_name(self): + return self._gtk_page_setup.get_paper_size().get_display_name() + + def set_paper_name(self, x): + psize = Gtk.PaperSize(gtk_paper_formats.get(x) or x) + self._gtk_page_setup.set_paper_size(psize) + + def get_paper_width(self): + return self._gtk_page_setup.get_paper_width(UNIT_POINTS) + + def set_paper_width(self, x): + self._gtk_page_setup.set_paper_width(x, UNIT_POINTS) + + def get_paper_height(self): + return self._gtk_page_setup.get_paper_height(UNIT_POINTS) + + def set_paper_height(self, x): + self._gtk_page_setup.set_paper_height(x, UNIT_POINTS) + + def get_orientation(self): + o = self._gtk_page_setup.get_orientation() + if o in (Gtk.PAGE_ORIENTATION_LANDSCAPE, + Gtk.PAGE_ORIENTATION_REVERSE_LANDSCAPE): + return 'landscape' + else: + return 'portrait' + + def set_orientation(self, x): + if x == 'landscape': + o = Gtk.PAGE_ORIENTATION_LANDSCAPE + else: + o = Gtk.PAGE_ORIENTATION_PORTRAIT + self._gtk_page_setup.set_orientation(o) + + def get_left_margin(self): + return self._left_margin + + def get_top_margin(self): + return self._top_margin + + def get_right_margin(self): + return self._right_margin + + def get_bottom_margin(self): + return self._bottom_margin + + def set_left_margin(self, x): + self._left_margin = x + + def set_top_margin(self, x): + self._top_margin = x + + def set_right_margin(self, x): + self._right_margin = x + + def set_bottom_margin(self, x): + self._bottom_margin = x + + def get_printer_name(self): + return self._printer_name + + def set_printer_name(self, x): + self._printer_name = x + +#------------------------------------------------------------------------------ + +class Printable(GPrintable): + + def print_view(self, page_setup, prompt = True): + global gtk_print_settings + paginator = Paginator(self, page_setup) + + def draw_page(_, gtk_print_context, page_num): + cairo_context = gtk_print_context.get_cairo_context() + canvas = Canvas._from_cairo_context(cairo_context) + paginator.draw_page(canvas, page_num) + + gtk_op = Gtk.PrintOperation() + gtk_op.set_print_settings(gtk_print_settings) + gtk_op.set_default_page_setup(page_setup._gtk_page_setup) + gtk_op.set_n_pages(paginator.num_pages) + gtk_op.set_use_full_page(True) + gtk_op.set_unit(UNIT_POINTS) + gtk_op.connect('draw-page', draw_page) + if prompt: + action = Gtk.PRINT_OPERATION_ACTION_PRINT_DIALOG + else: + action = Gtk.PRINT_OPERATION_ACTION_PRINT + result = gtk_op.run(action) + if result == Gtk.PRINT_OPERATION_RESULT_APPLY: + gtk_print_settings = gtk_op.get_print_settings() + +#------------------------------------------------------------------------------ + +def present_page_setup_dialog(page_setup): + old_setup = page_setup._gtk_page_setup + ps = Gtk.PrintSettings() + new_setup = Gtk.print_run_page_setup_dialog(None, old_setup, ps) + if get_gtk_state(old_setup) <> get_gtk_state(new_setup): + page_setup._gtk_page_setup = new_setup + return True + else: + return False diff --git a/GUI/GtkGI/RadioButtons.py b/GUI/GtkGI/RadioButtons.py new file mode 100644 index 0000000..ca51dc9 --- /dev/null +++ b/GUI/GtkGI/RadioButtons.py @@ -0,0 +1,35 @@ +# +# Python GUI - Radio buttons - Gtk +# + +from gi.repository import Gtk +from GUI.GRadioButtons import RadioButton as GRadioButton + +class RadioButton(GRadioButton): + + def __init__(self, title = "New Control", **kwds): + gtk_radiobutton = Gtk.RadioButton(None, title) + gtk_radiobutton.show() + self._gtk_connect(gtk_radiobutton, 'toggled', self._gtk_toggled_signal) + GRadioButton.__init__(self, _gtk_outer = gtk_radiobutton, **kwds) + + def _value_changed(self): + group = self._group + if group: + if self._value == group._value: + self._turn_on() + else: + group._turn_all_off() + + def _turn_on(self): + self._gtk_outer_widget.set_active(1) + + def _is_on(self): + return self._gtk_outer_widget.get_active() + + def _gtk_toggled_signal(self): + if self._is_on(): + group = self._group + if group and group._value <> self._value: + group._value = self._value + group.do_action() diff --git a/GUI/GtkGI/RadioGroups.py b/GUI/GtkGI/RadioGroups.py new file mode 100644 index 0000000..385e311 --- /dev/null +++ b/GUI/GtkGI/RadioGroups.py @@ -0,0 +1,35 @@ +# +# Python GUI - Radio groups - Gtk +# + +from gi.repository import Gtk +from GUI.GRadioGroups import RadioGroup as GRadioGroup + +class RadioGroup(GRadioGroup): + + def __init__(self, items = [], **kwds): + self._gtk_dummy_radiobutton = Gtk.RadioButton() + GRadioGroup.__init__(self, items, **kwds) + + def _item_added(self, item): + old_value = self._value + #item._gtk_outer_widget.set_group(self._gtk_dummy_radiobutton) + item._gtk_outer_widget.join_group(self._gtk_dummy_radiobutton) + self.value = old_value + + def _item_removed(self, item): + item._gtk_outer_widget.set_group(None) + if item._value == self._value: + self._value = None + self._turn_all_off() + + def _value_changed(self): + new_value = self._value + for item in self._items: + if item._value == new_value: + item._turn_on() + return + self._turn_all_off() + + def _turn_all_off(self): + self._gtk_dummy_radiobutton.set_active(1) diff --git a/GUI/GtkGI/ScrollableViews.py b/GUI/GtkGI/ScrollableViews.py new file mode 100644 index 0000000..f0929c5 --- /dev/null +++ b/GUI/GtkGI/ScrollableViews.py @@ -0,0 +1,89 @@ +# +# Python GUI - Scrollable Views - Gtk +# + +from gi.repository import Gtk +from GUI.Scrollables import Scrollable, gtk_scrollbar_breadth +from GUI.GScrollableViews import ScrollableView as GScrollableView, \ + default_extent, default_line_scroll_amount +from GUI.Geometry import offset_rect_neg + +class ScrollableView(GScrollableView, Scrollable): + + def __init__(self, extent = default_extent, + line_scroll_amount = default_line_scroll_amount, + **kwds): + gtk_scrolled_window = Gtk.ScrolledWindow() + gtk_scrolled_window.show() + GScrollableView.__init__(self, _gtk_outer = gtk_scrolled_window, + extent = extent, line_scroll_amount = line_scroll_amount, **kwds) + + # + # Properties + # + + def get_content_width(self): + w = self._size[0] + if self.hscrolling: + w -= gtk_scrollbar_breadth + return w + + def get_content_height(self): + h = self._size[1] + if self.vscrolling: + h -= gtk_scrollbar_breadth + return h + + def get_content_size(self): + return self.content_width, self.content_height + + def set_content_size(self, size): + w, h = size + d = gtk_scrollbar_breadth + if self.hscrolling: + w += d + if self.vscrolling: + h += d + self.size = (w, h) + + def get_extent(self): + return self._gtk_inner_widget.get_size() + + def set_extent(self, (w, h)): + self._gtk_inner_widget.set_size(w, h) + + def get_scroll_offset(self): + hadj, vadj = self._gtk_adjustments() + return int(hadj.get_value()), int(vadj.get_value()) + + def set_scroll_offset(self, (x, y)): + hadj, vadj = self._gtk_adjustments() + hadj.set_value(min(float(x), hadj.get_upper() - hadj.get_page_size())) + vadj.set_value(min(float(y), vadj.get_upper() - vadj.get_page_size())) + + def get_line_scroll_amount(self): + hadj, vadj = self._gtk_adjustments() + return hadj.get_step_increment(), vadj.get_step_increment() + + def set_line_scroll_amount(self, (dx, dy)): + hadj, vadj = self._gtk_adjustments() + hadj.set_step_increment(float(dx)) # Amazingly, ints are not + vadj.set_step_increment(float(dy)) # acceptable here. + + def invalidate_rect(self, rect): + GScrollableView.invalidate_rect(self, + offset_rect_neg(rect, self.scroll_offset)) + + # + # Internal + # + + def _gtk_adjustments(self): + gtk_widget = self._gtk_inner_widget + hadj = gtk_widget.get_hadjustment() + vadj = gtk_widget.get_vadjustment() + return hadj, vadj + + def _gtk_prepare_cairo_context(self, context): + x, y = self.scroll_offset + context.translate(-x, -y) diff --git a/GUI/GtkGI/Scrollables.py b/GUI/GtkGI/Scrollables.py new file mode 100644 index 0000000..35335a2 --- /dev/null +++ b/GUI/GtkGI/Scrollables.py @@ -0,0 +1,26 @@ +# +# Python GUI - Common code for scrollable components - Gtk +# + +from gi.repository import Gtk + +gtk_scroll_policies = [Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS] + +gtk_dummybar = Gtk.VScrollbar() +gtk_scrollbar_breadth = gtk_dummybar.get_preferred_width() +del gtk_dummybar + +class Scrollable(object): + + def get_hscrolling(self): + return self._gtk_outer_widget.get_property('hscrollbar-policy') <> Gtk.PolicyType.NEVER + + def set_hscrolling(self, value): + self._gtk_outer_widget.set_property('hscrollbar-policy', gtk_scroll_policies[value]) + + def get_vscrolling(self): + return self._gtk_outer_widget.get_property('vscrollbar-policy') <> Gtk.PolicyType.NEVER + + def set_vscrolling(self, value): + self._gtk_outer_widget.set_property('vscrollbar-policy', gtk_scroll_policies[value]) + diff --git a/GUI/GtkGI/Sliders.py b/GUI/GtkGI/Sliders.py new file mode 100644 index 0000000..35a9255 --- /dev/null +++ b/GUI/GtkGI/Sliders.py @@ -0,0 +1,154 @@ +# +# Python GUI - Slider - Gtk +# + +from gi.repository import Gtk +from GUI.GSliders import Slider as GSlider + +class Slider(GSlider): + + _gtk_tick_length = 8 + _gtk_tick_inset = 18 + + def __init__(self, orient = 'h', ticks = 0, **kwds): + self._orient = orient + self._ticks = ticks + self._discrete = False + self._live = True + self._gtk_ticks = None + length = 100 + gtk_adjustment = Gtk.Adjustment(upper = 1.0) + xs = 0.0 + ys = 0.0 + if orient == 'h': + gtk_scale = Gtk.HScale(gtk_adjustment) + gtk_scale.set_size_request(length, -1) + gtk_box = Gtk.VBox() + xs = 1.0 + elif orient == 'v': + gtk_scale = Gtk.VScale(gtk_adjustment) + gtk_scale.set_size_request(-1, length) + gtk_box = Gtk.HBox() + ys = 1.0 + else: + raise ValueError("Invalid orientation, should be 'h' or 'v'") + gtk_scale.set_draw_value(False) + self._gtk_scale = gtk_scale + gtk_box.pack_start(gtk_scale, True, True, 0) + self._gtk_box = gtk_box + if ticks: + self._gtk_create_ticks() + gtk_alignment = Gtk.Alignment.new(xalign = 0.5, yalign = 0.5, + xscale = xs, yscale = ys) + gtk_alignment.add(gtk_box) + gtk_alignment.show_all() + self._gtk_connect(gtk_adjustment, 'value-changed', self._gtk_value_changed) + self._gtk_connect(gtk_scale, 'change-value', self._gtk_change_value) + self._gtk_connect(gtk_scale, 'button-release-event', self._gtk_button_release) + self._gtk_scale = gtk_scale + self._gtk_adjustment = gtk_adjustment + self._gtk_enable_action = True + GSlider.__init__(self, _gtk_outer = gtk_alignment, **kwds) + + def get_min_value(self): + return self._min_value + + def set_min_value(self, x): + self._gtk_adjustment.lower = x + + def get_max_value(self): + return self._max_value + + def set_max_value(self, x): + self._gtk_adjustment.upper = x + + def get_value(self): + return self._gtk_adjustment.value + + def set_value(self, x): + self._gtk_enable_action = False + self._gtk_adjustment.value = x + self._gtk_enable_action = True + + def get_ticks(self): + return self._ticks + + def set_ticks(self, x): + self._ticks = x + if x: + self._gtk_create_ticks() + else: + self._gtk_destroy_ticks() + + def get_discrete(self): + return self._discrete + + def set_discrete(self, x): + self._discrete = x + + def get_live(self): + return self._live + + def set_live(self, x): + self._live = x + + def _gtk_create_ticks(self): + if not self._gtk_ticks: + gtk_ticks = Gtk.DrawingArea() + length = self._gtk_tick_length + if self._orient == 'h': + gtk_ticks.set_size_request(-1, length) + else: + gtk_ticks.set_size_request(length, -1) + self._gtk_ticks = gtk_ticks + self._gtk_connect(gtk_ticks, 'expose-event', self._gtk_draw_ticks) + self._gtk_box.pack_start(gtk_ticks, True, True, 0) + + def _gtk_destroy_ticks(self): + gtk_ticks = self._gtk_ticks + if gtk_ticks: + gtk_ticks.destroy() + self._gtk_ticks = None + + def _gtk_draw_ticks(self, event): + gtk_ticks = self._gtk_ticks + gdk_win = gtk_ticks.window + gtk_style = gtk_ticks.style + orient = self._orient + steps = self._ticks - 1 + _, _, w, h = gtk_ticks.allocation + u0 = self._gtk_tick_inset + v0 = 0 + if orient == 'h': + draw_line = gtk_style.paint_vline + u1 = w - u0 + v1 = h + else: + draw_line = gtk_style.paint_hline + u1 = h - u0 + v1 = w + state = Gtk.StateType.NORMAL + for i in xrange(steps + 1): + u = u0 + i * (u1 - u0) / steps + draw_line(gdk_win, state, None, gtk_ticks, "", v0, v1, u) + + def _gtk_value_changed(self): + if self._live and self._gtk_enable_action: + self.do_action() + + def _gtk_change_value(self, event_type, value): + gtk_adjustment = self._gtk_adjustment + vmin = gtk_adjustment.lower + vmax = gtk_adjustment.upper + value = min(max(vmin, value), vmax) + if self._discrete: + steps = self._ticks - 1 + if steps > 0: + q = round(steps * (value - vmin) / (vmax - vmin)) + value = vmin + q * (vmax - vmin) / steps + if gtk_adjustment.value <> value: + gtk_adjustment.value = value + return True + + def _gtk_button_release(self, gtk_event): + self.do_action() diff --git a/GUI/GtkGI/StdCursors.py b/GUI/GtkGI/StdCursors.py new file mode 100644 index 0000000..0b45984 --- /dev/null +++ b/GUI/GtkGI/StdCursors.py @@ -0,0 +1,29 @@ +# +# Python GUI - Standard Cursors - Gtk +# + +from gi.repository import Gdk +from GUI.Cursors import Cursor + +__all__ = [ + 'arrow', + 'ibeam', + 'crosshair', + 'fist', + 'hand', + 'finger', + 'invisible', +] + +arrow = Cursor._from_gtk_std_cursor(Gdk.CursorType.LEFT_PTR) +ibeam = Cursor._from_gtk_std_cursor(Gdk.CursorType.XTERM) +crosshair = Cursor._from_gtk_std_cursor(Gdk.CursorType.TCROSS) +fist = Cursor("cursors/fist.tiff") +hand = Cursor("cursors/hand.tiff") +finger = Cursor("cursors/finger.tiff") +invisible = Cursor._from_nothing() + +del Cursor + +def empty_cursor(): + return invisible diff --git a/GUI/GtkGI/StdFonts.py b/GUI/GtkGI/StdFonts.py new file mode 100644 index 0000000..6f5c1a5 --- /dev/null +++ b/GUI/GtkGI/StdFonts.py @@ -0,0 +1,9 @@ +# +# Python GUI - Standard Fonts - Gtk +# + +from gi.repository import Gtk +from GUI.Fonts import Font + +system_font = Font._from_pango_description(Gtk.Label().get_style().font_desc) +application_font = Font._from_pango_description(Gtk.Entry().get_style().font_desc) diff --git a/GUI/GtkGI/StdMenus.py b/GUI/GtkGI/StdMenus.py new file mode 100644 index 0000000..70264a3 --- /dev/null +++ b/GUI/GtkGI/StdMenus.py @@ -0,0 +1,48 @@ +# +# Python GUI - Standard Menus - Gtk +# + +from GUI.Menus import Menu +from GUI.MenuLists import MenuList + +_file_menu_items = [ + ("New/N", 'new_cmd'), + ("Open.../O", 'open_cmd'), + ("Close/W", 'close_cmd'), + "-", + ("Save/S", 'save_cmd'), + ("Save As...", 'save_as_cmd'), + ("Revert", 'revert_cmd'), + "-", + ("Page Setup...", 'page_setup_cmd'), + ("Print.../P", 'print_cmd'), + "-", + ("Quit/Q", 'quit_cmd'), +] + +_edit_menu_items = [ + ("Undo/Z", 'undo_cmd'), + ("Redo/^Z", 'redo_cmd'), + "-", + ("Cut/X", 'cut_cmd'), + ("Copy/C", 'copy_cmd'), + ("Paste/V", 'paste_cmd'), + ("Clear", 'clear_cmd'), + "-", + ("Select All/A", 'select_all_cmd'), + "-", + ("Preferences...", 'preferences_cmd'), +] + +_help_menu_items = [ + ("About ", 'about_cmd'), +] + +#------------------------------------------------------------------------------ + +def basic_menus(substitutions = {}): + return MenuList([ + Menu("File", _file_menu_items, substitutions = substitutions), + Menu("Edit", _edit_menu_items, substitutions = substitutions), + Menu("Help", _help_menu_items, special = True, substitutions = substitutions), + ]) diff --git a/GUI/GtkGI/Tasks.py b/GUI/GtkGI/Tasks.py new file mode 100644 index 0000000..6d71038 --- /dev/null +++ b/GUI/GtkGI/Tasks.py @@ -0,0 +1,37 @@ +# +# PyGUI - Tasks - Gtk +# + +import gobject +from GUI.GTasks import Task as GTask + +class Task(GTask): + + def __init__(self, proc, interval, repeat = 0, start = 1): + self._proc = proc + self._gtk_interval = int(interval * 1000) + self._repeat = repeat + self._gtk_timeout_id = None + if start: + self.start() + + def get_scheduled(self): + return self._gtk_timeout_id is not None + + def start(self): + if self._gtk_timeout_id is None: + self._gtk_timeout_id = GObject.timeout_add(self._gtk_interval, + self._gtk_fire) + + def stop(self): + id = self._gtk_timeout_id + if id is not None: + GObject.source_remove(id) + self._gtk_timeout_id = None + + def _gtk_fire(self): + self._proc() + if self._repeat: + return 1 + else: + self._gtk_timeout_id = None diff --git a/GUI/GtkGI/TextEditors.py b/GUI/GtkGI/TextEditors.py new file mode 100644 index 0000000..1ab4f5c --- /dev/null +++ b/GUI/GtkGI/TextEditors.py @@ -0,0 +1,119 @@ +# +# Python GUI - Text Editor - Gtk +# + +import pango, gtk +from GUI.Globals import application +from GUI.GTextEditors import TextEditor as GTextEditor +from GUI.Scrollables import Scrollable +from GUI.Fonts import Font + +class TextEditor(GTextEditor, Scrollable): + + _font = None + + def __init__(self, scrolling = 'hv', **kwds): + gtk_sw = Gtk.ScrolledWindow() + gtk_sw.show() + gtk_tv = Gtk.TextView() + gtk_tv.show() + gtk_sw.add(gtk_tv) + gtk_tb = Gtk.TextBuffer() + self._gtk_textbuffer = gtk_tb + gtk_tv.set_buffer(self._gtk_textbuffer) + tag = Gtk.TextTag() + tabs = Pango.TabArray(1, True) + tabs.set_tab(0, Pango.TabAlign.LEFT, 28) + tag.set_property('tabs', tabs) + tag.set_property('tabs-set', True) + self._gtk_tag = tag + gtk_tb.get_tag_table().add(tag) + GTextEditor.__init__(self, _gtk_outer = gtk_sw, _gtk_inner = gtk_tv, + _gtk_focus = gtk_tv, **kwds) + self.set_hscrolling('h' in scrolling) + self.set_vscrolling('v' in scrolling) + if 'h' not in scrolling: + gtk_tv.set_wrap_mode(Gtk.WrapMode.WORD) + self._gtk_apply_tag() + + def _gtk_get_sel_iters(self): + gtk_textbuffer = self._gtk_textbuffer + sel_iters = gtk_textbuffer.get_selection_bounds() + if not sel_iters: + insert_mark = gtk_textbuffer.get_insert() + insert_iter = gtk_textbuffer.get_iter_at_mark(insert_mark) + sel_iters = (insert_iter, insert_iter) + return sel_iters + + def _gtk_apply_tag(self): + tb = self._gtk_textbuffer + tb.apply_tag(self._gtk_tag, tb.get_start_iter(), tb.get_end_iter()) + + def get_selection(self): + tb = self._gtk_textbuffer + bounds = tb.get_selection_bounds() + if bounds: + return (bounds[0].get_offset(), bounds[1].get_offset()) + else: + i = tb.get_property('cursor-position') + return (i, i) + + def set_selection(self, value): + tb = self._gtk_textbuffer + start = tb.get_iter_at_offset(value[0]) + end = tb.get_iter_at_offset(value[1]) + tb.select_range(start, end) + + def get_text(self): + tb = self._gtk_textbuffer + start = tb.get_start_iter() + end = tb.get_end_iter() + return tb.get_slice(start, end) + + def set_text(self, text): + self._gtk_textbuffer.set_text(text) + self._gtk_apply_tag() + + def get_text_length(self): + return self._gtk_textbuffer.get_end_iter().get_offset() + + def get_font(self): + font = self._font + if not font: + font = Font._from_pango_description(self._gtk_inner_widget.style.font_desc) + return font + + def set_font(self, f): + self._font = f + tv = self._gtk_inner_widget + tv.modify_font(f._pango_description) + + def get_tab_spacing(self): + tabs = self._gtk_tag.get_property('tabs') + return tabs.get_tab(0)[1] + + def set_tab_spacing(self, x): + tabs = Pango.TabArray(1, True) + tabs.set_tab(0, Pango.TabAlign.LEFT, x) + self._gtk_tag.set_property('tabs', tabs) + + def cut_cmd(self): + self.copy_cmd() + self.clear_cmd() + + def copy_cmd(self): + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + text = gtk_textbuffer.get_text(start_iter, end_iter, 1) + if text: + application().set_clipboard(text) + + def paste_cmd(self): + text = application().get_clipboard() + self.clear_cmd() + self._gtk_textbuffer.insert_at_cursor(text) + + def clear_cmd(self): + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + gtk_textbuffer.delete(start_iter, end_iter) diff --git a/GUI/GtkGI/TextFields.py b/GUI/GtkGI/TextFields.py new file mode 100644 index 0000000..00c8035 --- /dev/null +++ b/GUI/GtkGI/TextFields.py @@ -0,0 +1,209 @@ +#----------------------------------------------------------------------------- +# +# Python GUI - Text fields - Gtk +# +#----------------------------------------------------------------------------- + +from gi.repository import Gtk, Gdk +from GUI.Properties import overridable_property +from GUI.Applications import application +from GUI.StdFonts import application_font +from GUI.GTextFields import TextField as GTextField +from GUI.GtkUtils import GtkFixedSize, mix_in + +gtk_margins = (2, 2, 0, 2) +gtk_border_size = 2 +gtk_white = Gdk.RGBA(1.0, 1.0, 1.0, 1.0) + +class TextField(GTextField): + + _pass_key_events_to_platform = True + + _multiline = 0 + + def __init__(self, font = application_font, lines = 1, + multiline = 0, password = 0, **kwds): + self._multiline = multiline + lm, tm, rm, bm = gtk_margins + if multiline: + gtk_textbuffer = Gtk.TextBuffer() + gtk_textview = Gtk.TextView.new_with_buffer(gtk_textbuffer) + gtk_textview.set_margin_left(lm) + gtk_textview.set_margin_top(tm) + gtk_textview.set_margin_right(rm) + gtk_textview.set_margin_bottom(bm) + gtk_box = Gtk.EventBox() + gtk_box.add(gtk_textview) + state = Gtk.StateFlags.NORMAL + #bg = gtk_textview.get_style_context().get_background_color(state) # doesn't work + #print "TextField: bg =", bg ### + bg = gtk_white + gtk_box.override_background_color(state, bg) + gtk_outer = PyGUI_GtkFrame() + gtk_outer.add(gtk_box) +# gtk_alignment = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0) +# gtk_alignment.set_padding(tm, bm, lm, rm) +# gtk_alignment.add(gtk_textview) +# gtk_box = Gtk.EventBox() +# gtk_box.add(gtk_alignment) +# gtk_box.modify_bg(Gtk.StateType.NORMAL, +# gtk_textview.get_style().base[Gtk.StateType.NORMAL]) +# gtk_frame = Gtk.Frame() +# gtk_frame.set_shadow_type(Gtk.ShadowType.IN) +# gtk_frame.add(gtk_box) + self._gtk_textbuffer = gtk_textbuffer + gtk_text_widget = gtk_textview +# gtk_outer = gtk_frame + else: + gtk_entry = Gtk.Entry() + self._gtk_entry = gtk_entry + gtk_text_widget = gtk_entry + gtk_outer = gtk_entry + self._font = font + #gtk_text_widget.modify_font(font._pango_description) + gtk_text_widget.override_font(font._pango_description) + #border_size = gtk_outer.get_style().ythickness # not working + #print "TextFields: border size =", border_size ### + self._vertical_padding = tm + 2 * gtk_border_size + bm + #line_height = font.text_size("X")[1] + line_height = font.line_height + height = self._vertical_padding + lines * line_height + gtk_outer.set_size_request(-1, height) + self._password = password + if password: + if not multiline: + self._gtk_entry.set_visibility(0) + else: + raise ValueError("The password option is not supported for multiline" + " TextFields on this platform") + gtk_outer.show_all() + GTextField.__init__(self, + _gtk_outer = gtk_outer, + _gtk_title = gtk_text_widget, + _gtk_focus = gtk_text_widget, + _gtk_input = gtk_text_widget, + multiline = multiline, **kwds) + + def get_text(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start = gtk_textbuffer.get_start_iter() + end = gtk_textbuffer.get_end_iter() + return self._gtk_textbuffer.get_text(start, end, 1) + else: + return self._gtk_entry.get_text() + + def set_text(self, text): + if self._multiline: + self._gtk_textbuffer.set_text(text) + else: + self._gtk_entry.set_text(text) + + def get_selection(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + start = start_iter.get_offset() + end = end_iter.get_offset() + sel = (start, end) + else: + sel = self._gtk_get_sel_positions() + return sel + + def _gtk_get_sel_iters(self): + gtk_textbuffer = self._gtk_textbuffer + sel_iters = gtk_textbuffer.get_selection_bounds() + if not sel_iters: + insert_mark = gtk_textbuffer.get_insert() + insert_iter = gtk_textbuffer.get_iter_at_mark(insert_mark) + sel_iters = (insert_iter, insert_iter) + return sel_iters + + def _gtk_get_sel_positions(self): + gtk_entry = self._gtk_entry + sel = gtk_entry.get_selection_bounds() + if not sel: + pos = gtk_entry.get_position() + sel = (pos, pos) + return sel + + def _set_selection(self, start, end): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter = gtk_textbuffer.get_iter_at_offset(start) + end_iter = gtk_textbuffer.get_iter_at_offset(end) + gtk_textbuffer.select_range(start_iter, end_iter) + else: + self._gtk_entry.select_region(start, end) + + def set_selection(self, (start, end)): + self._set_selection(start, end) + self.become_target() + + def get_multiline(self): + return self._multiline + + def get_password(self): + return self._password + + def _select_all(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start = gtk_textbuffer.get_start_iter() + end = gtk_textbuffer.get_end_iter() + gtk_textbuffer.select_range(start, end) + else: + self._gtk_entry.select_region(0, -1) + + def select_all(self): + self._select_all() + self.become_target() + + def cut_cmd(self): + self.copy_cmd() + self.clear_cmd() + + def copy_cmd(self): + if self._password: + return + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + text = gtk_textbuffer.get_text(start_iter, end_iter, 1) + else: + start, end = self._gtk_get_sel_positions() + text = self._gtk_entry.get_chars(start, end) + if text: + application().set_clipboard(text) + + def paste_cmd(self): + text = application().get_clipboard() + self.clear_cmd() + if self._multiline: + self._gtk_textbuffer.insert_at_cursor(text) + else: + gtk_entry = self._gtk_entry + pos = gtk_entry.get_position() + gtk_entry.insert_text(text, pos) + gtk_entry.set_position(pos + len(text)) + + def clear_cmd(self): + if self._multiline: + gtk_textbuffer = self._gtk_textbuffer + start_iter, end_iter = self._gtk_get_sel_iters() + gtk_textbuffer.delete(start_iter, end_iter) + else: + start, end = self._gtk_get_sel_positions() + self._gtk_entry.delete_text(start, end) + + def _untargeted(self): + self._set_selection(0, 0) + + def _tab_in(self): + self._select_all() + GTextField._tab_in(self) + +#----------------------------------------------------------------------------- + +class PyGUI_GtkFrame(Gtk.Frame): + mix_in(GtkFixedSize) diff --git a/GUI/GtkGI/ViewBases.py b/GUI/GtkGI/ViewBases.py new file mode 100644 index 0000000..7c58c8b --- /dev/null +++ b/GUI/GtkGI/ViewBases.py @@ -0,0 +1,42 @@ +# +# Python GUI - View Base - Gtk +# + +from gi.repository import Gtk +from GUI.GViewBases import ViewBase as GViewBase + +class ViewBase(GViewBase): + + def __init__(self, **kwds): + GViewBase.__init__(self, **kwds) + self._gtk_connect(self._gtk_inner_widget, 'realize', self._gtk_realize) + + def track_mouse(self): + finished = 0 + while not finished: + self._mouse_event = None + while not self._mouse_event: + Gtk.main_iteration() + event = self._mouse_event + if event.kind == 'mouse_up': + finished = 1 + yield event + + def _cursor_changed(self): + gtk_widget = self._gtk_inner_widget + gdk_window = gtk_widget.get_window() + if gdk_window: + cursor = self._cursor + if cursor: + gdk_window.set_cursor(self._cursor._gtk_cursor) + else: + gdk_window.set_cursor(None) + + def _gtk_realize(self): + self._cursor_changed() + + def _targeted(self): + self.targeted() + + def _untargeted(self): + self.untargeted() diff --git a/GUI/GtkGI/Views.py b/GUI/GtkGI/Views.py new file mode 100644 index 0000000..36b1f6b --- /dev/null +++ b/GUI/GtkGI/Views.py @@ -0,0 +1,5 @@ +# +# Python GUI - Views - Gtk +# + +from GUI.GViews import View diff --git a/GUI/GtkGI/Windows.py b/GUI/GtkGI/Windows.py new file mode 100644 index 0000000..333bc96 --- /dev/null +++ b/GUI/GtkGI/Windows.py @@ -0,0 +1,259 @@ +# +# Python GUI - Windows - Gtk version +# + +import sys +from gi.repository import Gtk +from gi.repository import Gdk +from GUI.GGeometry import sub_pt +from GUI.Components import _gtk_find_component +from GUI.Containers import Container +from GUI.Applications import application +from GUI.GWindows import Window as GWindow + +_default_width = 200 +_default_height = 200 + +_modal_styles = ('modal_dialog', 'alert') +_dialog_styles = ('nonmodal_dialog', 'modal_dialog', 'alert') + +class Window(GWindow): + + #_pass_key_events_to_platform = False + + _size = (_default_width, _default_height) + _gtk_menubar = None + _need_menubar_update = 0 + _target = None + + def __init__(self, style = 'standard', title = "New Window", + movable = 1, closable = 1, hidable = None, resizable = 1, + zoomable = 1, **kwds): + self._all_menus = [] + modal = style in _modal_styles + if hidable is None: + hidable = not modal + self._resizable = resizable + gtk_win = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) + gtk_win.set_gravity(Gdk.Gravity.STATIC) + gtk_win.set_decorated(movable or closable or hidable or zoomable) + gtk_win.set_resizable(resizable) + gtk_win.set_modal(style in _modal_styles) + gtk_content = Gtk.Layout() + gtk_content.show() + if style in _dialog_styles: + gtk_win.set_type_hint(Gdk.WindowTypeHint.DIALOG) + gtk_win.add(gtk_content) + else: + self._gtk_create_menubar() + gtk_box = Gtk.VBox() + gtk_box.show() + gtk_box.pack_start(self._gtk_menubar, 0, 0, 0) + gtk_box.pack_end(gtk_content, 1, 1, 0) + gtk_win.add(gtk_box) + self._need_menubar_update = 1 + self._gtk_connect(gtk_win, 'configure-event', self._gtk_configure_event) + self._gtk_connect(gtk_win, 'key-press-event', self._gtk_key_press_event) + self._gtk_connect(gtk_win, 'delete-event', self._gtk_delete_event) + #GtkInput.__init__(self, gtk_content) + GWindow.__init__(self, _gtk_outer = gtk_win, _gtk_inner = gtk_content, + _gtk_focus = gtk_content, _gtk_input = gtk_content, + title = title, closable = closable) + self.set_size((_default_width, _default_height)) + self.set(**kwds) + self.become_target() + + def _gtk_create_menubar(self): + gtk_menubar = Gtk.MenuBar() + gtk_dummy_item = Gtk.MenuItem.new_with_label("") + gtk_menubar.append(gtk_dummy_item) + gtk_menubar.show_all() + h = gtk_menubar.size_request().height + gtk_menubar.set_size_request(-1, h) + gtk_dummy_item.set_submenu(None) + self._gtk_menubar = gtk_menubar + self._gtk_connect(gtk_menubar, 'button-press-event', + self._gtk_menubar_button_press_event) + + def destroy(self): + self.hide() + GWindow.destroy(self) + + def set_menus(self, x): + GWindow.set_menus(self, x) + self._need_menubar_update = 1 + if self.visible: + self._gtk_update_menubar() + + def get_title(self): + return self._gtk_outer_widget.get_title() + + def set_title(self, new_title): + self._gtk_outer_widget.set_title(new_title) + + def set_position(self, v): + self._position = v + self._gtk_outer_widget.move(*v) + + def set_size(self, new_size): + w, h = new_size + if self._resizable: + h += self._gtk_menubar_height() + gtk_resize = self._gtk_outer_widget.resize + else: + gtk_resize = self._gtk_inner_widget.set_size_request + gtk_resize(max(w, 1), max(h, 1)) + self._size = new_size + + def _gtk_configure_event(self, gtk_event): + gtk_win = self._gtk_outer_widget + self._position = gtk_win.get_position() + #self._update_size(gtk_win.get_size()) + w, h = gtk_win.get_size() + #w, h = self._gtk_inner_widget.get_size() + #w, h = self._gtk_inner_widget.size_request() + old_size = self._size + new_size = (w, h - self._gtk_menubar_height()) + #new_size = (w, h) + #print "Window._gtk_configure_event:", old_size, "->", new_size ### + self._size = new_size + if old_size <> new_size: + self._resized(sub_pt(new_size, old_size)) + + def get_visible(self): + return self._gtk_outer_widget.get_property('visible') + + def set_visible(self, new_v): + old_v = self.visible + self._gtk_outer_widget.set_property('visible', new_v) + if new_v and not old_v and self._need_menubar_update: + self._gtk_update_menubar() + + def show(self): + self.set_visible(1) + self._gtk_outer_widget.present() + +# def key_down(self, event): +# if event.char == '\t': +# if event.shift: +# self._tab_to_prev() +# else: +# self._tab_to_next() +# else: +# self.pass_to_next_handler('key_down', event) + + def get_target(self): + target = _gtk_find_component(self._gtk_outer_widget.get_focus()) + return target or self + + def _screen_rect(self): + w = Gdk.Screen.width() + h = Gdk.Screen.height() + return (0, 0, w, h) + + def _gtk_menubar_height(self): + mb = self._gtk_menubar + if mb: + h = mb.size_request().height + else: + h = 0 + #print "Window._gtk_menubar_height -->", h ### + return h + + def _gtk_delete_event(self, event): + try: + self.close_cmd() + except: + sys.excepthook(*sys.exc_info()) + return 1 + + def _gtk_update_menubar(self): + # + # Update the contents of the menubar after either the application + # menu list or this window's menu list has changed. We only add + # the menu titles at this stage; the menus themselves are attached + # during menu setup. We also attach the accel groups associated + # with the new menus. + # + # Things would be simpler if we could attach the menus here, + # but attempting to share menus between menubar items provokes + # a warning from Gtk, even though it otherwise appears to work. + # + gtk_menubar = self._gtk_menubar + gtk_window = self._gtk_outer_widget + # Remove old accel groups + for menu in self._all_menus: + gtk_window.remove_accel_group(menu._gtk_accel_group) + # Detach any existing menus and remove old menu titles + if gtk_menubar: + for gtk_menubar_item in gtk_menubar.get_children(): + gtk_menubar_item.set_submenu(None) + gtk_menubar_item.destroy() + # Install new menu list + #all_menus = application().menus + self.menus + all_menus = application()._effective_menus_for_window(self) + self._all_menus = all_menus + # Create new menu titles and attach accel groups + for menu in all_menus: + if gtk_menubar: + gtk_menubar_item = Gtk.MenuItem.new_with_label(menu._title) + gtk_menubar_item.show() + gtk_menubar.append(gtk_menubar_item) + gtk_window.add_accel_group(menu._gtk_accel_group) + self._need_menubar_update = 0 + + def _gtk_menubar_button_press_event(self, event): + # A button press has occurred in the menu bar. Before pulling + # down the menu, perform menu setup and attach the menus to + # the menubar items. + self._gtk_menu_setup() + for (gtk_menubar_item, menu) in \ + zip(self._gtk_menubar.get_children(), self._all_menus): + gtk_menu = menu._gtk_menu + attached_widget = gtk_menu.get_attach_widget() + if attached_widget and attached_widget is not gtk_menubar_item: + attached_widget.remove_submenu() + gtk_menubar_item.set_submenu(gtk_menu) + + def _gtk_key_press_event(self, gtk_event): + # Intercept key presses with the Control key down and update + # menus, in case this is a keyboard equivalent for a menu command. + if gtk_event.get_state() & Gdk.ModifierType.CONTROL_MASK: + #print "Window._gtk_key_press_event: doing menu setup" + self._gtk_menu_setup() + # It appears that GtkWindow caches accelerators, and updates + # the cache in an idle task after accelerators change. This + # would be too late for us, so we force it to be done now. + self._gtk_outer_widget.emit("keys-changed") + #print "Window._gtk_key_press_event: done menu setup" + + def _gtk_menu_setup(self): + #print "Windows: enter _gtk_menu_setup" ### + application()._perform_menu_setup(self._all_menus) + #print "Windows: exit _gtk_menu_setup" ### + + def _default_key_event(self, event): + self.pass_event_to_next_handler(event) + if event._originator is self: + event._not_handled = True + + def dispatch(self, message, *args): + self.target.handle(message, *args) + + +_gtk_menubar_height = None + +def _gtk_find_menubar_height(): + global _gtk_menubar_height + if _gtk_menubar_height is None: + print "Windows: Finding menubar height" + item = Gtk.MenuItem("X") + bar = Gtk.MenuBar() + bar.append(item) + bar.show_all() + w, h = bar.size_request() + _gtk_menubar_height = h + print "...done" + return _gtk_menubar_height + +#_gtk_menubar_height = _gtk_find_menubar_height() diff --git a/GUI/Resources/cursors/arrow.hot b/GUI/Resources/cursors/arrow.hot new file mode 100644 index 0000000..8835c07 --- /dev/null +++ b/GUI/Resources/cursors/arrow.hot @@ -0,0 +1 @@ +4 4 diff --git a/GUI/Resources/cursors/arrow.tiff b/GUI/Resources/cursors/arrow.tiff new file mode 100644 index 0000000..2232993 Binary files /dev/null and b/GUI/Resources/cursors/arrow.tiff differ diff --git a/GUI/Resources/cursors/crosshair.hot b/GUI/Resources/cursors/crosshair.hot new file mode 100644 index 0000000..11c8dd7 --- /dev/null +++ b/GUI/Resources/cursors/crosshair.hot @@ -0,0 +1 @@ +7 7 diff --git a/GUI/Resources/cursors/crosshair.tiff b/GUI/Resources/cursors/crosshair.tiff new file mode 100644 index 0000000..e15aa9d Binary files /dev/null and b/GUI/Resources/cursors/crosshair.tiff differ diff --git a/GUI/Resources/cursors/finger.hot b/GUI/Resources/cursors/finger.hot new file mode 100644 index 0000000..e846a7c --- /dev/null +++ b/GUI/Resources/cursors/finger.hot @@ -0,0 +1 @@ +5 0 diff --git a/GUI/Resources/cursors/finger.tiff b/GUI/Resources/cursors/finger.tiff new file mode 100644 index 0000000..55e059c Binary files /dev/null and b/GUI/Resources/cursors/finger.tiff differ diff --git a/GUI/Resources/cursors/fist.hot b/GUI/Resources/cursors/fist.hot new file mode 100644 index 0000000..99ca153 --- /dev/null +++ b/GUI/Resources/cursors/fist.hot @@ -0,0 +1 @@ +8 8 diff --git a/GUI/Resources/cursors/fist.tiff b/GUI/Resources/cursors/fist.tiff new file mode 100644 index 0000000..961b33d Binary files /dev/null and b/GUI/Resources/cursors/fist.tiff differ diff --git a/GUI/Resources/cursors/hand.hot b/GUI/Resources/cursors/hand.hot new file mode 100644 index 0000000..99ca153 --- /dev/null +++ b/GUI/Resources/cursors/hand.hot @@ -0,0 +1 @@ +8 8 diff --git a/GUI/Resources/cursors/hand.tiff b/GUI/Resources/cursors/hand.tiff new file mode 100644 index 0000000..2441917 Binary files /dev/null and b/GUI/Resources/cursors/hand.tiff differ diff --git a/GUI/Resources/cursors/ibeam.hot b/GUI/Resources/cursors/ibeam.hot new file mode 100644 index 0000000..1a99162 --- /dev/null +++ b/GUI/Resources/cursors/ibeam.hot @@ -0,0 +1 @@ +4 5 diff --git a/GUI/Resources/cursors/ibeam.tiff b/GUI/Resources/cursors/ibeam.tiff new file mode 100644 index 0000000..70d7462 Binary files /dev/null and b/GUI/Resources/cursors/ibeam.tiff differ diff --git a/GUI/Resources/cursors/poof.hot b/GUI/Resources/cursors/poof.hot new file mode 100644 index 0000000..b0597be --- /dev/null +++ b/GUI/Resources/cursors/poof.hot @@ -0,0 +1 @@ +11 4 diff --git a/GUI/Resources/cursors/poof.tiff b/GUI/Resources/cursors/poof.tiff new file mode 100644 index 0000000..c8cc981 Binary files /dev/null and b/GUI/Resources/cursors/poof.tiff differ diff --git a/GUI/ToDo/TODO.txt b/GUI/ToDo/TODO.txt new file mode 100644 index 0000000..4dd15ed --- /dev/null +++ b/GUI/ToDo/TODO.txt @@ -0,0 +1,5 @@ +Fix TextView.selection_changed so that it only invalidates +what is necessary. + +Check invalidation system and revamp if necessary to support +regions. diff --git a/GUI/ToDo/Tests/textview.py b/GUI/ToDo/Tests/textview.py new file mode 100644 index 0000000..3a0d61a --- /dev/null +++ b/GUI/ToDo/Tests/textview.py @@ -0,0 +1,16 @@ +from GUI import Window, TextView, TextModel, Font, application +from TestViews import TestView + +app = application() +app.set_menus(app.std_menus()) + +win = Window(width = 300, height = 200) +text = TextModel() +text.set_text("Hello\nWorld!") +view = TextView(width = 300, height = 200, model = text) +view.set_font(Font("Helvetica", 14, [])) +win.add(view) +view.become_target() +win.show() + +app.run() diff --git a/GUI/ToDo/TextModels.py b/GUI/ToDo/TextModels.py new file mode 100644 index 0000000..f626e8f --- /dev/null +++ b/GUI/ToDo/TextModels.py @@ -0,0 +1,184 @@ +# +# TextModels +# + +import string +from array import array +from Models import Model + +class TextModel(Model): + """A TextModel is a Model holding a character string + suitable for viewing and editing with a TextView.""" + + _text = None + _sel_start = (0, 0) + _sel_end = (0, 0) + + # + # The text is kept in a list of strings. There is an + # implicit newline at the end of every string except the + # last one. There is always at least one string in the + # list; a completely empty text is represented by a + # list containing one empty string. + # + # Positions in the text are represented by a tuple + # (line, col), where line is the line number (0-based) + # and col is the position within the line (0 to the + # number of characters in the line). + # + # Pieces of text to be inserted or extracted are + # represented as simple strings, with lines separated + # by newlines. + # + # Views may be sent the messages: + # + # text_changed(start_pos, multiline) + # where start_pos is the start of the affected text and + # multiline is true if more than one line is affected. + # + # selection_changed(pos1, pos2) + # where pos1 and pos2 are the extremes of the range + # of text affected. + # + + def __init__(self, *args, **kw): + apply(Model.__init__, (self,)+args, kw) + self._text = [""] + + def text(self): + "Return the whole text as a string." + return string.join(self._text, '\n') + + def set_text(self, s): + "Replace the whole text with a string." + return self.set_chars((0, 0), self.end(), s) + + def num_lines(self): + "Return the number of lines." + return len(self._text) + + def line(self, i): + """Return the specified line as a string, without any trailing newline""" + return self._text[i] + + def end(self): + "Return the ending position of the text." + lines = self._text + return (len(lines) - 1, len(lines[-1])) + + def char(self, (line, col)): + "Return the character at (line, col)." + return self._text[line][col] + + def chars(self, (line1, col1), (line2, col2)): + "Return the text between (line1, col1) and (line2, col2)." + text = self._text + if line1 == line2: + return text[line1][col1:col2] + else: + a = text[line1][col1:] + b = text[line1 + 1 : line2 - 1] + c = text[line2][:col2] + return string.joinfields([a] + b + [c], '\n') + +## def set_char(self, (line, col), c): +## "Replace the character at (line, col)." +## self._text[line][col] = c +## self.notify_views('char_changed', (line, col)) + + def set_chars(self, (line1, col1), (line2, col2), s): + """Replace the text between (line1, col1) and (line2, col2). + Returns the line and column of the new endpoint of the + inserted text.""" + #print "TextModel.set_chars", (line1, col1), (line2, col2), repr(s) ### + text = self._text + #print "...old text =", repr(text) ### + + # Step 1: Delete old text + if line1 == line2: + line = text[line1] + text[line1] = line[:col1] + line[col2:] + else: + text[line1 : line2 + 1] = [text[line1][:col1] + text[line2][col2:]] + #a = text[line1][:col1] + #b = text[line2][col2:] + #text[line1 : line2 + 1] = [a + b] + + # Step 2: Insert new text + ss = string.splitfields(s, '\n') + line = text[line1] + if len(ss) == 1: + text[line1] = line[:col1] + ss[0] + line[col1:] + else: + text[line1 : line1 + 1] = ( + [line[:col1] + ss[0]] + + ss[1:-1] + + [ss[-1] + line[col1:]]) + + # Step 3: Calculate new endpoint + #print "...ss =", repr(ss) ### + if len(ss) == 1: + line3 = line1 + col3 = col1 + len(s) + else: + line3 = line1 + len(ss) - 1 + col3 = len(ss[-1]) + new_end = (line3, col3) + #print "...new_end =", new_end + #print "...new text:", self._text + multiline = line2 <> line1 and line3 <> line1 + self.notify_views('text_changed', (line1, col1), multiline) + self.set_selection(self._sel_start, + self.min_pos(self._sel_end, new_end)) + return new_end + + def selection(self): + "Return the endpoints of the current selection." + return (self._sel_start, self._sel_end) + + def set_selection(self, new_start, new_end): + "Set the endpoints of the current selection." + new_start, new_end = self.normalise_range(new_start, new_end) + old_start = self._sel_start + old_end = self._sel_end + if old_start <> new_start or old_end <> new_end: + self._sel_start = new_start + self._sel_end = new_end + self.notify_views('selection_changed', + self.min_pos(old_start, new_start), + self.max_pos(old_end, new_end)) + + def replace_selection(self, new_text): + "Replace the selected text with the given string." + new_end = self.set_chars(self._sel_start, self._sel_end, new_text) + self.set_selection(self._sel_start, new_end) + #print "TextModels.replace_selection: selection =", ( ### + # self._sel_start, self._sel_end) ### + + def max_line_length(self): + "Return the number of characters in the longest line." + return max(map(len, self._text)) + + # + # Internal + # + + def min_pos(self, pos1, pos2): + line1, col1 = pos1 + line2, col2 = pos2 + if line1 < line2 or (line1 == line2 and col1 < col2): + return pos1 + else: + return pos2 + + def max_pos(self, pos1, pos2): + line1, col1 = pos1 + line2, col2 = pos2 + if line1 > line2 or (line1 == line2 and col1 > col2): + return pos1 + else: + return pos2 + + def normalise_range(self, pos1, pos2): + "Order two positions so that pos1 <= pos2." + return self.min_pos(pos1, pos2), self.max_pos(pos1, pos2) diff --git a/GUI/ToDo/TextViews.py b/GUI/ToDo/TextViews.py new file mode 100644 index 0000000..4a46b7e --- /dev/null +++ b/GUI/ToDo/TextViews.py @@ -0,0 +1,373 @@ +# +# TextViews +# + +from math import floor +from Views import View +from Colors import Color +from Fonts import application_font +from Applications import application + +class TextView(View): + """A TextView provides an editable view of + a TextModel.""" + + _fg = Color(0, 0, 0) + _bg = Color(1, 1, 1) + _font = application_font() + _line_height = _font.height() + _ascent = _font.ascent() + _left_margin = 5 + _right_margin = 5 + _top_margin = 5 + _bottom_margin = 5 + _caret_on = 1 + _caret_rect = None + + def __init__(self, *args, **kw): + apply(View.__init__, (self,) + args, kw) + self.update_caret_rect() + + # + # Properties + # + + def font(self): + "Return the font property." + return self._font + + def set_font(self, f): + "Set the font property." + self._font = f + self._line_height = f.height() + self._ascent = f.ascent() + self.update_extent() + self.update_caret_rect() + + def margins(self): + "Return the text margins." + return (self._left_margin, self._top_margin, + self._right_margin, self._bottom_margin) + + def set_margins(self, l, t, r, b): + "Set the text margins." + self._left_margin = l + self._top_margin = t + self._right_margin = r + self._bottom_margin = b + self.update_extent() + self.update_caret_rect() + self.invalidate() + + # + # Callbacks + # + + def draw(self, c): + debug = 0 + if debug: + print "TextView.draw:" ### + text = self.model() + sel_start, sel_end = text.selection() + sel_start_line, sel_start_col = sel_start + sel_end_line, sel_end_col = sel_end + text_end_line, text_end_col = text.end() +## if sel_start == sel_end and self._caret_on: +## caret_line = sel_start_line +## else: +## caret_line = None + (minx, miny), (maxx, maxy) = self.viewed_rect() + min_line = max(self.y_to_line(miny), 0) + max_line = min(self.y_to_line(maxy) + 1, text_end_line) + fg = self._fg + bg = self._bg + c.set_font(self._font) + #ext_right = self.extent_right() + #ext_bottom = self.extent_bottom() + #hilite_right_edge = self.viewed_rect()[1][0] + right, bottom = self.viewed_rect()[1] + top_margin = self._top_margin + left_margin = self._left_margin + bottom_margin = self._bottom_margin + line_height = self._line_height + ascent = self._ascent + if debug: + print "...min_line =", min_line + print "...max_line =", max_line +## print "...caret_line =", caret_line + print "...line_height =", line_height + # Erase top and left margin area + c.set_forecolor(bg) + c.fill_rect(((0, 0), (right, top_margin))) + c.fill_rect(((0, top_margin), (left_margin, bottom))) + # Draw relevant lines + y = top_margin + for line in xrange(min_line, max_line + 1): + if debug: + print "...drawing line", line + chars = text.line(line) + line_end_col = len(chars) + if line == sel_start_line: + hilite_start_col = sel_start_col + elif line > sel_start_line: + hilite_start_col = 0 + else: + hilite_start_col = line_end_col + if line == sel_end_line: + hilite_end_col = sel_end_col + elif line < sel_end_line: + hilite_end_col = line_end_col + else: + hilite_end_col = 0 + hilite_to_eol = line >= sel_start_line and line < sel_end_line + if debug: + print "......hilite_start_col =", hilite_start_col ### + print "......hilite_end_col =", hilite_end_col ### + print "......line_end_col =", line_end_col ### + x = left_margin + #base = y + ascent + # Draw chars before hilite + if hilite_start_col > 0: + x = self.draw_chars( + c, chars, x, y, 0, hilite_start_col, fg, bg) +## if line == caret_line: +## caret_x = x +## if debug: +## print "......caret_x =", caret_x + # Draw hilited chars + if hilite_start_col < hilite_end_col: + x = self.draw_chars( + c, chars, x, y, hilite_start_col, hilite_end_col, bg, fg) + # Draw chars after hilite + if hilite_end_col < line_end_col: + x = self.draw_chars( + c, chars, x, y, hilite_end_col, line_end_col, fg, bg) +## # Draw caret +## if line == caret_line: +## if debug: +## print "......drawing caret at", (caret_x, y) +## c.set_forecolor(fg) +## c.fill_rect(((caret_x, y), (caret_x + 1, y + line_height))) + # Erase to end of line + if x < right: + if hilite_to_eol: + c.set_forecolor(fg) + else: + c.set_forecolor(bg) + r = ((x, y), (right, y + line_height)) + if debug: + print "...erasing", r ### + c.fill_rect(r) + y = y + line_height + # Erase to bottom of extent + c.fill_rect(((0, y), (right, bottom))) + # Draw caret + if self._caret_on: + r = self._caret_rect + if r: + c.set_forecolor(fg) + c.fill_rect(r) + + def draw_chars(self, c, line, x, y, start_col, end_col, fg, bg): + debug = 0 + if debug: + print ".........TextView.draw_chars:", \ + repr(line), "at", (x, y), "col", start_col, "to", end_col ### + print "............using", fg, "on", bg ### + chars = line[start_col:end_col] + w = self._font.width(chars) + c.set_forecolor(bg) + c.fill_rect(((x, y), (x + w, y + self._line_height))) + c.set_forecolor(fg) + c.set_backcolor(bg) + c.moveto(x, y + self._ascent) + c.show(chars) + return x + w + + + def line_to_y(self, line): + return self._top_margin + line * self._line_height + + def col_to_x(self, line, col): + return self._left_margin + self._font.width(self.model().line(line)[:col]) + + def y_to_line(self, y): + #print "TextView.y_to_line:", y + #print "...self._top_margin =", self._top_margin + #print "...self._line_height =", self._line_height + return int((y - self._top_margin) / self._line_height) + + def x_to_col(self, line, x): + chars = self.model().line(line) + if chars is not None: + return self._font.x_to_pos(chars, x - self._left_margin) + else: + return 0 + + def pt_to_pos(self, (x, y)): + text = self.model() + line = self.y_to_line(y) + if line < 0: + return (0, 0) + elif line < text.num_lines(): + col = self.x_to_col(line, x) + return (line, col) + else: + return text.end() + + def selection_changed(self, (line1, col1), (line2, col2)): + top = self.line_to_y(line1) + if line1 == line2: + left = self.col_to_x(line1, col1) + right = self.col_to_x(line1, col2) + bottom = top + self._line_height + else: + left = self.extent_left() + right = self.extent_right() + bottom = self.line_to_y(line2 + 1) + self.invalidate_rect(((left, top), (right, bottom))) + self.update_caret_rect() + + def text_changed(self, (line, col), multiline): + self.update_extent() + top = self.line_to_y(line) + right = self.extent_right() + if not multiline: + left = self.col_to_x(line, col) + bottom = top + self._line_height + else: + left = self.extent_left() + bottom = self.viewed_rect()[1][1] + if top < bottom: + self.invalidate_rect(((left, top), (right, bottom))) + + def click(self, e): + text = self.model() + pos1 = self.pt_to_pos(e.where()) + #print "TextView.click: pos1=", pos1 + text.set_selection(pos1, pos1) + while self.track_mouse(): + pos2 = self.pt_to_pos(self.get_mouse().where()) + #print "TextView.click: pos2=", pos2 + text.set_selection(pos1, pos2) + + def key(self, e): + c = e.char + if c == '\b' or c == '\177': + self.backspace() + else: + if c == '\r': + c = '\n' + self.insert(c) + + def update_extent(self): + text = self.model() + max_line, _ = text.end() + width = text.max_line_length() * self._font.width("m") + height = (max_line + 1) * self._line_height + self.set_extent( + ((0, 0), (self._left_margin + width + self._right_margin, + self._top_margin + height + self._bottom_margin))) + + # + # Caret + # + + def blink(self): + #print "TextView.blink" ### + self.set_caret(not self._caret_on) + + def set_caret(self, on): + # print "TextView.set_caret:", on ### + self._caret_on = on + r = self._caret_rect + if r: + #print "...invalidating", r ### + self.invalidate_rect(r) + + def update_caret_rect(self): + old_r = self._caret_rect + text = self.model() + sel_start, sel_end = text.selection() + if sel_start == sel_end: + line, col = sel_start + #print "TextView.update_caret_rect:" ### + #print "...line =", line ### + #print "...line_height =", self._line_height ### + y = self.line_to_y(line) + x = self.col_to_x(line, col) + new_r = ((x - 1, y), (x, y + self._line_height)) + else: + new_r = None + if old_r <> new_r: + if old_r: + self.invalidate_rect(old_r) + if new_r: + self.invalidate_rect(new_r) + self._caret_rect = new_r + self.set_caret(1) + self.reset_blink() + + # + # Menu commands + # + + def setup_menus(self, m): + text = self.model() + start, end = text.selection() + if start <> end: + m.enable('cut_cmd') + m.enable('copy_cmd') + m.enable('clear_cmd') + if application().clipboard(): + m.enable('paste_cmd') + View.setup_menus(self, m) + + def cut_cmd(self): + self.copy_cmd() + self.clear_cmd() + + def copy_cmd(self): + text = self.model() + start, end = text.selection() + chars = text.chars(start, end) + #print "TextView.copy_cmd: chars =", repr(chars) ### + application().set_clipboard(chars) + + def paste_cmd(self): + self.insert(str(application().clipboard())) + + def clear_cmd(self): + self.insert("") + + # + # Text modification + # + + def insert(self, chars): + """Replace selection with the given chars and position insertion + point just after the inserted chars.""" + text = self.model() + text.replace_selection(chars) + _, end = text.selection() + text.set_selection(end, end) + + def backspace(self): + """Delete the selection, or if the selection is empty, delete + the character just before the selection, if any.""" + text = self.model() + start, end = text.selection() + if start == end: + if start <> (0, 0): + line, col = start + if col > 0: + col = col - 1 + else: + line = line - 1 + col = len(text.line(line)) + start = (line, col) + text.set_chars(start, end, "") + text.set_selection(start, start) + else: + self.clear_cmd() + diff --git a/GUI/Version.py b/GUI/Version.py new file mode 100644 index 0000000..326513e --- /dev/null +++ b/GUI/Version.py @@ -0,0 +1 @@ +version = '2.5.3' diff --git a/GUI/Win32/AlertClasses.py b/GUI/Win32/AlertClasses.py new file mode 100644 index 0000000..6157c34 --- /dev/null +++ b/GUI/Win32/AlertClasses.py @@ -0,0 +1,7 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Alert - Win32 +# +#-------------------------------------------------------------------- + +from GUI.GAlertClasses import Alert, Alert2, Alert3 diff --git a/GUI/Win32/Application.py b/GUI/Win32/Application.py new file mode 100755 index 0000000..3acd317 --- /dev/null +++ b/GUI/Win32/Application.py @@ -0,0 +1,115 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Application - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32clipboard as wcb, win32api as api, \ + win32gui as gui, win32process as wp +from GUI import export +from GUI import Component, Window, WinUtils +from GUI.GApplications import Application as GApplication + +class Application(GApplication): + + def __init__(self, *args, **kwds): + self._win_recycle_list = [] + self._win_app = ui.GetApp() + self._win_app.AttachObject(self) + self._win_app.SetMainFrame(WinUtils.win_none) + GApplication.__init__(self, *args, **kwds) + + def set_menus(self, x): + #print "Application.set_menus" ### + GApplication.set_menus(self, x) + for window in self.windows: + window._win_menus_changed() + + def _event_loop(self, window): + if window: + window._begin_modal() + try: + self._win_app.Run() + finally: + if window: + window._end_modal() + + def _exit_event_loop(self): + api.PostQuitMessage(0) + + def zero_windows_allowed(self): + return False + + def get_target(self): + try: + win = ui.GetFocus() + except ui.error: + win = None + if isinstance(win, Component): + return win + else: + return self + + def get_target_window(self): + win = ui.GetActiveWindow() + if isinstance(win, Window): + return win + + def OnIdle(self, n): + #print "Application.OnIdle" ### + trash = self._win_recycle_list + while trash: + trash.pop().DestroyWindow() + self._win_idle() + return 0 + + def _win_idle(self): + self._check_for_no_windows() + + def _check_for_no_windows(self): + #print "Application._check_for_no_windows" ### + apid = wp.GetCurrentProcessId() + #print "... apid =", apid ### + htop = gui.GetDesktopWindow() + hwin = gui.GetWindow(htop, wc.GW_CHILD) + while hwin: + wpid = wp.GetWindowThreadProcessId(hwin)[1] + if wpid == apid: + #print "... hwin", hwin ### + if gui.GetWindowLong(hwin, wc.GWL_STYLE) & wc.WS_VISIBLE: + #print "...... is visible" ### + return + hwin = gui.GetWindow(hwin, wc.GW_HWNDNEXT) + #print "... none visible" ### + self.no_visible_windows() + +# def PreTranslateMessage(self, msg): +# print "Application.PreTranslateMessage:", msg ### + + def _win_recycle(self, win): + # It's not safe to destroy a window inside code called from its + # own OnCommand handler, so we use this method to delay it until + # a better time. + self._win_recycle_list.append(win) + + def query_clipboard(self): + wcb.OpenClipboard() + result = wcb.IsClipboardFormatAvailable(wc.CF_TEXT) + wcb.CloseClipboard() + return result + + def get_clipboard(self): + wcb.OpenClipboard() + try: + result = wcb.GetClipboardData() + except TypeError: + result = None + wcb.CloseClipboard() + return result + + def set_clipboard(self, x): + wcb.OpenClipboard() + wcb.SetClipboardData(wc.CF_TEXT, x) + wcb.CloseClipboard() + +export(Application) diff --git a/GUI/Win32/BaseAlert.py b/GUI/Win32/BaseAlert.py new file mode 100644 index 0000000..aa59b41 --- /dev/null +++ b/GUI/Win32/BaseAlert.py @@ -0,0 +1,61 @@ +#-------------------------------------------------------------------- +# +# PyGUI - BaseAlert - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32gui as gui, win32api as api +from GUI import export +from GUI import application +from GUI.WinUtils import win_bg_color +from GUI import View +from GUI.GBaseAlerts import BaseAlert as GBaseAlert + +win_icon_ids = { + 'stop': wc.IDI_HAND, + 'caution': wc.IDI_EXCLAMATION, + 'note': wc.IDI_ASTERISK, + 'query': wc.IDI_QUESTION, +} + +win_icon_size = ( + api.GetSystemMetrics(wc.SM_CXICON), + api.GetSystemMetrics(wc.SM_CYICON) +) + +def win_load_icon(id): + return gui.LoadIcon(0, id) + +class AlertIcon(View): + + _win_transparent = True + + def __init__(self, id, **kwds): + View.__init__(self, size = win_icon_size, **kwds) + #hwnd = self._win.GetSafeHwnd() + self.win_icon = win_load_icon(id) + + def draw(self, c, r): + gfx = c._win_graphics + hdc = gfx.GetHDC() + gui.DrawIcon(hdc, 0, 0, self.win_icon) + gfx.ReleaseHDC(hdc) + +# def draw(self, c, r): +# dc = c._win_dc +# dc.DrawIcon((0, 0), self.win_icon) + +class BaseAlert(GBaseAlert): + + _win_icon = None + + def _layout_icon(self, kind): + id = win_icon_ids.get(kind) + if id: + icon = AlertIcon(id, position = (self._left_margin, self._top_margin)) + self.add(icon) + return icon.size + else: + return (0, 0) + +export(BaseAlert) diff --git a/GUI/Win32/BaseFileDialogs.py b/GUI/Win32/BaseFileDialogs.py new file mode 100644 index 0000000..8bfcb90 --- /dev/null +++ b/GUI/Win32/BaseFileDialogs.py @@ -0,0 +1,95 @@ +#-------------------------------------------------------------------- +# +# PyGUI - File Dialogs - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32api as api +from GUI.Files import FileRef, DirRef + +win_ofn_flags = wc.OFN_FILEMUSTEXIST | wc.OFN_PATHMUSTEXIST | wc.OFN_HIDEREADONLY \ + | wc.OFN_NOCHANGEDIR | wc.OFN_OVERWRITEPROMPT + +def win_filter(file_types): + filters = [] + if file_types: + for ftype in file_types: + suffix = ftype.suffix + if suffix: + pattern = "*.%s" % suffix + filters.append("%s (%s)|%s" % (ftype.name, pattern, pattern)) + return "|".join(filters) + "||" + +def win_fix_prompt(prompt): + for s in ("as:", ":"): + if prompt.lower().endswith(s): + prompt = prompt[:-len(s)] + return prompt.strip() + +def win_set_prompt(dlog, prompt): + dlog.SetOFNTitle(win_fix_prompt(prompt)) + +def fileref(path, file_type = None): + if file_type: + suffix = file_type.suffix + if suffix: + ext = "." + suffix + if not path.endswith(suffix): + path += ext + return FileRef(path = path) + +def _request_old_file(prompt, default_dir, file_types, multiple): + flags = win_ofn_flags + if multiple: + flags |= wc.OFN_ALLOWMULTISELECT + filter = win_filter(file_types) + dlog = ui.CreateFileDialog(True, None, None, flags, filter) + win_set_prompt(dlog, prompt) + if default_dir: + dlog.SetOFNInitialDir(default_dir.path) + code = dlog.DoModal() + if code == 1: # IDOK + if multiple: + return map(fileref, dlog.GetPathNames()) + else: + return fileref(dlog.GetPathName()) + +def _request_old_dir(prompt, default_dir): + from win32com.shell import shell as sh + import win32com.shell.shellcon as sc + win_bif_flags = sc.BIF_RETURNONLYFSDIRS # | sc.BIF_EDITBOX | wc.BIF_VALIDATE + if default_dir: + def callback(hwnd, msg, lp, data): + if msg == sc.BFFM_INITIALIZED: + api.SendMessage(hwnd, sc.BFFM_SETSELECTION, True, default_dir.path) + else: + callback = None + (idl, name, images) = sh.SHBrowseForFolder(None, None, + win_fix_prompt(prompt), win_bif_flags, callback) + if idl: + return DirRef(sh.SHGetPathFromIDList(idl)) + +def _request_old_dirs(prompt, default_dir): + raise NotImplementedError("Requesting multiple directories") + +def _request_old(prompt, default_dir, file_types, dir, multiple): + if dir: + if multiple: + return _request_old_dirs(prompt, default_dir) + else: + return _request_old_dir(prompt, default_dir) + else: + return _request_old_file(prompt, default_dir, file_types, multiple) + +def _request_new(prompt, default_dir, default_name, file_type, dir): + if file_type: + filter = win_filter([file_type]) + else: + filter = "" # None + dlog = ui.CreateFileDialog(False, None, default_name, win_ofn_flags, filter) + win_set_prompt(dlog, prompt) + if default_dir: + dlog.SetOFNInitialDir(default_dir.path) + code = dlog.DoModal() + if code == 1: # IDOK + return fileref(dlog.GetPathName(), file_type) diff --git a/GUI/Win32/Button.py b/GUI/Win32/Button.py new file mode 100644 index 0000000..b5ebef0 --- /dev/null +++ b/GUI/Win32/Button.py @@ -0,0 +1,63 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Button - Win32 +# +#-------------------------------------------------------------------- + +from time import sleep +import win32con as wc, win32ui as ui, win32gui as gui +from GUI import export +from GUI.ButtonBases import ButtonBase +from GUI.GButtons import Button as GButton + +win_hpad = 40 + +win_style_map = { + 'normal': wc.BS_PUSHBUTTON, + 'default': wc.BS_DEFPUSHBUTTON, + 'cancel': wc.BS_PUSHBUTTON, +} + +def win_style(style): + try: + return win_style_map[style] + except KeyError: + raise ValueError("Invalid Button style %r" % style) + +#-------------------------------------------------------------------- + +class Button(ButtonBase, GButton): + + _vertical_padding = 10 + + _color = None + + def __init__(self, title = "New Button", **kwds): + font = self._win_predict_font(kwds) + style = kwds.pop('style', 'normal') + self._style = style + w = font.width(title) + win_hpad + h = self._calc_height(font) + win = self._win_create_button(title, win_style(style), w, h) + GButton.__init__(self, _win = win, **kwds) + + def get_style(self): + return self._style + + def set_style(self, x): + self._style = x + self._win.SetButtonStyle(win_style(x)) + + def flash(self): + win = self._win + win.SetState(True) + sleep(0.05) + win.SetState(False) + + def _win_bn_clicked(self): + self.do_action() + + def _win_activate(self): + self.do_action() + +export(Button) \ No newline at end of file diff --git a/GUI/Win32/ButtonBases.py b/GUI/Win32/ButtonBases.py new file mode 100644 index 0000000..579899f --- /dev/null +++ b/GUI/Win32/ButtonBases.py @@ -0,0 +1,25 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Button base - Win32 +# +#-------------------------------------------------------------------- + +class ButtonBase(object): + + def key_down(self, event): + if not event.auto: + c = event.char + if c == ' ' or c == '\r': + self._win.SetState(True) + else: + GControl.key_down(self, event) + + def key_up(self, event): + c = event.char + if c == ' ' or c == '\r': + if self._win.GetState() & 4: + self._win.SetState(False) + self._win_activate() + else: + GControl.key_down(self, event) + diff --git a/GUI/Win32/Canvas.py b/GUI/Win32/Canvas.py new file mode 100644 index 0000000..ecdba0b --- /dev/null +++ b/GUI/Win32/Canvas.py @@ -0,0 +1,336 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Canvas - Win32 +# +#-------------------------------------------------------------------- + +from math import sin, cos, pi +import win32con as wc, win32ui as ui, win32gui as gui +from win32con import PS_SOLID, BS_SOLID, RGN_AND +#from win32ui import CreatePen, CreateBrush +#from win32gui import CloseFigure, PathToRegion, AngleArc +from GUI import export +import GUI.GDIPlus as gdip +from GUI.StdColors import black, white +from GUI.StdFonts import application_font +from GUI.WinUtils import win_null_brush +from GUI.GCanvases import Canvas as GCanvas + +deg = pi / 180 + +def ir(x, i = int, r = round): + return i(r(x)) + +def irr(rect, ir = ir): + l, t, r, b = rect + return (ir(l), ir(t), ir(r), ir(b)) + +#-------------------------------------------------------------------- + +class GState(object): + + pencolor = black + fillcolor = black + textcolor = black + backcolor = white + pensize = 1 + font = application_font + + win_pen = gdip.Pen(pencolor._win_argb, 1) + win_fill_brush = gdip.SolidBrush(fillcolor._win_argb) + win_text_brush = gdip.SolidBrush(textcolor._win_argb) + win_bg_brush = gdip.SolidBrush(backcolor._win_argb) + + def __init__(self, clone = None): + if clone: + self.__dict__.update(clone.__dict__) + +#-------------------------------------------------------------------- + +class Canvas(GCanvas): + + _current_point = None + +# def __init__(self, win_graphics, dc = None): +# if not dc: +# print "Canvas.__init__: before get dc: clip bounds =", win_graphics.GetClipBounds() ### +# dc = ui.CreateDCFromHandle(win_graphics.GetHDC()) +# print "Canvas.__init__: after get dc: clip bounds =", win_graphics.GetClipBounds() ### +# dc.SetBkMode(wc.TRANSPARENT) +# dc.SetTextAlign(wc.TA_LEFT | wc.TA_BASELINE | wc.TA_UPDATECP) +# print "Canvas.__init__: clip bounds =", win_graphics.GetClipBounds() ### +# self._win_graphics = win_graphics +# self._win_dc = dc +# self._win_hdc = dc.GetHandleOutput() +# self._win_path = gdip.GraphicsPath() +# self._state = GState() +# self._stack = [] + + def __init__(self, win_graphics, for_printing = False): + self._win_graphics = win_graphics + self._win_path = gdip.GraphicsPath() + self._state = GState() + self._stack = [] + if for_printing: + unit = gdip.UnitPoint + win_graphics.SetPageUnit(unit) + #else: + # unit = gdip.UnitPixel + +# dpix = win_graphics.GetDpiX() +# dpiy = win_graphics.GetDpiY() +# print "Canvas: dpi =", dpix, dpiy ### +# win_graphics.SetPageUnit(gdip.UnitPoint) +# if not for_printing: +# sx = 72.0 / dpix +# sy = 72.0 / dpiy +# self.scale(sx, sy) + + def _from_win_dc(cls, dc): + return cls._from_win_hdc(dc.GetSafeHdc()) + + _from_win_dc = classmethod(_from_win_dc) + + def _from_win_hdc(cls, hdc, **kwds): + win_graphics = gdip.Graphics.from_hdc(hdc) + return cls(win_graphics, **kwds) + + _from_win_hdc = classmethod(_from_win_hdc) + + def _from_win_image(cls, win_image): + win_graphics = gdip.Graphics.from_image(win_image) + #print "Canvas._from_win_image: win_graphics =", win_graphics ### + #print "... clip bounds =", win_graphics.GetClipBounds() ### + return cls(win_graphics) + + _from_win_image = classmethod(_from_win_image) + + def get_pencolor(self): + return self._state.pencolor + + def set_pencolor(self, c): + state = self._state + state.pencolor = c + state.win_pen = gdip.Pen(c._win_argb, state.pensize) + + def get_fillcolor(self): + return self._state.fillcolor + + def set_fillcolor(self, c): + state = self._state + state.fillcolor = c + state.win_fill_brush = gdip.SolidBrush(c._win_argb) + + def get_textcolor(self): + return self._state.textcolor + + def set_textcolor(self, c): + state = self._state + state.textcolor = c + state.win_text_brush = gdip.SolidBrush(c._win_argb) + + def get_backcolor(self): + return self._state.backcolor + + def set_backcolor(self, c): + state = self._state + state.backcolor = c + state.win_bg_brush = gdip.SolidBrush(c._win_argb) + + def get_pensize(self): + return self._state.pensize + + def set_pensize(self, x): + state = self._state + state.pensize = x + state.win_pen = gdip.Pen(state.pencolor._win_argb, x) + + def get_current_point(self): + return self._current_point + + def get_font(self): + return self._state.font + + def set_font(self, f): + self._state.font = f + + def newpath(self): + self._win_path.Reset() + + def moveto(self, x, y): + p = (x, y) + self._current_point = p + self._win_path.StartFigure() + + def lineto(self, x, y): + x0, y0 = self._current_point + self._win_path.AddLine_4f(x0, y0, x, y) + self._current_point = (x, y) + + def curveto(self, p1, p2, p3): + p0 = self._current_point + self._win_path.AddBezier_4p(p0, p1, p2, p3) + self._current_point = p3 + + def arc(self, c, r, a0, a1): + g = self._win_path + g.AddArc_p3f(c, r, a0, a1) + self._current_point = g.GetLastPoint() + + def closepath(self): + self._win_path.CloseFigure() + self._current_point = None + + def fill(self): + self._win_graphics.FillPath(self._state.win_fill_brush, self._win_path) + + def stroke(self): + self._win_graphics.DrawPath(self._state.win_pen, self._win_path) + + def erase(self): + g = self._win_graphics + g.SetSourceCopyMode() + g.FillPath(self._state.win_bg_brush, self._win_path) + g.SetSourceOverMode() + + def show_text(self, text): + font = self._state.font + gf = font._win_gdip_font + x, y = self._current_point + brush = self._state.win_text_brush + g = self._win_graphics + w = g.DrawAndMeasureStringWidth_2f(text, gf, x, y, brush) + self._current_point = x + w, y + +## +## GDI+ screws up some fonts (e.g. Times) for some reason. +## Using plain GDI to draw text for now. +## +# def show_text(self, text): +# state = self._state +# x, y = self._current_point +# dc = self._win_dc +# dc.SelectObject(state.font._win_font) +# dc.SetTextColor(state.textcolor._win_color) +# dc.MoveTo(ir(x), ir(y)) +# dc.TextOut(20, 20, text) +# self._current_point = dc.GetCurrentPosition() + + def clip(self): + self._win_graphics.SetClip_PI(self._win_path) + + def rectclip(self, rect): + self._win_graphics.SetClip_rI(rect) + + def gsave(self): + old_state = self._state + old_state.win_state = self._win_graphics.Save() + self._stack.append(old_state) + self._state = GState(old_state) + + def grestore(self): + old_state = self._stack.pop() + self._win_graphics.Restore(old_state.win_state) + self._state = old_state + + # Rectangles + + def rect(self, rect): + self._win_path.AddRectangle_r(rect) + self._current_point = None + + def fill_rect(self, rect): + self._win_graphics.FillRectangle_r(self._state.win_fill_brush, rect) + + def stroke_rect(self, rect): + self._win_graphics.DrawRectangle_r(self._state.win_pen, rect) + + def erase_rect(self, rect): + self._win_graphics.FillRectangle_r(self._state.win_bg_brush, rect) + + # Ovals + + def oval(self, rect): + self._win_path.AddEllipse_r(rect) + self._current_point = None + + def fill_oval(self, rect): + self._win_graphics.FillEllipse_r(self._state.win_fill_brush, rect) + + def stroke_oval(self, rect): + self._win_graphics.DrawEllipse_r(self._state.win_pen, rect) + + def erase_oval(self, rect): + self._win_graphics.FillEllipse_r(self._state.win_bg_brush, rect) + + # Arcs + + def stroke_arc(self, c, r, a0, a1): + self._win_graphics.DrawArc_3pf(self._state.win_pen, c, r, a0, a1) + + # Wedges + + def wedge(self, c, r, a0, a1): + self._win_path.AddPie_p3f(c, r, a0, a1) + self._current_point = None + + def stroke_wedge(self, c, r, a0, a1): + self._win_graphics.DrawPie_p3f(self._state.win_pen, c, r, a0, a1) + + def fill_wedge(self, c, r, a0, a1): + self._win_graphics.FillPie_p3f(self._state.win_fill_brush, c, r, a0, a1) + + def erase_wedge(self, c, r, a0, a1): + self._win_graphics.FillPie_p3f(self._state.win_bg_brush, c, r, a0, a1) + + # Polylines + + def lines(self, points): + self._win_path.AddLines_pv(points) + self._current_point = points[-1] + + def linesto(self, points): + self.lines([self._current_point] + points) + + def stroke_lines(self, points): + self._win_graphics.DrawLines_pv(self._state.win_pen, points) + + # Polygons + + def poly(self, points): + self._win_path.AddPolygon_pv(points) + self._current_point = None + + def stroke_poly(self, points): + self._win_graphics.DrawPolygon_pv(self._state.win_pen, points) + + def fill_poly(self, points): + self._win_graphics.FillPolygon_pv(self._state.win_fill_brush, points) + + def erase_poly(self, points): + self._win_graphics.FillPolygon_pv(self._state.win_bg_brush, points) + + # Polycurves + + def curves(self, points): + self._win_path.AddBeziers_pv(points) + self._current_point = points[-1] + + def curvesto(self, points): + self.curves([self._current_point] + points) + + def stroke_curves(self, points): + self._win_graphics.DrawBeziers_pv(self._state.win_pen, points) + + # Transformations + + def translate(self, dx, dy): + self._win_graphics.Translate_2f(dx, dy) + + def scale(self, sx, sy): + self._win_graphics.Scale_2f(sx, sy) + + def rotate(self, rotation): + self._win_graphics.Rotate_1f(rotation) + +export(Canvas) diff --git a/GUI/Win32/CheckBox.py b/GUI/Win32/CheckBox.py new file mode 100644 index 0000000..7a8f651 --- /dev/null +++ b/GUI/Win32/CheckBox.py @@ -0,0 +1,78 @@ +#-------------------------------------------------------------------- +# +# PyGUI - CheckBox - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import export +from GUI.StdFonts import system_font +from GUI.ButtonBases import ButtonBase +from GUI.GCheckBoxes import CheckBox as GCheckBox + +win_check_size = 13 +win_hpad = 5 + +win_styles = ( + [wc.BS_CHECKBOX, wc.BS_AUTOCHECKBOX], + [wc.BS_3STATE, wc.BS_AUTO3STATE], +) + +win_states = ( + [False, True], + [False, True, 'mixed'], +) + +class CheckBox(ButtonBase, GCheckBox): + + #_win_transparent = True + + def __init__(self, title = "New Checkbox", **kwds): + font = self._win_predict_font(kwds) + self._auto_toggle = kwds.pop('auto_toggle', True) + self._mixed = kwds.get('mixed', False) + w = font.width(title) + win_hpad + win_check_size + h = max(self._calc_height(font), win_check_size) + win_style = self._win_button_style() + win = self._win_create_button(title, win_style, w, h) + GCheckBox.__init__(self, _win = win, **kwds) + + def get_auto_toggle(self): + return win_styles[self._mixed].index(self._win.GetButtonStyle()) != 0 + + def set_auto_toggle(self, x): + self._auto_toggle = bool(x) + self._win_update_button_style() + + def set_mixed(self, v): + GCheckBox.set_mixed(self, v) + self._win_update_button_style() + + def get_on(self): + return win_states[self._mixed][self._win.GetCheck() & 0x3] + + def set_on(self, x): + try: + state = win_states[self._mixed].index(x) + except ValueError: + raise ValueError("Invalid CheckBox state '%s'" % x) + self._win.SetCheck(state) + + def _win_update_button_style(self): + self._win.SetButtonStyle(self._win_button_style()) + + def _win_button_style(self): + return win_styles[self._mixed][self._auto_toggle] + + def _win_bn_clicked(self): + #print "CheckBox._win_bn_clicked:", self ### + self.do_action() + + def _win_activate(self): + if self.auto_toggle: + states = win_states[self._mixed] + i = states.index(self.on) + self.on = states[(i+1) % len(states)] + self.do_action() + +export(CheckBox) diff --git a/GUI/Win32/Color.py b/GUI/Win32/Color.py new file mode 100644 index 0000000..08a7e63 --- /dev/null +++ b/GUI/Win32/Color.py @@ -0,0 +1,69 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Color - Win32 +# +#-------------------------------------------------------------------- + +from __future__ import division +import win32con as wc, win32ui as ui +from GUI import export +from GUI.GColors import Color as GColor + +#-------------------------------------------------------------------- + +class Color(GColor): + # _win_color 00BBGGRR + # _win_argb AARRGGBB + + _win_brush_cache = None + + def get_red(self): + return self._red + + def get_green(self): + return self._green + + def get_blue(self): + return self._blue + + def get_alpha(self): + return self._alpha + + def _get_win_brush(self): + b = self._win_brush_cache + if not b: + b = ui.CreateBrush(wc.BS_SOLID, self._win_color, 0) + self._win_brush_cache = b + return b + + _win_brush = property(_get_win_brush) + + def _from_win_color(cls, c): + self = cls.__new__(cls) + self._win_color = c + r = c & 0xff + g = (c >> 8) & 0xff + b = (c >> 16) & 0xff + self._red = r / 255 + self._green = g / 255 + self._blue = b / 255 + self._alpha = 1.0 + self._win_argb = 0xff000000 | (r << 16) | (g << 8) | b + return self + + _from_win_color = classmethod(_from_win_color) + + def _from_win_argb(cls, c): + self = cls.__new__() + self._win_argb = c + a = (c >> 24) & 0xff + r = (c >> 16) & 0xff + g = (c >> 8) & 0xff + b = c & 0xff + self._red = r / 255 + self._green = g / 255 + self._blue = b / 255 + self._alpha = a / 255 + self._win_color = (b << 16) | (g << 8) | r + +export(Color) diff --git a/GUI/Win32/Colors.py b/GUI/Win32/Colors.py new file mode 100644 index 0000000..0adabb6 --- /dev/null +++ b/GUI/Win32/Colors.py @@ -0,0 +1,31 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Color utilities - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32api as api +from GUI import Color + +def rgb(red, green, blue, alpha = 1.0): + color = Color() + color._red = red + color._green = green + color._blue = blue + color._alpha = alpha + color._win_color = ( + int(red * 255) | + int(green * 255) << 8 | + int(blue * 255) << 16) + color._win_argb = ( + int(blue * 255) | + int(green * 255) << 8 | + int(red * 255) << 16 | + int(alpha * 255) << 24) + return color + +selection_forecolor = Color._from_win_color( + api.GetSysColor(wc.COLOR_HIGHLIGHTTEXT)) + +selection_backcolor = Color._from_win_color( + api.GetSysColor(wc.COLOR_HIGHLIGHT)) diff --git a/GUI/Win32/Component.py b/GUI/Win32/Component.py new file mode 100755 index 0000000..0fa1212 --- /dev/null +++ b/GUI/Win32/Component.py @@ -0,0 +1,263 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Component - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32gui as gui, win32api as api + +from GUI import export +from GUI.Geometry import sub_pt +from GUI import application +from GUI.WinUtils import win_none, win_event_messages +from GUI.WinEvents import win_message_to_event, win_prev_key_state +from GUI.Exceptions import Cancel +from GUI.GComponents import Component as GComponent, transform_coords + +win_swp_flags = wc.SWP_NOACTIVATE | wc.SWP_NOZORDER +win_sws_flags = win_swp_flags | wc.SWP_NOMOVE + +# Virtual key code to menu key char +win_menu_key_map = { + 0xc0: '`', + 0xbd: '-', + 0xbb: '=', + 0xdb: '[', + 0xdd: ']', + 0xba: ';', + 0xde: "'", + 0xbc: ',', + 0xbe: '.', + 0xbf: '/', + 0xdc: '\\', +} + +# Virtual key codes of modifier keys +win_virt_modifiers = (0x10, 0x11, 0x12, 0x14, 0x90) + +# Translate virtual key code to menu key char +def win_translate_virtual_menu_key(virt): + if 0x41 <= virt <= 0x5a or 0x30 <= virt <= 0x39: + return chr(virt) + else: + return win_menu_key_map.get(virt) + +#-------------------------------------------------------------------- + +class Component(GComponent): + + _has_local_coords = True + _win_hooks_events = False + _win_transparent = False + _win_captures_mouse = False + + _h_scroll_offset = 0 + _v_scroll_offset = 0 + _win_tracking_mouse = False + + def __init__(self, _win, **kwds): + if self._win_transparent: + _win.ModifyStyleEx(0, wc.WS_EX_TRANSPARENT, 0) + self._win = _win + self._bounds = self._win_get_actual_bounds() + _win.AttachObject(self) + self._win_install_event_hooks() + GComponent.__init__(self, **kwds) + + def destroy(self): + GComponent.destroy(self) + wo = self._win + if wo: + wo.AttachObject(None) + wo.ShowWindow(wc.SW_HIDE) + application()._win_recycle(wo) + #self._win = None + + def _win_get_flag(self, flag): + return self._win.GetStyle() & flag != 0 + + def _win_set_flag(self, b, flag, swp_flags = 0): + if b: + state = flag + else: + state = 0 + self._win.ModifyStyle(flag, state, swp_flags) + + def _win_set_ex_flag(self, b, flag, swp_flags = 0): + if b: + state = flag + else: + state = 0 + self._win.ModifyStyleEx(flag, state, swp_flags) + + def _change_container(self, new_container): + GComponent._change_container(self, new_container) + if new_container: + win_new_parent = new_container._win + else: + win_new_parent = win_none + hwnd = self._win.GetSafeHwnd() + gui.SetParent(hwnd, win_new_parent.GetSafeHwnd()) + if new_container: + self._win_move_window(self._bounds) + + def _win_install_event_hooks(self): + def hook(message): + return self._win_event_message(message) + win = self._win + for msg in win_event_messages: + win.HookMessage(hook, msg) + win.HookMessage(self._win_wm_setfocus, wc.WM_SETFOCUS) + win.HookMessage(self._win_wm_killfocus, wc.WM_KILLFOCUS) + + def _win_wm_setfocus(self, msg): + #print "Component._win_wm_setfocus:", self ### + self.targeted() + return True + + def targeted(self): + pass + + def _win_wm_killfocus(self, msg): + #print "Component._win_wm_killfocus:", self ### + self.untargeted() + return True + + def untargeted(self): + pass + + def _win_on_ctlcolor(self, dc, typ): + pass + +# def OnCtlColor(self, dc, comp, typ): +# #print "Component.OnCtlColor" ### +# meth = getattr(comp, '_win_on_ctlcolor', None) +# if meth: +# return meth(dc, typ) + + def get_bounds(self): + return self._bounds + + def set_bounds(self, rect): + self._win_move_window(rect) + self._win_change_bounds(rect) + + def _win_change_bounds(self, rect): + l0, t0, r0, b0 = self._bounds + l1, t1, r1, b1 = rect + w0 = r0 - l0 + h0 = b0 - t0 + w1 = r1 - l1 + h1 = b1 - t1 + self._bounds = rect + if w0 != w1 or h0 != h1: + self._resized((w1 - w0, h1 - h0)) + + def _win_move_window(self, bounds): + container = self.container + if container: + rect = container._win_adjust_bounds(bounds) + self._win.MoveWindow(rect) + + def _win_adjust_bounds(self, bounds): + # Scrollable views override this to adjust for the scroll offset. + return bounds + + def _win_get_actual_bounds(self): + win = self._win + parent = win.GetParent() + sbounds = win.GetWindowRect() + return parent._win.ScreenToClient(sbounds) + + def become_target(self): + #print "Component.become_target:", self ### + window = self.window + if window: + if window._win_is_active(): + #print "...setting focus" ### + self._win.SetFocus() + else: + #print "...saving focus in", window ### + window._win_saved_target = self + + def invalidate_rect(self, r): + #print "Component.invalidate_rect:", self, r ### + self._invalidate_rect(r) + if self._win_transparent: + cont = self.container + if cont: + cont.invalidate_rect(self.local_to_container(r)) + + def _invalidate_rect(self, r): + self._win.InvalidateRect(r) + + def local_to_global(self, p): + return self._win.ClientToScreen(p) + + def global_to_local(self, p): + return self._win.ScreenToClient(p) + + def container_to_local(self, p): + return transform_coords(self.container, self, p) + + def local_to_container(self, p): + return transform_coords(self, self.container, p) + + def _win_event_message(self, message): + try: + if 0: + from WinUtils import win_message_name ### + print "Component._win_event_message: %s 0x%08x 0x%08x" % ( ### + win_message_name(message[1]), + message[2] & 0xffffffff, + message[3] & 0xffffffff) ### + event = win_message_to_event(message, self) + kind = event.kind + if kind.startswith('key') and message[2] in win_virt_modifiers: + # Do not produce Events for modifier keys + return True + if kind == 'mouse_down' and self._win_captures_mouse: + self._win.SetCapture() + if self._win_tracking_mouse: + if 'mouse' in kind: + self._win_mouse_event = event + api.PostQuitMessage(0) + pass_message = False + else: + if kind == 'key_down' and event.control and event.char: + key = win_translate_virtual_menu_key(message[2]) + top = self._win.GetTopLevelFrame() + if top._win_possible_menu_key(key, event.shift, event.option): + return False + pass_message = not self._event_custom_handled(event) + if kind == 'mouse_up' and self._win_captures_mouse: + self._win.ReleaseCapture() +# #<<< +# if kind.startswith('key'): +# if pass_message: +# print "Component._win_event_message: passing", event ### +# else: +# print "Component._win_event_message: absorbing", event ### +# #>>> + return pass_message + except Cancel: + pass + except: + application().report_error() +# print "Component._win_event_message: Posting quit message with 1" ### +# api.PostQuitMessage(1) + + def _win_dump_flags(self): + from WinUtils import win_deconstruct_style, win_deconstruct_style_ex + print "%s.%s: style:" % (self.__class__.__module__, self.__class__.__name__) + win_deconstruct_style(self._win.GetStyle()) + win_deconstruct_style_ex(self._win.GetExStyle()) + +# def PreTranslateMessage(self, message): +# print "Component.PreTranslateMessage:", self, \ +# message[0], win_message_name(message[1]), \ +# message[2] +# +#from WinUtils import win_message_name + +export(Component) diff --git a/GUI/Win32/Container.py b/GUI/Win32/Container.py new file mode 100755 index 0000000..91f6ba4 --- /dev/null +++ b/GUI/Win32/Container.py @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Container - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc +from win32api import HIWORD, LOWORD +from GUI import export +from GUI.Geometry import add_pt, sub_pt +from GUI.WinUtils import WinMessageReflector +from GUI.GContainers import Container as GContainer + +class Container(GContainer, WinMessageReflector): + + def get_content_size(self): + return sub_pt(self.size, self._win_content_size_adjustment()) + + def set_content_size(self, size): + self.size = add_pt(size, self._win_content_size_adjustment()) + + def _win_content_size_adjustment(self): + win = self._win + wl, wt, wr, wb = win.GetWindowRect() + cl, ct, cr, cb = win.GetClientRect() + return ((wr - wl) - (cr - cl), (wb - wt) - (cb - ct)) + +export(Container) + diff --git a/GUI/Win32/Control.py b/GUI/Win32/Control.py new file mode 100644 index 0000000..a7d576f --- /dev/null +++ b/GUI/Win32/Control.py @@ -0,0 +1,87 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Control - Win32 +# +#-------------------------------------------------------------------- + +from math import ceil +import win32con as wc, win32ui as ui +from GUI import export +from GUI.StdColors import black +from GUI.StdFonts import system_font +from GUI.WinUtils import win_none, win_null_hbrush +from GUI.GControls import Control as GControl + +class Control(GControl): + + _vertical_padding = 5 # Amount to add when calculating height from font size + + _color = black + _just = 'left' + _font = None + + def get_title(self): + return self._win.GetWindowText() + + def set_title(self, x): + self._win.SetWindowText(x) + + def get_enabled(self): + return self._win.IsWindowEnabled() + + def set_enabled(self, x): + self._win.EnableWindow(x) + +# def get_visible(self, x): +# self._win.IsWindowVisible() +# +# def set_visible(self, x): +# if x: +# self._win.ShowWindow(wc.SW_SHOW) +# else: +# self._win.ShowWindow(wc.SW_HIDE) + + def get_font(self): + return self._font + + def set_font(self, x): + self._font = x + self._win.SetFont(x._win_font) + self.invalidate() + + def get_color(self): + return self._color + + def set_color(self, x): + self._color = x + self.invalidate() + + def get_just(self): + return self._just + + def set_just(self, x): + self._just = x + self.invalidate() + + def _win_create_button(self, title, style, w, h): + w = int(ceil(w)) + win = ui.CreateButton() + win.CreateWindow(title, style, (0, 0, w, h), win_none, 0) + #if self._win_transparent: + # win.ModifyStyleEx(0, wc.WS_EX_TRANSPARENT, 0) + win.ShowWindow(wc.SW_SHOW) + return win + + def _win_on_ctlcolor(self, dc, typ): + #print "Control._win_on_ctlcolor:", self ### + c = self._color + if c: + dc.SetTextColor(c._win_color) + if self._win_transparent: + dc.SetBkMode(wc.TRANSPARENT) + return win_null_hbrush + + def _win_predict_font(self, kwds): + return kwds.setdefault('font', system_font) + +export(Control) diff --git a/GUI/Win32/Cursor.py b/GUI/Win32/Cursor.py new file mode 100644 index 0000000..a93ee1d --- /dev/null +++ b/GUI/Win32/Cursor.py @@ -0,0 +1,36 @@ +#-------------------------------------------------------------------------- +# +# Python GUI - Cursors - Win32 +# +#-------------------------------------------------------------------------- + +import win32gui as gui +from GUI import export +from GUI.GCursors import Cursor as GCursor + +class Cursor(GCursor): + + def _from_win_cursor(cls, hcursor): + cursor = cls.__new__(cls) + cursor._win_cursor = hcursor + return cursor + + _from_win_cursor = classmethod(_from_win_cursor) + + def __str__(self): + return "" % self._win_cursor + + def _init_from_image_and_hotspot(self, image, hotspot): + #print "Cursor._init_from_image_and_hotspot:" ### + hicon = image._win_image.GetHICON() + iconinfo = gui.GetIconInfo(hicon) + gui.DestroyIcon(hicon) + flag, xhot, yhot, hbmMask, hbmColor = iconinfo + xhot, yhot = hotspot + cursorinfo = (True, xhot, yhot, hbmMask, hbmColor) + win_cursor = gui.CreateIconIndirect(cursorinfo) + gui.DeleteObject(hbmMask) + gui.DeleteObject(hbmColor) + self._win_cursor = win_cursor + +export(Cursor) diff --git a/GUI/Win32/Dialog.py b/GUI/Win32/Dialog.py new file mode 100644 index 0000000..a2ec5ad --- /dev/null +++ b/GUI/Win32/Dialog.py @@ -0,0 +1,10 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Dialog - Win32 +# +#-------------------------------------------------------------------- + +from GUI import export +from GUI.GDialogs import Dialog + +export(Dialog) diff --git a/GUI/Win32/DrawableContainer.py b/GUI/Win32/DrawableContainer.py new file mode 100644 index 0000000..d4f1484 --- /dev/null +++ b/GUI/Win32/DrawableContainer.py @@ -0,0 +1,68 @@ +#-------------------------------------------------------------------- +# +# PyGUI - DrawableContainer - Win32 +# +#-------------------------------------------------------------------- + +from GUI import export +from GUI import Canvas +from GUI.Geometry import rect_topleft, rect_size, offset_rect, empty_rect +from GUI.GDrawableContainers import DrawableContainer as GDrawableContainer +import GUI.GDIPlus as gdi + +class DrawableContainer(GDrawableContainer): + + _double_buffer = True + _win_paint_broken = False + + def update(self): + self._win.UpdateWindow() + + def with_canvas(self, body): + win = self._win + dc = win.GetDC() + self._win_prepare_dc(dc) + try: + canvas = Canvas._from_win_dc(dc) + body(canvas) + finally: + win.ReleaseDC(dc) + + def OnPaint(self): + if not self._win_paint_broken: + try: + win = self._win + dc, ps = win.BeginPaint() + try: + win_update_rect = ps[2] + if not empty_rect(win_update_rect): + #print "DrawableContainer.OnPaint: win_update_rect =", win_update_rect ### + scroll_offset = self._win_scroll_offset() + view_update_rect = offset_rect(win_update_rect, scroll_offset) + if self._double_buffer: + dx, dy = rect_topleft(view_update_rect) + width, height = rect_size(view_update_rect) + buffer = gdi.Bitmap(width, height) + canvas = Canvas._from_win_image(buffer) + canvas.translate(-dx, -dy) + self.draw(canvas, view_update_rect) + graphics = gdi.Graphics.from_dc(dc) + src_rect = (0, 0, width, height) + graphics.DrawImage_rr(buffer, win_update_rect, src_rect) + else: + self._win_prepare_dc(dc) + canvas = Canvas._from_win_dc(dc) + self.draw(canvas, view_update_rect) + finally: + win.EndPaint(ps) + except Exception: + self._win_paint_broken = True + raise + + def _win_prepare_dc(self, dc): + dc.SetWindowOrg(self._win_scroll_offset()) + + def _win_scroll_offset(self): + return (0, 0) + +export(DrawableContainer) diff --git a/GUI/Win32/EditCmdHandler.py b/GUI/Win32/EditCmdHandler.py new file mode 100644 index 0000000..f778e13 --- /dev/null +++ b/GUI/Win32/EditCmdHandler.py @@ -0,0 +1,22 @@ +# +# PyGUI - Edit command handling - Win32 +# + +from GUI import export +from GUI.GEditCmdHandlers import EditCmdHandler as GEditCmdHandler + +class EditCmdHandler(GEditCmdHandler): + + def cut_cmd(self): + self._win.Cut() + + def copy_cmd(self): + self._win.Copy() + + def paste_cmd(self): + self._win.Paste() + + def clear_cmd(self): + self._win.Clear() + +export(EditCmdHandler) diff --git a/GUI/Win32/Event.py b/GUI/Win32/Event.py new file mode 100644 index 0000000..f2fc337 --- /dev/null +++ b/GUI/Win32/Event.py @@ -0,0 +1,15 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Event - Win32 +# +#-------------------------------------------------------------------- + +from GUI import export +from GUI.GEvents import Event as GEvent + +class Event(GEvent): + + def _platform_modifiers_str(self): + return "" + +export(Event) diff --git a/GUI/Win32/Files.py b/GUI/Win32/Files.py new file mode 100755 index 0000000..253c947 --- /dev/null +++ b/GUI/Win32/Files.py @@ -0,0 +1,7 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Files - Win32 +# +#-------------------------------------------------------------------- + +from GUI.GFiles import FileRef, DirRef, FileType diff --git a/GUI/Win32/Font.py b/GUI/Win32/Font.py new file mode 100644 index 0000000..649d663 --- /dev/null +++ b/GUI/Win32/Font.py @@ -0,0 +1,219 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Font - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32gui as gui, win32ui as ui, win32api as api +from GUI import export +from WinUtils import win_none +import GUI.GDI as gdi +import GUI.GDIPlus as gdip +from GUI.GFonts import Font as GFont + +#win_family_map = { +# "Decorative": wc.FF_DECORATIVE | wc.DEFAULT_PITCH, +# "Fixed": wc.FF_MODERN | wc.FIXED_PITCH, +# "Courier": wc.FF_MODERN | wc.FIXED_PITCH, +# "Modern": wc.FF_MODERN | wc.DEFAULT_PITCH, +# "Serif": wc.FF_ROMAN | wc.VARIABLE_PITCH, +# "Roman": wc.FF_ROMAN | wc.VARIABLE_PITCH, +# "Times": wc.FF_ROMAN | wc.VARIABLE_PITCH, +# "Sans": wc.FF_SWISS | wc.VARIABLE_PITCH, +# "Helvetica": wc.FF_SWISS | wc.VARIABLE_PITCH, +# "Script": wc.FF_SCRIPT | wc.DEFAULT_PITCH, +# "Cursive": wc.FF_SCRIPT | wc.DEFAULT_PITCH, +#} + +#win_default_pf = wc.FF_DONTCARE | wc.DEFAULT_PITCH + +def win_create_font(**kwds): + # Work around bug in CreateFont + for name in 'italic', 'underline': + if name in kwds and not kwds[name]: + del kwds[name] + return ui.CreateFont(kwds) + +#def win_pf_to_name(pf): +# if pf & 0x3 == wc.FIXED_PITCH: +# return "Fixed" +# for name, npf in win_family_map.iteritems(): +# if pf & 0xf0 == npf & 0xf0: +# return name +# return "Unknown" + +win_generic_family_map = { + "Sans": "Arial", + "Serif": "Times New Roman", + "Fixed": "Courier New", + "Times": "Times New Roman", + "Courier": "Courier New", +} + +# PyWin32 build 212 and earlier negate the value of the height +# passed to CreateFont. + +pywin32_info = api.GetFileVersionInfo(api.__file__, '\\') +pywin32_build = pywin32_info['FileVersionLS'] >> 16 +if pywin32_build <= 212: + win_height_sign = 1 +else: + win_height_sign = -1 + +#-------------------------------------------------------------------- + +class Font(GFont): + # _win_font PyCFont + # _win_gfont GDIPlus.Font + + def __init__(self, family = "Times", size = 12, style = []): + win_family = win_generic_family_map.get(family, family) + self._family = family + self._win_family = win_family + self._size = size + self._style = style + if 'bold' in style: + win_weight = wc.FW_BOLD + else: + win_weight = wc.FW_NORMAL + #print "Font: calling win_create_font" ### + height = int(round(size)) + #print "Font: height =", height ### + win_font = win_create_font( + name = win_family, + height = win_height_sign * height, + weight = win_weight, + italic = 'italic' in style) + #pitch_and_family = 0) ###win_family_map.get(family, win_default_pf)) + self._win_font = win_font + self._win_update_metrics() + #global dc ### + #dc = win_none.GetDC() + #dc.SelectObject(win_font) + #self._win_gfont = gdip.Font.from_hdc(dc.GetSafeHdc()) + #win_none.ReleaseDC(dc) + +# def __init__(self, family = "Times", size = 12, style = []): +# self._family = family +# self._size = size +# self._style = style +# hfont = gdi.create_hfont(family, size, style) +# self._win_hfont = hfont +# self._win_update_metrics() + + def get_family(self): + return self._family + + def get_size(self): + return self._size + + def get_style(self): + return self._style + + def get_ascent(self): + return self._ascent + + def get_descent(self): + return self._descent + + def get_leading(self): + return self._leading + + def get_cap_height(self): + return self._ascent - self._internal_leading + + def get_x_height(self): + return self._ascent - self._internal_leading - self._descent + + def get_height(self): + return self._ascent + self._descent + + def get_line_height(self): + return self._ascent + self._descent + self._leading + + def _win_update_metrics(self): + dc = win_none.GetDC() + dc.SelectObject(self._win_font) + met = dc.GetTextMetrics() + self._ascent = met['tmAscent'] + self._descent = met['tmDescent'] + self._internal_leading = met['tmInternalLeading'] + self._leading = met['tmExternalLeading'] + self._win_overhang = met['tmOverhang'] + #print "Font: tmOverhang =", self._win_overhang ### + win_none.ReleaseDC(dc) + self._win_gdip_font = gdip.Font(self._win_family, self._size, self._style) + + def _width(self, s): + dc = win_none.GetDC() + dc.SelectObject(self._win_font) + w, h = dc.GetTextExtent(s) + win_none.ReleaseDC(dc) + return w + +# def _width(self, s): +# dc = win_none.GetDC() +# g = gdip.Graphics.from_hdc(dc.GetSafeHdc()) +# w = g.MeasureStringWidth(s, self._win_gdip_font) +# win_none.ReleaseDC(dc) +# return w + + def info(self): + return "" % \ + (self.family, self.size, self.style, self.ascent, self.descent, + self.leading, self.height, self.cap_height, self.x_height, + self.line_height) + + def tm_info(self): + win = ui.CreateWnd() + dc = win.GetDC() + dc.SelectObject(self._win_font) + tm = dc.GetTextMetrics() + win.ReleaseDC(dc) + return tm + +# def _from_win_logfont(cls, lf): +# #print "Font._from_win_logfont:", lf ### +# #for name in dir(lf): ### +# # print name, "=", getattr(lf, name) ### +# font = cls.__new__(cls) +# font._family = win_pf_to_name(lf.lfPitchAndFamily) +# font._size = abs(lf.lfHeight) +# style = [] +# if lf.lfWeight >= wc.FW_BOLD: +# style.append('bold') +# if lf.lfItalic: +# style.append('italic') +# font._style = style +# font._win_font = win_create_font( +# width = lf.lfWidth, +# #height = abs(lf.lfHeight), +# height = lf.lfHeight, +# weight = lf.lfWeight, +# italic = lf.lfItalic, +# underline = lf.lfUnderline, +# pitch_and_family = lf.lfPitchAndFamily, +# charset = lf.lfCharSet) +# font._win_update_metrics() +# return font +# +# _from_win_logfont = classmethod(_from_win_logfont) + + def _from_win(cls, win): + dc = win.GetDC() + family = dc.GetTextFace() + tm = dc.GetTextMetrics() + #print family, tm + size = tm['tmAscent'] - tm['tmInternalLeading'] + tm['tmDescent'] + style = [] + if tm['tmWeight'] >= wc.FW_BOLD: + style.append('bold') + if tm['tmItalic']: + style.append('italic') + win.ReleaseDC(dc) + return Font(family, size, style) + + _from_win = classmethod(_from_win) + +export(Font) diff --git a/GUI/Win32/Frame.py b/GUI/Win32/Frame.py new file mode 100644 index 0000000..ab70d2b --- /dev/null +++ b/GUI/Win32/Frame.py @@ -0,0 +1,24 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Frame - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import export +from GUI.WinUtils import win_none +from GUI.GFrames import Frame as GFrame + +win_flags = wc.WS_CHILD | wc.WS_VISIBLE + +class Frame(GFrame): + + _win_transparent = True + + def __init__(self, **kwds): + w, h = self._default_size + win = ui.CreateWnd() + win.CreateWindow(None, None, win_flags, (0, 0, w, h), win_none, 0) + GFrame.__init__(self, _win = win, **kwds) + +export(Frame) diff --git a/GUI/Win32/GDI.py b/GUI/Win32/GDI.py new file mode 100644 index 0000000..bf5765e --- /dev/null +++ b/GUI/Win32/GDI.py @@ -0,0 +1,50 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Win32 - GDI +# +#-------------------------------------------------------------------- + +LF_FACESIZE = 32 + +from ctypes import * +from ctypes.wintypes import * + +class LOGFONT(Structure): + _fields_ = [ ('lfHeight', c_long), + ('lfWidth', c_long), + ('lfEscapement', c_long), + ('lfOrientation', c_long), + ('lfWeight', c_long), + ('lfItalic', c_byte), + ('lfUnderline', c_byte), + ('lfStrikeOut', c_byte), + ('lfCharSet', c_byte), + ('lfOutPrecision', c_byte), + ('lfClipPrecision', c_byte), + ('lfQuality', c_byte), + ('lfPitchAndFamily', c_byte), + ('lfFaceName', c_char * LF_FACESIZE) ] + + def __init__(self): + self.lfHeight = 10 + self.lfWidth = 0 + self.lfEscapement = 10 + self.lfOrientation = 0 + self.lfUnderline = 0 + self.lfStrikeOut = 0 + self.lfCharSet = 0 # ANSI_CHARSET + #self.lfPitchAndFamily = 0 + self.lfOutPrecision = 0 + self.lfClipPrecision = 0 + self.lfQuality = 0 + self.lfPitchAndFamily = 2 + +def create_hfont(family, size, style): + lf = LOGFONT() + lf.lfFaceName = family + lf.lfHeight = size + if 'italic' in style: + lf.lfItalic = 1 + if 'bold' in style: + lf.lfWeight = 10 + return windll.gdi32.CreateFontIndirectA(byref(lf)) diff --git a/GUI/Win32/GDIPlus.py b/GUI/Win32/GDIPlus.py new file mode 100644 index 0000000..788bfa7 --- /dev/null +++ b/GUI/Win32/GDIPlus.py @@ -0,0 +1,494 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Win32 - GDI Plus +# +#-------------------------------------------------------------------- + +from ctypes import * +from ctypes.wintypes import BOOL +try: + from numpy import ndarray, float32 +except ImportError: + class ndarray(object): + pass + +#wg = windll.gdiplus +wg = oledll.gdiplus + +#-------------------------------------------------------------------- + +# enum Unit + +UnitWorld = 0 +UnitDisplay = 1 +UnitPixel = 2 +UnitPoint = 3 + +# enum FillMode + +FillModeAlternate = 0 + +# enum CombineMode + +CombineModeIntersect = 1 + +# enum MatrixOrder + +MatrixOrderPrepend = 0 +MatrixOrderAppend = 1 + +# Pixel Formats + +# In-memory pixel data formats: +# bits 0-7 = format index +# bits 8-15 = pixel size (in bits) +# bits 16-23 = flags +# bits 24-31 = reserved + +PixelFormatIndexed = 0x00010000 # Indexes into a palette +PixelFormatGDI = 0x00020000 # Is a GDI-supported format +PixelFormatAlpha = 0x00040000 # Has an alpha component +PixelFormatPAlpha = 0x00080000 # Pre-multiplied alpha +PixelFormatExtended = 0x00100000 # Extended color 16 bits/channel +PixelFormatCanonical = 0x00200000 + +PixelFormat1bppIndexed = (1 | ( 1 << 8) | PixelFormatIndexed | PixelFormatGDI) +PixelFormat4bppIndexed = (2 | ( 4 << 8) | PixelFormatIndexed | PixelFormatGDI) +PixelFormat8bppIndexed = (3 | ( 8 << 8) | PixelFormatIndexed | PixelFormatGDI) +PixelFormat16bppGrayScale = (4 | (16 << 8) | PixelFormatExtended) +PixelFormat16bppRGB555 = (5 | (16 << 8) | PixelFormatGDI) +PixelFormat16bppRGB565 = (6 | (16 << 8) | PixelFormatGDI) +PixelFormat16bppARGB1555 = (7 | (16 << 8) | PixelFormatAlpha | PixelFormatGDI) +PixelFormat24bppRGB = (8 | (24 << 8) | PixelFormatGDI) +PixelFormat32bppRGB = (9 | (32 << 8) | PixelFormatGDI) +PixelFormat32bppARGB = (10 | (32 << 8) | PixelFormatAlpha | PixelFormatGDI | PixelFormatCanonical) +PixelFormat32bppPARGB = (11 | (32 << 8) | PixelFormatAlpha | PixelFormatPAlpha | PixelFormatGDI) +PixelFormat48bppRGB = (12 | (48 << 8) | PixelFormatExtended) +PixelFormat64bppARGB = (13 | (64 << 8) | PixelFormatAlpha | PixelFormatCanonical | PixelFormatExtended) +PixelFormat64bppPARGB = (14 | (64 << 8) | PixelFormatAlpha | PixelFormatPAlpha | PixelFormatExtended) + +# enum FontStyle + +FontStyleBold = 1 +FontStyleItalic = 2 +FontStyleUnderline = 4 +FontStyleStrikeout = 8 + +class PointF(Structure): + _fields_ = [("x", c_float), ("y", c_float)] + +class RectF(Structure): + _fields_ = [ + ("x", c_float), ("y", c_float), + ("width", c_float), ("height", c_float)] + +#-------------------------------------------------------------------- + +def rect_args(rect): + l, t, r, b = rect + return c_float(l), c_float(t), c_float(r - l), c_float(b - t) + +def points_args(points): + if isinstance(points, ndarray) and points.flags['C_CONTIGUOUS'] and points.dtype == float32: + #print "GDIPlus.points_args: using ndarray" ### + n = points.size // 2 + buf = points.ctypes.data + else: + n = len(points) + buf = (PointF * n)() + for i, p in enumerate(points): + buf[i].x, buf[i].y = p + return buf, n + +def arc_args(c, r, a0, a1): + x, y = c + d = c_float(2 * r) + return c_float(x - r), c_float(y - r), d, d, \ + c_float(a0), c_float((a1 - a0) % 360.0) + +#-------------------------------------------------------------------- + +class GdiplusStartupInput(Structure): + + _fields_ = [ + ('GdiplusVersion', c_uint), + ('DebugEventCallback', c_void_p), + ('SuppressBackgroundThread', BOOL), + ('SuppressExternalCodecs', BOOL), + ] + + def __init__(self): + Structure.__init__(self) + self.GdiplusVersion = 1 + self.DebugEventCallback = None + self.SuppressBackgroundThread = 0 + self.SuppressExternalCodecs = 0 + +StartupInput = GdiplusStartupInput() +token = c_ulong() +wg.GdiplusStartup(pointer(token), pointer(StartupInput), None) + +#-------------------------------------------------------------------- + +class Pen(object): + + def __init__(self, argb, size): + ptr = c_void_p() + wg.GdipCreatePen1(argb, c_float(size), UnitWorld, byref(ptr)) + self.ptr = ptr + + def __del__(self, wg = wg): + wg.GdipDeletePen(self.ptr) + +#-------------------------------------------------------------------- + +class SolidBrush(object): + + def __init__(self, argb): + ptr = c_void_p() + wg.GdipCreateSolidFill(argb, byref(ptr)) + self.ptr = ptr + + def __del__(self, wg = wg): + wg.GdipDeleteBrush(self.ptr) + + def __str__(self): + argb = c_ulong() + wg.GdipGetSolidFillColor(self.ptr, byref(argb)) + return "" % argb.value + +#-------------------------------------------------------------------- + +class Font(object): + + def __init__(self, family, size, style): + uname = create_unicode_buffer(family) + fam = c_void_p() + wg.GdipCreateFontFamilyFromName(uname, None, byref(fam)) + flags = 0 + if 'bold' in style: + flags |= FontStyleBold + if 'italic' in style: + flags |= FontStyleItalic + ptr = c_void_p() + wg.GdipCreateFont(fam, c_float(size), flags, UnitWorld, byref(ptr)) + self.ptr = ptr + wg.GdipDeleteFontFamily(fam) + + def from_hdc(cls, hdc): + self = cls.__new__(cls) + ptr = c_void_p() + wg.GdipCreateFontFromDC(hdc, byref(ptr)) + self.ptr = ptr + return self + + from_hdc = classmethod(from_hdc) + + def __del__(self, wg = wg): + wg.GdipDeleteFont(self.ptr) + +#-------------------------------------------------------------------- + +class Image(object): + + def __str__(self): + return "" % self.ptr.value + + def from_file(cls, path): + self = cls.__new__(cls) + ptr = c_void_p() + upath = create_unicode_buffer(path) + self._create_from_file(upath, ptr) + self.ptr = ptr + return self + + from_file = classmethod(from_file) + + def _create_from_file(self, upath, ptr): + wg.GdipLoadImageFromFile(upath, byref(ptr)) + + def __del__(self, wg = wg): + wg.GdipDisposeImage(self.ptr) + + def GetWidth(self): + uint = c_uint() + wg.GdipGetImageWidth(self.ptr, byref(uint)) + return uint.value + + def GetHeight(self): + uint = c_uint() + wg.GdipGetImageHeight(self.ptr, byref(uint)) + return uint.value + +#-------------------------------------------------------------------- + +class Bitmap(Image): + + def __init__(self, width, height): + ptr = c_void_p() + format = PixelFormat32bppARGB + wg.GdipCreateBitmapFromScan0(width, height, 0, format, None, byref(ptr)) + self.ptr = ptr + #print "GDIPlus.Bitmap:", (width, height), repr(self), "ptr =", self.ptr ### + + def _create_from_file(self, upath, ptr): + wg.GdipCreateBitmapFromFile(upath, byref(ptr)) + + def from_data(cls, width, height, format, data): + self = cls.__new__(cls) + ptr = c_void_p() + bits_per_pixel = (format >> 8) & 0xff + row_stride = (width * bits_per_pixel) >> 3 + wg.GdipCreateBitmapFromScan0(width, height, row_stride, format, data, byref(ptr)) + self.ptr = ptr + return self + + from_data = classmethod(from_data) + + def __str__(self): + return "" % self.ptr.value + + def GetHICON(self): + hicon = c_ulong() + wg.GdipCreateHICONFromBitmap(self.ptr, byref(hicon)) + return hicon.value + + def GetPixel(self, x, y): + c = c_ulong() + wg.GdipBitmapGetPixel(self.ptr, x, y, byref(c)) + return c.value + + def SetPixel(self, x, y, c): + wg.GdipBitmapSetPixel(self.ptr, x, y, c) + +#-------------------------------------------------------------------- + +class GraphicsPath(object): + + def __init__(self): + ptr = c_void_p() + wg.GdipCreatePath(FillModeAlternate, byref(ptr)) + self.ptr = ptr + + def __del__(self, wg = wg): + wg.GdipDeletePath(self.ptr) + + def Reset(self): + wg.GdipResetPath(self.ptr) + + def StartFigure(self): + wg.GdipStartPathFigure(self.ptr) + + def AddLine_4f(self, x0, y0, x1, y1): + wg.GdipAddPathLine(self.ptr, + c_float(x0), c_float(y0), c_float(x1), c_float(y1)) + + def AddBezier_4p(self, p0, p1, p2, p3): + x0, y0 = p0 + x1, y1 = p1 + x2, y2 = p2 + x3, y3 = p3 + wg.GdipAddPathBezier(self.ptr, + c_float(x0), c_float(y0), c_float(x1), c_float(y1), + c_float(x2), c_float(y2), c_float(x3), c_float(y3)) + + def AddBeziers_pv(self, points): + wg.GdipAddPathBeziers(self.ptr, *points_args(points)) + + def AddRectangle_r(self, rect): + wg.GdipAddPathRectangle(self.ptr, *rect_args(rect)) + + def AddEllipse_r(self, rect): + wg.GdipAddPathEllipse(self.ptr, *rect_args(rect)) + + def AddArc_p3f(self, c, r, a0, a1): + wg.GdipAddPathArc(self.ptr, *arc_args(c, r, a0, a1)) + + def AddPie_p3f(self, c, r, a0, a1): + wg.GdipAddPathPie(self.ptr, *arc_args(c, r, a0, a1)) + + def AddLines_pv(self, points): + wg.GdipAddPathLine2(self.ptr, *points_args(points)) + + def AddPolygon_pv(self, points): + wg.GdipAddPathPolygon(self.ptr, *points_args(points)) + + def CloseFigure(self): + wg.GdipClosePathFigure(self.ptr) + + def GetLastPoint(self): + p = PointF() + wg.GdipGetPathLastPoint(self.ptr, byref(p)) + return p.x, p.y + +#-------------------------------------------------------------------- + +class Graphics(object): + + def from_hdc(cls, hdc): + self = cls.__new__(cls) + ptr = c_void_p() + wg.GdipCreateFromHDC(c_ulong(hdc), byref(ptr)) + self.ptr = ptr + return self + + from_hdc = classmethod(from_hdc) + + def from_dc(cls, dc): + return cls.from_hdc(dc.GetSafeHdc()) + + from_dc = classmethod(from_dc) + + def from_image(cls, image): + #print "Graphics.from_image:", repr(image) ### + #print "...", image ### + self = cls.__new__(cls) + ptr = c_void_p() + wg.GdipGetImageGraphicsContext(image.ptr, byref(ptr)) + self.ptr = ptr + return self + + from_image = classmethod(from_image) + + def __del__(self, wg = wg): + wg.GdipDeleteGraphics(self.ptr) + + def __str__(self): + return "" % self.ptr.value + + def GetHDC(self): + hdc = c_long() + wg.GdipGetDC(self.ptr, byref(hdc)) + return hdc.value + + def ReleaseHDC(self, hdc): + wg.GdipReleaseDC(self.ptr, hdc) + + def GetDpiX(self): + result = c_float() + wg.GdipGetDpiX(self.ptr, byref(result)) + return result.value + + def GetDpiY(self): + result = c_float() + wg.GdipGetDpiY(self.ptr, byref(result)) + return result.value + + def SetPageUnit(self, unit): + self.unit = unit + wg.GdipSetPageUnit(self.ptr, unit) + + def GetClipBounds(self): + r = RectF() + wg.GdipGetClipBounds(self.ptr, byref(r)) + return (r.x, r.y, r.x + r.width, r.y + r.height) + + def Save(self): + state = c_uint() + wg.GdipSaveGraphics(self.ptr, byref(state)) + return state.value + + def Restore(self, state): + wg.GdipRestoreGraphics(self.ptr, state) + + def DrawImage_rr(self, image, dst_rect, src_rect): + sl, st, sr, sb = src_rect + dl, dt, dr, db = dst_rect + wg.GdipDrawImageRectRect(self.ptr, image.ptr, + c_float(dl), c_float(dt), c_float(dr - dl), c_float(db - dt), + c_float(sl), c_float(st), c_float(sr - sl), c_float(sb - st), + UnitPixel, None, None, None) + + def DrawPath(self, pen, path): + wg.GdipDrawPath(self.ptr, pen.ptr, path.ptr) + + def FillPath(self, brush, path): + wg.GdipFillPath(self.ptr, brush.ptr, path.ptr) + + def DrawAndMeasureStringWidth_2f(self, text, font, x, y, brush): + wtext = unicode(text) + n = len(text) + pos = PointF(x, y) + flags = 5 # DriverStringOptions CmapLookup+RealizedAdvance + b = RectF() + wg.GdipDrawDriverString(self.ptr, wtext, n, font.ptr, brush.ptr, + byref(pos), flags, None) + wg.GdipMeasureDriverString(self.ptr, wtext, n, font.ptr, byref(pos), + flags, None, byref(b)) + return b.width + + def MeasureStringWidth(self, text, font): + wtext = unicode(text) + n = len(text) + pos = PointF(0, 0) + flags = 5 # DriverStringOptions CmapLookup+RealizedAdvance + b = RectF() + wg.GdipMeasureDriverString(self.ptr, wtext, n, font.ptr, byref(pos), + flags, None, byref(b)) + return b.width + + def SetClip_PI(self, path): + wg.GdipSetClipPath(self.ptr, path.ptr, CombineModeIntersect) + + def SetClip_rI(self, rect): + x, y, w, h = rect_args(rect) + wg.GdipSetClipRect(self.ptr, x, y, w, h, CombineModeIntersect) + + def DrawRectangle_r(self, pen, rect): + wg.GdipDrawRectangle(self.ptr, pen.ptr, *rect_args(rect)) + + def FillRectangle_r(self, brush, rect): + #print "Graphics.FillRectangle_r:", self, brush, rect ### + #print "... clip bounds =", self.GetClipBounds() ### + wg.GdipFillRectangle(self.ptr, brush.ptr, *rect_args(rect)) + + def DrawEllipse_r(self, pen, rect): + wg.GdipDrawEllipse(self.ptr, pen.ptr, *rect_args(rect)) + + def FillEllipse_r(self, brush, rect): + wg.GdipFillEllipse(self.ptr, brush.ptr, *rect_args(rect)) + + def DrawArc_3pf(self, pen, c, r, a0, a1): + wg.GdipDrawArc(self.ptr, pen.ptr, *arc_args(c, r, a0, a1)) + + def DrawPie_p3f(self, pen, c, r, a0, a1): + wg.GdipDrawPie(self.ptr, pen.ptr, *arc_args(c, r, a0, a1)) + + def FillPie_p3f(self, brush, c, r, a0, a1): + wg.GdipFillPie(self.ptr, brush.ptr, *arc_args(c, r, a0, a1)) + + def DrawPolygon_pv(self, pen, points): + wg.GdipDrawPolygon(self.ptr, pen.ptr, *points_args(points)) + + def FillPolygon_pv(self, brush, points): + buf, n = points_args(points) + wg.GdipFillPolygon(self.ptr, brush.ptr, buf, n, FillModeAlternate) + + def DrawBeziers_pv(self, pen, points): + wg.GdipDrawBeziers(self.ptr, pen.ptr, *points_args(points)) + + def DrawLines_pv(self, pen, points): + wg.GdipDrawLines(self.ptr, pen.ptr, *points_args(points)) + + def Translate_2f(self, dx, dy): + wg.GdipTranslateWorldTransform(self.ptr, c_float(dx), c_float(dy), + MatrixOrderAppend) + + def Scale_2f(self, sx, sy): + wg.GdipScaleWorldTransform(self.ptr, c_float(sx), c_float(sy), + MatrixOrderAppend) + + def Rotate_1f(self, r): + wg.GdipRotateWorldTransform(self.ptr, c_float(r), + MatrixOrderAppend) + + + + def GetTransform(self): + matrix = c_void_p() + elems = (c_float * 6)() + wg.GdipCreateMatrix(byref(matrix)) + wg.GdipGetWorldTransform(self.ptr, matrix) + wg.GdipGetMatrixElements(matrix, elems) + wg.GdipDeleteMatrix(matrix) + return list(elems) diff --git a/GUI/Win32/GL.py b/GUI/Win32/GL.py new file mode 100644 index 0000000..6f51d5e --- /dev/null +++ b/GUI/Win32/GL.py @@ -0,0 +1,270 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - OpenGL - Win32 +# +#------------------------------------------------------------------------------ + +import win32con as wc, win32ui as ui, win32gui as gui +#import OpenGL.GL as gl ### +import GUI.GDIPlus as gdi +from OpenGL import WGL as wgl +from OpenGL.WGL import ChoosePixelFormat +#print "Using ctypes ChoosePixelFormat" +#from WGL import ChoosePixelFormat +from GUI.WinUtils import win_none +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI.GGLConfig import GLConfig as GGLConfig, GLConfigError +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture + +win_style = wc.WS_VISIBLE | wc.WS_CLIPCHILDREN | wc.WS_CLIPSIBLINGS +win_default_size = GGLView._default_size +win_default_rect = (0, 0, win_default_size[0], win_default_size[1]) + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + + def _as_win_pixelformat(self, mode): + #print "GLConfig._as_win_pixelformat: mode =", mode ### + pf = wgl.PIXELFORMATDESCRIPTOR() + flags = wgl.PFD_SUPPORT_OPENGL + if mode == 'screen' or mode == 'both': + #print "GLConfig: requesting screen drawing" ### + flags |= wgl.PFD_DRAW_TO_WINDOW + if self._double_buffer: + flags |= wgl.PFD_DOUBLEBUFFER | wgl.PFD_SWAP_EXCHANGE + else: + flags |= wgl.PFD_DOUBLEBUFFER_DONTCARE + if mode == 'pixmap' or mode == 'both': + #print "GLConfig: requesting pixmap drawing" ### + flags |= wgl.PFD_DRAW_TO_BITMAP | wgl.PFD_SUPPORT_GDI + if not self._depth_buffer: + flags |= wgl.PFD_DEPTH_DONTCARE + if self._stereo: + flags |= wgl.PFD_STEREO + else: + flags |= wgl.PFD_STEREO_DONTCARE + pf.dwFlags = flags & 0xffffffff + pf.iPixelType = wgl.PFD_TYPE_RGBA + #pf.cColorBits = 3 * self._color_size + #pf.cColorBits = 32 ### + pf.cRedBits = pf.cGreenBits = pf.cBluedBits = self._color_size + if self._alpha: + pf.cAlphaBits = self._alpha_size + pf.cAuxBuffers = self._aux_buffers + if self._depth_buffer: + pf.cDepthBits = self._depth_size + if self._stencil_buffer: + pf.cStencilBits = self._stencil_size + if self._accum_buffer: + pf.cAccumBits = 3 * self._accum_size + pf.iLayerType = wgl.PFD_MAIN_PLANE + return pf + + def _from_win_pixelformat(cls, pf): + self = cls.__new__(cls) + flags = pf.dwFlags + self._double_buffer = flags & wgl.PFD_DOUBLEBUFFER != 0 + self._alpha = pf.cAlphaSize > 0 + self._color_size = pf.cColorBits + self._alpha_size = pf.cAlphaSize + self._stereo = flags & wgl.PFD_STEREO != 0 + self._aux_buffers = pf.cAuxBuffers + self._depth_buffer = pf.cDepthBits > 0 + self._depth_size = pf.cDepthBits + self._stencil_buffer = pf.cStencilBits > 0 + self._stencil_size = pf.cStencilBits + self._accum_size = pf.cAccumBits + self._accum_buffer = self._accum_size > 0 + self._multisample = False + self._samples_per_pixel = 1 + return self + + def _check_win_pixelformat(self, pf, mode): + flags = pf.dwFlags + if mode == 'screen' or mode == 'both': + if not flags & wgl.PFD_DRAW_TO_WINDOW: + raise GLConfigError("Rendering to screen not supported") + if mode == 'pixmap' or mode == 'both': + if not flags & wgl.PFD_DRAW_TO_BITMAP: + raise GLConfigError("Rendering to pixmap not supported") + if self._alpha and pf.cAlphaBits == 0: + raise GLConfigError("Alpha channel not available") + if self._stereo and not flags & wgl.PFD_STEREO: + raise GLConfigError("Stereo buffer not available") + if self._aux_buffers and pf.cAuxBuffers == 0: + raise GLConfigError("Auxiliary buffers not available") + if self._depth_buffer and pf.cDepthBits == 0: + raise GLConfigError("Depth buffer not available") + if self._stencil_buffer and pf.cStencilBits == 0: + raise GLConfigError("Stencil buffer not available") + if self.accum_buffer and pf.cAccumBits == 0: + raise GLConfigError("Accumulation buffer not available") + + def _win_supported_pixelformat(self, hdc, mode): + reqpf = self._as_win_pixelformat(mode) + #print "GLConfig._win_supported_pixelformat" ### + #print "Requested format:" ### + #win_dump_pixelformat(reqpf) ### + #print "GLConfig: Choosing pixel format for hdc", hdc ### + ipf = wgl.ChoosePixelFormat(hdc, reqpf) + #print "... result =", ipf ### + actpf = wgl.PIXELFORMATDESCRIPTOR() + #print "GLConfig: Describing pixel format", ipf, "for hdc", hdc ### + wgl.DescribePixelFormat(hdc, ipf, actpf.nSize, actpf) + #print "Actual format:" ### + #win_dump_pixelformat(actpf) ### + return ipf, actpf + + def supported(self, mode = 'both'): + dc = win_none.GetDC() + hdc = dc.GetSafeHdc() + ipf, actpf = self._win_supported_pixelformat(hdc, mode) + win_none.ReleaseDC(dc) + return GLConfig._from_win_pixelformat(actpf) + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + + def __init__(self, config = None, share_group = None, **kwds): + config = GLConfig._from_args(config, kwds) + win = ui.CreateWnd() + win.CreateWindow(None, None, win_style, win_default_rect, + win_none, 0) + dc = win.GetDC() + hdc = dc.GetSafeHdc() + GLContext.__init__(self, share_group, config, hdc, 'screen') + GGLView.__init__(self, _win = win) + self.set(**kwds) + self._with_context(hdc, self._init_context) + win.ReleaseDC(dc) + +# def _init_context(self): +# print "GL_VENDOR:", gl.glGetString(gl.GL_VENDOR) +# print "GL_RENDERER:", gl.glGetString(gl.GL_RENDERER) +# print "GL_VERSION:", gl.glGetString(gl.GL_VERSION) +# print "GL_EXTENSIONS:" +# for name in gl.glGetString(gl.GL_EXTENSIONS).split(): +# print " ", name +# GGLView._init_context(self) + + def destroy(self): + GLContext.destroy(self) + GGLView.destroy(self) + + def with_context(self, proc, flush = False): + win = self._win + dc = win.GetDC() + hdc = dc.GetSafeHdc() + try: + self._with_context(hdc, proc, flush) + finally: + win.ReleaseDC(dc) + + def OnPaint(self): + #print "GLView.OnPaint" ### + win = self._win + dc, ps = win.BeginPaint() + try: + hdc = dc.GetSafeHdc() + self._with_context(hdc, self._render, True) + finally: + win.EndPaint(ps) + + def _resized(self, delta): + self.with_context(self._update_viewport) + +#------------------------------------------------------------------------------ + +#class GLPixmap(GGLPixmap): +# +# def __init__(self, width, height, config = None, share_group = None, **kwds): +# print "GLPixmap:", width, height, kwds ### +# config = GLConfig._from_args(config, kwds) +# image = gdi.Bitmap(width, height) +# self._win_image = image +# graphics = gdi.Graphics.from_image(image) +# self._win_graphics = graphics +# hdc = graphics.GetHDC() +# self._win_hdc = hdc +# GLContext.__init__(self, share_group, config, hdc, 'pixmap') +# self._with_context(hdc, self._init_context) +# print "GLPixmap: done" ### +# +# def __del__(self): +# graphics = self._win_graphics +# graphics.ReleaseHDC(self._win_hdc) +# +# def with_context(self, proc, flush = False): +# try: +# self._with_context(self._hdc, proc, flush) +# finally: +# graphics.ReleaseHDC(hdc) + +#------------------------------------------------------------------------------ + +class GLPixmap(GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + #print "GLPixmap:", width, height, kwds ### + config = GLConfig._from_args(config, kwds) + dc0 = win_none.GetDC() + dc = dc0.CreateCompatibleDC(dc0) + bm = ui.CreateBitmap() + bm.CreateCompatibleBitmap(dc0, width, height) + win_none.ReleaseDC(dc0) + dc.SelectObject(bm) + self._win_dc = dc + self._win_bm = bm + hdc = dc.GetSafeHdc() + win_dump_bitmap(bm) ### + GLContext.__init__(self, share_group, config, hdc, 'pixmap') + self._with_context(hdc, self._init_context) + #print "GLPixmap: done" ### + + def with_context(self, proc, flush = False): + hdc = self._win_dc.GetSafeHdc() + self._with_context(hdc, proc, flush) + +#------------------------------------------------------------------------------ + +def win_dump_pixelformat(pf): + print "nSize =", pf.nSize + print "nVersion =", pf.nVersion + print "dwFlags = 0x%08x" % pf.dwFlags + print "iPixelType =", pf.iPixelType + print "cColorBits =", pf.cColorBits + print "cRedBits =", pf.cRedBits + print "cRedShift =", pf.cRedShift + print "cGreenBits =", pf.cGreenBits + print "cGreenShift =", pf.cGreenShift + print "cBlueBits =", pf.cBlueBits + print "cBlueShift =", pf.cBlueShift + print "cAlphaBits =", pf.cAlphaBits + print "cAlphaShift =", pf.cAlphaShift + print "cAccumBits =", pf.cAccumBits + print "cAccumRedBits =", pf.cAccumRedBits + print "cAccumGreenBits =", pf.cAccumGreenBits + print "cAccumBlueBits =", pf.cAccumBlueBits + print "cDepthBits =", pf.cDepthBits + print "cStencilBits =", pf.cStencilBits + print "cAuxBuffers =", pf.cAuxBuffers + print "iLayerType =", pf.iLayerType + print "bReserved =", pf.bReserved + print "dwLayerMask =", pf.dwLayerMask + print "dwVisibleMask =", pf.dwVisibleMask + print "dwDamageMask =", pf.dwDamageMask + +def win_dump_bitmap(bm): + info = bm.GetInfo() + print "bmType =", info['bmType'] + print "bmWidth =", info['bmWidth'] + print "bmHeight =", info['bmHeight'] + print "bmWidthBytes =", info['bmWidthBytes'] + print "bmPlanes =", info['bmPlanes'] + print "bmBitsPixel =", info['bmBitsPixel'] + + \ No newline at end of file diff --git a/GUI/Win32/GLContexts.py b/GUI/Win32/GLContexts.py new file mode 100644 index 0000000..ca3ee26 --- /dev/null +++ b/GUI/Win32/GLContexts.py @@ -0,0 +1,51 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - GLContext - Win32 +# +#------------------------------------------------------------------------------ + +import OpenGL as gl +from OpenGL import WGL as wgl +from GUI.GGLContexts import GLContext as GGLContext + +class GLContext(GGLContext): + # _win_dc Device context + # _win_context WGL context + # _win_dblbuf Is double buffered + + def __init__(self, share_group, config, hdc, mode): + #print "GLContext: mode =", mode ### + GGLContext.__init__(self, share_group) + shared_context = self._get_shared_context() + if shared_context: + share_ctx = shared_context._win_context + else: + share_ctx = None + ipf, actpf = config._win_supported_pixelformat(hdc, mode) + config._check_win_pixelformat(actpf, mode) + #print "GLContext: Setting pixel format", ipf, "for hdc", hdc ### + wgl.SetPixelFormat(hdc, ipf, actpf) + #print "GLContext: Creating context for hdc", hdc ### + ctx = wgl.wglCreateContext(hdc) + if share_ctx: + wgl.wglShareLists(share_ctx, ctx) + self._win_context = ctx + self._win_dblbuf = actpf.dwFlags & wgl.PFD_DOUBLEBUFFER != 0 + + def destroy(self): + wgl.wglDeleteContext(self._win_context) + + def _with_context(self, hdc, proc, flush = False): + old_hdc = wgl.wglGetCurrentDC() + old_ctx = wgl.wglGetCurrentContext() + result = wgl.wglMakeCurrent(hdc, self._win_context) + try: + self._with_share_group(proc) + if flush: + if self._win_dblbuf: + wgl.SwapBuffers(hdc) + else: + gl.glFlush() + finally: + wgl.wglMakeCurrent(old_hdc, old_ctx) + \ No newline at end of file diff --git a/GUI/Win32/GLContexts_arb.py b/GUI/Win32/GLContexts_arb.py new file mode 100644 index 0000000..1b9d85a --- /dev/null +++ b/GUI/Win32/GLContexts_arb.py @@ -0,0 +1,52 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - GLContext - Win32 +# +#------------------------------------------------------------------------------ + +import OpenGL as gl +from OpenGL import WGL as wgl +import WGL +from GUI.GGLContexts import GLContext as GGLContext + +class GLContext(GGLContext): + # _win_context WGL context + # _win_dblbuf Is double buffered + + def __init__(self, share_group, config, hdc, mode): + print "GLContext: mode =", mode ### + GGLContext.__init__(self, share_group) + shared_context = self._get_shared_context() + if shared_context: + share_ctx = shared_context._win_context + else: + share_ctx = None + ipf, act_attrs = config._win_supported_pixelformat(hdc, mode) + if ipf is None: + raise GLConfigError + #config._check_win_pixelattrs(act_attrs, mode) + print "GLContext: Setting pixel format", ipf, "for hdc", hdc ### + WGL.SetPixelFormat(hdc, ipfs) + ctx = wgl.wglCreateContext(hdc) + if share_ctx: + wgl.wglShareLists(share_ctx, ctx) + self._win_context = ctx + self._win_dblbuf = actpf.dwFlags & wgl.PFD_DOUBLEBUFFER != 0 + + def destroy(self): + wgl.wglDeleteContext(self._win_context) + + def _with_context(self, hdc, proc, flush = False): + old_hdc = wgl.wglGetCurrentDC() + old_ctx = wgl.wglGetCurrentContext() + result = wgl.wglMakeCurrent(hdc, self._win_context) + try: + self._with_share_group(proc) + if flush: + if self._win_dblbuf: + wgl.SwapBuffers(hdc) + else: + gl.glFlush() + finally: + wgl.wglMakeCurrent(old_hdc, old_ctx) + \ No newline at end of file diff --git a/GUI/Win32/GLContexts_nonarb.py b/GUI/Win32/GLContexts_nonarb.py new file mode 100644 index 0000000..3db0d41 --- /dev/null +++ b/GUI/Win32/GLContexts_nonarb.py @@ -0,0 +1,51 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - GLContext - Win32 +# +#------------------------------------------------------------------------------ + +import OpenGL as gl +from OpenGL import WGL as wgl +from GUI.GGLContexts import GLContext as GGLContext + +class GLContext(GGLContext): + # _win_dc Device context + # _win_context WGL context + # _win_dblbuf Is double buffered + + def __init__(self, share_group, config, hdc, mode): + print "GLContext: mode =", mode ### + GGLContext.__init__(self, share_group) + shared_context = self._get_shared_context() + if shared_context: + share_ctx = shared_context._win_context + else: + share_ctx = None + ipf, actpf = config._win_supported_pixelformat(hdc, mode) + config._check_win_pixelformat(actpf, mode) + print "GLContext: Setting pixel format", ipf, "for hdc", hdc ### + wgl.SetPixelFormat(hdc, ipf, actpf) + print "GLContext: Creating context for hdc", hdc ### + ctx = wgl.wglCreateContext(hdc) + if share_ctx: + wgl.wglShareLists(share_ctx, ctx) + self._win_context = ctx + self._win_dblbuf = actpf.dwFlags & wgl.PFD_DOUBLEBUFFER != 0 + + def destroy(self): + wgl.wglDeleteContext(self._win_context) + + def _with_context(self, hdc, proc, flush = False): + old_hdc = wgl.wglGetCurrentDC() + old_ctx = wgl.wglGetCurrentContext() + result = wgl.wglMakeCurrent(hdc, self._win_context) + try: + self._with_share_group(proc) + if flush: + if self._win_dblbuf: + wgl.SwapBuffers(hdc) + else: + gl.glFlush() + finally: + wgl.wglMakeCurrent(old_hdc, old_ctx) + \ No newline at end of file diff --git a/GUI/Win32/GLTextures.py b/GUI/Win32/GLTextures.py new file mode 100644 index 0000000..c3fb318 --- /dev/null +++ b/GUI/Win32/GLTextures.py @@ -0,0 +1,10 @@ +# +# PyGUI - OpenGL Textures - Win32 +# + +from GUI.GGLTextures import Texture as GTexture + +class Texture(GTexture): + + def _gl_get_texture_data(self, image): + raise NotImplementedError("Loading texture from image not yet implemented for Win32") diff --git a/GUI/Win32/GL_arb.py b/GUI/Win32/GL_arb.py new file mode 100644 index 0000000..6a4d167 --- /dev/null +++ b/GUI/Win32/GL_arb.py @@ -0,0 +1,264 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - OpenGL - Win32 +# +#------------------------------------------------------------------------------ + +import win32con as wc, win32ui as ui, win32gui as gui +import GDIPlus as gdi +import WGL +from GUI.Components import win_none +from GUI.OpenGL import WGL as wgl +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI.GGLConfig import GLConfig as GGLConfig, GLConfigError +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture + +win_style = wc.WS_VISIBLE | wc.WS_CLIPCHILDREN | wc.WS_CLIPSIBLINGS +win_default_size = GGLView._default_size +win_default_rect = (0, 0, win_default_size[0], win_default_size[1]) + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + + def _as_win_pixelattrs(self, mode): + print "GLConfig._as_arb_pixelattrs: mode =", mode ### + attrs = {} + attrs[wgl.WGL_SUPPORT_OPENGL_ARB] = True + if mode == 'screen' or mode == 'both': + print "GLConfig: requesting screen drawing" ### + attrs[wgl.WGL_DRAW_TO_WINDOW_ARB] = True + if self._double_buffer: + attrs[wgl.WGL_DOUBLE_BUFFER_ARB] = True + if mode == 'pixmap' or mode == 'both': + print "GLConfig: requesting pixmap drawing" ### + attrs[wgl.WGL_DRAW_TO_PBUFFER_ARB] = True + if self._stereo: + attrs[wgl.WGL_STEREO_ARB] = True + attrs[wgl.WGL_PIXEL_TYPE_ARB] = wgl.WGL_TYPE_RGBA_ARB + bits = self._color_size + attrs[wgl.WGL_RED_BITS_ARB] = bits + attrs[wgl.WGL_GREEN_BITS_ARB] = bits + attrs[wgl.WGL_BLUE_BITS_ARB] = bits + if self._alpha: + attrs[wgl.WGL_ALPHA_BITS_ARB] = self._alpha_size + attrs[wgl.WGL_AUX_BUFFERS_ARB] = self._aux_buffers + if self._depth_buffer: + attrs[wgl.WGL_DEPTH_BITS_ARB] = self._depth_size + if self._stencil_buffer: + attrs[wgl.WGL_STENCIL_BITS_ARB] = self._stencil_size + if self._accum_buffer: + bits = self._accum_size + attrs[wgl.WGL_ACCUM_RED_BITS_ARB] = bits + attrs[wgl.WGL_ACCUM_GREEN_BITS_ARB] = bits + attrs[wgl.WGL_ACCUM_BLUE_BITS_ARB] = bits + return attrs + + def _from_win_pixelattrs(cls, attrs): + self = cls.__new__(cls) + self._double_buffer = attrs[wgl.WGL_DOUBLE_BUFFER_ARB] + self._color_size = attrs[wgl.WGL_COLOR_BITS_ARB] // 3 + self._alpha_size = attrs[wgl.WGL_ALPHA_BITS_ARB] + self._alpha = self._alpha_size > 0 + self._stereo = attrs[wgl.WGL_STEREO_ARB] #flags & wgl.PFD_STEREO != 0 + self._aux_buffers = attrs[wgl.WGL_AUX_BUFFERS_ARB] > 0 + self._depth_size = attrs[wgl.WGL_DEPTH_BITS_ARB] + self._depth_buffer = self._depth_size > 0 + self._stencil_size = attrs[wgl.WGL_STENCIL_BITS_ARB] + self._stencil_buffer = self._stencil_bits > 0 + self._accum_size = attrs[wgl.WGL_ACCUM_BITS_ARB] // 3 + self._accum_buffer = self._accum_size > 0 + self._multisample = False + self._samples_per_pixel = 1 + return self + +# def _check_win_pixelattrs(self, attrs, mode): +# if mode == 'screen' or mode == 'both': +# if not attrs[wgl.WGL_DRAW_TO_WINDOW_ARB]: +# raise GLConfigError("Rendering to screen not supported") +# if mode == 'pixmap' or mode == 'both': +# if not attrs[wgl.WGL_DRAW_TO_PBUFFER_ARB]: +# raise GLConfigError("Rendering to pixmap not supported") +# if self._alpha and attrs[wgl.WGL_ALPHA_BITS_ARB] == 0: +# raise GLConfigError("Alpha channel not available") +# if self._stereo and not attrs[wgl.WGL_STEREO_ARB]: +# raise GLConfigError("Stereo buffer not available") +# if self._aux_buffers and attrs]wgl.WGL_AUX_BUFFERS_ARB] == 0: +# raise GLConfigError("Auxiliary buffers not available") +# if self._depth_buffer and attrs[wgl.WGL_DEPTH_BITS_ARB] == 0: +# raise GLConfigError("Depth buffer not available") +# if self._stencil_buffer and attrs[wgl.WGL_STENCIL_BITS] == 0: +# raise GLConfigError("Stencil buffer not available") +# if self.accum_buffer and attrs[wgl.WGL_ACCUM_BITS] == 0: +# raise GLConfigError("Accumulation buffer not available") + + _win_query_pixelattr_keys = [ + wgl.WGL_SUPPORT_OPENGL_ARB, + wgl.WGL_DRAW_TO_WINDOW_ARB, + wgl.WGL_DOUBLE_BUFFER_ARB, + wgl.WGL_DRAW_TO_PBUFFER_ARB, + wgl.WGL_STEREO_ARB, + wgl.WGL_PIXEL_TYPE_ARB, + wgl.WGL_COLOR_BITS_ARB, + wgl.WGL_ALPHA_BITS_ARB, + wgl.WGL_AUX_BUFFERS_ARB, + wgl.WGL_DEPTH_BITS_ARB, + wgl.WGL_STENCIL_BITS_ARB, + wgl.WGL_ACCUM_BITS_ARB, + ] + + def _win_supported_pixelformat(self, hdc, mode): + req_attrs = self._as_win_pixelattrs(mode) + print "GLConfig: Choosing pixel format for hdc", hdc ### + print "Requested attributes:", req_attrs ### + req_array = WGL.attr_array(req_attrs) + print "Requested array:", req_array ### + ipfs, nf = wgl.wglChoosePixelFormatEXT(hdc, req_array, None, 1) + print "Pixel formats:", ipfs ### + print "No. of formats:", nf ### + if not ipfs: + req_attrs[wgl.WGL_DOUBLE_BUFFER_ARB] = not self._double_buffer + req_array = WGL.attr_array(req_attrs) + ipfs, nf = wglChoosePixelFormatARB(hdc, req_array, None, 1) + if not ipfs: + return None, None + print "GLConfig: Describing pixel format", ipf, "for hdc", hdc ### + keys = _win_query_pixelattr_keys + values = wglGetPixelFormatAttribivARB(hdc, ipf, 0, keys) + print "Actual values:", values ### + act_attrs = WGL.attr_dict(keys, values) + print "Actual attrs:", act_attrs ### + return ipfs[0], act_attrs + + def supported(self, mode = 'both'): + dc = win_none.GetDC() + hdc = dc.GetSafeHdc() + ipf, act_attrs = self._win_supported_pixelformat(hdc, mode) + win_none.ReleaseDC(dc) + if ipf is None: + return None + return GLConfig._from_win_pixelattrs(act_attrs) + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + + def __init__(self, config = None, share_group = None, **kwds): + config = GLConfig._from_args(config, kwds) + win = ui.CreateWnd() + win.CreateWindow(None, None, win_style, win_default_rect, + win_none, 0) + dc = win.GetDC() + hdc = dc.GetSafeHdc() + GLContext.__init__(self, share_group, config, hdc, 'screen') + GGLView.__init__(self, _win = win, **kwds) + self._with_context(hdc, self._init_context) + win.ReleaseDC(dc) + + def destroy(self): + GLContext.destroy(self) + GGLView.destroy(self) + + def with_context(self, proc, flush = False): + win = self._win + dc = win.GetDC() + hdc = dc.GetSafeHdc() + try: + self._with_context(hdc, proc, flush) + finally: + win.ReleaseDC(dc) + + def OnPaint(self): + #print "GLView.OnPaint" ### + win = self._win + dc, ps = win.BeginPaint() + try: + hdc = dc.GetSafeHdc() + self._with_context(hdc, self.render, True) + finally: + win.EndPaint(ps) + + def _resized(self, delta): + self.with_context(self._update_viewport) + +#------------------------------------------------------------------------------ + +#class GLPixmap(GGLPixmap): +# +# def __init__(self, width, height, config = None, share_group = None, **kwds): +# print "GLPixmap:", width, height, kwds ### +# config = GLConfig._from_args(config, kwds) +# image = gdi.Bitmap(width, height) +# self._win_image = image +# graphics = gdi.Graphics.from_image(image) +# self._win_graphics = graphics +# hdc = graphics.GetHDC() +# self._win_hdc = hdc +# GLContext.__init__(self, share_group, config, hdc, 'pixmap') +# self._with_context(hdc, self._init_context) +# print "GLPixmap: done" ### +# +# def __del__(self): +# graphics = self._win_graphics +# graphics.ReleaseHDC(self._win_hdc) +# +# def with_context(self, proc, flush = False): +# try: +# self._with_context(self._hdc, proc, flush) +# finally: +# graphics.ReleaseHDC(hdc) + +#------------------------------------------------------------------------------ + +class GLPixmap(GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + print "GLPixmap:", width, height, kwds ### + config = GLConfig._from_args(config, kwds) + pyhdc = gui.CreateCompatibleDC(0) + dc = ui.CreateDCFromHandle(pyhdc) + hdc = dc.GetSafeHdc() + hbm = gui.CreateCompatibleBitmap(hdc, width, height) + bm = ui.CreateBitmapFromHandle(hbm) + dc.SelectObject(bm) + self._win_dc = dc + self._win_hbm = hbm + self._win_bm = bm + GLContext.__init__(self, share_group, config, hdc, 'pixmap') + self._with_context(hdc, self._init_context) + print "GLPixmap: done" ### + + def with_context(self, proc, flush = False): + hdc = self._win_dc.GetSafeHdc() + self._with_context(hdc, proc, flush) + +#------------------------------------------------------------------------------ + +def win_dump_pixelformat(pf): + print "nSize =", pf.nSize + print "nVersion =", pf.nVersion + print "dwFlags = 0x%08x" % pf.dwFlags + print "iPixelType =", pf.iPixelType + print "cColorBits =", pf.cColorBits + print "cRedBits =", pf.cRedBits + print "cRedShift =", pf.cRedShift + print "cGreenBits =", pf.cGreenBits + print "cGreenShift =", pf.cGreenShift + print "cBlueBits =", pf.cBlueBits + print "cBlueShift =", pf.cBlueShift + print "cAlphaBits =", pf.cAlphaBits + print "cAlphaShift =", pf.cAlphaShift + print "cAccumBits =", pf.cAccumBits + print "cAccumRedBits =", pf.cAccumRedBits + print "cAccumGreenBits =", pf.cAccumGreenBits + print "cAccumBlueBits =", pf.cAccumBlueBits + print "cDepthBits =", pf.cDepthBits + print "cStencilBits =", pf.cStencilBits + print "cAuxBuffers =", pf.cAuxBuffers + print "iLayerType =", pf.iLayerType + print "bReserved =", pf.bReserved + print "dwLayerMask =", pf.dwLayerMask + print "dwVisibleMask =", pf.dwVisibleMask + print "dwDamageMask =", pf.dwDamageMask diff --git a/GUI/Win32/GL_nonarb.py b/GUI/Win32/GL_nonarb.py new file mode 100644 index 0000000..67ba5bc --- /dev/null +++ b/GUI/Win32/GL_nonarb.py @@ -0,0 +1,258 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - OpenGL - Win32 +# +#------------------------------------------------------------------------------ + +import win32con as wc, win32ui as ui, win32gui as gui +import GUI.GDIPlus as gdi +from GUI.Components import win_none +from GUI.OpenGL import WGL as wgl +from GUI.GGLViews import GLView as GGLView +from GUI.GGLPixmaps import GLPixmap as GGLPixmap +from GUI.GGLConfig import GLConfig as GGLConfig, GLConfigError +from GUI.GLContexts import GLContext +from GUI.GLTextures import Texture + +win_style = wc.WS_VISIBLE | wc.WS_CLIPCHILDREN | wc.WS_CLIPSIBLINGS +win_default_size = GGLView._default_size +win_default_rect = (0, 0, win_default_size[0], win_default_size[1]) + +#------------------------------------------------------------------------------ + +class GLConfig(GGLConfig): + + def _as_win_pixelformat(self, mode): + print "GLConfig._as_win_pixelformat: mode =", mode ### + pf = wgl.PIXELFORMATDESCRIPTOR() + flags = wgl.PFD_SUPPORT_OPENGL + if mode == 'screen' or mode == 'both': + print "GLConfig: requesting screen drawing" ### + flags |= wgl.PFD_DRAW_TO_WINDOW + if self._double_buffer: + flags |= wgl.PFD_DOUBLEBUFFER | wgl.PFD_SWAP_EXCHANGE + else: + flags |= wgl.PFD_DOUBLEBUFFER_DONTCARE + if mode == 'pixmap' or mode == 'both': + print "GLConfig: requesting pixmap drawing" ### + flags |= wgl.PFD_DRAW_TO_BITMAP | wgl.PFD_SUPPORT_GDI + if not self._depth_buffer: + flags |= wgl.PFD_DEPTH_DONTCARE + if self._stereo: + flags |= wgl.PFD_STEREO + else: + flags |= wgl.PFD_STEREO_DONTCARE + pf.dwFlags = flags & 0xffffffff + pf.iPixelType = wgl.PFD_TYPE_RGBA + #pf.cColorBits = 3 * self._color_size + pf.cColorBits = 32 ### + pf.cAlphaBits = 8 ### + pf.cAlphaShift = 24 ### + if self._alpha: + pf.cAlphaBits = self._alpha_size + pf.cAuxBuffers = self._aux_buffers + if self._depth_buffer: + pf.cDepthBits = self._depth_size + if self._stencil_buffer: + pf.cStencilBits = self._stencil_size + if self._accum_buffer: + pf.cAccumBits = 3 * self._accum_size + pf.iLayerType = wgl.PFD_MAIN_PLANE + return pf + + def _from_win_pixelformat(cls, pf): + self = cls.__new__(cls) + flags = pf.dwFlags + self._double_buffer = flags & wgl.PFD_DOUBLEBUFFER != 0 + self._alpha = pf.cAlphaSize > 0 + self._color_size = pf.cColorBits + self._alpha_size = pf.cAlphaSize + self._stereo = flags & wgl.PFD_STEREO != 0 + self._aux_buffers = pf.cAuxBuffers + self._depth_buffer = pf.cDepthBits > 0 + self._depth_size = pf.cDepthBits + self._stencil_buffer = pf.cStencilBits > 0 + self._stencil_size = pf.cStencilBits + self._accum_size = pf.cAccumBits + self._accum_buffer = self._accum_size > 0 + self._multisample = False + self._samples_per_pixel = 1 + return self + + def _check_win_pixelformat(self, pf, mode): + flags = pf.dwFlags + if mode == 'screen' or mode == 'both': + if not flags & wgl.PFD_DRAW_TO_WINDOW: + raise GLConfigError("Rendering to screen not supported") + if mode == 'pixmap' or mode == 'both': + if not flags & wgl.PFD_DRAW_TO_BITMAP: + raise GLConfigError("Rendering to pixmap not supported") + if self._alpha and pf.cAlphaBits == 0: + raise GLConfigError("Alpha channel not available") + if self._stereo and not flags & wgl.PFD_STEREO: + raise GLConfigError("Stereo buffer not available") + if self._aux_buffers and pf.cAuxBuffers == 0: + raise GLConfigError("Auxiliary buffers not available") + if self._depth_buffer and pf.cDepthBits == 0: + raise GLConfigError("Depth buffer not available") + if self._stencil_buffer and pf.cStencilBits == 0: + raise GLConfigError("Stencil buffer not available") + if self.accum_buffer and pf.cAccumBits == 0: + raise GLConfigError("Accumulation buffer not available") + + def _win_supported_pixelformat(self, hdc, mode): + reqpf = self._as_win_pixelformat(mode) + print "GLConfig._win_supported_pixelformat" ### + print "Requested format:" ### + win_dump_pixelformat(reqpf) ### + print "GLConfig: Choosing pixel format for hdc", hdc ### + ipf = wgl.ChoosePixelFormat(hdc, reqpf) + print "... result =", ipf ### + actpf = wgl.PIXELFORMATDESCRIPTOR() + print "GLConfig: Describing pixel format", ipf, "for hdc", hdc ### + wgl.DescribePixelFormat(hdc, ipf, actpf.nSize, actpf) + print "Actual format:" ### + win_dump_pixelformat(actpf) ### + return ipf, actpf + + def supported(self, mode = 'both'): + dc = win_none.GetDC() + hdc = dc.GetSafeHdc() + ipf, actpf = self._win_supported_pixelformat(hdc, mode) + win_none.ReleaseDC(dc) + return GLConfig._from_win_pixelformat(actpf) + +#------------------------------------------------------------------------------ + +class GLView(GGLView): + + def __init__(self, config = None, share_group = None, **kwds): + config = GLConfig._from_args(config, kwds) + win = ui.CreateWnd() + win.CreateWindow(None, None, win_style, win_default_rect, + win_none, 0) + dc = win.GetDC() + hdc = dc.GetSafeHdc() + GLContext.__init__(self, share_group, config, hdc, 'screen') + GGLView.__init__(self, _win = win, **kwds) + self._with_context(hdc, self._init_context) + win.ReleaseDC(dc) + + def destroy(self): + GLContext.destroy(self) + GGLView.destroy(self) + + def with_context(self, proc, flush = False): + win = self._win + dc = win.GetDC() + hdc = dc.GetSafeHdc() + try: + self._with_context(hdc, proc, flush) + finally: + win.ReleaseDC(dc) + + def OnPaint(self): + #print "GLView.OnPaint" ### + win = self._win + dc, ps = win.BeginPaint() + try: + hdc = dc.GetSafeHdc() + self._with_context(hdc, self.render, True) + finally: + win.EndPaint(ps) + + def _resized(self, delta): + self.with_context(self._update_viewport) + +#------------------------------------------------------------------------------ + +class GLPixmap(GGLPixmap): + + def __init__(self, width, height, config = None, share_group = None, **kwds): + print "GLPixmap:", width, height, kwds ### + config = GLConfig._from_args(config, kwds) + image = gdi.Bitmap(width, height) + self._win_image = image + graphics = gdi.Graphics.from_image(image) + self._win_graphics = graphics + hdc = graphics.GetHDC() + self._win_hdc = hdc + GLContext.__init__(self, share_group, config, hdc, 'pixmap') + self._with_context(hdc, self._init_context) + print "GLPixmap: done" ### + + def __del__(self): + graphics = self._win_graphics + graphics.ReleaseHDC(self._win_hdc) + + def with_context(self, proc, flush = False): + try: + self._with_context(self._hdc, proc, flush) + finally: + graphics.ReleaseHDC(hdc) + +#------------------------------------------------------------------------------ + +#class GLPixmap(GGLPixmap): +# +# def __init__(self, width, height, config = None, share_group = None, **kwds): +# print "GLPixmap:", width, height, kwds ### +# config = GLConfig._from_args(config, kwds) +# dc0 = win_none.GetDC() +# dc = dc0.CreateCompatibleDC(dc0) +# bm = ui.CreateBitmap() +# ###bm.CreateCompatibleBitmap(dc0, width, height) +# bm.CreateCompatibleBitmap(dc0, 1, 1) +# win_none.ReleaseDC(dc0) +# dc.SelectObject(bm) +# self._win_dc = dc +# self._win_bm = bm +# hdc = dc.GetSafeHdc() +# win_dump_bitmap(bm) ### +# GLContext.__init__(self, share_group, config, hdc, 'pixmap') +# self._with_context(hdc, self._init_context) +# print "GLPixmap: done" ### +# +# def with_context(self, proc, flush = False): +# hdc = self._win_dc.GetSafeHdc() +# self._with_context(hdc, proc, flush) + +#------------------------------------------------------------------------------ + +def win_dump_pixelformat(pf): + print "nSize =", pf.nSize + print "nVersion =", pf.nVersion + print "dwFlags = 0x%08x" % pf.dwFlags + print "iPixelType =", pf.iPixelType + print "cColorBits =", pf.cColorBits + print "cRedBits =", pf.cRedBits + print "cRedShift =", pf.cRedShift + print "cGreenBits =", pf.cGreenBits + print "cGreenShift =", pf.cGreenShift + print "cBlueBits =", pf.cBlueBits + print "cBlueShift =", pf.cBlueShift + print "cAlphaBits =", pf.cAlphaBits + print "cAlphaShift =", pf.cAlphaShift + print "cAccumBits =", pf.cAccumBits + print "cAccumRedBits =", pf.cAccumRedBits + print "cAccumGreenBits =", pf.cAccumGreenBits + print "cAccumBlueBits =", pf.cAccumBlueBits + print "cDepthBits =", pf.cDepthBits + print "cStencilBits =", pf.cStencilBits + print "cAuxBuffers =", pf.cAuxBuffers + print "iLayerType =", pf.iLayerType + print "bReserved =", pf.bReserved + print "dwLayerMask =", pf.dwLayerMask + print "dwVisibleMask =", pf.dwVisibleMask + print "dwDamageMask =", pf.dwDamageMask + +def win_dump_bitmap(bm): + info = bm.GetInfo() + print "bmType =", info['bmType'] + print "bmWidth =", info['bmWidth'] + print "bmHeight =", info['bmHeight'] + print "bmWidthBytes =", info['bmWidthBytes'] + print "bmPlanes =", info['bmPlanes'] + print "bmBitsPixel =", info['bmBitsPixel'] + + \ No newline at end of file diff --git a/GUI/Win32/Geometry.py b/GUI/Win32/Geometry.py new file mode 100755 index 0000000..e81b148 --- /dev/null +++ b/GUI/Win32/Geometry.py @@ -0,0 +1,7 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Geometry - Win32 +# +#-------------------------------------------------------------------- + +from GUI.GGeometry import * diff --git a/GUI/Win32/Image.py b/GUI/Win32/Image.py new file mode 100644 index 0000000..0cf7b38 --- /dev/null +++ b/GUI/Win32/Image.py @@ -0,0 +1,16 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Image - Win32 +# +#-------------------------------------------------------------------- + +import GDIPlus as gdi +from GUI import export +from GUI.GImages import Image as GImage + +class Image(GImage): + + def _init_from_file(self, path): + self._win_image = gdi.Bitmap.from_file(path) + +export(Image) diff --git a/GUI/Win32/ImageBase.py b/GUI/Win32/ImageBase.py new file mode 100644 index 0000000..2f935c8 --- /dev/null +++ b/GUI/Win32/ImageBase.py @@ -0,0 +1,22 @@ +#-------------------------------------------------------------------- +# +# PyGUI - ImageBase - Win32 +# +#-------------------------------------------------------------------- + +from GUI import export +from GUI.GImageBases import ImageBase as GImageBase + +class ImageBase(GImageBase): + # _win_image GdiPlus.Image + + def get_width(self): + return self._win_image.GetWidth() + + def get_height(self): + return self._win_image.GetHeight() + + def draw(self, canvas, src_rect, dst_rect): + canvas._win_graphics.DrawImage_rr(self._win_image, dst_rect, src_rect) + +export(ImageBase) diff --git a/GUI/Win32/Label.py b/GUI/Win32/Label.py new file mode 100644 index 0000000..a562e5b --- /dev/null +++ b/GUI/Win32/Label.py @@ -0,0 +1,81 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Label - Win32 +# +#-------------------------------------------------------------------- + +from math import ceil +import win32con as wc, win32ui as ui +from GUI import export +from GUI.StdColors import black +from GUI.StdFonts import system_font +from GUI.WinUtils import win_none +from GUI.GLabels import Label as GLabel + +win_style = wc.WS_CLIPSIBLINGS | wc.WS_VISIBLE + +win_dt_format = wc.DT_NOPREFIX | wc.DT_SINGLELINE | wc.DT_NOCLIP + +win_dt_align_map = { + 'l': wc.DT_LEFT | win_dt_format, + 'c': wc.DT_CENTER | win_dt_format, + 'r': wc.DT_RIGHT | win_dt_format, +} + +#-------------------------------------------------------------------- + +class Label(GLabel): + + _win_transparent = True + + _font = None + _color = black + _just = 'l' + + def __init__(self, text = "New Label", **kwds): + self._set_lines(text) + lines = self._lines + font = self._win_predict_font(kwds) + w = 0 + for line in lines: + w = max(w, font.width(line)) + w = int(ceil(w)) + h = self._calc_height(font, len(lines)) + win = ui.CreateWnd() + win.CreateWindow(None, None, win_style, (0, 0, w, h), win_none, 0) + #win.ModifyStyleEx(0, wc.WS_EX_TRANSPARENT, 0) + GLabel.__init__(self, _win = win, **kwds) + + def get_text(self): + return "\n".join(self._lines) + + def set_text(self, x): + self._set_lines(x) + self.invalidate() + + def _set_lines(self, x): + self._lines = x.split("\n") + + def OnPaint(self): + win = self._win + dc, paint_struct = win.BeginPaint() + font = self._font + win_font = font._win_font + dc.SetBkMode(wc.TRANSPARENT) + dc.SelectObject(win_font) + c = self._color._win_color + #print "Label.OnPaint: win color = 0x%08x" % c + dc.SetTextColor(c) + rm = self.width + y = 0 + h = font.line_height + just = self._just[:1] + dt_format = win_dt_align_map[just] + for line in self._lines: + r = (0, y, rm, y + h) + dc.DrawText(line, r, dt_format) + y += h + win.EndPaint(paint_struct) + +export(Label) + diff --git a/GUI/Win32/ListButton.py b/GUI/Win32/ListButton.py new file mode 100644 index 0000000..ce46d56 --- /dev/null +++ b/GUI/Win32/ListButton.py @@ -0,0 +1,47 @@ +#-------------------------------------------------------------------- +# +# PyGUI - ListButton - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32gui as gui +from GUI import export +from GUI.WinUtils import win_none +from GUI.WinComboBox import CreateComboBox +from GUI.GListButtons import ListButton as GListButton + +class ListButton(GListButton): + + _pass_key_events_to_platform = True + + def __init__(self, **kwds): + titles, values = self._extract_initial_items(kwds) + self._titles = titles + self._values = values + win = CreateComboBox(win_none, (0, 0), (100, 320), wc.CBS_DROPDOWNLIST) + win.ShowWindow() + self._win_update_items(win) + GListButton.__init__(self, _win = win, **kwds) + + def _update_items(self): + self._win_update_items(self._win) + + def _win_update_items(self, win): + win.ResetContent() + for title in self._titles: + win.AddString(title) + + def _get_selected_index(self): + return self._win.GetCurSel() + + def _set_selected_index(self, x): + try: + self._win.SetCurSel(x) + except ui.error: + pass + + def _cbn_sel_change(self): + self.do_action() + + +export(ListButton) diff --git a/GUI/Win32/Menu.py b/GUI/Win32/Menu.py new file mode 100644 index 0000000..9db062a --- /dev/null +++ b/GUI/Win32/Menu.py @@ -0,0 +1,66 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Menu - Win32 +# +#-------------------------------------------------------------------- + +import win32ui as ui, win32con as wc +from GUI import export +from GUI.WinMenus import win_command_to_id +from GUI.GMenus import Menu as GMenu + +class Menu(GMenu): + + def __init__(self, *args, **kwds): + GMenu.__init__(self, *args, **kwds) + + def _update_platform_menu(self): + # Don't need to do anything here because platform menu item + # states are updated by HookCommandUpdate handlers. + pass + + def _win_create_menu(self): + # Create a fresh platform menu reflecting the current items. Need + # to do this because it's not possible to share submenu handles + # between windows. + self._rebuild_platform_menu() + win_menu = self._win_menu + self._win_menu = None # So we don't accidentally try to reuse it + return win_menu + + def _clear_platform_menu(self): + self._win_menu = ui.CreatePopupMenu() + +# def _clear_platform_menu(self): +# #print "Menu._clear_platform_menu:", self ### +# bypos = wc.MF_BYPOSITION +# win_menu = self._win_menu +# n = win_menu.GetMenuItemCount() +# for i in xrange(n-1, -1, -1): +# win_menu.DeleteMenu(i, bypos) + + def _add_separator_to_platform_menu(self): + #print "Menu._add_separator_to_platform_menu:", self ### + self._win_menu.AppendMenu(wc.MF_SEPARATOR, 0) + + def _add_item_to_platform_menu(self, item, name, command_name, *args): + #print "Menu._add_item_to_platform_menu:", self, item, name ### + win_text = name.replace("&", "&&") + key = item._key + if key: + win_text += "\tCtrl+" + if item._shift: + win_text += "Shift+" + if item._option: + win_text += "Alt+" + win_text += key + flags = wc.MF_STRING + # These are done by HookCommandUpdate handler + #if not item.enabled: + # flags |= wc.MF_GRAYED + #if item.checked: + # flags |= wc.MF_CHECKED + id = win_command_to_id(command_name, *args) + self._win_menu.AppendMenu(flags, id, win_text) + +export(Menu) diff --git a/GUI/Win32/Numerical.py b/GUI/Win32/Numerical.py new file mode 100644 index 0000000..2f99f42 --- /dev/null +++ b/GUI/Win32/Numerical.py @@ -0,0 +1,49 @@ +#-------------------------------------------------------------- +# +# PyGUI - NumPy interface - Windows +# +#-------------------------------------------------------------- + +from numpy import ndarray, uint8 +from GUI import GDIPlus as gdi +from GUI import Image + +def image_from_ndarray(array, format, size = None): + """ + Creates an Image from a numpy ndarray object. The format + may be 'RGB' or 'RGBA'. If a size is specified, the array + will be implicitly reshaped to that size, otherwise the size + is inferred from the first two dimensions of the array. + """ + if array.itemsize <> 1: + raise ValueError("Color component size must be 1 byte") + if size is None: + shape = array.shape + if len(shape) <> 3: + raise ValueError("Array has wrong number of dimensions") + height, width, pixel_size = shape + if pixel_size <> len(format): + raise ValueError("Last dimension of array does not match format") + else: + width, height = size + pixel_size = len(format) + data_size = array.size + if data_size <> width * height * pixel_size: + raise ValueError("Array has wrong shape for specified size and format") + shape = (height, width, pixel_size) + array = array.reshape(shape) + swapped = ndarray(shape, uint8) + swapped[..., 0] = array[..., 2] + swapped[..., 1] = array[..., 1] + swapped[..., 2] = array[..., 0] + if pixel_size == 4: + fmt = gdi.PixelFormat32bppARGB + swapped[..., 3] = array[..., 3] + else: + fmt = gdi.PixelFormat24bppRGB + data = swapped.tostring() + bitmap = gdi.Bitmap.from_data(width, height, fmt, data) + image = Image.__new__(Image) + image._win_image = bitmap + image._data = data + return image diff --git a/GUI/Win32/PIL.py b/GUI/Win32/PIL.py new file mode 100644 index 0000000..67df552 --- /dev/null +++ b/GUI/Win32/PIL.py @@ -0,0 +1,33 @@ +#-------------------------------------------------------------- +# +# PyGUI - PIL interface - Windows +# +#-------------------------------------------------------------- + +from __future__ import absolute_import +from GUI import GDIPlus as gdi +from GUI import Image +from Image import merge + +def image_from_pil_image(pil_image): + """Creates an Image from a Python Imaging Library (PIL) + Image object.""" + pil_image.load() + mode = pil_image.mode + w, h = pil_image.size + if mode == "RGB": + r, g, b = pil_image.split() + pil_image = merge(mode, (b, g, r)) + fmt = gdi.PixelFormat24bppRGB + elif mode == "RGBA": + r, g, b, a = pil_image.split() + pil_image = merge(mode, (b, g, r, a)) + fmt = gdi.PixelFormat32bppARGB + else: + raise ValueError("Unsupported PIL image mode '%s'" % mode) + data = pil_image.tostring() + bitmap = gdi.Bitmap.from_data(w, h, fmt, data) + image = Image.__new__(Image) + image._win_image = bitmap + image._data = data + return image diff --git a/GUI/Win32/Pixmap.py b/GUI/Win32/Pixmap.py new file mode 100644 index 0000000..1ed6390 --- /dev/null +++ b/GUI/Win32/Pixmap.py @@ -0,0 +1,20 @@ +#-------------------------------------------------------------------------- +# +# Python GUI - Pixmaps - Win32 +# +#-------------------------------------------------------------------------- + +import GDIPlus as gdi +from GUI import export +from GUI import Canvas +from GUI.GPixmaps import Pixmap as GPixmap + +class Pixmap(GPixmap): + + def __init__(self, width, height): + self._win_image = gdi.Bitmap(width, height) + + def with_canvas(self, proc): + proc(Canvas._from_win_image(self._win_image)) + +export(Pixmap) diff --git a/GUI/Win32/Printing.py b/GUI/Win32/Printing.py new file mode 100644 index 0000000..8e5b9db --- /dev/null +++ b/GUI/Win32/Printing.py @@ -0,0 +1,255 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Printing - Win32 +# +#------------------------------------------------------------------------------ + +import WinPageSetup as wps, WinPrint as wp +import win32print as wp2 +from GUI.GPrinting import PageSetup as GPageSetup, Printable as GPrintable, \ + Paginator +from GUI import Canvas + +#------------------------------------------------------------------------------ + +win_paper_names = { + 1: "Letter", # Letter 8 1/2 x 11 in + 2: "Letter Small", # Letter Small 8 1/2 x 11 in + 3: "Tabloid", # Tabloid 11 x 17 in + 4: "Ledger", # Ledger 17 x 11 in + 5: "Legal", # Legal 8 1/2 x 14 in + 6: "Statement", # Statement 5 1/2 x 8 1/2 in + 7: "Executive", # Executive 7 1/4 x 10 1/2 in + 8: "A3", # A3 297 x 420 mm + 9: "A4", # A4 210 x 297 mm + 10: "A4 Small", # A4 Small 210 x 297 mm + 11: "A5", # A5 148 x 210 mm + 12: "B4 (JIS)", # B4 (JIS) 250 x 354 + 13: "B5 (JIS)", # B5 (JIS) 182 x 257 mm + 14: "Folio", # Folio 8 1/2 x 13 in + 15: "Quarto", # Quarto 215 x 275 mm + 16: "10x14", # 10x14 in + 17: "11x17", # 11x17 in + 18: "Note", # Note 8 1/2 x 11 in + 19: "Envelope #9", # Envelope #9 3 7/8 x 8 7/8 + 20: "Envelope #10", # Envelope #10 4 1/8 x 9 1/2 + 21: "Envelope #11", # Envelope #11 4 1/2 x 10 3/8 + 22: "Envelope #12", # Envelope #12 4 \276 x 11 + 23: "Envelope #14", # Envelope #14 5 x 11 1/2 + 24: "C Sheet", # C size sheet + 25: "D Sheet", # D size sheet + 26: "E Sheet", # E size sheet + 27: "Envelope DL", # Envelope DL 110 x 220mm + 28: "Envelope C5", # Envelope C5 162 x 229 mm + 29: "Envelope C3", # Envelope C3 324 x 458 mm + 30: "Envelope C4", # Envelope C4 229 x 324 mm + 31: "Envelope C6", # Envelope C6 114 x 162 mm + 32: "Envelope C65", # Envelope C65 114 x 229 mm + 33: "Envelope B4", # Envelope B4 250 x 353 mm + 34: "Envelope B5", # Envelope B5 176 x 250 mm + 35: "Envelope B6", # Envelope B6 176 x 125 mm + 36: "Envelope", # Envelope 110 x 230 mm + 37: "Envelope Monarch", # Envelope Monarch 3.875 x 7.5 in + 38: "6 3/4 Envelope", # 6 3/4 Envelope 3 5/8 x 6 1/2 in + 39: "US Std Fanfold", # US Std Fanfold 14 7/8 x 11 in + 40: "German Std Fanfold", # German Std Fanfold 8 1/2 x 12 in + 41: "German Legal Fanfold", # German Legal Fanfold 8 1/2 x 13 in + 42: "B4", # B4 (ISO) 250 x 353 mm + 43: "Japanese Postcard", # Japanese Postcard 100 x 148 mm + 44: "9x11", # 9 x 11 in + 45: "10x11", # 10 x 11 in + 46: "15x11", # 15 x 11 in + 47: "Envelope Invite", # Envelope Invite 220 x 220 mm + #48: "", # RESERVED--DO NOT USE + #49: "", # RESERVED--DO NOT USE + 50: "Letter Extra", # Letter Extra 9 \275 x 12 in + 51: "Legal Extra", # Legal Extra 9 \275 x 15 in + 52: "Tabloid Extra", # Tabloid Extra 11.69 x 18 in + 53: "A4 Extra", # A4 Extra 9.27 x 12.69 in + 54: "Letter Transverse", # Letter Transverse 8 \275 x 11 in + 55: "A4 Transverse", # A4 Transverse 210 x 297 mm + 56: "Letter Extra Transverse", # Letter Extra Transverse 9\275 x 12 in + 57: "SuperA", # SuperA/SuperA/A4 227 x 356 mm + 58: "SuperB", # SuperB/SuperB/A3 305 x 487 mm + 59: "Letter Plus", # Letter Plus 8.5 x 12.69 in + 60: "A4 Plus", # A4 Plus 210 x 330 mm + 61: "A5 Transverse", # A5 Transverse 148 x 210 mm + 62: "B5 (JIS) Transverse", # B5 (JIS) Transverse 182 x 257 mm + 63: "A3 Extra", # A3 Extra 322 x 445 mm + 64: "A5 Extra", # A5 Extra 174 x 235 mm + 65: "B5 (ISO) Extra", # B5 (ISO) Extra 201 x 276 mm + 66: "A2", # A2 420 x 594 mm + 67: "A3 Transverse", # A3 Transverse 297 x 420 mm + 68: "A3 Extra Transverse", # A3 Extra Transverse 322 x 445 mm +} + +win_paper_codes = dict([(name, code) + for (code, name) in win_paper_names.iteritems()]) + +def ti_to_pts(x): + return x * 0.072 + +def pts_to_ti(x): + return int(round(x / 0.072)) + +#------------------------------------------------------------------------------ + +class PageSetup(GPageSetup): + + def __new__(cls): + self = GPageSetup.__new__(cls) + self._win_psd = wps.get_defaults() + return self + + def __init__(self): + self.margins = (36, 36, 36, 36) + + def __getstate__(self): + psd = self._win_psd + state = GPageSetup.__getstate__(self) + state['_win_devmode'] = wps.get_handle_contents(psd.hDevMode) + state['_win_devnames'] = wps.get_handle_contents(psd.hDevNames) + return state + + def __setstate__(self, state): + psd = self._win_psd + dm = state.pop('_win_devmode', None) + dn = state.pop('_win_devnames', None) + GPageSetup.__setstate__(self, state) + if dm: + wps.GlobalFree(psd.hDevMode) + psd.hDevMode = handle_with_contents(dm) + if dn: + wps.GlobalFree(psd.hDevNames) + psd.hDevNames = handle_with_contents(dn) + + def _win_lock_devmode(self): + return wps.lock_devmode_handle(self._win_psd.hDevMode) + + def _win_unlock_devmode(self): + wps.GlobalUnlock(self._win_psd.hDevMode) + + def get_printable_rect(self): + psd = self._win_psd + pw, ph = self.paper_size + mm = psd.rtMinMargin + ml = ti_to_pts(mm.left) + mt = ti_to_pts(mm.top) + mr = ti_to_pts(mm.right) + mb = ti_to_pts(mm.bottom) + return (ml, mt, pw - mr, ph - mb) + + def get_paper_name(self): + dm = self._win_lock_devmode() + result = win_paper_names.get(dm.dmPaperSize, "Custom") + self._win_unlock_devmode() + return result + + def set_paper_name(self, name): + dm = self._win_lock_devmode() + dm.dmPaperSize = win_paper_codes.get(name, 0) + self._win_unlock_devmode() + + def get_paper_width(self): + return ti_to_pts(self._win_psd.ptPaperSize.x) + + def get_paper_height(self): + return ti_to_pts(self._win_psd.ptPaperSize.y) + + def set_paper_width(self, v): + self._win_psd.ptPaperSize.x = pts_to_ti(v) + + def set_paper_height(self, v): + self._win_psd.ptPaperSize.y = pts_to_ti(v) + + def get_left_margin(self): + return ti_to_pts(self._win_psd.rtMargin.left) + + def get_top_margin(self): + return ti_to_pts(self._win_psd.rtMargin.top) + + def get_right_margin(self): + return ti_to_pts(self._win_psd.rtMargin.right) + + def get_bottom_margin(self): + return ti_to_pts(self._win_psd.rtMargin.bottom) + + def set_left_margin(self, v): + self._win_psd.rtMargin.left = pts_to_ti(v) + + def set_top_margin(self, v): + self._win_psd.rtMargin.top = pts_to_ti(v) + + def set_right_margin(self, v): + self._win_psd.rtMargin.right = pts_to_ti(v) + + def set_bottom_margin(self, v): + self._win_psd.rtMargin.bottom = pts_to_ti(v) + + def get_orientation(self): + dm = self._win_lock_devmode() + result = win_orientation_names.get(dm.dmOrientation, 'portrait') + self._win_unlock_devmode() + return result + + def set_orientation(self, v): + dm = self._win_lock_devmode() + dm.dmOrientation = win_orientation_codes.get(v, 1) + self._win_unlock_devmode() + + def get_printer_name(self): + dm = self._win_lock_devmode() + result = dm.dmDeviceName + self._win_unlock_devmode() + return result + + def set_printer_name(self, v): + dm = self._win_lock_devmode() + dm.dmDeviceName = v + self._win_unlock_devmode() + +#------------------------------------------------------------------------------ + +class Printable(GPrintable): + + def print_view(self, page_setup, prompt = True): + paginator = Paginator(self, page_setup) + psd = page_setup._win_psd + pd = wp.PRINTDLG() + pd.hDevMode = psd.hDevMode + pd.hDevNames = psd.hDevNames + pd.nMinPage = 1 + pd.nMaxPage = paginator.num_pages + pd.nCopies = 1 + if wp.PrintDlg(pd): + title = self.print_title + di = wp.DOCINFO() + di.lpszDocName = title + devnames = wps.DevNames(psd.hDevNames) + if devnames.output == "FILE:": + from FileDialogs import request_new_file + f = request_new_file("Print '%s' to file:" % title) + if not f: + return + output_path = f.path + di.lpszOutput = output_path + try: + hdc = pd.hDC + dx, dy = wp.GetPrintingOffset(hdc) + print "TODO: Printable: Implement a Cancel dialog" ### + #wp.install_abort_proc(hdc) + wp.StartDoc(hdc, di) + for page_num in xrange(pd.nFromPage - 1, pd.nToPage): + wp.StartPage(hdc) + canvas = Canvas._from_win_hdc(hdc, for_printing = True) + canvas.translate(-dx, -dy) + paginator.draw_page(canvas, page_num) + wp.EndPage(hdc) + wp.EndDoc(hdc) + finally: + wp.DeleteDC(hdc) + +#------------------------------------------------------------------------------ + +def present_page_setup_dialog(page_setup): + return wps.PageSetupDlg(page_setup._win_psd) diff --git a/GUI/Win32/RadioButton.py b/GUI/Win32/RadioButton.py new file mode 100644 index 0000000..308b87d --- /dev/null +++ b/GUI/Win32/RadioButton.py @@ -0,0 +1,57 @@ +#-------------------------------------------------------------------- +# +# PyGUI - RadioButton - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import export +from GUI.StdFonts import system_font +from GUI.ButtonBases import ButtonBase +from GUI.GRadioButtons import RadioButton as GRadioButton + +win_check_size = 13 +win_hpad = 5 + +class RadioButton(ButtonBase, GRadioButton): + + #_win_transparent = True + + def __init__(self, title = "New Radio Button", **kwds): + font = self._win_predict_font(kwds) + w = font.width(title) + win_hpad + win_check_size + h = max(self._calc_height(font), win_check_size) + win_style = wc.BS_RADIOBUTTON + win = self._win_create_button(title, win_style, w, h) + GRadioButton.__init__(self, _win = win, **kwds) + + def _value_changed(self): + self._win_update() + + def _win_update(self): + group = self._group + if group: + state = self._value == group._value + else: + state = False + self._win.SetCheck(state) + +# Unbelievably, a BN_CLICKED message is sent when the +# button is focused, making it impossible to tell whether +# it was clicked or tabbed into. +# +# def _win_bn_clicked(self): +# print "RadioButton._win_bn_clicked" ### +# self._win_activate() + + def _win_activate(self): + group = self._group + if group: + group.value = self._value + + def mouse_up(self, event): + if self._win.GetState() & 0x4: # highlight + self._win_activate() + GRadioButton.mouse_up(self, event) + +export(RadioButton) diff --git a/GUI/Win32/RadioGroup.py b/GUI/Win32/RadioGroup.py new file mode 100644 index 0000000..e9a8e7d --- /dev/null +++ b/GUI/Win32/RadioGroup.py @@ -0,0 +1,22 @@ +#-------------------------------------------------------------------- +# +# PyGUI - RadioGroup - Win32 +# +#-------------------------------------------------------------------- + +from GUI import export +from GUI.GRadioGroups import RadioGroup as GRadioGroup + +class RadioGroup(GRadioGroup): + + def _item_added(self, item): + item._win_update() + + def _item_removed(self, item): + item._win_update() + + def _value_changed(self): + for item in self._items: + item._win_update() + +export(RadioGroup) diff --git a/GUI/Win32/ScrollableView.py b/GUI/Win32/ScrollableView.py new file mode 100644 index 0000000..a06fd67 --- /dev/null +++ b/GUI/Win32/ScrollableView.py @@ -0,0 +1,178 @@ +#-------------------------------------------------------------------- +# +# PyGUI - ScrollableView - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32gui as gui +from GUI import export +from GUI.WinUtils import win_plain_class, win_none +from GUI import Canvas +from GUI.GScrollableViews import ScrollableView as GScrollableView, \ + default_extent, default_line_scroll_amount, default_scrolling +from GUI.Geometry import add_pt, sub_pt, offset_rect, offset_rect_neg +from GUI.GDrawableContainers import default_size +from GUI.Geometry import offset_rect_neg + +win_style = wc.WS_CHILD | wc.WS_CLIPCHILDREN | wc.WS_CLIPSIBLINGS | \ + wc.WS_VISIBLE ###| wc.WS_VSCROLL| wc.WS_HSCROLL +win_ex_style = 0 # wc.WS_EX_CLIENTEDGE +win_default_rect = (0, 0, default_size[0], default_size[1]) +win_scroll_flags = wc.SW_INVALIDATE | wc.SW_SCROLLCHILDREN +win_swp_flags = wc.SWP_DRAWFRAME #| wc.SWP_FRAMECHANGED + +class ScrollableView(GScrollableView): + + _line_scroll_amount = default_line_scroll_amount + _extent = (500, 500) + + def __init__(self, **kwds): + kwds.setdefault('border', True) + kwds.setdefault('extent', default_extent) + kwds.setdefault('scrolling', default_scrolling) + win = ui.CreateWnd() + win.CreateWindowEx(win_ex_style, win_plain_class, None, win_style, win_default_rect, win_none, 0) + win.HookMessage(self._win_wm_hscroll, wc.WM_HSCROLL) + win.HookMessage(self._win_wm_vscroll, wc.WM_VSCROLL) + GScrollableView.__init__(self, _win = win) + self.set(**kwds) + + def get_hscrolling(self): + return self._win_get_flag(wc.WS_HSCROLL) + + def get_vscrolling(self): + return self._win_get_flag(wc.WS_VSCROLL) + + def set_hscrolling(self, x): + #print "ScrollableView.set_hscrolling:", x ### + self._win_set_flag(x, wc.WS_HSCROLL, win_swp_flags) + self._win_update_h_scrollbar() + + def set_vscrolling(self, x): + #print "ScrollableView.set_vscrolling:", x ### + self._win_set_flag(x, wc.WS_VSCROLL, win_swp_flags) + self._win_update_v_scrollbar() + + def set_border(self, x): + self._border = x + self._win_set_ex_flag(x, wc.WS_EX_CLIENTEDGE, win_swp_flags) + + def get_line_scroll_amount(self): + return self._line_scroll_amount + + def get_extent(self): + return self._extent + + def set_extent(self, extent): + self._extent = extent + self._win_update_scroll_sizes() + + def get_scroll_offset(self): + return self._h_scroll_offset, self._v_scroll_offset + + def set_scroll_offset(self, p): + px = int(round(p[0])) + py = int(round(p[1])) + if px <> self._h_scroll_offset or py <> self._v_scroll_offset: + self._win_update_scroll_offset(px, py) + + def _win_update_scroll_sizes(self): + self._win_update_scroll_offset(self._h_scroll_offset, self._v_scroll_offset) + + def _win_update_scroll_offset(self, px, py): + ex, ey = self.extent + vx, vy = self.content_size + xmax = max(0, ex - vx) + ymax = max(0, ey - vy) + x = max(0, min(px, xmax)) + y = max(0, min(py, ymax)) + self._win_scroll_to(x, y) + self._win_update_h_scrollbar() + self._win_update_v_scrollbar() + + def _win_update_h_scrollbar(self): + self._win_update_scrollbar(self.hscrolling, wc.SB_HORZ, 0) + + def _win_update_v_scrollbar(self): + self._win_update_scrollbar(self.vscrolling, wc.SB_VERT, 1) + + def _win_update_scrollbar(self, enabled, nbar, i): + # It is important not to update a disabled scrollbar, or + # subtle problems occur. + if enabled: + #print "ScrollableView._win_update_scrollbar:", enabled, nbar, i ### + f = wc.SIF_DISABLENOSCROLL + info = (f, 0, self.extent[i], self.content_size[i], self.scroll_offset[i]) + self._win.SetScrollInfo(nbar, info, True) + + def _scroll_range(self): + return (xmax, ymax) + + def _win_scroll_to(self, x, y): + dx = self._h_scroll_offset - x + dy = self._v_scroll_offset - y + if dx or dy: + hwnd = self._win.GetSafeHwnd() + gui.ScrollWindowEx(hwnd, dx, dy, None, None, None, win_scroll_flags) + self._h_scroll_offset = x + self._v_scroll_offset = y + + def set_bounds(self, bounds): + GScrollableView.set_bounds(self, bounds) + self._win_update_scroll_sizes() + + def _invalidate_rect(self, r): + win = self._win + s = self.scroll_offset + self._win.InvalidateRect(offset_rect_neg(r, s)) + + def local_to_global(self, p): + q = sub_pt(p, self.scroll_offset) + return self._win.ClientToScreen(q) + + def global_to_local(self, g): + q = self._win.ScreenToClient(g) + return add_pt(q, self.scroll_offset) + +# def _win_prepare_dc(self, dc): +# dc.SetWindowOrg(self.scroll_offset) + + def _win_scroll_offset(self): + return self.scroll_offset + + def _win_wm_hscroll(self, message): + code = message[2] & 0xffff + if code == 0: + self.scroll_line_left() + elif code == 1: + self.scroll_line_right() + elif code == 2: + self.scroll_page_left() + elif code == 3: + self.scroll_page_right() + elif code == 5: + x = self._win_thumb_track_pos(wc.SB_HORZ) + self.scroll_offset = (x, self._v_scroll_offset) + + def _win_wm_vscroll(self, message): + code = message[2] & 0xffff + if code == 0: + self.scroll_line_up() + elif code == 1: + self.scroll_line_down() + elif code == 2: + self.scroll_page_up() + elif code == 3: + self.scroll_page_down() + elif code == 5: + y = self._win_thumb_track_pos(wc.SB_VERT) + self.scroll_offset = (self._h_scroll_offset, y) + + def _win_thumb_track_pos(self, nbar): + info = self._win.GetScrollInfo(nbar) + return info[5] + + def _win_adjust_bounds(self, bounds): + return offset_rect_neg(bounds, self.scroll_offset) + +export(ScrollableView) diff --git a/GUI/Win32/ScrollableView_mfc.py b/GUI/Win32/ScrollableView_mfc.py new file mode 100644 index 0000000..07d1b16 --- /dev/null +++ b/GUI/Win32/ScrollableView_mfc.py @@ -0,0 +1,128 @@ +#-------------------------------------------------------------------- +# +# PyGUI - ScrollableView - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI.Components import win_none +from GUI.Canvases import Canvas +from GUI.GScrollableViews import ScrollableView as GScrollableView, \ + default_extent, default_line_scroll_amount +from GUI.Geometry import add_pt, sub_pt, offset_rect, offset_rect_neg + +win_style = ui.AFX_WS_DEFAULT_VIEW + +#print "ScrollableViews: Creating dummy doc" +# PyWin32 insists on being given a CDocument when creating a CScrollView, +# and doesn't provide any way of creating a real one without using a resource. + +if 1: + # The following uses a resource included in win32ui.pyd. + import pywin.mfc.object + win_dummy_doc_template = pywin.mfc.object.CmdTarget( + ui.CreateDocTemplate(ui.IDR_PYTHONTYPE)) + ui.GetApp().AddDocTemplate(win_dummy_doc_template) + def win_get_dummy_doc(): + return win_dummy_doc_template.DoCreateDoc() +else: + # The following hack creates something that looks enough + # like a CDocument to keep it happy. But it doesn't work with + # pywin32 builds later than 212. + win_dummy_doc = ui.CreateRichEditView().GetDocument() + def win_get_dummy_doc(): + return win_dummy_doc + +class ScrollableView(GScrollableView): + + _line_scroll_amount = default_line_scroll_amount + + def __init__(self, **kwds): + kwds.setdefault('extent', default_extent) + doc = win_get_dummy_doc() + win = ui.CreateView(doc) + #win.CreateWindow(win_none, 0, win_style, (0, 0, 100, 100)) + win.CreateWindow(win_none, ui.AFX_IDW_PANE_FIRST, win_style, (0, 0, 100, 100)) + GScrollableView.__init__(self, _win = win) + self.set(**kwds) + + def get_hscrolling(self): + return self._win.GetStyle() & wc.WS_HSCROLL != 0 + + def get_vscrolling(self): + return self._win.GetStyle() & wc.WS_VSCROLL != 0 + + def set_hscrolling(self, x): + self._win_set_flag(x, wc.WS_HSCROLL) + + def set_vscrolling(self, x): + self._win_set_flag(x, wc.WS_VSCROLL) + + def get_line_scroll_amount(self): + return self._line_scroll_amount + + def get_extent(self): + return self._win.GetTotalSize() + + def set_extent(self, extent): + self._win_update_scroll_sizes(extent) + + def get_scroll_offset(self): + return self._win.GetScrollPosition() + + def set_scroll_offset(self, p): + px, py = p + ex, ey = self.extent + vx, vy = self.content_size + xmax = max(0, ex - vx) + ymax = max(0, ey - vy) + x = max(0, min(px, xmax)) + y = max(0, min(py, ymax)) + self._win.ScrollToPosition((x, y)) + + def set_bounds(self, bounds): + GScrollableView.set_bounds(self, bounds) + extent = self._win.GetTotalSize() + self._win_update_scroll_sizes(extent) + + def _invalidate_rect(self, r): + win = self._win + s = win.GetScrollPosition() + self._win.InvalidateRect(offset_rect_neg(r, s)) + + def local_to_global(self, p): + win = self._win + q = sub_pt(p, win.GetScrollPosition()) + return win.ClientToScreen(q) + + def global_to_local(self, g): + win = self._win + q = win.ScreenToClient(g) + return add_pt(q, win.GetScrollPosition()) + +# def global_to_local(self, g): +# win = self._win +# l = win.ScreenToClient(g) +# s = win.GetScrollPosition() +# q = add_pt(l, s) +# print "ScrollableView.global_to_local: g =", g, "l =", l, "s =", s, "q =", q ### +# return q + + def _win_update_scroll_sizes(self, extent): + ph = self.h_page_scroll_amount() + pv = self.v_page_scroll_amount() + ls = self.line_scroll_amount + self._win.SetScrollSizes(wc.MM_TEXT, extent, (ph, pv), ls) + + def OnDraw(self, dc): + #print "ScrollableView.OnDraw" ### + update_rect = dc.GetClipBox() + canvas = Canvas._from_win_dc(dc) + self.draw(canvas, update_rect) + + def _win_prepare_dc(self, dc, pinfo = None): + self._win.OnPrepareDC(dc, None) + + + + diff --git a/GUI/Win32/Slider.py b/GUI/Win32/Slider.py new file mode 100644 index 0000000..187f948 --- /dev/null +++ b/GUI/Win32/Slider.py @@ -0,0 +1,157 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Slider - Win32 +# +#-------------------------------------------------------------------- + +from __future__ import division +import win32con as wc, win32ui as ui +from GUI import export +from GUI.WinUtils import win_none +from GUI.GSliders import Slider as GSlider + +win_base_flags = wc.WS_CHILD | wc.WS_VISIBLE + +win_styles = { + 'h': 0x0, # TBS_HORZ | TBS_BOTTOM + #'v': 0x6, # TBS_VERT | TBS_LEFT + 'v': 0x2, # TBS_VERT | TBS_RIGHT +} + +win_no_ticks = 0x10 # TBS_NOTICKS +win_continuous_range = 10000 + +class Slider(GSlider): + + #_win_transparent = True + _default_breadth = 20 + + _min_value = 0.0 + _max_value = 1.0 + _discrete = False + _ticks = 0 + _live = True + + def __init__(self, orient, **kwds): + win = ui.CreateSliderCtrl() + win_flags = win_base_flags | win_no_ticks + try: + style = win_styles[orient] + except KeyError: + raise ValueError("Invalid Slider orientation %r" % orient) + l = self._default_length + b = self._default_breadth + if orient == 'h': + rect = (0, 0, l, b) + else: + rect = (0, 0, b, l) + win.CreateWindow(style | win_flags, rect, win_none, 0) + win.SetRange(0, win_continuous_range) + GSlider.__init__(self, _win = win, **kwds) + + def get_value(self): + win = self._win + p = win.GetPos() + q = win.GetRangeMax() + x0 = self._min_value + x1 = self._max_value + return x0 + (x1 - x0) * p / q + + def set_value(self, x): + win = self._win + x0 = self._min_value + x1 = self._max_value + q = win.GetRangeMax() + p = int(round(q * (x - x0) / (x1 - x0))) + self._win.SetPos(p) + + def get_min_value(self): + return self._min_value + + def set_min_value(self, x): + self._min_value = x + self._win_update_range() + + def get_max_value(self): + return self._max_value + + def set_max_value(self, x): + self._max_value = x + self._win_update_range() + + def get_ticks(self): + return self._ticks + + def set_ticks(self, n): + self._ticks = n + self._win_update_ticks() + + def get_discrete(self): + return self._discrete + + def set_discrete(self, d): + if self._discrete != d: + self._discrete = d + self._win_update_range() + + def get_live(self): + return self._live + + def set_live(self, x): + self._live = x + + def _win_update_range(self): + if self._discrete: + x1 = max(0, self._ticks - 1) + else: + x1 = win_continuous_range + self._win.SetRange(0, x1) + self._win_update_ticks() + + def _win_update_ticks(self): + #print "Slider._win_update_ticks" ### + win = self._win + n = self._ticks + if n >= 2: + if self._discrete: + f = 1 + else: + f = int(round(win_continuous_range / (n - 1))) + win.ModifyStyle(win_no_ticks, 0) + win.ClearTics(False) + for i in xrange(n + 1): + win.SetTic(int(round(i * f))) + else: + win.ModifyStyle(0, win_no_ticks) + + def _win_wm_scroll(self, code): + #print "Slider._win_wm_scroll:", code ### + if self._live: + report = code != 8 + else: + report = code == 8 + if report: + self.do_action() + + def _win_on_ctlcolor(self, dc, typ): + #print "Slider._win_on_ctlcolor:", self ### + return self._win.OnCtlColor(dc, self._win, typ) + + def key_down(self, event): + k = event.key + if k == 'left_arrow' or k == 'up_arrow': + self._nudge_value(-1) + elif k == 'right_arrow' or k == 'down_arrow': + self._nudge_value(1) + else: + GSlider.key_down(self, event) + + def _nudge_value(self, d): + if not self.discrete: + d *= (win_continuous_range // 100) + win = self._win + p = win.GetPos() + win.SetPos(p + d) + self.do_action() + +export(Slider) diff --git a/GUI/Win32/StdCursors.py b/GUI/Win32/StdCursors.py new file mode 100644 index 0000000..7031023 --- /dev/null +++ b/GUI/Win32/StdCursors.py @@ -0,0 +1,45 @@ +#-------------------------------------------------------------------------- +# +# Python GUI - Standard Cursors - Win32 +# +#-------------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import Cursor + +__all__ = [ + 'arrow', + 'ibeam', + 'crosshair', + 'fist', + 'hand', + 'finger', + 'invisible', +] + +def win_get_std_cursor(id): + app = ui.GetApp() + win_app = getattr(app, '_win_app', app) + hcursor = win_app.LoadStandardCursor(id) + return Cursor._from_win_cursor(hcursor) + +arrow = win_get_std_cursor(wc.IDC_ARROW) +ibeam = win_get_std_cursor(wc.IDC_IBEAM) +crosshair = win_get_std_cursor(wc.IDC_CROSS) +fist = Cursor("cursors/fist.tiff") +hand = Cursor("cursors/hand.tiff") +finger = win_get_std_cursor(wc.IDC_HAND) +invisible = Cursor._from_win_cursor(0) + +def empty_cursor(): + return invisible + +# Win32 only +wait = win_get_std_cursor(wc.IDC_WAIT) +up_arrow = win_get_std_cursor(wc.IDC_UPARROW) +size_all = win_get_std_cursor(wc.IDC_SIZEALL) +size_w_e = win_get_std_cursor(wc.IDC_SIZEWE) +size_n_s = win_get_std_cursor(wc.IDC_SIZENS) +size_nw_se = win_get_std_cursor(wc.IDC_SIZENWSE) +size_ne_sw = win_get_std_cursor(wc.IDC_SIZENESW) + diff --git a/GUI/Win32/StdFonts.py b/GUI/Win32/StdFonts.py new file mode 100644 index 0000000..fc2255e --- /dev/null +++ b/GUI/Win32/StdFonts.py @@ -0,0 +1,37 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Standard Fonts - Win32 +# +#-------------------------------------------------------------------- + +from __future__ import division +import win32con as wc, win32gui as gui, win32ui as ui +from GUI import Font +from GUI.WinUtils import win_none + +def _get_win_ppi(): + dc = win_none.GetDC() + ppi = dc.GetDeviceCaps(wc.LOGPIXELSY) + win_none.ReleaseDC(dc) + return ppi + +_win_ppi = _get_win_ppi() + +def _win_pts_to_pixels(x): + return int(round(x * _win_ppi / 72)) + +def _win_stock_font(id): + h = gui.GetStockObject(id) + lf = gui.GetObject(h) + return Font._from_win_logfont(lf) + +#system_font = _win_stock_font(wc.SYSTEM_FONT) +#system_font = _win_stock_font(17) # DEFAULT_GUI_FONT +#system_font = Font._from_win(win_none) +#system_font = Font("System", 13) +#system_font = Font("Tahoma", 10) +#system_font = Font("Tahoma", 11) +system_font = Font("Tahoma", _win_pts_to_pixels(8)) +#print "StdFonts: System font ascent =", system_font.ascent +#print "StdFonts: System font descent =", system_font.descent +application_font = system_font diff --git a/GUI/Win32/StdMenus.py b/GUI/Win32/StdMenus.py new file mode 100755 index 0000000..17497e5 --- /dev/null +++ b/GUI/Win32/StdMenus.py @@ -0,0 +1,53 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Menus - Win32 +# +#-------------------------------------------------------------------- + +from GUI.GStdMenus import build_menus, \ + fundamental_cmds, help_cmds, pref_cmds, file_cmds, print_cmds, edit_cmds + +_file_menu_items = [ + ("New/N", 'new_cmd'), + ("Open.../O", 'open_cmd'), + ("Close/W", 'close_cmd'), + "-", + ("Save/S", 'save_cmd'), + ("Save As...", 'save_as_cmd'), + ("Revert", 'revert_cmd'), + "-", + ("Page Setup...", 'page_setup_cmd'), + ("Print.../P", 'print_cmd'), + "-", + ("Exit/Q", 'quit_cmd'), +] + +_edit_menu_items = [ + ("Undo/Z", 'undo_cmd'), + ("Redo/^Z", 'redo_cmd'), + "-", + ("Cut/X", 'cut_cmd'), + ("Copy/C", 'copy_cmd'), + ("Paste/V", 'paste_cmd'), + ("Clear", 'clear_cmd'), + "-", + ("Select All/A", 'select_all_cmd'), + "-", + ("Preferences...", 'preferences_cmd'), +] + +_help_menu_items = [ + ("About ", 'about_cmd'), +] + +#------------------------------------------------------------------------------ + +def basic_menus(substitutions = {}, include = None, exclude = None): + return build_menus([ + ("File", _file_menu_items, False), + ("Edit", _edit_menu_items, False), + ("Help", _help_menu_items, True), + ], + substitutions = substitutions, + include = include, + exclude = exclude) diff --git a/GUI/Win32/Task.py b/GUI/Win32/Task.py new file mode 100644 index 0000000..d9f1a20 --- /dev/null +++ b/GUI/Win32/Task.py @@ -0,0 +1,80 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Task - Win32 +# +#-------------------------------------------------------------------- + +from weakref import WeakValueDictionary +import win32con as wc, win32ui as ui +from GUI import export, application +from GUI.WinUtils import win_none +from GUI.GTasks import Task as GTask + +#-------------------------------------------------------------------- + +class TimerWnd(object): + + def __init__(self): + win = ui.CreateFrame() + win.CreateWindow(None, "", 0, (0, 0, 0, 0)) + win.AttachObject(self) + self._win = win + self.tasks = WeakValueDictionary() + + def schedule(self, task): + self.cancel(task) + event_id = id(task) + timer_id = self._win.SetTimer(event_id, task._milliseconds) + if not timer_id: + raise ValueError("Out of timers") + task._win_timer_id = timer_id + self.tasks[event_id] = task + + def cancel(self, task): + timer_id = task._win_timer_id + if timer_id: + self._win.KillTimer(timer_id) + task._win_timer_id = None + + def OnTimer(self, event_id): + #print "TimerWnd.OnTimer:", event_id + task = self.tasks.get(event_id) + if task: + if not task._repeat: + self.cancel(task) + task._proc() + # We do this so that the application can't get starved of idle time + # by a repeatedly-firing Task: + application()._win_idle() + +timer_wnd = TimerWnd() + +#-------------------------------------------------------------------- + +class Task(GTask): + + _win_timer_id = 0 + + def __init__(self, proc, interval, repeat = False, start = True): + self._proc = proc + self._milliseconds = int(1000 * interval) + self._repeat = repeat + if start: + self.start() + + def __del__(self, timer_wnd = timer_wnd): + timer_wnd.cancel(self) + + def get_interval(self): + return self._milliseconds / 1000.0 + + def get_repeat(self): + return self._repeat + + def start(self): + timer_wnd.schedule(self) + + def stop(self): + timer_wnd.cancel(self) + +export(Task) diff --git a/GUI/Win32/TextEditor.py b/GUI/Win32/TextEditor.py new file mode 100644 index 0000000..0892a8b --- /dev/null +++ b/GUI/Win32/TextEditor.py @@ -0,0 +1,85 @@ +# +# Python GUI - Text Editor - Win32 +# + +from __future__ import division +import win32con as wc, win32ui as ui +from GUI import export +from GUI.GTextEditors import TextEditor as GTextEditor +from GUI.WinUtils import win_none +from GUI.StdFonts import application_font + +PFM_TABSTOPS = 0x10 +MAX_TAB_STOPS = 32 +LOGPIXELSX = 88 + +ui.InitRichEdit() + +class TextEditor(GTextEditor): + + _pass_key_events_to_platform = True + + def __init__(self, scrolling = 'hv', **kwds): + win_style = ui.AFX_WS_DEFAULT_VIEW #| wc.WS_HSCROLL | wc.WS_VSCROLL + win = ui.CreateRichEditView() + ctl = win.GetRichEditCtrl() + self._win_ctl = ctl + if 'h' in scrolling: + win.SetWordWrap(0) # Must do before CreateWindow + win.CreateWindow(win_none, 1, win_style, (0, 0, 100, 100)) + #if 'v' not in scrolling: + # Disabled because it doesn't work properly -- auto-scrolling is prevented + # but a vertical scroll bar still appears when text goes past bottom of window. + #ctl.SetOptions(wc.ECOOP_XOR, wc.ECO_AUTOVSCROLL) # Must do after CreateWindow + ctl.SetOptions(wc.ECOOP_XOR, wc.ECO_NOHIDESEL) + win.ShowScrollBar(wc.SB_BOTH, False) + # We allow automatic scroll bar show/hide -- resistance is futile + win.ShowWindow() + kwds.setdefault('font', application_font) + GTextEditor.__init__(self, _win = win, **kwds) +# self.tab_spacing = self.font.width("X") * 4 ### + + def get_selection(self): + return self._win.GetSel() + + def set_selection(self, value): + self._win.SetSel(*value) + + def get_text(self): + return self._win.GetWindowText() + + def set_text(self, text): + self._win.SetWindowText(text) + + def get_text_length(self): + return self._win.GetTextLength() + + def get_font(self): + return self._font + + def set_font(self, x): + self._font = x + self._win.SetFont(x._win_font) + self.invalidate() + + def get_tab_spacing(self): + pf = self._win_ctl.GetParaFormat() + tabs = pf[8] + if tabs: + return tabs[0] // 20 + else: + return 36 + + def set_tab_spacing(self, x): + dc = self._win.GetDC() + dpi = dc.GetDeviceCaps(LOGPIXELSX) + mask = PFM_TABSTOPS + twips = 1440 * x / dpi + tabs = [int(round((i + 1) * twips)) for i in xrange(MAX_TAB_STOPS)] + pf = (mask, 0, 0, 0, 0, 0, 0, tabs) + old_sel = self.selection + self.select_all() + self._win_ctl.SetParaFormat(pf) + self.selection = old_sel + +export(TextEditor) diff --git a/GUI/Win32/TextField.py b/GUI/Win32/TextField.py new file mode 100644 index 0000000..e8d0406 --- /dev/null +++ b/GUI/Win32/TextField.py @@ -0,0 +1,104 @@ +#-------------------------------------------------------------------- +# +# PyGUI - TextField - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import export +from GUI.StdFonts import application_font +from GUI.WinUtils import win_none +from GUI.GTextFields import TextField as GTextField + +win_vpad = 5 +win_style = wc.WS_VISIBLE | wc.WS_CLIPSIBLINGS | wc.ES_AUTOHSCROLL # | wc.WS_TABSTOP +win_ex_style = wc.WS_EX_CLIENTEDGE +win_multiline_style = wc.ES_MULTILINE | wc.ES_AUTOVSCROLL +win_password_style = wc.ES_PASSWORD + +win_just_mask = wc.ES_LEFT | wc.ES_CENTER | wc.ES_RIGHT + +win_just_flags = { + 'l': wc.ES_LEFT, + 'c': wc.ES_CENTER, + 'r': wc.ES_RIGHT, +} + +class TextField(GTextField): + + _pass_key_events_to_platform = True + + def __init__(self, **kwds): + font = kwds.setdefault('font', application_font) + multiline = kwds.setdefault('multiline', False) + password = kwds.pop('password', False) + self._multiline = multiline + self._password = password + h = self._calc_height(font) + flags = win_style + if multiline: + flags |= win_multiline_style + if password: + flags |= win_password_style + win = ui.CreateEdit() + # Border can get lost if we construct it with too big a rect, so + # we set the initial size after creation. + win.CreateWindow(flags, (0, 0, 0, 0), win_none, 0) + win.ModifyStyleEx(0, win_ex_style) + win.MoveWindow((0, 0, 100, h)) + GTextField.__init__(self, _win = win, **kwds) + + def get_text(self): + return self._win.GetWindowText().replace("\r\n", "\n") + + def set_text(self, x): + self._win.SetWindowText(x.replace("\n", "\r\n")) + + def set_just(self, x): + self._just = x + try: + flags = win_just_flags[x[:1]] + except KeyError: + raise ValueError("Invalid TextField justification %r" % x) + self._win.ModifyFlags(win_just_mask, flags) + + def get_selection(self): + sel = self._win.GetSel() + if self._multiline: + sel = self._win_adjust_sel(sel, -1) + return sel + + def set_selection(self, sel): + if self._multiline: + sel = self._win_adjust_sel(sel, 1) + self._win.SetSel(*sel) + self.become_target() + + def _win_adjust_sel(self, sel, d): + text = self._win.GetWindowText() + if d > 0: + text = text.replace("\r\n", "\n") + nl = "\n" + else: + nl = "\r\n" + def adj(x): + return x + d * text.count(nl, 0, x) + return map(adj, sel) + + def get_multiline(self): + return self._multiline + + def get_password(self): + return self._password + + def _tab_in(self): + self.select_all() + + def key_down(self, event): + #print "TextField.key_down" ### + if event.char == "\t": + self.pass_event_to_next_handler(event) + else: + GTextField.key_down(self, event) + +export(TextField) diff --git a/GUI/Win32/View.py b/GUI/Win32/View.py new file mode 100644 index 0000000..6ffad68 --- /dev/null +++ b/GUI/Win32/View.py @@ -0,0 +1,24 @@ +#-------------------------------------------------------------------- +# +# PyGUI - View - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui +from GUI import export +from GUI.WinUtils import win_none +from GUI.GViews import View as GView + +win_style = wc.WS_VISIBLE +win_default_rect = (0, 0, GView._default_size[0], GView._default_size[1]) + +class View(GView): + + def __init__(self, **kwds): + win = ui.CreateWnd() + win.CreateWindow(None, None, win_style, win_default_rect, + win_none, 0) + GView.__init__(self, _win = win) + self.set(**kwds) + +export(View) diff --git a/GUI/Win32/ViewBase.py b/GUI/Win32/ViewBase.py new file mode 100644 index 0000000..4724a39 --- /dev/null +++ b/GUI/Win32/ViewBase.py @@ -0,0 +1,56 @@ +#-------------------------------------------------------------------- +# +# PyGUI - ViewBase - Win32 +# +#-------------------------------------------------------------------- + +import win32gui as gui +from GUI import export +from GUI import application +from GUI.GViewBases import ViewBase as GViewBase + +class ViewBase(GViewBase): + + _win_captures_mouse = True + + _cursor = None + +# def track_mouse(self): +# #print "ViewBase.track_mouse: enter" ### +# self._win_tracking_mouse = True +# try: +# while 1: +# application().event_loop() +# event = self._win_mouse_event +# yield event +# if event.kind == 'mouse_up': +# break +# finally: +# self._win_tracking_mouse = False +# #print "ViewBase.track_mouse: exit" ### + + def track_mouse(self): + self._win_tracking_mouse = True + while 1: + application().event_loop() + event = self._win_mouse_event + yield event + if event.kind == 'mouse_up': + break + self._win_tracking_mouse = False + + def get_cursor(self): + return self._cursor + + def set_cursor(self, c): + self._cursor = c + + def OnSetCursor(self, wnd, hit, message): + if hit == 1: # HTCLIENT + cursor = self._cursor + if cursor: + gui.SetCursor(cursor._win_cursor) + return + self._win.OnSetCursor(wnd._win, hit, message) + +export(ViewBase) diff --git a/GUI/Win32/WGL.py b/GUI/Win32/WGL.py new file mode 100644 index 0000000..de26d95 --- /dev/null +++ b/GUI/Win32/WGL.py @@ -0,0 +1,22 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - WGL - Win32 +# +#------------------------------------------------------------------------------ + +from ctypes import * +gdi32 = windll.gdi32 + +def SetPixelFormat(hdc, ipf): + gdi32.SetPixelFormat(hdc, ipf, None) + +def attr_array(d): + a = [] + for k, v in d.iteritems(): + a.append(k) + a.append(v) + a.append(0) + return a + +def attr_dict(keys, values): + return dict(zip(keys, values)) diff --git a/GUI/Win32/WinComboBox.py b/GUI/Win32/WinComboBox.py new file mode 100644 index 0000000..2cd9941 --- /dev/null +++ b/GUI/Win32/WinComboBox.py @@ -0,0 +1,37 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Win32 - ComboBox +# +#-------------------------------------------------------------------- + +import win32ui as ui, win32con as wc, win32gui as gui + +class ComboBox(object): + + def __init__(self, parent, pos, size, style = wc.CBS_DROPDOWNLIST): + parent_hwnd = parent.GetSafeHwnd() + self.hwnd = gui.CreateWindow("COMBOBOX", "Blarg", + wc.WS_CHILD | wc.CBS_DROPDOWNLIST, + pos[0], pos[1], size[0], size[1], + parent_hwnd, 0, 0, None) + self.pycwnd = ui.CreateWindowFromHandle(self.hwnd) + print "ComboBox: pycwnd =", self.pycwnd ### + + def __del__(self): + gui.DestroyWindow(self.hwnd) + + def ShowWindow(self): + gui.ShowWindow(self.hwnd, wc.SW_SHOW) + + def AddString(self, text): + print "ComboBox: Adding string %r" % text ### + gui.SendMessage(self.hwnd, wc.CB_ADDSTRING, 0, text) + + +def CreateComboBox(parent, pos, size, style = wc.CBS_DROPDOWNLIST): + parent_hwnd = parent.GetSafeHwnd() + hwnd = gui.CreateWindow("COMBOBOX", "Blarg", + wc.WS_CHILD | wc.CBS_DROPDOWNLIST, + pos[0], pos[1], size[0], size[1], + parent_hwnd, 0, 0, None) + return ui.CreateWindowFromHandle(hwnd) diff --git a/GUI/Win32/WinEvents.py b/GUI/Win32/WinEvents.py new file mode 100644 index 0000000..70ab715 --- /dev/null +++ b/GUI/Win32/WinEvents.py @@ -0,0 +1,109 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Event utilities - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32api as api, win32gui as gui, win32ui as ui +from GUI import Event + +win_message_map = { + wc.WM_KEYDOWN: ('key_down', None), + wc.WM_KEYUP: ('key_up', None), + wc.WM_SYSKEYDOWN: ('key_down', None), + wc.WM_SYSKEYUP: ('key_up', None), + wc.WM_MOUSEMOVE: ('mouse_move', None), + wc.WM_LBUTTONDOWN: ('mouse_down', 'left'), + wc.WM_LBUTTONDBLCLK: ('mouse_down', 'left'), + wc.WM_LBUTTONUP: ('mouse_up', 'left'), + wc.WM_MBUTTONDOWN: ('mouse_down', 'middle'), + wc.WM_MBUTTONDBLCLK: ('mouse_down', 'middle'), + wc.WM_MBUTTONUP: ('mouse_up', 'middle'), + wc.WM_RBUTTONDOWN: ('mouse_down', 'right'), + wc.WM_RBUTTONDBLCLK: ('mouse_down', 'right'), + wc.WM_RBUTTONUP: ('mouse_up', 'right'), +} + +win_special_keys = { + 0x70: 'f1', + 0x71: 'f2', + 0x72: 'f3', + 0x73: 'f4', + 0x74: 'f5', + 0x75: 'f6', + 0x76: 'f7', + 0x77: 'f8', + 0x78: 'f9', + 0x79: 'f10', + 0x7a: 'f11', + 0x7b: 'f12', + 0x91: 'f14', + 0x13: 'f15', + #0x2d: 'help', + 0x2d: 'insert', + 0x2e: 'delete', + 0x24: 'home', + 0x23: 'end', + 0x21: 'page_up', + 0x22: 'page_down', + 0x25: 'left_arrow', + 0x27: 'right_arrow', + 0x26: 'up_arrow', + 0x28: 'down_arrow', +} + +win_button_flags = wc.MK_LBUTTON | wc.MK_MBUTTON | wc.MK_RBUTTON +win_prev_key_state = 1 << 30 + +win_last_mouse_down = None +win_dbl_time = gui.GetDoubleClickTime() / 1000.0 # 0.25 +win_dbl_xdist = api.GetSystemMetrics(wc.SM_CXDOUBLECLK) +win_dbl_ydist = api.GetSystemMetrics(wc.SM_CYDOUBLECLK) + +def win_message_to_event(message, target = None): + hwnd, msg, wParam, lParam, milliseconds, gpos = message + kind, button = win_message_map[msg] + time = milliseconds / 1000.0 + if kind == 'mouse_move' and wParam & win_button_flags <> 0: + kind = 'mouse_drag' + if target: + lpos = target.global_to_local(gpos) + else: + lpos = gpos + event = Event() + event.kind = kind + event.global_position = gpos + event.position = lpos + event.time = time + event.button = button + shift = api.GetKeyState(wc.VK_SHIFT) & 0x80 <> 0 + control = api.GetKeyState(wc.VK_CONTROL) & 0x80 <> 0 + option = api.GetKeyState(wc.VK_MENU) & 0x80 <> 0 + event.shift = event.extend_contig = shift + event.control = event.extend_noncontig = control + event.option = option + vkey = None + if kind == 'mouse_down': + global win_last_mouse_down + last = win_last_mouse_down + if last and last.button == button and time - last.time <= win_dbl_time: + x0, y0 = last.global_position + x1, y1 = gpos + if abs(x1 - x0) <= win_dbl_xdist and abs(y1 - y0) <= win_dbl_ydist: + event.num_clicks = last.num_clicks + 1 + win_last_mouse_down = event + elif kind == 'key_down' or kind == 'key_up': + event.unichars = ui.TranslateVirtualKey(wParam) + event.char = event.unichars.decode('ascii') + event._keycode = wParam + if wParam == 0x0d: + if (lParam & 0x1000000): + event.key = 'enter' + else: + event.key = 'return' + else: + event.key = win_special_keys.get(wParam) or event.char + if kind == 'key_down': + event.auto = lParam & win_prev_key_state <> 0 + return event + diff --git a/GUI/Win32/WinMenus.py b/GUI/Win32/WinMenus.py new file mode 100644 index 0000000..c08c29d --- /dev/null +++ b/GUI/Win32/WinMenus.py @@ -0,0 +1,62 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Menu utilities - Win32 +# +#-------------------------------------------------------------------- + +from weakref import WeakKeyDictionary, WeakValueDictionary +import win32con as wc, win32ui as ui +from GUI import application + +win_command_map = {} +win_command_list = [] + +def win_command_to_id(name, index = None): + if index is not None: + key = (name, index) + else: + key = name + id = win_command_map.get(key) + if not id: + id = len(win_command_list) + 1 + win_command_map[key] = id + win_command_list.append(key) + application()._win_app.HookCommandUpdate(win_command_update, id) + return id + +def win_command_update(cmd): + win_menu = cmd.m_pMenu + if win_menu: + menu = win_get_menu_for_hmenu(win_menu.GetHandle()) + if menu: + item = menu._get_flat_item(cmd.m_nIndex) + cmd.Enable(item.enabled) + cmd.SetCheck(bool(item.checked)) + +def win_id_to_command(id): + if 1 <= id <= len(win_command_list): + return win_command_list[id - 1] + +win_hmenu_to_menubar = WeakValueDictionary() + +def win_get_menu_for_hmenu(hmenu): + menubar = win_hmenu_to_menubar.get(hmenu) + if menubar: + return menubar.hmenu_to_menu.get(hmenu) + +#-------------------------------------------------------------------- + +class MenuBar(object): + # Wrapper around a PyCMenu + + def __init__(self): + self.win_menu = ui.CreateMenu() + self.hmenu_to_menu = {} + + def append_menu(self, menu): + win_menu = menu._win_create_menu() + hmenu = win_menu.Detach() + self.win_menu.AppendMenu(wc.MF_POPUP | wc.MF_STRING, hmenu, menu.title) + win_hmenu_to_menubar[hmenu] = self + self.hmenu_to_menu[hmenu] = menu + diff --git a/GUI/Win32/WinPageSetup.py b/GUI/Win32/WinPageSetup.py new file mode 100644 index 0000000..0c026ad --- /dev/null +++ b/GUI/Win32/WinPageSetup.py @@ -0,0 +1,168 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Win32 API - Page setup dialog +# +#------------------------------------------------------------------------------ + +from ctypes import * +from ctypes.wintypes import * + +comdlg32 = windll.comdlg32 +kernel32 = windll.kernel32 + +CCHDEVICENAME = 32 +CCHFORMNAME = 32 + +PSD_MARGINS = 0x2 +PSD_INTHOUSANDTHSOFINCHES = 0x4 +PSD_RETURNDEFAULT = 0x400 + +LPPAGESETUPHOOK = c_void_p +LPPAGEPAINTHOOK = c_void_p + +class PAGESETUPDLG(Structure): + _fields_ = [ + ('lStructSize', DWORD), + ('hwndOwner', HWND), + ('hDevMode', HGLOBAL), + ('hDevNames', HGLOBAL), + ('Flags', DWORD), + ('ptPaperSize', POINT), + ('rtMinMargin', RECT), + ('rtMargin', RECT), + ('hInstance', HINSTANCE), + ('lCustData', LPARAM), + ('lpfnPageSetupHook', LPPAGESETUPHOOK), + ('lpfnPagePaintHook', LPPAGEPAINTHOOK), + ('lpPageSetupTemplateName', LPCSTR), + ('hPageSetupTemplate', HGLOBAL), + ] + + def __del__(self): + print "PAGESETUPDLG.__del__" ### + GlobalFree(self.hDevMode) + GlobalFree(self.hDevNames) + +class DEVMODE(Structure): + _fields_ = [ + ('dmDeviceName', c_char * CCHDEVICENAME), + ('dmSpecVersion', WORD), + ('dmDriverVersion', WORD), + ('dmSize', WORD), + ('dmDriverExtra', WORD), + ('dmFields', DWORD), + ('dmOrientation', c_short), + ('dmPaperSize', c_short), + ('dmPaperLength', c_short), + ('dmPaperWidth', c_short), + ('dmScale', c_short), + ('dmCopies', c_short), + ('dmDefaultSource', c_short), + ('dmPrintQuality', c_short), + ('dmColor', c_short), + ('dmDuplex', c_short), + ('dmYResolution', c_short), + ('dmTTOption', c_short), + ('dmCollate', c_short), + ('dmFormName', c_char * CCHFORMNAME), + ('dmLogPixels', WORD), + ('dmBitsPerPel', DWORD), + ('dmPelsWidth', DWORD), + ('dmPelsHeight', DWORD), + ('dmDisplayFlags', DWORD), + ('dmDisplayFrequency', DWORD), + ('dmICMMethod', DWORD), + ('dmICMIntent', DWORD), + ('dmMediaType', DWORD), + ('dmDitherType', DWORD), + ('dmReserved1', DWORD), + ('dmReserved2', DWORD), + ('dmPanningWidth', DWORD), + ('dmPanningHeight', DWORD), + ] + +class DEVNAMES(Structure): + _fields_ = [ + ('wDriverOffset', WORD), + ('wDeviceOffset', WORD), + ('wOutputOffset', WORD), + ('wDefault', WORD), + ] + +_PageSetupDlg = comdlg32.PageSetupDlgA +_PageSetupDlg.argtypes = [POINTER(PAGESETUPDLG)] + +GlobalAlloc = kernel32.GlobalAlloc +GlobalAlloc.argtypes = [UINT, DWORD] + +GlobalSize = kernel32.GlobalSize +GlobalSize.argtypes = [HGLOBAL] + +GlobalLock = kernel32.GlobalLock +GlobalLock.argtypes = [HGLOBAL] + +GlobalUnlock = kernel32.GlobalUnlock +GlobalUnlock.argtypes = [HGLOBAL] + +GlobalFree = kernel32.GlobalFree +GlobalFree.argtypes = [HGLOBAL] + +def PageSetupDlg(psd): + psd.Flags = PSD_INTHOUSANDTHSOFINCHES | PSD_MARGINS + return bool(_PageSetupDlg(psd)) + +def get_handle_contents(h): + n = GlobalSize(h) + p = GlobalLock(h) + data = string_at(p, n) + GlobalUnlock(h) + return data + +def handle_with_contents(data): + n = len(data) + h = GlobalAlloc(n) + p = GlobalLock(h) + memmove(p, data, n) + GlobalUnlock(h) + return h + +def lock_devmode_handle(h): + p = c_void_p(GlobalLock(h)) + dmp = cast(p, POINTER(DEVMODE)) + return dmp[0] + +class DevNames(object): + + def __init__(self, hdevnames): + a = GlobalLock(hdevnames) + p = c_void_p(a) + dnp = cast(p, POINTER(DEVNAMES)) + dn = dnp[0] + self.driver = c_char_p(a + dn.wDriverOffset).value + self.device = c_char_p(a + dn.wDeviceOffset).value + self.output = c_char_p(a + dn.wOutputOffset).value + self.default = dn.wDefault + GlobalUnlock(hdevnames) + +def get_defaults(): + psd = PAGESETUPDLG() + psd.lStructSize = sizeof(PAGESETUPDLG) + psd.Flags = PSD_INTHOUSANDTHSOFINCHES | PSD_RETURNDEFAULT + _PageSetupDlg(psd) + return psd + +if __name__ == "__main__": + def dump_psd(header, psd): + print "%s:" % header, psd + for name, _ in PAGESETUPDLG._fields_: + print " %s = %r" % (name, getattr(psd, name)) + psd = PAGESETUPDLG() + psd.lStructSize = sizeof(PAGESETUPDLG) + dump_psd("Initial psd", psd) + result = _PageSetupDlg(psd) + print "Result:", result + dump_psd("Final psd", psd) + #print "DevMode:", repr(get_handle_contents(psd.hDevMode)[:sizeof(DEVMODE)]) + dm = lock_devmode_handle(psd.hDevMode) + print "dmDeviceName:", dm.dmDeviceName + #print "DevNames:", repr(get_handle_contents(psd.hDevNames)) diff --git a/GUI/Win32/WinPrint.py b/GUI/Win32/WinPrint.py new file mode 100644 index 0000000..054bebe --- /dev/null +++ b/GUI/Win32/WinPrint.py @@ -0,0 +1,129 @@ +#------------------------------------------------------------------------------ +# +# PyGUI - Win32 API - Printing +# +#------------------------------------------------------------------------------ + +from ctypes import * +from ctypes.wintypes import * + +comdlg32 = windll.comdlg32 +gdi32 = windll.gdi32 + +PD_ALLPAGES = 0x0 +PD_RETURNDC = 0x100 + +LOGPIXELSX = 88 +LOGPIXELSY = 90 +PHYSICALOFFSETX = 112 +PHYSICALOFFSETY = 113 + +LPDWORD = POINTER(DWORD) +LPHANDLE = POINTER(HANDLE) +LPPOINT = POINTER(POINT) +LPPRINTHOOKPROC = c_void_p +LPSETUPHOOKPROC = c_void_p +ABORTPROC = CFUNCTYPE(c_int, HDC, c_int) +LPPRINTER_DEFAULTS = c_void_p + +class PRINTDLG(Structure): + + _pack_ = 2 + _fields_ = [ + ('lStructSize', DWORD), + ('hwndOwner', HWND), + ('hDevMode', HGLOBAL), + ('hDevNames', HGLOBAL), + ('hDC', HDC), + ('Flags', DWORD), + ('nFromPage', WORD), + ('nToPage', WORD), + ('nMinPage', WORD), + ('nMaxPage', WORD), + ('nCopies', WORD), + ('hInstance', HINSTANCE), + ('lCustData', LPARAM), + ('lpfnPrintHook', LPPRINTHOOKPROC), + ('lpfnSetupHook', LPSETUPHOOKPROC), + ('lpPrintTemplateName', LPCSTR), + ('lpSetupTemplateName', LPCSTR), + ('hPrintTemplate', HGLOBAL), + ('hSetupTemplate', HGLOBAL), + ] + + def __init__(self): + self.lStructSize = sizeof(PRINTDLG) + +class DOCINFO(Structure): + + _fields_ = [ + ('cbSize', c_int), + ('lpszDocName', LPCSTR), + ('lpszOutput', LPCSTR), + ('lpszDatatype', LPCSTR), + ('fwType', DWORD), + ] + + def __init__(self): + self.cbSize = sizeof(DOCINFO) + +class FORM_INFO_1_W(Structure): + + _fields_ = [ + ('Flags', DWORD), + ('pName', LPWSTR), + ('Size', SIZEL), + ('ImageableArea', RECTL), + ] + +_PrintDlg = comdlg32.PrintDlgA +_PrintDlg.argtypes = [POINTER(PRINTDLG)] + +SetAbortProc = gdi32.SetAbortProc +SetAbortProc.argtypes = [HDC, ABORTPROC] + +StartDoc = gdi32.StartDocA +StartDoc.argtypes = [HDC, POINTER(DOCINFO)] + +StartPage = gdi32.StartPage +StartPage.argtypes = [HDC] + +EndPage = gdi32.EndPage +EndPage.argtypes = [HDC] + +EndDoc = gdi32.EndDoc +EndDoc.argtypes = [HDC] + +DeleteDC = gdi32.DeleteDC +DeleteDC.argtypes = [HDC] + +CommDlgExtendedError = comdlg32.CommDlgExtendedError +CommDlgExtendedError.argtypes = [] + +def PrintDlg(pd): + pd.nFromPage = pd.nMinPage + pd.nToPage = pd.nMaxPage + pd.Flags = PD_RETURNDC + #if pd.nMaxPage > pd.nMinPage: + # pd.Flags |= PD_PAGENUMS + result = _PrintDlg(pd) + if result == 0: + err = CommDlgExtendedError() + if err <> 0: + raise EnvironmentError("Common Dialog error %s" % err) + return bool(result) + +def GetPrintingOffset(hdc): + dpix = gdi32.GetDeviceCaps(hdc, LOGPIXELSX) + dpiy = gdi32.GetDeviceCaps(hdc, LOGPIXELSY) + offx = gdi32.GetDeviceCaps(hdc, PHYSICALOFFSETX) + offy = gdi32.GetDeviceCaps(hdc, PHYSICALOFFSETY) + return 72.0 * offx / dpix, 72.0 * offy / dpiy + +def abort_proc(hdc, err): + return printing_aborted + +def install_abort_proc(hdc): + global printing_aborted + printing_aborted = False + SetAbortProc(hdc, ABORTPROC(abort_proc)) diff --git a/GUI/Win32/WinUtils.py b/GUI/Win32/WinUtils.py new file mode 100644 index 0000000..eb28d2f --- /dev/null +++ b/GUI/Win32/WinUtils.py @@ -0,0 +1,149 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Win32 - Utilities +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32gui as gui, win32api as api +from win32api import HIWORD, LOWORD +from GUI import application +from GUI.Exceptions import Cancel, InternalError + +win_dlog_class = "#32770" +win_color3dhilight = api.GetSysColor(wc.COLOR_3DHILIGHT) +win_color3dlight = api.GetSysColor(wc.COLOR_3DLIGHT) +win_color3dface = api.GetSysColor(wc.COLOR_3DFACE) +win_color3dshadow = api.GetSysColor(wc.COLOR_3DSHADOW) +win_menubar_height = api.GetSystemMetrics(wc.SM_CYMENU) +win_bg_color = api.GetSysColor(wc.COLOR_3DFACE) +win_screen_width = api.GetSystemMetrics(wc.SM_CXFULLSCREEN) +win_screen_height = api.GetSystemMetrics(wc.SM_CYFULLSCREEN) +win_screen_rect = (0, 0, win_screen_width, win_screen_height) + +#win_bg_brush = ui.CreateBrush(wc.BS_SOLID, win_color3dface, 0) +#win_bg_hbrush = win_bg_brush.GetSafeHandle() + +# An empty brush for not painting anything with +win_null_brush = ui.CreateBrush(wc.BS_NULL, 0, 0) +win_null_hbrush = win_null_brush.GetSafeHandle() + +# All components hook the following messages + +win_event_messages = ( + wc.WM_KEYDOWN, wc.WM_KEYUP, + wc.WM_SYSKEYDOWN, wc.WM_SYSKEYUP, + wc.WM_MOUSEMOVE, + wc.WM_LBUTTONDOWN, wc.WM_LBUTTONDBLCLK, wc.WM_LBUTTONUP, + wc.WM_MBUTTONDOWN, wc.WM_MBUTTONDBLCLK, wc.WM_MBUTTONUP, + wc.WM_RBUTTONDOWN, wc.WM_RBUTTONDBLCLK, wc.WM_RBUTTONUP, + #wc.WM_MOUSELEAVE, +) + +# Dummy CWnd for use as parent of containerless components. +# Also used as the main frame of the CWinApp. + +win_none = ui.CreateFrame() +win_none.CreateWindow(None, "", 0, (0, 0, 10, 10)) + +win_plain = ui.CreateWnd() +win_plain.CreateWindow(None, None, 0, (0, 0, 10, 10), win_none, 0) +win_plain_class = gui.GetClassName(win_plain.GetSafeHwnd()) + +#-------------------------------------------------------------------- + +win_command_map = { + 0: '_win_bn_clicked', # BN_CLICKED + wc.CBN_SELCHANGE: '_cbn_sel_change', +} + +class WinMessageReflector(object): + + def _win_install_event_hooks(self, win): + win.HookMessage(self._win_wm_scroll, wc.WM_HSCROLL) + win.HookMessage(self._win_wm_scroll, wc.WM_VSCROLL) + +# +# Disabled for now because overriding control colours +# doesn't seem to work properly on XP. +# +# def OnCtlColor(self, dc, comp, typ): +# #print "WinMessageReflector.OnCtlColor" ### +# meth = getattr(comp, '_win_on_ctlcolor', None) +# if meth: +# return meth(dc, typ) + + def _win_wm_scroll(self, message): + #print "WinMessageReflector._win_wm_scroll:", self, message ### + wParam = message[2] + code = wParam & 0xffff + lParam = message[3] + self._forward_reflected_message(lParam, '_win_wm_scroll', code) + + def OnCommand(self, wParam, lParam): + #print "WinMessageReflector.OnCommand: code = 0x%04x 0x%04x lParam = 0x%08x" % ( + # HIWORD(wParam), LOWORD(wParam), lParam) + try: + code = HIWORD(wParam) + id = LOWORD(wParam) + if id: + self._win_menu_command(id) + else: + name = win_command_map.get(code) + if name: + self._forward_reflected_message(lParam, name) + except Cancel: + pass + except: + application().report_error() + + def _forward_reflected_message(self, lParam, method_name, *args): + wnd = ui.CreateWindowFromHandle(lParam) + meth = getattr(wnd, method_name, None) + if meth: + meth(*args) + + def _win_menu_command(self, id): + raise InternalError("_win_menu_command called on non-window: %r" % self) + +win_none_wrapper = WinMessageReflector() +win_none_wrapper._win = win_none +win_none_wrapper._win_install_event_hooks(win_none) +win_none.AttachObject(win_none_wrapper) + +#-------------------------------------------------------------------- +# +# Debugging routines +# + +win_message_names = {} + +win_exclude_names = ["WM_MOUSEFIRST"] + +for name, value in wc.__dict__.iteritems(): + if name.startswith("WM_") and name not in win_exclude_names: + win_message_names[value] = name + +def win_message_name(num): + return win_message_names.get(num) or num + +def dump_flags(flags): + for name in wc.__dict__.iterkeys(): + if name.startswith("WS_") and not name.startswith("WS_EX"): + value = wc.__dict__[name] + if flags & value: + print "%20s = 0x%08x" % (name, value & 0xffffffffL) + +def win_deconstruct_style(flags): + win_do_deconstruct_style(flags, "WS_", "WS_EX_") + +def win_deconstruct_style_ex(flags): + win_do_deconstruct_style(flags, "WS_EX_") + +def win_do_deconstruct_style(flags, prefix, not_prefix = None): + d = wc.__dict__ + for name in d.iterkeys(): + if name.startswith(prefix): + if not not_prefix or not name.startswith(not_prefix): + value = d[name] + if value and flags & value == value: + print "%25s 0x%08x" % (name, value) diff --git a/GUI/Win32/Window.py b/GUI/Win32/Window.py new file mode 100755 index 0000000..d30f1c6 --- /dev/null +++ b/GUI/Win32/Window.py @@ -0,0 +1,343 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Window - Win32 +# +#-------------------------------------------------------------------- + +import win32con as wc, win32ui as ui, win32gui as gui, win32api as api +from GUI import export +from GUI import WinUtils +from GUI.Geometry import rect_size, sub_pt +from GUI import application +from GUI.Exceptions import Cancel +from GUI.WinEvents import win_message_to_event +from GUI.WinMenus import MenuBar, win_id_to_command +from GUI.GMenus import search_list_for_key +from GUI import Component +from GUI.GWindows import Window as GWindow + +#-------------------------------------------------------------------- + +capabilities = ('hidable', 'zoomable', 'resizable', 'movable', 'closable') + +win_defaults = { + 'standard': (1, 1, 1, 1, 1), + 'nonmodal_dialog': (1, 0, 0, 1, 1), + 'modal_dialog': (0, 0, 0, 1, 1), + 'alert': (0, 0, 0, 1, 1), + 'fullscreen': (0, 0, 0, 0, 0), +} + +win_base_flags = wc.WS_CLIPCHILDREN +win_border_flags = wc.WS_DLGFRAME + +win_capability_flags = { + 'hidable': wc.WS_MINIMIZEBOX | wc.WS_SYSMENU, + 'zoomable': wc.WS_MAXIMIZEBOX | wc.WS_SYSMENU, + 'resizable': wc.WS_THICKFRAME, + 'movable': wc.WS_BORDER, + 'closable': wc.WS_SYSMENU, +} + +win_no_menu_styles = ('nonmodal_dialog', 'modal_dialog', + 'alert', 'fullscreen') + +win_ex_flags = 0 #wc.WS_EX_WINDOWEDGE +win_no_ex_flags = wc.WS_EX_CLIENTEDGE + +def win_calculate_flags(style, kwds): + # Calculate window flags from the options present in kwds, and + # fill in kwds with default values for missing options that need + # to be passed to the base class constructor. + flags = win_base_flags + if style != 'fullscreen': + flags |= win_border_flags + try: + defaults = win_defaults[style] + except KeyError: + raise ValueError("Invalid window style '%s'" % style) + for name, default in zip(capabilities, defaults): + value = kwds.pop(name, default) + if name == 'closable': + kwds[name] = value + if value: + flags |= win_capability_flags[name] + return flags + +#def win_adjust_flags(flags, kwds, option_name, opt_flags): +# option = kwds.pop(option_name, None) +# if option is not None: +# if option: +# flags |= opt_flags +# else: +# flags &= ~opt_flags +# return flags + +def win_next_wnd(wnd): + wnd = getattr(wnd, '_win', wnd) + #print "win_next_wnd:", wnd ### + return wnd.GetWindow(wc.GW_HWNDNEXT) + +#-------------------------------------------------------------------- + +class Window(GWindow): + + _win_hooks_events = True + _win_has_menubar = True + _win_captures_mouse = True + + _win_need_menubar_update = True + _win_saved_target = False + _win_fullscreen = False + + def __init__(self, **kwds): + style = kwds.get('style', 'standard') + flags = win_calculate_flags(style, kwds) + #if style == 'fullscreen': + # rect = WinUtils.win_screen_rect + #else: + rect = (0, 0, self._default_width, self._default_height) + frame = ui.CreateFrame() + frame.CreateWindow(None, "New Window", 0, rect) + hwnd = frame.GetSafeHwnd() + #api.SetClassLong(hwnd, wc.GCL_HBRBACKGROUND, win_bg_hbrush) + api.SetClassLong(hwnd, wc.GCL_HBRBACKGROUND, 0) +# print "Window: Setting style:" ### +# win_deconstruct_style(flags) ### + frame.ModifyStyle(-1, flags) +# print "Window: Style is now:" ### +# win_deconstruct_style(frame.GetStyle()) ### + frame.ModifyStyleEx(win_no_ex_flags, win_ex_flags) + if style == 'fullscreen': + self._win_fullscreen = True + frame.HookMessage(self._win_wm_initmenu, wc.WM_INITMENU) + self._win = frame + if style in win_no_menu_styles: + self._win_has_menubar = False + else: + self._win_set_empty_menubar() + kwds['closable'] = flags & wc.WS_CAPTION <> 0 + GWindow.__init__(self, _win = frame, **kwds) + + def OnPaint(self): + win = self._win + dc, ps = win.BeginPaint() + rect = win.GetClientRect() + dc.FillSolidRect(rect, WinUtils.win_bg_color) + if self._win_has_menubar: + l, t, r, b = rect + dc.Draw3dRect((l, t, r + 1, t + 2), + WinUtils.win_color3dshadow, WinUtils.win_color3dhilight) + win.EndPaint(ps) + + def _win_install_event_hooks(self): + self._win.HookMessage(self._wm_activate, wc.WM_ACTIVATE) + #self._win.HookMessage(self._wm_setfocus, wc.WM_SETFOCUS) + self._win.HookMessage(self._wm_windowposchanging, wc.WM_WINDOWPOSCHANGING) + self._win.HookMessage(self._wm_windowposchanged, wc.WM_WINDOWPOSCHANGED) + GWindow._win_install_event_hooks(self) + + def _wm_activate(self, msg): + wParam = msg[2] + #print "Window._wm_activate:", msg ### + #print "...wParam =", wParam ### + if wParam == wc.WA_INACTIVE: + #print "Window: Deactivating:", self ### + try: + target = ui.GetFocus() + #print "...target =", target ### + except ui.error, e: + #print "...no target", e ### + target = None + if isinstance(target, Component) and target is not self: + #print "...saving target", target ### + self._win_saved_target = target + + def _win_wm_setfocus(self, msg): + #print "Window._win_wm_setfocus:", self ### + target = self._win_saved_target + if target and target.window == self: + #print "...restoring target", target ### + target._win.SetFocus() + self._win_saved_target = None + else: + GWindow._win_wm_setfocus(self, msg) + + def get_target(self): + if self._win_is_active(): + try: + target = ui.GetFocus() + except ui.error: + target = None + if target and isinstance(target, Component): + return target + return self._saved_target or self + + def _win_is_active(self): + try: + active_win = ui.GetActiveWindow() + except ui.error: + active_win = None + return active_win is self + +# def _wm_setfocus(self, *args): +# print "Window._wm_setfocus:", args ### + + def _wm_windowposchanging(self, message): + #print "Window._wm_windowposchanging" + self._win_old_size = rect_size(self._bounds) + #print "...old size =", self._win_old_size + + def _wm_windowposchanged(self, message): + #print "Window._wm_windowposchanged" + old_size = self._win_old_size + new_bounds = self._win_get_actual_bounds() + self._bounds = new_bounds + new_size = rect_size(new_bounds) + #print "...new size =", new_size + if old_size != new_size: + self._resized(sub_pt(new_size, old_size)) + + def _win_set_empty_menubar(self): + # A completely empty menu bar collapses to zero height, and + # controlling the window bounds is too complicated if the + # menu bar comes and goes, so we add a dummy item to it. + menubar = MenuBar() + menubar.win_menu.AppendMenu(0, 0, "") + self._win.SetMenu(menubar.win_menu) + self._win_menubar = menubar + + def get_title(self): + return self._win.GetWindowText() + + def set_title(self, x): + self._win.SetWindowText(x) + + def get_visible(self): + return self._win.IsWindowVisible() + + def set_visible(self, x): + #print "Window.set_visible:", x, self ### + if x: + self._win_update_menubar() + self._win.ShowWindow() + else: + self._win.ShowWindow(wc.SW_HIDE) + + def _show(self): + self._win_update_menubar() + win = self._win + if self._win_fullscreen: + win.ShowWindow(wc.SW_SHOWMAXIMIZED) +# win.SetWindowPos(wc.HWND_TOP, (0, 0, 0, 0), +# wc.SWP_NOMOVE | wc.SWP_NOSIZE | wc.SWP_SHOWWINDOW) + win.ShowWindow(wc.SW_SHOWNORMAL) + win.SetActiveWindow() + +# def get_bounds(self): +# win = self._win +# r = win.ClientToScreen(win.GetClientRect()) +# return r + + def _win_get_actual_bounds(self): + win = self._win + return win.ClientToScreen(win.GetClientRect()) + + def _win_move_window(self, rect): + win = self._win + l, t, r, b = win.CalcWindowRect(rect) + if self._win_has_menubar: + t -= WinUtils.win_menubar_height + self._win.MoveWindow((l, t, r, b)) + + def set_menus(self, x): + GWindow.set_menus(self, x) + self._win_menus_changed() + + def _stagger(self): + #print "Window._stagger:", self ### + win = win_next_wnd(self._win) + while win and not (isinstance(win, Window) and win.visible): + #print "...win =", win ### + win = win_next_wnd(win) + if win: + l, t, r, b = win._win.GetWindowRect() + hwnd = self._win.GetSafeHwnd() + gui.SetWindowPos(hwnd, 0, l + 20, t + 20, 0, 0, + wc.SWP_NOSIZE | wc.SWP_NOZORDER) + + def OnClose(self): + #print "Window:", self, "OnClose" + try: + self.close_cmd() + except Cancel: + pass + except: + application().report_error() + + def _win_menus_changed(self): + self._win_need_menubar_update = True + if self.visible: + self._win_update_menubar() + + def _win_update_menubar(self): + #print "Window._win_update_menubar:", self ### + if self._win_need_menubar_update: + all_menus = application()._effective_menus_for_window(self) + self._all_menus = all_menus + if self._win_has_menubar: + if all_menus: + menubar = MenuBar() + for menu in all_menus: + menubar.append_menu(menu) + self._win.SetMenu(menubar.win_menu) + self._win_menubar = menubar + else: + self._win_set_empty_menubar() + self._win_need_menubar_update = False + + def _win_wm_initmenu(self, message): + #print "Window._win_wm_initmenu:", self ### + self._win_perform_menu_setup() + + def _win_perform_menu_setup(self): + #print "Window._win_perform_menu_setup:", self ### + application()._perform_menu_setup(self._all_menus) + + def _win_menu_command(self, id): + command = win_id_to_command(id) + if command: + application().dispatch_menu_command(command) + + def _win_possible_menu_key(self, key, shift, option): + self._win_perform_menu_setup() + command = search_list_for_key(self._all_menus, key, shift, option) + if command: + application().dispatch_menu_command(command) + return True + + def _screen_rect(self): + return WinUtils.win_screen_rect + + def modal_event_loop(self): + disabled = [] + for window in application().windows: + if window is not self: + if not window._win.EnableWindow(False): + #print "Window.modal_event_loop: disabled", window.title ### + disabled.append(window) + status = self._win.RunModalLoop(0) + if status: + print "Window._modal_event_loop:", self, "status =", status ### + #raise Cancel + for window in disabled: + #print "Window.modal_event_loop: enabling", window.title ### + window._win.EnableWindow(True) + if status <> 0: ### + from GUI.Exceptions import InternalError ### + raise InternalError("RunModalLoop returned %s" % status) ### + + def exit_modal_event_loop(self): + self._win.EndModalLoop(0) + +export(Window) diff --git a/GUI/__init__.py b/GUI/__init__.py new file mode 100755 index 0000000..5dc533b --- /dev/null +++ b/GUI/__init__.py @@ -0,0 +1,94 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Top level package initialisation +# +#-------------------------------------------------------------------- + +import sys, types + +# The first item of each of the following pairs is the name of a module +# to try to import. If the import is successful, the platform-dependent +# directory named by the second item is used. + +_versions = [ + ("objc", "Cocoa"), + ("nt", "Win32"), + ("gi.repository.Gtk", "GtkGI"), + ("gtk", "Gtk"), +] + +# +# The following function exports a class or function from a submodule in +# such a way that it appears to have been defined directly at the top +# level of the package. By giving the submodule that defines the class the +# same name as the class, this provides an autoloading facility that is +# friendly to application bundling tools. +# + +_preserve = [] # Keeps references to replaced modules so they aren't cleared + +def export(obj): + qname = "%s.%s" % (__name__, obj.__name__) + obj.__module__ = __name__ + mod = sys.modules[qname] + _preserve.append(mod) + sys.modules[qname] = obj + +# +# The environment variable PYGUI_IMPLEMENTATION may be set to the +# name of one of the platform-dependent directories to force that +# implementation to be used. This can be useful if more than one +# PyGUI implementation is usable on your setup. +# + +from os import environ as _env +_platdir = _env.get("PYGUI_IMPLEMENTATION") +if not _platdir: + for _testmod, _platdir in _versions: + try: + __import__(_testmod) + break + except ImportError: + continue + else: + raise ImportError("Unable to find an implementation of PyGUI for this installation") + +if _env.get("PYGUI_IMPLEMENTATION_DEBUG"): + sys.stderr.write("PyGUI: Using implementation: %s\n" % _platdir) + +# +# Append the chosen platform-dependent directory to the search +# path for submodules of this package. +# + +from os.path import join as _join +_here = __path__[0] +__path__.append(_join(_here, _platdir)) +__path__.append(_join(_here, "Generic")) + +# +# Import global functions +# + +from GUI.Globals import application, run +from GUI.Colors import rgb + +# +# Set up initial resource search path +# + +from GUI import Resources +Resources._add_file_path(__file__) +_main_dir = sys.path[0] +Resources._add_directory_path(_main_dir) +Resources._add_directory_path(_main_dir, 1) +#import __main__ +#Resources._add_module_path(__main__) +#Resources._add_module_path(__main__, 1) +#print "GUI: resource_path =", Resources.resource_path ### + +# +# Perform global initialisation +# + +import GUI.Application diff --git a/GUI/py2exe.py b/GUI/py2exe.py new file mode 100644 index 0000000..8c6541b --- /dev/null +++ b/GUI/py2exe.py @@ -0,0 +1,25 @@ +#-------------------------------------------------------------------- +# +# PyGUI - Fix py2exe to handle dynamic pywin32 module search paths +# +#-------------------------------------------------------------------- + +import sys + +# py2exe 0.6.4 introduced a replacement modulefinder. +# This means we have to add package paths there, not to the built-in +# one. If this new modulefinder gets integrated into Python, then +# we might be able to revert this some day. +# if this doesn't work, try import modulefinder +try: + import py2exe.mf as modulefinder +except ImportError: + import modulefinder +import win32com +for p in win32com.__path__[1:]: + modulefinder.AddPackagePath("win32com", p) +for extra in ["win32com.shell"]: #,"win32com.mapi" + __import__(extra) + m = sys.modules[extra] + for p in m.__path__[1:]: + modulefinder.AddPackagePath(extra, p)