The Artima Developer Community
Sponsored Link

Agile Buzz Forum
How To Create A Custom Widget - Action Part Button

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
James Robertson

Posts: 29924
Nickname: jarober61
Registered: Jun, 2003

David Buck, Smalltalker at large
How To Create A Custom Widget - Action Part Button Posted: Sep 15, 2004 2:11 PM
Reply to this message Reply

This post originated from an RSS feed registered with Agile Buzz by James Robertson.
Original Post: How To Create A Custom Widget - Action Part Button
Feed Title: Pollock
Feed URL: http://www.cincomsmalltalk.com/rssBlog/pollock-rss.xml
Feed Description: Pollock - the next VW GUI
Latest Agile Buzz Posts
Latest Agile Buzz Posts by James Robertson
Latest Posts From Pollock

Advertisement

Today we'll add the button part to our Calendar


Action Part

We'll start off with the basic code to create the button, named #createActionPart, and then explain it:

	CalendarArtist>>createActionPart
		actionPart := #{Pollock.Button} value new.
		actionPart frame: (FractionalFrame fractionLeft: 1 top: 0 right: 1 bottom: 1).
		actionPart frame leftOffset: [self buttonWidth negated].
		actionPart setArtist: (pane widgetPolicy dropDownListButtonArtistClass on: actionPart).
		actionPart interiorDecoration: pane borderPolicyClass dropDownListButtonDecoration.
		actionPart setEnclosingPane: pane.

actionPart is already an instance variable in ActionDisplayArtist... we use it here to hold on to our button. Then, we give our button a Frame, in this case, a FractionalFrame. We do this because our button is inside our Calendar pane, and no matter how big we make the outer pane, we want the button part to lay itself out inside itself. Notice that we gave the left fraction a value of 1. That butts it up against the right hand side. We follow that up with a value for the left offset. Here, we use a constant, so we'll put it in an method:

	CalendarArtist>>buttonWidth
		^19

How do we know that the button should be 19 wide? We've done some research on various Guidelines, and have found that for Windows 9x/ME/2k, the suggested height of a drop down widget is 23 pixels high. The button in the drop down is square when that default size is used. So, why didn't I use the value 23? Well, that's the whole height of the Calendar. There is an interior decoration, in this case the lowered border we are using, and if we subtract the top and bottom area that takes up, 2 pixels each, we end up with 19. So, when/if the Calendar is set to be it's default height, 23, then our button part will be 19x19, and a perfect square! For future reference, the default height of a MacOSX drop down like widget is 20. Motif doesn't specify a default height, so when we get to it, we'll use 23.


Display

Now we just have to write the code to display the button. We do this in the displayOn: method.

	CalendarArtist>>displayOn: aGraphicsContext
		| oldClippingRectangle |
		self isVisible ifFalse: [^self].
		actionPart ifNil: [self createActionPart].
		oldClippingRectangle := aGraphicsContext clippingBounds.
		(oldClippingRectangle intersects: self frameVisibleRectangle) ifFalse: [^self].
		aGraphicsContext clippingRectangle: (self frameVisibleRectangle intersect: oldClippingRectangle).
		self hasInteriorDecoration ifTrue: [self interiorDecoration displayOn: aGraphicsContext in: self frameVisibleRectangle].
		(oldClippingRectangle intersects: self frameDisplayableRectangle) ifTrue:
			[aGraphicsContext clippingRectangle: (self frameDisplayableRectangle intersect: oldClippingRectangle).
			actionPart displayOn: aGraphicsContext].
		aGraphicsContext clippingRectangle: oldClippingRectangle.

The two blue lines above show the changes. You see we lazy create the button (action part). This is a convention used in all ActionDisplay widgets. We don't have to do it that way... we could have created an initialize method and put the call to #createActionPart in there instead.

Finally we put the call to have the action part display itself inside the "displayable rectangle" block. This ensures that our button will only display inside our interior decoration. So let's try it out. Before we do, let's modify our #openWindowWithCalendar method, to put the origin of our pane at 10 @ 10. Also, let's make the extent 23, so it will display with what we now know is the default height. These changes will make it easier to see what's going on when we open it:

	CalendarTest>>openWindowWithCalendar
		self openWindow.
		calendar := Calendar new.
		calendar frame origin: 10 @ 10.
		calendar frame extent: 100 @ 23.
		window addComponent: calendar

Now, we execute this:

	CalendarTest new openWindowWithCalendar

Details, Details, Details

And... Hmmmm. The button is there, but instead of being inside the bounds of our interior decoration, it overlaps it. The reason for this is a detail I left out last time. When you add an interior decoration, you have to tell the world about the extra area you have carved out of the inside displayable area of the pane. We do this in our #interiorDecoration: method:

	CalendarArtist>>interiorDecoration: anInteriorDecorationBorder
		interiorDecoration := anInteriorDecorationBorder.
		interiorDecoration artist setPane: pane.
		self frame clipDisplayableBounds: (self edgeRectangleIncludingBorderEdge: anInteriorDecorationBorder)

There are two steps in this. First, we tell the interior decoration that the pane it should use as the "owner" pane, is the Calendar pane itself. This facilitates the double dispatch displaying I talked about last time. Also, unlike first class panes, the artist for an interior decoration doesn't automatically point back to the decoration itself. This is also to help with that double dispatching. It's a detail you normally don't have to worry about, just do it.

Next we tell the frame to clip the displayable bounds. This method takes what in Pollock is called an "Edge Rectangle." In fact, it's just a rectangle, but instead of describing an area, the top, left, bottom and right values are used to say how big an edge is. In this case, we have a Pollock framework method all ready to use that takes our decoration (#edgeRectangleIncludingBorderEdge), and supply us with an edge rectangle just right for sending to clipDisplayableBounds.


Get Fancy

Now, we execute the test method again and ... OOooohhh... Nice!... The button is now inside the interior decoration just like it should be. Let's have some fun and make it even cooler! Vassili Bykov, our Tools maven took the time to make a small icon that we can put inside our button. Here's his code for the icon:

	CalendarArtist class>>calendarImage
		"UIMaskEditor new openOnClass: self andSelector: #calendarImage"

		<resource: #image>
		^((Image extent: 13@15 depth: 5 bitsPerPixel: 8 palette: 
		(Graphics.MappedPalette withColors: (#(#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(0 0 0)) 
		#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7227 7227 7227))
		#(#{Graphics.ColorValue} \#scaledRed:scaledGreen:scaledBlue: #(6970 7003 6970)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: 
		#(8030 8030 8030)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(8095 8127 8127)) #(#{Graphics.ColorValue}
		#scaledRed:scaledGreen:scaledBlue: #(8191 8191 8191)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(8127 8095 8127))
		#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7773 7773 7806)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: 
		#(7998 8030 8030)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7773 7806 7773)) #(#{Graphics.ColorValue}
		#scaledRed:scaledGreen:scaledBlue: #(7195 7195 7227)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(5011 5011 5011)) 
		#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7356 7388 7356)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: 
		#(7516 7516 7516)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7356 7356 7356)) #(#{Graphics.ColorValue} 
		#scaledRed:scaledGreen:scaledBlue: #(7516 7516 7484)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7902 7902 7934)) 
		#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7902 7934 7902)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: 
		#(7484 7516 7516)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7902 7902 7902)) #(#{Graphics.ColorValue} 
		#scaledRed:scaledGreen:scaledBlue: #(7067 7099 7099)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(7099 7099 7067)) 
		#(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(5493 1542 1542)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue:
		#(7645 7645 7645)) #(#{Graphics.ColorValue} #scaledRed:scaledGreen:scaledBlue: #(8127 8127 8127)) #(#{Graphics.ColorValue} 
		#scaledRed:scaledGreen:scaledBlue: #(2762 2762 2762)) ) collect: [:each | (each at: 1) value perform: (each at: 2) withArguments: (each at: 3)])) 
		usingBits: (ByteArray fromPackedString: 'FQ$YFQ$YFQ$YFQ$YFP@@@A$EAPTEAPTEAPTEAP@@@@@YAP,KB0,KAPTKB0T@@@@@FPTEAPTEAPTEAPTE@@@@@A$EAPTEAQ$EFPTVF@@@@@@YAPTEAPTEAPTEFA@@@@@@FPTYAQ$EFPTYAAXI@@@@@A$EAPTEAPTEA!@IA0@@@@@YAQ$EFPTYFA$IE!\@@@@@FPTEAPTEFA@IA1\L@@@@@A$EFPTYFA$PFQ\VC @@@@@YAPTEF@ QBQ\OC@(@@@@@FPTYA!$IBQ\OC (T@@@@@A$EFA@IBQ\RC DU@ @@@@@Y@@@@@@@@@@@@@@@@@@@@')) 
		convertForGraphicsDevice: Screen default)

Before you say "I'm Not Going To Enter That!", I know, I know... But, don't forget... I publish all of this on the Cincom Public Repository. So you can pretend to write this part yourself, and this time, load it instead.

So, now we have a basic image thing, but to use it, we need to create a Pollock DisplayImage to hold it, and give it a frame. Here's the code for that:

	 CalendarArtist>>calendarButtonImage
		| displayImage |
		displayImage := #{Pollock.DisplayImage} value image: self class calendarImage.
		displayImage frame: (AlignmentFrame new).
		^displayImage

Then we add this image to our action button when we create it:

	CalendarArtist>>createActionPart
		actionPart := #{Pollock.Button} value new.
		actionPart frame: (FractionalFrame fractionLeft: 1 top: 0 right: 1 bottom: 1).
		actionPart frame leftOffset: [self buttonWidth negated].
		actionPart setArtist: (pane widgetPolicy dropDownListButtonArtistClass on: actionPart).
		actionPart interiorDecoration: pane borderPolicyClass dropDownListButtonDecoration.
		actionPart addComponent: self calendarButtonImage.
		actionPart setEnclosingPane: pane.

Now, we execute it again and ... wow, we're really doing a nice job!


Press The Button

But, if you are like me, and you moved the mouse and clicked on the button we added, you probably saw something not so nice. An error saying "Message not understood: #quietlyRequestActivationFor:"

When a pane is added as a subpane of another pane, there is a mechanism of double dispatching that allows the subpane to configure itself, depending on it's needs. This is called the "setup for becoming a subpane" mechanism. By default, the AbstractPane has a method called #setupForBecomingSubpane: which does nothing. However, if we look at several other implementors, in particular, DropDownList, we see that it does several things. Most important for our issue is that it sets up the keyboard processor for the pane. In Pollock and Wrapper, the keyboard processor, besides forwarding keystrokes to panes that want to relate to them, also keeps a collection of all panes that are allowed to get focus. Well, actually, it keeps track of their controllers, but that's a detail we won't have to deal with.

When this method is called, the caller sends itself as a the parameter. The bottom line is that in order for a widget to get focus, it needs to have it's keyboard processor set up for it. Luckily, all we have to do is call another built in Pollock framework method to get that to happen. So, we create our own #setupForBecomingSubpane: like so:

	Calendar>>setupForBecomingSubpane: aPane 
		self isOpen
			ifTrue: [self setupKeyboardFor: aPane]
			ifFalse: [self configure: #setupKeyboardFor: for: self argument: aPane].
		self triggerEvent: #addedAsSubpane

Last line first, all panes always send #addedAsSubpane at the end of being set up. This facilitates any other panes on your window or form to listen when a pane is added, and possibly react. We'll actually use this later. For now, we can just call it part of the boiler plate.

The first part first asks if the pane is open. A pane is only open when it has been added to a window or other pane, and the window has been opened. It doesn't matter if that window is "iconized" or not. Just if it's been put on the screen. There is a reason we care about this. Pollock has a rule that you can send any pane augmentation message to any widget at any time, without regard to if the window that hosts the pane is open or not. There are some messages, like in our case, getting the keyboard processor, that aren't actually available until after the window is open!

That would put Pollock in a bind... if it didn't have a mechanism to deal with it. The mechanism is #configure:for:. What the #configure:for: messages do is capture a "message" that needs to be sent before a window is open, and then replay it just after the window has actually opened. As you see, it looks sort of like a perform method. The first parameter is the message to send. The second parameter is the eventual executor. In our case, we have to send an argument, so we use the #configure:for:argument: form, and give the last parameter the parameter that the eventual #setupKeyboardFor: needs. #setupKeyboardFor: is a Pollock framework method that takes care of making sure our widget gets properly registered with the keyboard processor so that it can get focus.

Where Pollock says "Any Message, Any Time", that is a responsibility of Pollock's widgets. Since we are creating a new widget, it becomes our responsibility. This is just the first of many that we'll come across in this series. Fortunately, as you see, Pollock makes it E-Z and not a burden. None the less: TANSTAAFL.

Ok, we'll open our test window again, and now press the button,... and ... Darn! Another error message! "Message not understood: #showMouseIndicator:"

This is a part of the ActionDisplay framework that was put in early, but will be refactored out early in 2005. Basically, when you click and hold the mouse over a Radio or CheckBox, in some looks, the circle or box respectively changes the interior color until you release. This turns out not to be needed for our Calendar, nor for other drop down widgets. No matter, we can clear that up easily because the Calendar itself never needs to track mouse up or mouse down events!:

	CalendarAgent>>trackMouseDownEvent: ignore

-+-+-

	CalendarAgent>>trackMouseUpEvent: ignore

Now if we press on the button, nothing bad happens. We'll leave it at that for now. We can run all of our tests and everything works.

The above is published as version 1.6 in the Package named "Pollock-Calendar" on the Cincom public repository.

You may notice, that while nothing bad happens, nothing good happens either. The Button in our Calendar doesn't react like a button, instead it just sits there when we press it. Next time we'll discuss the above track mouse methods, as well as make our button act like a button.


And So It Goes
Sames

Read: How To Create A Custom Widget - Action Part Button

Topic: OOPSLA panel gets some attention Previous Topic   Next Topic Topic: Near Basel today?

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use