Jump to content
XCOMUFO & Xenocide

Python Dialogs (maybe)


dteviot

Recommended Posts

The objective is to make it as straightforward as possible to create a modal dialog in Xenocide.

 

The intention is the steps go something like this:

1. A falgard .layout file is created. (A simple template file may be provided as a starting point.)

2. A python file for the dialog is created. (Again, we have a template file for a starting point.) The programmer “fills in the blanks” in the template.

3. We add the necessary links to hook the python into the system.

 

Ideally we will also have:

4. A check list of the steps required.

5. Standards for naming the files, where the files are placed, and how the classes/functions in the python file are named.

 

 

So, the way I see the code for dialogs work is something like this. (Note that the basic idea is similar to how dialogs work in MFC. )

 

Every Python dialog is a class that is derived from a base “ModalDialog” class.

The base class has a number of virtual methods so that a person just needs to plug-in the code they need.

If the dialog needs to return information, then the appropriate members are added to the python class.

 

It might be easier to show what I mean by an example.

 

So. Here’s what a base class might look like (please forgive the use of pseudo C++, my python is limited.)

 

Base class

class ModalDialog
{
  enum { IDOK, IDCANCEL } dialogResult;

virtual attachHandlers();
virtual thingsToDoBeforeShowingDialog();
virtual thingsToDoAfterShowingDialog();
dialogResult doModal()
{
	// create and hookup channel		 
	attachHandlers();
	thingsToDoBeforeShowingDialog();
	// resolve strings
	// stop gametime
	// show dialog, do dialog, get response
	thingsToDoAfterShowingDialog();
	return response;
}

// assorted other helper functions that might be useful. E.g.
ConnectEvent(EventType, ItemOnLayout, HandlerFunction);
}

 

Now assume we’re going to do a dialog with an edit control . (E.g. it asks the user the name for a newly purchased aircraft.) So we create a new dialog something like this:

class CraftNameDlg
{
public string craftName;
void handleOk() 
{ craftName = cegui.WindowMgr.GetWindow(“EditControl”).GetText()); };

attachHandlers() { self.ConnectEvent(“ButtonPress”, “OkButton”, self.handleOk) };
}

 

And we can use the dialog like this:

	CraftNameDlg dlg();
dlg.craftName = “DefaultName’;
if (dlg.doModal() == IDOK)
{
	// fetch name from dlg.craftName
}

Link to comment
Share on other sites

A basic question - are all dialogs modal?

I can't see dialogs that let you choose a point on the globe (or the battlescape when we have it) as modal.

 

I do like the idea that we'll have a list of steps on how to make dialogs and a preset way of naming them.

I just don't see the need for dialog classes specifically to make the dialog in a lot of the cases. Mostly we can use a wrapper that'll give us the simpler show/hide functions.

Are there good examples of doing something before/after the dialog that go beyond simple pause/unpause?

 

If the dialog doesn't have any input, we could even make a generator that'll do 90% of the work for us in connecting events and return values(using a hidden handler class we don't even care about), beyond which the dialog doesn't really have anything to do.

A dialog handler though would probably do something based on these return values. That could be made a class if needed.

 

Of course, a more complex dialogs - one that has input(s) and/or buttons that effect other dialog components - may require a class, especially since it'll mostly need real handlers with some logic beyond "if X button pressed then do this".

 

The connectEvent function is a nice idea. I already made a function similar to it, except that it can accept multiple events of the same type at once.

Example of syntax (from earthscreen.py):

# hook up the pan/zoom buttons
connectHandlers("Planetview", str(cegui.PushButton.EventClicked),
			[("PanLeft",  self.onPanLeft),
			 ("PanRight", self.onPanRight),
			 ("PanUp",	self.onPanUp),
			 ("PanDown",  self.onPanDown),
			 ("ZoomIn",   self.onZoomIn),
			 ("ZoomOut",  self.onZoomOut)
			 ])

With first element of the tuples being the last part of the buttons' names and the second being the handler.

Of course, if we'll name all handlers starting with on+buttonName, we may not even need the second element but just the 'self' reference itself ^_^

Link to comment
Share on other sites

Idea of a 90% auto-generated function creating a dialog, getting a value and returning it (destroying the dialog in the process):

def XyzDialog(heading,text,other_arguments.....):
#code loading the layout and setting up whatever the arguments defined

handler = genHandler(dialog,
					 [("OK", True),
					  ("Cancel", False),
					  ("Add", "+"),
					  ("Remove", "-"),
					 ])

return runModalDialog(dialog, handler)

The tuple this time is (button, return value), with return value being anything you'd like, including enums.

Link to comment
Share on other sites

After some thought...

What genHandler in the last idea does is generate a Handler class each time it's called, create one instance of it and connect all events to it.

If the generation of a handler class was taken as a one-time occurance, the idea of classes derived from a ModalDialog or a BasicDialog looks logical.

The classes could be created by the programmer, but in any dialog class that's just made for button selection all handle functions could be generated in a similar way to the genHandler call, only one time on class creation.

A function to do simple dialog execution and value return can still exist. There'll probably only be a need for one function like this though, and a much simpler one too.

Link to comment
Share on other sites

I was thinking that all dialogs would derive from a base class for a number of reasons.

 

The main reason for deriving all dialogs from a base class was one of consistency. I suspect most dialogs will be sufficently complex that they will be best implemented as classes. Therefore, instead of having multiple ways of creating dialogs, there is just one way. That will work for all cases. This should give us:

1. Reduced code base.

2. Reduced amount to learn.

3. Reduced code duplication.

4. Reduced testing.

 

Note, for most simple dialogs we will probaby just use the "message box" class.

And if the base class is correctly designed, the amount of work required to create a small dialog should be the same as the generator approach.

 

The second reason for deriving from a base class is it gives us a "namespace" to put useful utility functions.

e.g. Translate text on dialog, start/stop gametime, handler setup helper functions, and even standard handler functions. e.g. onClose()

 

As regards non-modal (modeless) dialogs, in fact, all our dialogs are modeless, we just (sort of) simulate it by waiting on a connection. Thus, calling the base class "ModalDialog" is probably a poor choice. But the idea still stands, all dialogs would derive from the common base class. If we want modal behavior, we can call doModal(), if we want modeless behavior, we call the approriate function(s) in the base class. Thus creating a modal or modeless dialog is kept as similar as possible.

 

As regards doing work before/after the pause. For the Save/Load Game dialog there will be a single dialog, setting a flag in the constructor will change the text on the leftmost button from "Load" to "Save", and do similar changes to the windows title. It will also need to populate the list box with the previously saved games. I suspect most dialogs will need some sort of "init" function that takes the data they're supposed to display and put it on the dialog's child windows.

Link to comment
Share on other sites

Well, we've got no real modality yet, just blocking everything behind the newly created dialog. Waiting for a response happens in it's own tasklet.

To get real modality, we need to get back the MessageBoxes (MessageQueue maybe?) class to make other message/query dialogs wait for their turn.

 

I looked over the Gui List thread and I think these are the types of dialogs(windows?) we have:

 

Message box (one close button)

Query dialogs(ok/cancel, action on research done/ufo found?)

MODAL

Simple class derived from MessageBox? (MessageBox deriving from BaseDialog, only adding a channel)

Create all handler functions in the class with a generator function

Use external run(messageBoxObject) function to get the user's choice

 

Complex query dialogs(set base name)

MODAL/NON-MODAL

Classes with user-written handlers

Still use external run(messageBoxObject) function to get the user's choice

Example: picking base location could be done inside the class, so the function spawned by clicking BUILD BASE button will just wait for a response once for position and another time for name...or maybe even only once for a tuple from a single dialog.

 

Actor-centered dialogs(intercept)

ANY TYPE

Classes with user-written handlers (can connect using connectHandlers)

 

Interactive dialogs/windows (save/load, transactions?)

MODAL/SCREENS

Classes with user-written handlers (can connect using connectHandlers)

 

What do you (all) think?

Link to comment
Share on other sites

Forgot one type.

There may be some dialogs that act like parts of a Screen, like the Options dialog - it can be created by a BaseDialog, but all it's events are already automatically registered by CEGUI to module-scoped functions, so no code is needed to register or run it. Just doModal (?) and hide/destroy when closed.

Link to comment
Share on other sites

Message box (one close button)

Query dialogs(ok/cancel, action on research done/ufo found?)

MODAL

Simple class derived from MessageBox? (MessageBox deriving from BaseDialog, only adding a channel)

Create all handler functions in the class with a generator function

Use external run(messageBoxObject) function to get the user's choice

 

Yes, this is a fairly standard problem, and the standard solution is to provide a function along the lines of:

int MessageBox(string const& textToShow, int ButtonsToShow)

And we’ve already made some progress towards this with scripts/graphics/messagebox.py and scripts/graphics/yesnodialog.py. (Although it would probably be nice to merge them together.) We may also want to have a “modeless” messagebox with no button. It would be used in the case LarsW listed, where we want a dialog to say something like “Click on the position on the earth where you want your new base built.” When user is giving the “build base” command sequence.

 

As regards the implementation, I’m thinking that the MessageBox function is just a wrapper for a dialog implemented using the proposed “standard framework.”

 

Complex query dialogs(set base name)

 

As regards the other Complex, Actor-centered and Interactive dialogs, I fail to see any real difference between them. For simple dialogs I don’t see any significant difference in the generator based technique vs. a class. But, I believe when the interaction gets complicated (e.g. the File Save/Load dialog) the class model scales better. So why have two methods?

 

In fact, far as I can see, if the dialog requires embedded logic, you can’t use the genHandler technique. Its only use is to construct “popup menu” type dialogs, where the player picks an option from a menu, but that’s it. If I’m wrong, could you explain how the genHandler can be used for a dialog that asks user to name a newly purchased aircraft?

Link to comment
Share on other sites

Message box (one close button)

Query dialogs(ok/cancel, action on research done/ufo found?)

Yes, this is a fairly standard problem, and the standard solution is to provide a function along the lines of:

int MessageBox(string const& textToShow, int ButtonsToShow)

...

We may also want to have a “modeless” messagebox with no button. It would be used in the case LarsW listed, where we want a dialog to say something like “Click on the position on the earth where you want your new base built.” When user is giving the “build base” command sequence.

 

As regards the implementation, I’m thinking that the MessageBox function is just a wrapper for a dialog implemented using the proposed “standard framework.”

I don't like the idea of one class with a miriad of buttons just because there might be that many :/

If you meant that the messageBox function hides the classes, then yeah, something can be done.

My current idea (taken from working code):

#in messagebox.py ?
...
MessageBox = generateHandler("messagebox_simple", 
										(("Affirmative", 0),))

YesNoResponse = Enum('NEGATIVE', 'AFFIRMATIVE')
YesNoDialog = generateHandlerClass("messagebox_general",
						  (("First", YesNoResponse.AFFIRMATIVE),
						   ("Second", YesNoResponse.NEGATIVE)),
						  YesNoResponse.NEGATIVE) # close dialog response
YesNoDialog.__doc__ = '''Create a dialog with some text and Yes/No buttons.''' #optional documentation
#somewhere in the code
result = runDialog("your text", YesNoDialog)

Of course, the return values could be replace by numbers, or text...

Anyway, for simple dialogs like this I don't see any reason to write anything yourself

...including class definitions :P

 

BTW, that is what this generator does - create a class derived from a "BaseDialog" class and fill it with the correct functions. YesNoDialog is a class here.

There's no problem creating a "modeless" messagebox. Just supply generateHandlerClass with an empty list for generated events.

 

Complex query dialogs(set base name)
I agree with "everything is a class" here, but all the simple dialog classes can just be generated because anything else would be copy-paste.

 

In fact, far as I can see, if the dialog requires embedded logic, you can’t use the genHandler technique. Its only use is to construct “popup menu” type dialogs, where the player picks an option from a menu, but that’s it. If I’m wrong, could you explain how the genHandler can be used for a dialog that asks user to name a newly purchased aircraft?
No, you're correct. That's exactly what I said before.

 

My point is that an input dialog can still return it's values through a channel.

You can write the handlers yourself, but still use runDialog() on it, because the behaviour when looking from the outside will be exactly the same - open dialog, return value, close dialog.

The handler functions of - let's call it NameInputDialog - will just send the text input or a None.

 

And just to annoy JK

Au contrair, generateHandlerClass can still be used with something like NameInputDialog...

though I suspect that for any case even a bit more complex deriving is the only way ^^;

NameInputDialog = generateHandler("messagebox_inputtext",
							  (("Negative", None),),
							  None)
NameInputDialog.__doc__ = '''Create a name input dialog.'''

def handleAffirmative(self,args):
self.channel.send(
	self.dialog.getChild(self.dialog.Name[:-5] + "/InputText").Text)
NameInputDialog.handleAffirmative = handleAffirmative

Edited by reist
Link to comment
Share on other sites

OK, what I think we have here is a failure to communicate.

 

Message box (one close button)

Query dialogs(ok/cancel, action on research done/ufo found?)

Yes, this is a fairly standard problem, and the standard solution is to provide a function along the lines of:

int MessageBox(string const& textToShow, int ButtonsToShow)

...

We may also want to have a “modeless” messagebox with no button. It would be used in the case LarsW listed, where we want a dialog to say something like “Click on the position on the earth where you want your new base built.” When user is giving the “build base” command sequence.

 

As regards the implementation, I’m thinking that the MessageBox function is just a wrapper for a dialog implemented using the proposed “standard framework.”

I don't like the idea of one class with a miriad of buttons just because there might be that many :/

If you meant that the messageBox function hides the classes, then yeah, something can be done.

My current idea (taken from working code):

#in messagebox.py ?
...
MessageBox = generateHandler("messagebox_simple", 
										(("Affirmative", 0),))

YesNoResponse = Enum('NEGATIVE', 'AFFIRMATIVE')
YesNoDialog = generateHandlerClass("messagebox_general",
						  (("First", YesNoResponse.AFFIRMATIVE),
						   ("Second", YesNoResponse.NEGATIVE)),
						  YesNoResponse.NEGATIVE) # close dialog response
YesNoDialog.__doc__ = '''Create a dialog with some text and Yes/No buttons.''' #optional documentation
#somewhere in the code
result = runDialog("your text", YesNoDialog)

Of course, the return values could be replace by numbers, or text...

Anyway, for simple dialogs like this I don't see any reason to write anything yourself

...including class definitions :P

 

BTW, that is what this generator does - create a class derived from a "BaseDialog" class and fill it with the correct functions. YesNoDialog is a class here.

There's no problem creating a "modeless" messagebox. Just supply generateHandlerClass with an empty list for generated events.

I believe we are in general agreement here. In that:

1. We should have a standard MessageBox() function with an interface similar to my proposal.

2. The implementation of MessageBox()'s internals will be done using the "standard dialog framework".

 

The exact details of the implementation (is it multiple dialogs with a switch, or one smart dialog), is in dispute, but this is an implementation detail we don't need to argue over at this point in time, we have a more fundamental one.

 

Complex query dialogs(set base name)
I agree with "everything is a class" here, but all the simple dialog classes can just be generated because anything else would be copy-paste.

 

In fact, far as I can see, if the dialog requires embedded logic, you can’t use the genHandler technique. Its only use is to construct “popup menu” type dialogs, where the player picks an option from a menu, but that’s it. If I’m wrong, could you explain how the genHandler can be used for a dialog that asks user to name a newly purchased aircraft?
No, you're correct. That's exactly what I said before.

 

My point is that an input dialog can still return it's values through a channel.

You can write the handlers yourself, but still use runDialog() on it, because the behaviour when looking from the outside will be exactly the same - open dialog, return value, close dialog.

The handler functions of - let's call it NameInputDialog - will just send the text input or a None.

 

And just to annoy JK

Au contrair, generateHandlerClass can still be used with something like NameInputDialog...

though I suspect that for any case even a bit more complex deriving is the only way ^^;

NameInputDialog = generateHandler("messagebox_inputtext",
							  (("Negative", None),),
							  None)
NameInputDialog.__doc__ = '''Create a name input dialog.'''

def handleAffirmative(self,args):
self.channel.send(
	self.dialog.getChild(self.dialog.Name[:-5] + "/InputText").Text)
NameInputDialog.handleAffirmative = handleAffirmative

I think we're also in agreement here that when we need to create a dialog that's more than a messageBox at miminum we need to:

1. Define function(s) to process player's input.

2. Hook up the function(s) to the .layout.

3. Run the dialog

4. Return any results to the user

 

And we want to allow the programmer to implement this as easily as possible.

 

Where we differ is HOW to do it.

I believe the most general solution would be a class based solution. So for the above example (a dialog that returns text) all that would be needed is to define a class (derived from the base class) with a logic function similar to the hanldeAffirmative, and another function to specify how to connect up handleAffirmative to the .layout - simlar to your generateHandler. Then user invokes the base classe's doModal().

The biggest difference would be that the user's input would be retreived from a member of the dialog, instead of being passed back on a channel.

 

In this case, the code required to implement the dialog will be pretty much the same size as for your generator function. (The difference in size would be a few lines, at most.)

 

And I believe we're in agreement that when the logic starts getting more complicated, we really will want to build the dialog as a class.

So, as I said before, I dont' see any real benefit from providing 2 ways to create dialogs. It's not going to save the programmer much typing, and it increases the intellectual effort.

 

I'd like to have one way of doing dialogs. We don't have two different ways of doing things. (Which I really hate. It makes reading and writing code more difficult. You having to ask yourself, Why are there two ways? How are they different? What are the advantages/disadvantages of the two methods? Which is right for a given situation?

Link to comment
Share on other sites

You mean that you don't like the idea that simple classes will be fully generated into the hirarchy?

Instead you want both types of classes to be written by the programmer completely, with only the connectHandlers and the likes (+ a good hirarchy) to ease implementation.

 

Did I understand you correctly?

 

Just adding something: why shouldn't we use channels anyway, no matter if the code's generated or not?

They make using simple dialogs really simple. I mean, you're going to switch to a different function anyway.

With channels, you spawn a function and in it can just continue the code flow, like this:

result = runningSomeDialog
if result ==...
...

Link to comment
Share on other sites

(09:29:08) dteviot: As regards channels, I think that may be a case of my not understanding them well enough to be comfortable with them.

(09:29:59) reist: dteviot: then explaining them to you would help :)

(09:30:13) reist: i can make it a pretty short explanation

(09:30:33) dteviot: would be nice.

(09:31:28) reist: channels are a way for tasklets to communicate, but also lock each other out

(09:31:42) reist: like a queue

(09:32:30) reist: so one tasklets can create output and another wait to receive it (reminds me some threading+locking material)

(09:33:14) reist: anyway, if you have channels, then you can send a response from the class

(09:33:53) reist: the code waiting for the class will get it and continue on

(09:34:18) reist: if not, then either you need to register a function for the dialog to call to pass the value to

(09:34:25) reist: and some special way to dispose of the dialog

(09:34:37) reist: or some other weirded stuff

(09:35:11) reist: btw, you can't wait on a channel in the main thread (the one CEGUI calls in response to events)

(09:35:27) reist: cause then you lock it, and lock the whole application with it

(09:35:53) reist: so python just refuses to do it, throwing an exception

(09:42:49) dteviot: again, i'm working so forgive the delays. This bit I don't follow.

(09:42:51) dteviot: (09:34:18) reist: if not, then either you need to register a function for the dialog to call to pass the value to

(09:42:51) dteviot: (09:34:25) reist: and some special way to dispose of the dialog

(09:43:05) dteviot: Why can't you create the dialog

(09:43:35) dteviot: put results other than OK/Cancel into a member of the dialog

(09:44:16) dteviot: call a doModal() that will create the CEGUI dialog, and delete it when user hits OK, Cancel or what ever end event(s) you've declared.

(09:44:35) dteviot: Retrieve the additional info from the dialog's member.

(09:45:11) dteviot: and allow python's garbage collector to dispose of the python dialog

(09:45:20) reist: dteviot: because it ruins the flow

(09:45:32) reist: the moment you call doModal you have to finish the function

(09:45:46) reist: so that the graphics and rest of the game continue to run

(09:45:46) dteviot: In this case, the base classes doModal() will create a tasklet, and block on a connection.

(09:47:00) reist: doModal will still have to return immediately after creating this tasklet

(09:47:13) reist: the tasklet is what will run on

(09:47:20) dteviot: Ah, there's our misunderstanding.

(09:48:10) reist: :)

(09:48:42) dteviot: I think someone needs to document this, along with exactly how Python, tasklets, CEGUI and Ogre interact.

(09:48:51) dteviot: Any idea where the relevant docs are?

(09:49:18) reist: ...no

(09:49:25) reist: i don't think we wrote any

(09:49:51) reist: only Garo- , guyver6 and me worked on it

(09:50:00) reist: and we all know how it works

(09:50:09) reist: ...and rincewind i think

(09:50:25) dteviot: I don't. And anyone else coming in is going to have a very painful learning curve.

(09:50:37) reist: right

(09:52:08) reist: but...in C++ you know you register functions to events and none of your functions block

(09:52:29) reist: the difference in stackless python is that you have tasklets, and _they_ can block, but not on events

(09:52:33) reist: they block on channels

(09:53:58) dteviot: I'll have to think about this very carefully, and at moment, don't have the time.

(09:54:21) reist: ...i think there's a lot in the wiki that needs to be rewritten :/

(09:54:52) reist: dteviot: alright

(09:55:28) reist: do you want me to wait for tomorrow before coding a unified hirarchy for the dialogs?

(09:55:42) reist: or the day after?

(09:56:04) dteviot: I'd say, as you understand it, and I don't, proceed with the unified hirarachy.

(09:56:21) dteviot: We can always refactor/rewrite as we learn more.

(09:56:30) reist: yep :)

(09:56:43) dteviot: We're in general agreement with the main points, it's the details that differ.

(09:56:53) reist: and i'll add enough documentation in code to understand what's going on

(09:56:59) dteviot: Thanks.

(09:57:20) reist: np :)

(09:57:58) dteviot: final comment, do you want to propose some naming standards and file locations?

(09:58:39) dteviot: Oh yes, I think I figured out why the layout windows have names like grandparent/parent/window

(09:58:55) dteviot: It's because the sample layouts provided with CEGUI do that.

(09:59:16) reist: it's also because you can't reuse any names

(09:59:40) reist: and we don't want to always stick a prefix to the dialog/window

(10:00:03) reist: only on dialogs that can appear multiple times and such

(10:00:36) dteviot: So EVERY window needs a unique name?

(10:00:49) reist: every WIDGET

Link to comment
Share on other sites

×
×
  • Create New...