Mercurial > fife-parpg
changeset 205:54bfd1015b35
* PyChan event handling rework (part I)
** Unified listeners
** ...hopefully more robust attach/detach code.
* Added compat, layout and also the new autopsition feature.
* Documentation
* Minor style fixes in core.
author | phoku@33b003aa-7bff-0310-803a-e67f0ece8222 |
---|---|
date | Sat, 14 Mar 2009 12:13:29 +0000 |
parents | 5816ab527da8 |
children | 6214a0b91eb2 |
files | clients/pychan_demo/pychan_test.py doc/dependencies/dirdeps.dot doc/dependencies/filedeps.dot engine/core/gui/guilistener.i engine/core/util/base/resourceclass.cpp engine/core/util/resource/pool.h engine/core/video/animation.cpp engine/extensions/pychan/__init__.py engine/extensions/pychan/autoposition.py engine/extensions/pychan/compat.py engine/extensions/pychan/dialogs.py engine/extensions/pychan/events.py engine/extensions/pychan/exceptions.py engine/extensions/pychan/fonts.py engine/extensions/pychan/internal.py engine/extensions/pychan/layout.py engine/extensions/pychan/tools.py engine/extensions/pychan/widgets.py |
diffstat | 18 files changed, 1120 insertions(+), 136 deletions(-) [+] |
line wrap: on
line diff
--- a/clients/pychan_demo/pychan_test.py Sat Mar 14 12:03:56 2009 +0000 +++ b/clients/pychan_demo/pychan_test.py Sat Mar 14 12:13:29 2009 +0000 @@ -38,7 +38,7 @@ def testTimer(): import timer - timer.init( pychan.manager.engine.getTimeManager() ) + timer.init( pychan.manager.hook.engine.getTimeManager() ) def spam(): print "SPAM SPAM" return 1
--- a/doc/dependencies/dirdeps.dot Sat Mar 14 12:03:56 2009 +0000 +++ b/doc/dependencies/dirdeps.dot Sat Mar 14 12:13:29 2009 +0000 @@ -190,6 +190,7 @@ "model/structures" -> "util/math" "model/structures" -> "util/resource" "model/structures" -> "util/structures" + "model/structures" -> "util/time" "pathfinder" -> "model/metamodel" "pathfinder" -> "model/structures" "pathfinder" -> "util/base" @@ -263,6 +264,7 @@ "view/renderers" -> "util/log" "view/renderers" -> "util/math" "view/renderers" -> "util/structures" + "view/renderers" -> "util/time" "view/renderers" -> "video" "view/renderers" -> "video/fonts" "view/renderers" -> "video/sdl"
--- a/doc/dependencies/filedeps.dot Sat Mar 14 12:03:56 2009 +0000 +++ b/doc/dependencies/filedeps.dot Sat Mar 14 12:13:29 2009 +0000 @@ -165,6 +165,7 @@ "engine/core/gui/widgets/clicklabel.cpp" -> "util/base/exception.h" "engine/core/gui/widgets/clicklabel.cpp" -> "video/image.h" "engine/core/gui/widgets/icon2.cpp" -> "icon2.hpp" + "engine/core/gui/widgets/togglebutton.cpp" -> "togglebutton.h" "engine/core/gui/widgets/twobutton.cpp" -> "twobutton.h" "engine/core/loaders/native/audio_loaders/ogg_loader.cpp" -> "audio/soundclip.h" "engine/core/loaders/native/audio_loaders/ogg_loader.cpp" -> "ogg_loader.h" @@ -248,6 +249,7 @@ "engine/core/model/structures/instance.cpp" -> "util/base/exception.h" "engine/core/model/structures/instance.cpp" -> "util/log/logger.h" "engine/core/model/structures/instance.cpp" -> "util/math/fife_math.h" + "engine/core/model/structures/instance.cpp" -> "util/time/timemanager.h" "engine/core/model/structures/instance.h" -> "location.h" "engine/core/model/structures/instance.h" -> "model/metamodel/abstractvisual.h" "engine/core/model/structures/instance.h" -> "model/metamodel/object.h" @@ -636,6 +638,7 @@ "engine/core/view/renderers/genericrenderer.cpp" -> "model/structures/location.h" "engine/core/view/renderers/genericrenderer.cpp" -> "util/log/logger.h" "engine/core/view/renderers/genericrenderer.cpp" -> "util/math/fife_math.h" + "engine/core/view/renderers/genericrenderer.cpp" -> "util/time/timemanager.h" "engine/core/view/renderers/genericrenderer.cpp" -> "video/animation.h" "engine/core/view/renderers/genericrenderer.cpp" -> "video/animationpool.h" "engine/core/view/renderers/genericrenderer.cpp" -> "video/fonts/abstractfont.h"
--- a/engine/core/gui/guilistener.i Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/core/gui/guilistener.i Sat Mar 14 12:13:29 2009 +0000 @@ -22,36 +22,17 @@ %module fife %{ #include <guichan.hpp> -#include "gui/guilistener.h" +#include <guichan/actionevent.hpp> +#include <guichan/keyevent.hpp> %} namespace gcn { - %nodefaultctor; + %feature("director") MouseListener; class MouseListener { public: - }; - - class KeyListener { - public: - }; + virtual ~MouseListener(); - class ActionListener { - public: - }; - %clearnodefaultctor; -} - -namespace FIFE { - - %feature("director") GUIEventListener; - class GUIEventListener : - public gcn::MouseListener, public gcn::KeyListener, public gcn::ActionListener { - public: - GUIEventListener(); - virtual ~GUIEventListener(); - - /* Mouse events */ virtual void mouseEntered(gcn::MouseEvent& mouseEvent); virtual void mouseExited(gcn::MouseEvent& mouseEvent); virtual void mousePressed(gcn::MouseEvent& mouseEvent); @@ -61,9 +42,27 @@ virtual void mouseWheelMovedDown(gcn::MouseEvent& mouseEvent); virtual void mouseMoved(gcn::MouseEvent& mouseEvent); virtual void mouseDragged(gcn::MouseEvent& mouseEvent); - - virtual void action(const gcn::ActionEvent& actionEvent); - + protected: + MouseListener() { } }; + %feature("director") MouseListener; + class KeyListener { + public: + virtual ~KeyListener() { } + + virtual void keyPressed(gcn::KeyEvent& keyEvent) { } + virtual void keyReleased(gcn::KeyEvent& keyEvent) { } + protected: + KeyListener() { } + }; + + %feature("director") ActionListener; + class ActionListener { + public: + virtual ~ActionListener() { } + virtual void action(const gcn::ActionEvent& actionEvent) = 0; + }; } + +
--- a/engine/core/util/base/resourceclass.cpp Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/core/util/base/resourceclass.cpp Sat Mar 14 12:13:29 2009 +0000 @@ -56,7 +56,6 @@ void ResourceClass::setResourceLocation(const ResourceLocation& location) { delete m_location; - m_location = NULL; m_location = location.clone(); }
--- a/engine/core/util/resource/pool.h Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/core/util/resource/pool.h Sat Mar 14 12:13:29 2009 +0000 @@ -130,7 +130,6 @@ */ virtual void reset(); - protected: private: class PoolEntry { public:
--- a/engine/core/video/animation.cpp Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/core/video/animation.cpp Sat Mar 14 12:13:29 2009 +0000 @@ -46,16 +46,9 @@ } Animation::~Animation() { -// std::vector<FrameInfo>::const_iterator i(m_frames.begin()); -// while (i != m_frames.end()) { -// i->img->decRef(); -// i++; -// } + // note: we don't need to free the images, as they are handled via + // smart references. } - -// void Animation::addFrame(Image* image, unsigned int duration) { -// addFrame(ResourcePtr(image),duration); -// } void Animation::addFrame(ResourcePtr image, unsigned int duration) { FrameInfo info;
--- a/engine/extensions/pychan/__init__.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/__init__.py Sat Mar 14 12:13:29 2009 +0000 @@ -10,7 +10,7 @@ -------- - Simpler Interface - Very Basic XML Format support - - Very Basic Layout Engine + - Basic Layout Engine - Pseudo-Synchronous Dialogs. - Automagic background tiling (WIP) - Basic Styling support. @@ -24,8 +24,6 @@ - Documentation ( Allways not enough :-( ) - Handle Image Fonts - Move Font config files to XML, too ... - - Add support for fixed size 'Spacers' - - Add messageBox(text) - Implement real Menus - Implement StackWidget @@ -37,9 +35,9 @@ BUGS ---- - Focus problems with Widget.execute. - - Layout Bugs where max_size of a parent widget get's ignored. - Font.glyph_spacing is rendered incorrectly. - - It just looks ugly. + - Is this a bug? At least inconvenient. MouseEntered events are not distributed for freshly shown widget. + - It just looks bad. Problems -------- @@ -154,6 +152,8 @@ } } +A new style is added to pychan with L{internal.Manager.addStyle}. + The font is set via a string identifier pulled from a font definition in a PyChan configuration file. You have to load these by calling L{loadFonts} in your startup code:: @@ -225,8 +225,6 @@ 'manager' ] -import fife, pythonize - from widgets import * from exceptions import * @@ -244,9 +242,11 @@ @param engine: The FIFE engine object. """ - from manager import Manager + from compat import _munge_engine_hook + from internal import Manager global manager - manager = Manager(engine,debug) + + manager = Manager(_munge_engine_hook(engine),debug) # XML Loader @@ -326,6 +326,9 @@ def _createSpacer(self,cls,name,attrs): obj = cls(parent=self.root) + for k,v in attrs.items(): + self._setAttr(obj,k,v) + if hasattr(self.root,'add'): self.root.addSpacer(obj) else:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/autoposition.py Sat Mar 14 12:13:29 2009 +0000 @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" +Automatic widget positioning +============================ + +You can use the C{position_techinque} attribute +on top level widgets which can also be set from xml. + +For direct use call L{placeWidget}. +""" + +from internal import screen_width, screen_height +from exceptions import PyChanException + +EXPLICIT = "explicit" +AUTOMATIC = "automatic" + +TOP = "top" +LEFT = "left" +RIGHT = "right" +CENTER = "center" +BOTTOM = "bottom" + + +def _splicePosition(p): + if "+" in p: + technique,delta = p.split("+") + elif "-" in p: + technique,delta = p.split("-") + delta = '-' + delta + else: + technique,delta = p,0 + delta = int(delta) + return technique,delta + +def _parsePosition(position): + try: + if position == AUTOMATIC: + position = "center+0:center+0" + + x_pos,y_pos = position.split(":") + x_pos, x_delta = _splicePosition(x_pos) + y_pos, y_delta = _splicePosition(y_pos) + + if x_pos not in [EXPLICIT,LEFT,CENTER,RIGHT]: + raise + if y_pos not in [EXPLICIT,TOP,CENTER,BOTTOM]: + raise + except: + raise PyChanException("Malformed position definition: " + repr(position)) + return x_pos,x_delta,y_pos,y_delta + +def placeWidget(widget,position): + """ + Place a widget according to a string defining relative coordinates to screen borders. + + The position definition has to be of the form: C{"<x_pos><x_delta>:<y_pos><y_delta>"} + + C{<x_pos>} may be one of: + - left + - right + - center + - explicit + + C{<y_pos>} may be one of: + - top + - bottom + - center + - explicit + + C{explicit} means that the widgets x or y position will not be touched. The rest should be + self explanatory. + + C{<x_delta>} and C{<y_delta>} must be of the form: +pixel_number or -pixel_number. Or comletely + omitted. Note that the sign has to be there for for positive deltas, too. + + For brevity two shortcuts exist: + - "explict" -> "explict:explict" + - "automatic" -> "center:center" + + A few examples:: + "right-20:top" + "center:top+10" + "center:center" + + @param widget: The PyChan widget. + @param position: A position definition. + + If the position cannot be parsed a L{PyChanException} is thrown. + + """ + if position == EXPLICIT: + return + x_pos,x_delta,y_pos,y_delta = _parsePosition(position) + + x,y = widget.position + w,h = widget.size + + if x_pos == CENTER: + x = (screen_width()-w)/2 + x_delta + + if y_pos == CENTER: + y = (screen_height()-h)/2 + y_delta + + if x_pos == LEFT: + x = x_delta + + if y_pos == TOP: + y = y_delta + + if x_pos == RIGHT: + x = screen_width() - w + x_delta + + if y_pos == BOTTOM: + y = screen_height() - h + y_delta + + widget.position = x,y + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/compat.py Sat Mar 14 12:13:29 2009 +0000 @@ -0,0 +1,87 @@ +# coding: utf-8 + +in_fife = None +guichan = None + +def _import_guichan(): + global in_fife + + err_fife = "" + try: + import fife + in_fife = True + return fife + except ImportError, e: + err_fife = str(e) + + try: + import guichan + in_fife = False + return guichan + except ImportError, e: + import traceback + traceback.print_exc() + raise ImportError("Couldn't import neither fife nor guichan: fife:'%s' guichan:'%s'" % (err_fife,str(e))) +guichan = _import_guichan() + + + +def _munge_engine_hook(engine): + engine.translate_mouse_event = getattr(engine,'translate_mouse_event',lambda x : x ) + engine.translate_key_event = getattr(engine,'translate_key_event',lambda x : x ) + + if not in_fife: + return engine + if not isinstance(engine,fife.Engine): + return engine + + guimanager = engine.getGuiManager() + + def _fife_load_image(filename): + index = engine.imagePool.addResourceFromFile(filename) + return guichan.GuiImage(index,engine.getImagePool()) + + class hook: + pass + hook = hook() + + hook.add_widget = guimanager.add + hook.remove_widget = guimanager.remove + hook.default_font = engine.getDefaultFont() + hook.load_image = _fife_load_image + hook.translate_mouse_event = guimanager.translateMouseEvent + hook.translate_key_event = guimanager.translateKeyEvent + + hook.screen_width = engine.getRenderBackend().getScreenWidth() + hook.screen_height = engine.getRenderBackend().getScreenHeight() + + hook.engine = engine + return hook + + +class _multilistener(guichan.ActionListener,guichan.MouseListener,guichan.KeyListener): + def __init__(self): + guichan.ActionListener.__init__(self) + guichan.MouseListener.__init__(self) + guichan.KeyListener.__init__(self) + + +class _point(object): + def __init__(self,x=0,y=0): + self.x=0 + self.y=0 + +if in_fife: + fife = guichan + guichan.ActionListener._ActionListener_init__ = lambda x : x + #guichan.MouseListener.__init__ = lambda x : x + guichan.KeyListener.__init__ = lambda x : x +else: + guichan.Point = _point + guichan.ScrollArea.SHOW_AUTO = guichan.ScrollArea.ShowAuto + guichan.ScrollArea.SHOW_NEVER = guichan.ScrollArea.ShowNever + guichan.ScrollArea.SHOW_ALWAYS = guichan.ScrollArea.ShowAlways + +assert isinstance(_multilistener(),guichan.ActionListener) +assert isinstance(_multilistener(),guichan.MouseListener) +assert isinstance(_multilistener(),guichan.KeyListener)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/dialogs.py Sat Mar 14 12:13:29 2009 +0000 @@ -0,0 +1,197 @@ +# coding: utf-8 + +from pychan import loadXML +import pychan.tools +import widgets +from internal import get_manager, screen_width, screen_height +from StringIO import StringIO + +OK,YES,NO,CANCEL = True,True,False,None + +def print_event(**kwargs): + print kwargs + +class XMLDialog(object): + def __init__(self, xml, ok_field = None, cancel_field = None,initial_data={},data={}): + self.gui = loadXML(xml) + self.ok_field = ok_field + self.cancel_field = cancel_field + self.initial_data= initial_data + self.data= data + self.max_size=None + self.min_size=None + self.gui.capture(print_event,"mouseEntered") + + def execute(self): + self.gui.distributeInitialData(self.initial_data) + self.gui.distributeData(self.data) + + screen_w, screen_h = screen_width(), screen_height() + if self.max_size is None: + self.max_size = screen_w/2, screen_h/3 + if self.min_size is None: + self.min_size = screen_w/2, screen_h/4 + self.gui.max_size = self.max_size + self.gui.min_size = self.min_size + + resultMap = {} + if self.gui.findChild(name="okButton"): + resultMap["okButton"] = OK + + if self.gui.findChild(name="cancelButton"): + resultMap["cancelButton"] = CANCEL + + if self.gui.findChild(name="yesButton"): + resultMap["noButton"] = NO + + if self.gui.findChild(name="yesButton"): + resultMap["yesButton"] = YES + + ok = self.gui.execute(resultMap) + if ok: + return self.getOkResult() + return self.getCancelResult() + + def getOkResult(self): + if self.ok_field: + return self.gui.collectData(self.ok_field) + return True + + def getCancelResult(self): + if self.cancel_field: + return self.gui.collectData(self.cancel_field) + return False + +MESSAGE_BOX_XML = """\ +<Window name="window" title="Message"> +<ScrollArea> +<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/> +</ScrollArea> +<HBox> +<Spacer/><Button min_width="50" name="okButton" text="OK"/> +</HBox> +</Window> +""" + +YESNO_BOX_XML = """\ +<Window name="window" title="Question"> +<ScrollArea> +<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/> +</ScrollArea> +<HBox> +<Spacer/> +<Button min_width="50" name="yesButton" text="Yes"/> +<Button min_width="50" name="noButton" text="No"/> +</HBox> +</Window> +""" + +YESNOCANCEL_BOX_XML = """\ +<Window name="window" title="Question"> +<ScrollArea> +<Label wrap_text="1" text="$MESSAGE" name="message" vexpanding="1"/> +</ScrollArea> +<HBox> +<Spacer/> +<Button min_width="50" name="yesButton" text="Yes"/> +<Button min_width="50" name="noButton" text="No"/> +<Button min_width="50" name="cancelButton" text="Cancel"/> +</HBox> +</Window> +""" + +SELECT_BOX_XML = """\ +<Window name="window" title="Select"> +<Label wrap_text="1" text="$MESSAGE" name="message"/> +<ScrollArea> +<ListBox name="selection"> +</ListBox> +</ScrollArea> +<HBox> +<Spacer/> +<Button min_width="50" name="okButton" text="Select"/> +<Button min_width="50" name="cancelButton" text="Cancel"/> +</HBox> +</Window> +""" + +EXCEPTION_CATCHER_XML="""\ +<Window name="window" title="An exception occurred - what now?"> + <VBox hexpanding="1"> + <Label wrap_text="1" max_width="400" text="$MESSAGE" name="message"/> + <ScrollArea> + <Label text="$MESSAGE" name="traceback"/> + </ScrollArea> + </VBox> + <HBox> + <Spacer/> + <Button name="yesButton" text="Retry"/> + <Button name="noButton" text="Ignore"/> + <Button name="cancelButton" text="Reraise"/> + </HBox> +</Window> +""" + +def _make_text(message): + if callable(message): + message = message() + if hasattr(message,"read"): + message = message.read() + return message + +def message(message="",caption="Message"): + text = _make_text(message) + dialog = XMLDialog(StringIO(MESSAGE_BOX_XML), + initial_data={'message':text,'window':caption}) + dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50 + dialog.execute() + +def yesNo(message="",caption="Message"): + text = _make_text(message) + dialog = XMLDialog(StringIO(YESNO_BOX_XML), + initial_data={'message':text,'window':caption}) + dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50 + return dialog.execute() + +def yesNoCancel(message="",caption="Message"): + text = _make_text(message) + dialog = XMLDialog(StringIO(YESNOCANCEL_BOX_XML), + initial_data={'message':text,'window':caption}) + dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50 + return dialog.execute() + +def select(message="",options=[],caption="Message"): + text = _make_text(message) + dialog = XMLDialog(StringIO(SELECT_BOX_XML), + initial_data={'message':text,'window':caption}) + dialog.size = screen_width()/3, 2*screen_height()/3 + + dialog.gui.findChild(name="message").max_width = screen_width()/2 - 50 + listbox = dialog.gui.findChild(name="selection") + listbox.items = options + if dialog.execute(): + return listbox.selected_item + return None + +def trace(f): + import sys, traceback + def new_f(*args,**kwargs): + try: + return pychan.tools.applyOnlySuitable(f,*args,**kwargs) + + except Exception, e: + dialog = XMLDialog(StringIO(EXCEPTION_CATCHER_XML), + initial_data={'message':str(e)} + ) + tb = traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback) + dialog.gui.findChild(name="traceback").text = "".join(tb) + dialog.min_size = screen_width()/2,3*screen_height()/4 + dialog.max_size = screen_width()/2,3*screen_height()/4 + result = dialog.execute() + if result == YES: + return new_f(*args,**kwargs) + elif result == NO: + return + raise + return new_f +
--- a/engine/extensions/pychan/events.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/events.py Sat Mar 14 12:13:29 2009 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- #coding: utf-8 """ @@ -11,15 +12,39 @@ Nevertheless to understand how its supposed to work take a look at L{EventMapper} and L{EventListener} +Event callbacks +--------------- + +You can either write callbacks yourself or +use L{tools.callBackWithArguments} or L{tools.attrSetCallback} +to generate suitable callbacks. + +Here's an example callback:: + def dumpEventInfo(event=0,widget=0): + print widget, " received the event ", event + +Note the signature - C{event} and C{widget} are keyword +arguments passed to the callback. If doesn't accept either +C{event} or C{widget} as argument, these are not passed. + +This way a simple function which ignores C{event} or C{widget} +can be used, while they are available if needed. + +Currently only one callback can be set per event. In case +you don't want to write your own callback that dispatches +to different callbacks you can use L{tools.chainCallbacks}. + Available Events ---------------- """ -import fife +from compat import guichan + import exceptions -import manager +from internal import get_manager import tools +import traceback EVENTS = [ "mouseEntered", @@ -37,6 +62,11 @@ # Add the EVENTS to the docs. __doc__ += "".join([" - %s\n" % event for event in EVENTS]) +# The line before seems to leak the variable event into the global namespace ... remove that! +# This is a python problem, addressed in python3 +try: del event +except:pass + MOUSE_EVENT, KEY_EVENT, ACTION_EVENT = range(3) def getEventType(name): if "mouse" in name: @@ -50,8 +80,7 @@ You passed None as parameter to %s.capture, which would normally remove a mapped event. But there was no event mapped. Did you accidently call a function instead of passing it? """ - -class EventListener(fife.GUIEventListener): +class EventListenerBase(object): """ Redirector for event callbacks. Use *only* from L{EventMapper}. @@ -60,45 +89,92 @@ virtual methods are called from C++ to - listen to Guichan events. - When the module is first loaded the event handler - methods are auto-generated from the list L{EVENTS}. - This is effectively the same code as:: - def mouseEntered(self,event): - self._redirectEvent("mouseEntered",event) - - This way L{EVENTS} and the actually receivable events - are forced to be in sync. """ - def __init__(self,debug=True): - super(EventListener,self).__init__() + def __init__(self): + super(EventListenerBase,self).__init__() self.events = {} self.indent = 0 - self.debug = debug - self.guimanager = manager.Manager.manager.guimanager + self.debug = 1 + self.is_attached = False + + def attach(self,widget): + """ + Start receiving events. + No need to call this manually. + """ + + if self.is_attached: + return + if not self.events: + return + if self.debug: print "Attach:",self + self.doAttach(widget.real_widget) + self.widget = widget + self.is_attached = True + + def detach(self): + """ + Stop receiving events. + No need to call this manually. + """ + if not self.is_attached: + return + if self.debug: print "Detach:",self + self.doDetach(self.widget.real_widget) + self.widget = None + self.is_attached = False def _redirectEvent(self,name,event): self.indent += 4 - event = self.translateEvent(getEventType(name), event) - if name in self.events: - if self.debug: print "-"*self.indent, name - for f in self.events[name].itervalues(): - f( event ) - self.indent -= 4 + try: + event = self.translateEvent(getEventType(name), event) + if name in self.events: + if self.debug: print "-"*self.indent, name + for f in self.events[name].itervalues(): + f( event ) + + except: + print name, event + traceback.print_exc() + raise + + finally: + self.indent -= 4 def translateEvent(self,event_type,event): if event_type == MOUSE_EVENT: - return self.guimanager.translateMouseEvent(event) + return get_manager().hook.translate_mouse_event(event) if event_type == KEY_EVENT: - return self.guimanager.translateKeyEvent(event) + return get_manager().hook.translate_key_event(event) return event -def _redirect(name): - def redirectorFunc(self,event): - self._redirectEvent(name,event) - return redirectorFunc +class _ActionEventListener(EventListenerBase,guichan.ActionListener): + def __init__(self):super(_ActionEventListener,self).__init__() + def doAttach(self,real_widget): real_widget.addActionListener(self) + def doDetach(self,real_widget): real_widget.removeActionListener(self) + + def action(self,e): self._redirectEvent("action",e) + +class _MouseEventListener(EventListenerBase,guichan.MouseListener): + def __init__(self):super(_MouseEventListener,self).__init__() + def doAttach(self,real_widget): real_widget.addMouseListener(self) + def doDetach(self,real_widget): real_widget.removeMouseListener(self) -for event_name in EVENTS: - setattr(EventListener,event_name,_redirect(event_name)) + def mouseEntered(self,e): self._redirectEvent("mouseEntered",e) + def mouseExited(self,e): self._redirectEvent("mouseExited",e) + def mousePressed(self,e): self._redirectEvent("mousePressed",e) + def mouseReleased(self,e): self._redirectEvent("mouseReleased",e) + def mouseClicked(self,e): self._redirectEvent("mouseClicked",e) + def mouseMoved(self,e): self._redirectEvent("mouseMoved",e) + def mouseDragged(self,e): self._redirectEvent("mouseDragged",e) + +class _KeyEventListener(EventListenerBase,guichan.KeyListener): + def __init__(self):super(_KeyEventListener,self).__init__() + def doAttach(self,real_widget): real_widget.addKeyListener(self) + def doDetach(self,real_widget): real_widget.removeKeyListener(self) + + def keyPressed(self,e): self._redirectEvent("keyPressed",e) + def keyReleased(self,e): self._redirectEvent("keyReleased",e) class EventMapper(object): """ @@ -106,7 +182,7 @@ and derived classes. Every PyChan widget has an L{EventMapper} instance - as attribute *event_mapper*. + as attribute B{event_mapper}. This instance handles all necessary house-keeping. Such an event mapper can be either *attached* or @@ -124,44 +200,17 @@ def __init__(self,widget): super(EventMapper,self).__init__() self.widget = widget - self.listener = EventListener() + self.listener = { + KEY_EVENT : _KeyEventListener(), + ACTION_EVENT : _ActionEventListener(), + MOUSE_EVENT : _MouseEventListener(), + } self.is_attached = False - self.debug = manager.Manager.manager.debug + self.debug = get_manager().debug - def __del__(self): - self.detach() def __repr__(self): return "EventMapper(%s)" % repr(self.widget) - def attach(self): - """ - Start receiving events. - No need to call this manually. - """ - - if self.is_attached: - return - if not self.listener.events: - return - if self.debug: print "Attach:",self - self.widget.real_widget.addKeyListener( self.listener ) - self.widget.real_widget.addMouseListener( self.listener ) - self.widget.real_widget.addActionListener( self.listener ) - self.is_attached = True - - def detach(self): - """ - Stop receiving events. - No need to call this manually. - """ - - if not self.is_attached: - return - if self.debug: print "Detach:",self - self.widget.real_widget.removeKeyListener( self.listener ) - self.widget.real_widget.removeMouseListener( self.listener ) - self.widget.real_widget.removeActionListener( self.listener ) - self.is_attached = False def capture(self,event_name,callback,group_name): if event_name not in EVENTS: @@ -169,32 +218,48 @@ if callback is None: if self.isCaptured(event_name,group_name): - del self.listener.events[event_name][group_name] - if not self.listener.events[event_name]: - del self.listener.events[event_name] - if not self.listener.events: - self.detach() + self.removeEvent(event_name,group_name) elif self.debug: print CALLBACK_NONE_MESSAGE % str(self.widget) return + self.addEvent(event_name,callback,group_name) + def isCaptured(self,event_name,group_name="default"): + return ("%s/%s" % (event_name,group_name)) in self.getCapturedEvents() + + def getCapturedEvents(self): + events = [] + for event_type, listener in self.listener.items(): + for event_name, group in listener.events.items(): + for group_name in group.keys(): + events.append( "%s/%s" % (event_name, group_name) ) + return events + + def getListener(self,event_name): + return self.listener[getEventType(event_name)] + + def removeEvent(self,event_name,group_name): + listener = self.getListener(event_name) + del listener.events[event_name][group_name] + if not listener.events[event_name]: + del listener.events[event_name] + if not listener.events: + listener.detach() + + def addEvent(self,event_name,callback,group_name): if not callable(callback): raise RuntimeError("An event callback must be either a callable or None - not %s" % repr(callback)) def captured_f(event): tools.applyOnlySuitable(callback,event=event,widget=self.widget) - if event_name not in self.listener.events: - self.listener.events[event_name] = {group_name : captured_f} - else: - self.listener.events[event_name][group_name] = captured_f - self.attach() + listener = self.getListener(event_name) - def isCaptured(self,event_name,group_name="default"): - return event_name in self.listener.events and group_name in self.listener.events[event_name] - - def getCapturedEvents(self): - return self.listener.events.keys() + if event_name not in listener.events: + listener.events[event_name] = {group_name : captured_f} + else: + listener.events[event_name][group_name] = captured_f + listener.attach(self.widget) def splitEventDescriptor(name): @@ -210,3 +275,4 @@ L = L[0],L[1],"default" return L +
--- a/engine/extensions/pychan/exceptions.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/exceptions.py Sat Mar 14 12:13:29 2009 +0000 @@ -31,6 +31,6 @@ class PrivateFunctionalityError(RuntimeError): """ - Exception raised if private attributes/functions are used." + Exception raised if private attributes/functions are used. """ pass
--- a/engine/extensions/pychan/fonts.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/fonts.py Sat Mar 14 12:13:29 2009 +0000 @@ -5,7 +5,7 @@ class Font(object): def __init__(self,name,get): - from manager import Manager + from internal import get_manager self.font = None self.name = name self.typename = get("type") @@ -17,7 +17,7 @@ self.size = int(get("size")) self.antialias = int(get("antialias",1)) self.color = map(int,get("color","255,255,255").split(',')) - self.font = Manager.manager.guimanager.createFont(self.source,self.size,"") + self.font = get_manager().hook.engine.getGuiManager().createFont(self.source,self.size,"") if self.font is None: raise InitializationError("Could not load font %s" % name) @@ -62,7 +62,7 @@ """ Load fonts from a config file. These are then available via their name. """ - from manager import Manager + from internal import get_manager for font in Font.loadFromFile(filename): - Manager.manager.addFont(font) + get_manager().addFont(font)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/internal.py Sat Mar 14 12:13:29 2009 +0000 @@ -0,0 +1,208 @@ +# coding: utf-8 + +from compat import guichan, in_fife +import widgets +import fonts +from exceptions import * +from traceback import print_exc + +def get_manager(): + """ + Get the manager from inside pychan. + + To avoid cyclic imports write:: + from internal import get_manager + """ + return Manager.manager + +def screen_width(): + return get_manager().hook.screen_width + +def screen_height(): + return get_manager().hook.screen_height + +class Manager(object): + manager = None + + def __init__(self, hook, debug = False): + super(Manager,self).__init__() + self.hook = hook + self.debug = debug + + if in_fife: + if not hook.engine.getEventManager(): + raise InitializationError("No event manager installed.") + if not hook.engine.getGuiManager(): + raise InitializationError("No GUI manager installed.") + + self.fonts = {} + #glyphs = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/:();%`\'*#=[]"' + self.fonts['default'] = hook.default_font + + self.styles = {} + self.addStyle('default',DEFAULT_STYLE) + + Manager.manager = self + + # Setup synchronous dialogs + self.mainLoop = None + self.breakFromMainLoop = None + self.can_execute = False + + # Autopos + from autoposition import placeWidget + self.placeWidget = placeWidget + + def setupModalExecution(self,mainLoop,breakFromMainLoop): + """ + Setup synchronous execution of dialogs. + """ + self.mainLoop = mainLoop + self.breakFromMainLoop = breakFromMainLoop + self.can_execute = True + + def show(self,widget): + """ + Shows a widget on screen. Used by L{Widget.show} - do not use directly. + """ + self.placeWidget(widget, widget.position_technique) + self.hook.add_widget( widget.real_widget ) + + def hide(self,widget): + """ + Hides a widget again. Used by L{Widget.hide} - do not use directly. + """ + self.hook.remove_widget( widget.real_widget ) + + def setDefaultFont(self,name): + self.fonts['default'] = self.getFont(name) + + def getFont(self,name): + """ + B{pending deprecation} + + Returns a GuiFont identified by its name. + + @param name: A string identifier from the font definitions in pychans config files. + """ + if in_fife: + font = self.fonts.get(name) + if isinstance(font,guichan.GuiFont): + return font + if hasattr(font,"font") and isinstance(getattr(font,"font"),guichan.GuiFont): + return font.font + return fonts.parseFontSpec(name) + else: + return self.hook.get_font(name) + + def addFont(self,font): + """ + B{deprecated} + + Add a font to the font registry. It's not necessary to call this directly. + But it expects a L{Font} instance and throws an L{InitializationError} + otherwise. + + @param font: A L{Font} instance. + """ + if not isinstance(font,fonts.Font): + raise InitializationError("PyChan Manager expected a fonts.Font instance, not %s." % repr(font)) + self.fonts[font.name] = font + + def addStyle(self,name,style): + style = self._remapStyleKeys(style) + + for k,v in self.styles.get('default',{}).items(): + style[k] = style.get(k,v) + self.styles[name] = style + + def stylize(self,widget, style, **kwargs): + style = self.styles[style] + for k,v in style.get('default',{}).items(): + v = kwargs.get(k,v) + setattr(widget,k,v) + + cls = widget.__class__ + for applicable,specstyle in style.items(): + if not isinstance(applicable,tuple): + applicable = (applicable,) + if cls in applicable: + for k,v in specstyle.items(): + v = kwargs.get(k,v) + setattr(widget,k,v) + + def _remapStyleKeys(self,style): + # Remap class names, create copy: + def _toClass(class_): + if class_ == "default": + return class_ + + if type(class_) == type(widgets.Widget) and issubclass(class_,widgets.Widget): + return class_ + if not widgets.WIDGETS.has_key(str(class_)): + raise InitializationError("Can't resolve %s to a widget class." % repr(class_)) + return widgets.WIDGETS[str(class_)] + + style_copy = {} + for k,v in style.items(): + if isinstance(k,tuple): + new_k = tuple(map(_toClass,k)) + else: + new_k = _toClass(k) + style_copy[new_k] = v + return style_copy + + def loadImage(self,filename): + if not filename: + raise InitializationError("Empty Image file.") + return self.hook.load_image(filename) + +# Default Widget style. + +DEFAULT_STYLE = { + 'default' : { + 'border_size': 0, + 'margins': (0,0), + 'base_color' : guichan.Color(28,28,28), + 'foreground_color' : guichan.Color(255,255,255), + 'background_color' : guichan.Color(50,50,50), + }, + 'Button' : { + 'border_size': 2, + 'margins' : (5,2), + 'min_size' : (15,10), + }, + 'CheckBox' : { + 'border_size': 0, + }, + 'RadioButton' : { + 'border_size': 0, + 'background_color' : guichan.Color(0,0,0), + }, + 'Label' : { + 'border_size': 0, + }, + 'ClickLabel' : { + 'border_size': 0, + }, + 'ListBox' : { + 'border_size': 0, + }, + 'Window' : { + 'border_size': 0, + 'margins': (5,5), + 'opaque' : 1, + 'titlebar_height' : 12, + 'vexpanding' : 1, + #'background_image' : 'gui/backgrounds/background.png', + #'font' : 'samanata_large' + }, + 'TextBox' : { + }, + ('Container','HBox','VBox') : { + 'border_size': 0, + 'margins': (0,0), + 'padding':2, + #'background_image' : 'gui/backgrounds/background.png', + } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/engine/extensions/pychan/layout.py Sat Mar 14 12:13:29 2009 +0000 @@ -0,0 +1,278 @@ +# coding: utf-8 + +from attrs import IntAttr + +AlignTop, AlignBottom, AlignLeft, AlignRight, AlignCenter = range(5) +def isLayouted(widget): + return isinstance(widget,LayoutBase) + +class LayoutBase(object): + """ + This class is at the core of the layout engine. The two MixIn classes L{VBoxLayoutMixin} + and L{HBoxLayoutMixin} specialise on this by reimplementing the C{resizeToContent} and + the C{expandContent} methods. + + Dynamic Layouting + ----------------- + + The layout is calculated in the L{Widget.show} method. Thus if you modify the layout, + by adding or removing child widgets for example, you have to call L{widgets.Widget.adaptLayout} + so that the changes ripple through the widget hierachy. + + Internals + --------- + + At the core the layout engine works in two passes: + + Before a root widget loaded by the XML code is shown, its resizeToContent method + is called recursively (walking the widget containment relation in post order). + This shrinks all HBoxes and VBoxes to their minimum heigt and width. + After that the expandContent method is called recursively in the same order, + which will re-align the widgets if there is space left AND if a Spacer is contained. + + Inside bare Container instances (without a Layout MixIn) absolute positioning + can be used. + """ + def __init__(self,align = (AlignLeft,AlignTop), **kwargs): + self.align = align + self.spacer = [] + super(LayoutBase,self).__init__(**kwargs) + + def addSpacer(self,spacer): + self.spacer.append(spacer) + spacer.index = len(self.children) + + def xdelta(self,widget):return 0 + def ydelta(self,widget):return 0 + + def _applyHeight(self, spacers = []): + y = self.border_size + self.margins[1] + ydelta = map(self.ydelta,self.children) + for index, child in enumerate(self.children): + while spacers and spacers[0].index == index: + y += spacers.pop(0).size + child.y = y + y += ydelta.pop(0) + + def _adjustHeightWithSpacer(self): + pass + + def _applyWidth(self, spacers = []): + x = self.border_size + self.margins[0] + xdelta = map(self.xdelta,self.children) + for index, child in enumerate(self.children): + while spacers and spacers[0].index == index: + x += spacers.pop(0).size + child.x = x + x += xdelta.pop(0) + + def _expandWidthSpacer(self): + xdelta = map(self.xdelta,self.children) + xdelta += [spacer.min_size for spacer in self.spacer] + + available_space = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0] + + used_space = sum(xdelta) + if self.children: + used_space -= self.padding + if used_space >= available_space: + return + + expandable_items = self._getExpanders(vertical=False) + #print "AS/US - before",self,[o.width for o in expandable_items] + #print "SPACERS",self.spacer + + index = 0 + while used_space < available_space and expandable_items: + index = index % len(expandable_items) + + expander = expandable_items[index] + old_width = expander.width + expander.width += 1 + if old_width == expander.width: + expandable_items.pop(index) + else: + used_space += 1 + index += 1 + + #print "AS/US - after",self,[o.width for o in expandable_items] + #print "SPACERS",self.spacer + self._applyWidth(spacers = self.spacer[:]) + + def _expandHeightSpacer(self): + ydelta = map(self.ydelta,self.children) + ydelta += [spacer.min_size for spacer in self.spacer] + + available_space = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1] + + used_space = sum(ydelta) + if self.children: + used_space -= self.padding + + if used_space >= available_space: + return + + expandable_items = self._getExpanders(vertical=True) + #print "AS/US - before",self,[o.height for o in expandable_items] + + index = 0 + while used_space < available_space and expandable_items: + index = index % len(expandable_items) + + expander = expandable_items[index] + old_width = expander.height + expander.height += 1 + if old_width == expander.height: + expandable_items.pop(index) + else: + used_space += 1 + index += 1 + + #print "AS/US - after",self,[o.height for o in expandable_items] + self._applyHeight(spacers = self.spacer[:]) + + + def _getExpanders(self,vertical=True): + expanders = [] + spacers = self.spacer[:] + for index, child in enumerate(self.children): + if spacers and spacers[0].index == index: + expanders.append( spacers.pop(0) ) + #print self,child,child.expanding + if child.vexpanding and vertical: + expanders.append( child ) + if child.hexpanding and not vertical: + expanders.append( child ) + return expanders + spacers + + def _resetSpacers(self): + for spacer in self.spacer: + spacer.size = 0 + +class VBoxLayoutMixin(LayoutBase): + """ + A mixin class for a vertical layout. Do not use directly. + """ + def __init__(self,**kwargs): + super(VBoxLayoutMixin,self).__init__(**kwargs) + + def resizeToContent(self, recurse = True): + self._resetSpacers() + + max_w = self.getMaxChildrenWidth() + x = self.margins[0] + self.border_size + y = self.margins[1] + self.border_size + for widget in self.children: + widget.width = max_w + y += widget.height + self.padding + + if self.children: + y -= self.padding + + y += sum([spacer.min_size for spacer in self.spacer]) + + self.height = y + self.margins[1] + self.border_size + self._extra_border[1] + self.width = max_w + 2*x + self._extra_border[0] + + self._applyHeight(spacers = self.spacer[:]) + self._applyWidth() + + def expandContent(self): + self._expandHeightSpacer() + if not self.hexpanding:return + for widget in self.children: + widget.width = self.width - 2*self.margins[0] - 2*self.border_size - self._extra_border[0] + + + def ydelta(self,widget):return widget.height + self.padding + +class HBoxLayoutMixin(LayoutBase): + """ + A mixin class for a horizontal layout. Do not use directly. + """ + def __init__(self,**kwargs): + super(HBoxLayoutMixin,self).__init__(**kwargs) + + def resizeToContent(self, recurse = True): + self._resetSpacers() + + max_h = self.getMaxChildrenHeight() + x = self.margins[0] + self.border_size + y = self.margins[1] + self.border_size + for widget in self.children: + widget.height = max_h + x += widget.width + self.padding + if self.children: + x -= self.padding + x += sum([spacer.min_size for spacer in self.spacer]) + + self.width = x + self.margins[0] + self._extra_border[0] + self.height = max_h + 2*y + self._extra_border[1] + + self._applyHeight() + self._applyWidth(spacers = self.spacer[:]) + + def expandContent(self): + self._expandWidthSpacer() + if not self.vexpanding:return + for widget in self.children: + widget.height = self.height - 2*self.margins[1] - 2*self.border_size - self._extra_border[1] + + def xdelta(self,widget):return widget.width + self.padding + +class Spacer(object): + """ A spacer represents expandable or fixed 'whitespace' in the GUI. + + In a XML file you can get this by adding a <Spacer /> inside a VBox or + HBox element (Windows implicitly are VBox elements). + + Attributes + ---------- + + As with widgets a number of attributes can be set on a spacer (inside the XML definition). + + - min_size: Int: The minimal size this Spacer is allowed to have. + - max_size: Int: The maximal size this Spacer is allowed to have. + - fixed_size: Int: Set min_size and max_size to the same vale - effectively a Fixed size spacer. + + """ + + ATTRIBUTES = [ + IntAttr('min_size'), IntAttr('size'), IntAttr('max_size'), + IntAttr('fixed_size'), + ] + + def __init__(self,parent=None,**kwargs): + self._parent = parent + self.min_size = 0 + self.max_size = 1000 + self.size = 0 + + def __str__(self): + return "Spacer(parent.name='%s')" % getattr(self._parent,'name','None') + + def __repr__(self): + return "<Spacer(parent.name='%s') at %x>" % (getattr(self._parent,'name','None'),id(self)) + + def _getSize(self): + self.size = self._size + return self._size + def _setSize(self,size): + self._size = max(self.min_size, min(self.max_size,size)) + size = property(_getSize,_setSize) + + # Alias for size + width = property(_getSize,_setSize) + height = property(_getSize,_setSize) + + def _setFixedSize(self,size): + self.min_size = self.max_size = size + self.size = size + fixed_size = property(fset=_setFixedSize) + + def _isExpanding(self): + if self.min_size < self.max_size: + return 1 + return 0 + vexpanding = property(_isExpanding) + hexpanding = property(_isExpanding)
--- a/engine/extensions/pychan/tools.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/tools.py Sat Mar 14 12:13:29 2009 +0000 @@ -1,10 +1,14 @@ # coding: utf-8 +""" +Functional utilities designed for pychan use cases. +""" + import exceptions ### Functools ### -def applyOnlySuitable(func,**kwargs): +def applyOnlySuitable(func,*args,**kwargs): """ This nifty little function takes another function and applies it to a dictionary of keyword arguments. If the supplied function does not expect one or more of the @@ -20,11 +24,11 @@ #http://docs.python.org/lib/inspect-types.html if code.co_flags & 8: - return func(**kwargs) + return func(*args,**kwargs) for name,value in kwargs.items(): if name not in varnames: del kwargs[name] - return func(**kwargs) + return func(*args,**kwargs) def callbackWithArguments(callback,*args,**kwargs): """ @@ -91,13 +95,31 @@ do_calls.append(name[4:]) del kwargs[name] - def callback(widget=None): + def attrSet_callback(widget=None): for name,value in kwargs.items(): setattr(widget,name,value) for method_name in do_calls: method = getattr(widget,method_name) method() - return callback + return attrSet_callback + +def chainCallbacks(*args): + """ + Chains callbacks to be called one after the other. + + Example Usage:: + def print_event(event=0): + print event + def print_widget(widget=0): + print widget + callback = tools.chainCallbacks(doSomethingUseful, print_event, print_widget) + guiElement.capture(callback) + """ + callbacks = args + def chain_callback(event=0,widget=0): + for callback in callbacks: + applyOnlySuitable(callback, event=event, widget=widget) + return chain_callback def this_is_deprecated(func,message=None): if message is None:
--- a/engine/extensions/pychan/widgets.py Sat Mar 14 12:03:56 2009 +0000 +++ b/engine/extensions/pychan/widgets.py Sat Mar 14 12:13:29 2009 +0000 @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # coding: utf-8 ### Widget/Container Base Classes ### @@ -412,6 +413,8 @@ for name,data in dataMap.items(): widgetList = self.findChildren(name = name) if len(widgetList) != 1: + if get_manager().debug: + self.listNamedWidgets() raise RuntimeError("DistributeData can only handle widgets with unique names.") widgetList[0].setData(data) @@ -431,6 +434,8 @@ for name in widgetNames: widgetList = self.findChildren(name = name) if len(widgetList) != 1: + if get_manager().debug: + self.listNamedWidgets() raise RuntimeError("CollectData can only handle widgets with unique names.") dataMap[name] = widgetList[0].getData() @@ -461,6 +466,8 @@ for name in widgetNames: widgetList = self.findChildren(name = name) if len(widgetList) != 1: + if get_manager().debug: + self.listNamedWidgets() raise RuntimeError("CollectData can only handle widgets with unique names.") dataList.append( widgetList[0].getData() ) if len(dataList) == 1: