Background
----------

Squeak is a Smalltalk implementation.  The standard "full" image includes
an IRC client (runnable with "IRCConnection new openView" or by hitting
Escape, selecting "open..." from the "World" menu, and then selecting
"IRC chat" from the "open..." menu), which had a "new connection"
window that was absurdly sized on my machine.  So I fixed it.  At the
end of this mail I have attached my patch in the form of a change-set.
Save it in a file with a ".cs" extension.  Then, if you open a "file
list window" ("open..." "file list") and click on a file containing this
change-set, you should see a "fileIn" button appear next to the usual
"name", "date", and "size" button.  Clicking on it loads the code into
your running Squeak image.

Running notes on making these changes
-------------------------------------

This is a partly fictionalized account of how I went about exploring
some completely unfamiliar code early in my exploration of the Squeak
image.  I hope it conveys some of the sense of the environment.  You may
wish to play along, doing the steps I went through --- but don't follow
the "putting a self halt into the extent:" method if you can help it.

It's fictionalized because I started it rather late in the process of
the exploration, so I'd already forgotten a lot of the things I'd done
--- especially the mistakes --- and sometimes I didn't notice new
mistakes as I was making them.  I went back and redid things to see bits
I had forgotten.

I brought up an IRC client:
        "IRCConnection new openView"
which was nice but wasn't connected to a server.  The little menu icon
at the top of the scrollbar offered me some help here; there was a
'connect' item on the menu.  It brought up a usable dialog box, but the
dialog was absurdly large; 400x150 would have been fine, and it was
600x400.  I wanted to know why.

I inspected the 'connect' morph (right click on the menu item until
there was a halo around just the menu item, then select the white
wrench, then select "inspect morph") and found that it had a property
called "selector", with value #perform:orSendTo:, and a property called
"arguments", with value #(#openConnectionDialogue a
PluggableTextMorph(1309)).  

That looked like it might be the method being called by that menu item.
So I double-clicked 'openConnectionDialogue' to highlight it and hit
alt-b to browse the methods by that name, and found a list of one, in
IRCConnection.  I clicked on that method to see its source code, which
was a little under fifty lines long; it consisted of creating a
SystemWindow called "dialogue", adding a bunch of crap to it, and
finally calling "dialogue openInWorld."

So I right-clicked on the actual dialog window, selected the wrench,
selected "inspect morph", and lo and behold, the inspector window said
the dialog window was a SystemWindow.  It had a property named "bounds"
which seemed to have the size and position of the window in it --- it
changed when I resized the window by hand.  

So I set out to see who was setting "bounds" to such a large value.
Experimenting in the inspector showed that typing "self openInWorld" and
hitting alt-d ("do it") caused it to get blown way up again. I browsed
the class (clicked on "self" in the inspector and hit alt-b) to find out
who was setting "bounds".  I clicked the "inst vars" button in the
browser and selected "bounds" from the drop-down menu to get a list of
methods; unfortunately most of them only read "bounds" and didn't set
it.

It turns out that the inspector window has a menu item to see the
"methods storing into this inst var" which is what I should have used.
There were several candidates:

- Morph initialize and Morph basicInitialize set the bounds to "self
  defaultBounds", but "self defaultBounds" on the SystemWindow returns
  200x100, not 600x400.  I know this because I can type "self defaultBounds"
  in the inspector window and hit alt-p.
- Morph extent: sets the size of the window to whatever point you pass in.
  Hard to tell who's calling that.
- there's a corresponding Morph position: method to set the position.
- MorphicModel recomputeBounds does some stuff I don't understand, but
  running "self recomputeBounds" makes the window really small, not 600x400.
  I had a "self extent: [EMAIL PROTECTED]" in my inspector; whenever I screwed up
  the size of the window, I just arrowed up to it and hit alt-d to fix
  the window.

Nothing else looked very promising; it all seemed to be related to
moving or cloning or embedding windows, none of which were happening
here.  I figured it was probably somebody calling #extent:, but #extent:
had a lot of senders.  (I could click on extent: and hit alt-n to see
all nine hundred of them.)  There were even a dozen or so inside
SystemWindow.

I tried putting a "self halt." into Morph extent:, (I could edit it
right from the list of methods that stored into bounds, then hit alt-s
to make the change take effect) but that didn't work out so well; Squeak
ran out of memory and crashed after a minute or so.  Presumably the act
of trying to pop up a walkback window was hitting the self halt, which
was trying to pop up another walkback window, etc.  I restarted Squeak.

So I took a different tack: I looked at my SystemWindow browser to see
what happens when you openInWorld.  Except SystemWindow doesn't
implement openInWorld, so I had to double-click on "openInWorld" in my
inspector window and use alt-m to see who implements it.

Morph openInWorld looked like it was probably going to try to call
#openInWorld: (the inspector told me that "self couldOpenInMorphic" was
"true" when I typed it and hit alt-p) so I doubleclicked "openInWorld:"
and hit alt-m to see who implemented that.  Turns out SystemWindow has
an openInWorld: method, so I clicked it.  Lo and behold, it calls a
method called #bounds: with the argument (RealEstateAgent
initialFrameFor: self world: aWorld).  I guessed that #bounds: was a
method to set the bounds, and that the RealEstateAgent expression was
returning [EMAIL PROTECTED]

Unfortunately, I couldn't conveniently evaluate that expression in place
to see if it was in fact returning [EMAIL PROTECTED], since it involved aWorld.
So I double-clicked "initialFrameFor:" and used alt-shift-W to try to
find the method.  (I couldn't use alt-m because the method was called
#initialFrameFor:world:, not #initialFrameFor:.)

This brought up a list of message names containing "initialframefor:",
of which one was #initialFrameFor:world:, and it was implemented only by
the RealEstateAgent class.  It merely delegated to another method,
#initialFrameFor:initialExtent:world, but the argument following
"initialExtent:" was "aView initialExtent".

Now, aView in this case was my SystemWindow, so I typed "self
initialExtent" and hit alt-p in my SystemWindow inspector to see if it
was the one returning [EMAIL PROTECTED]  Sure enough, it was.

Now it remained only to find out why self initialExtent was returning
such a silly size, and to fix it.  My first attempt was typing "self
initialExtent: [EMAIL PROTECTED]" and hitting alt-d, but that just told me that
SystemWindow didn't understand the message #initialExtent.

So I did what I should have done earlier with openInWorld: I typed "self
initialExtent" and selected "debug it" from the middle-click menu.
After clicking the debugger's "Send" button a couple of times, I had
this stack trace visible:
RealEstateAgent class>>standardWindowExtent
UndefinedObject(Object)>>initialExtent
SystemWindow>>initialExtent

It turned out that SystemWindow>>initialExtent simply answered "model
initialExtent", and model was nil, so it inherited the default Object
implementation of initialExtent, which simply answered "RealEstateAgent
standardWindowExtent".

At this point it became apparent that I didn't need to dig in any
deeper, because I didn't really want to change RealEstateAgent
standardWindowExtent to return a size suitable for this little dialog
box.  So I clicked "Proceed" to close the debugger window.

Now, I had two things I could do: I could either give the "connect to an
IRC server" SystemWindow a "model", one whose initialExtent would return
the right thing, or I could make an "IRCConnectDialogue" class that
would have an override for #initialExtent and maybe also break up the
code in the openConnectionDialogue method into method-sized chunks.  Or,
as a third alternative, I could give SystemWindow an instance variable
that would be its return value for initialExtent if it didn't have a
model, but that didn't sit as well with me.

So I created a new class called IRCConnectionDialogue. I clicked on a
class in the right category in the System Browser to see the class
definition, then edit it until it looks the way you want, then hit alt-s
--- I made the superclass be SystemWindow and gave it no instance
variables) and I copied and pasted the text of the
openConnectionDialogue method into the class and renamed it
#initialize.  Then I had second thoughts and renamed it openInWorld.  I
removed the temp variable 'dialogue' from the list of temp variables at
the top of the method and replaced it with 'self' throughout the
function, except at the bottom where I called 'super openInWorld.'  Then
I hit alt-s, which told me about the places I'd forgotten and was still
using 'dialogue', so I fixed those.

(Find and replace goes like this: double-click "dialogue", hit alt-h,
type "self", then hit alt-j to find the next "dialogue" and replace it
with "self", and repeat until done.)

Now I had two methods, initialize and openInWorld.  So I clicked
"initialize" in the method list and selected "remove method (x)" from
the menu.  It asked me if I really wanted to remove a method with 609
senders.  I said yes, and it took a while, and I panicked a bit,
thinking I had just removed all of the implementations of "initialize"
--- which wouldn't leave a very functional Squeak.  But quick browsing
showed that other classes still had an "initialize" method.

Now, there were a bunch of references in openConnectionDialogue to
either instance variables of IRCConnection or "self".  On closer
inspection, the things that looked like instance variables were actually
inside of a #(literal array), and to my surprise, when I typed "#(a b c
d)" into my workspace and did alt-p, I got back "#(#a #b #c #d)" --- so
those apparent instance variables were actually symbols, which could be
used as message names or something.  Good thing, because there's no
"portAsString" instance variable of IRCConnection, although there is a
portAsString method.

So anyway, the references to self mostly have to do with what text
morphs and buttons do.  So I decided I should put all this stuff in yet
a third class, since the textboxes and buttons clearly need access to
the IRCConnection, and require you to do something like
"(IRCConnectionDialogue new for: self) openInWorld" in
IRCConnection>>openConnectionDialogue.  Double-clicking on "for:" and
hitting alt-m shows me that there's only one implementor of the method
#for:, and it's in some class called RandomEnvelope, so I don't think I
have to worry about a method-name conflict.

So I rename my new openInWorld method to "for: aConnection", replace the
"self"s in it that used to refer to the IRCConnection with
"aConnection", remove my "openInWorld" method (click, alt-x, confirm),
and then remove the "super openInWorld" call from the end of for:.

Now I go back to IRCConnection and change its openConnectionDialogue
method to do the above new for: crap, and then I try it out.  It appears
to work.  (Although the IRC client is kind of hard to use.)

Now to do what I came here to do in the first place: override
initialExtent to return something reasonable.  In the browser looking at
IRCConnectionDialogue, I click on "as yet unclassified" in the message
classification, and then click in the pane below to write an
initialExtent method that just does "^ [EMAIL PROTECTED]", then hit alt-s to make
it work.

That seems to have worked.

Now I'm going to refactor this code a little bit.  There's a loop body
that I think should be a method, and it appends to a temp variable list
that I think should be an instance variable.

In the process of attempting this refactoring, I accidentally deleted
the body of the loop and didn't save it anywhere.  But it wasn't a big
deal, because Squeak keeps all the previous versions of the method
around; I just clicked on the "versions" button, found the version
before the one I'd screwed up in the "previous versions" window, then
hit the "revert" button.

A couple of times I accidentally created a class called NameOfSubclass
and had to delete it.

I turned a lot of temporary variables into instance variables while
doing this.  At one point, I wanted to move the initializations of these
instance variables into something that happened once at instantiation
time, before for: got called, rather than inside for: itself.  In order
to find out whether 'initialize' got called, and whether it was before
for:, I defined an initialize method that just did "self inform:
'initialize called', (textEntryList asString)."  Then when I popped open
the dialog again, it told me when initialize was called, and it also
told me what textEntryList was at the time -- nil.

I also broke it, because I wasn't calling super initialize in my
override of initialize.  The debugger told me this because it was trying
to iterate over subMorphs or something, and subMorphs was nil.

So I added super initialize back in, and it worked.

Pulling the body of the loop out into a method allowed me to use my
inspector to add more fields to the connection dialog box simply by
calling a method; unfortunately the layout stuff didn't seem to work out
right.  This is perhaps unsurprising because the original code was doing
something that looked suspiciously like manually performing layout in a
very static sort of way.  That's the easy way to get grid layouts, I
guess.


The Change Set
--------------

'From Squeak3.6 of ''6 October 2003'' [latest update: #5429] on 16 June 2004 at 
3:06:44 am'!
"Change Set:            IRC Dialog Resizing
Date:                   16 June 2004
Author:                 Kragen Sitaker

I noticed that the IRC connection dialog box was a little over-large in my 
environment, so I thought I'd give it a different initialExtent.  This was a little 
harder than it seemed, since I didn't know anything about Morphic or Squeak, but 
eventually I got it working --- after pulling out the connect dialogue box into its 
own class.

I'm not delighted with the results --- although the dialogue box has a better size 
now, the layout is still pretty fragile.  Making it unfragile seems to require a grid 
layout system, and I can't find one of those yet.
"!

SystemWindow subclass: #IRCConnectionDialogue
        instanceVariableNames: 'textEntryList y yDelta '
        classVariableNames: ''
        poolDictionaries: ''
        category: 'Network-IRC Chat'!

!IRCConnection methodsFor: 'UI' stamp: 'kjs 6/16/2004 01:48'!
openConnectionDialogue
        "open a dialogue for making new connections"
        (IRCConnectionDialogue new for: self) openInWorld.
! !


!IRCConnectionDialogue methodsFor: 'as yet unclassified' stamp: 'kjs 6/16/2004 02:51'!
addEditableFor: aConnection method: aSymbol labeled: aString
        | textEntry descMorph |
        descMorph _ PluggableButtonMorph on: aConnection getState: nil action: nil.
        descMorph
                hResizing: #spaceFill;
                vResizing: #spaceFill;
                label: aString.
        self addMorph: descMorph  frame: ([EMAIL PROTECTED] extent: [EMAIL PROTECTED]).

        textEntry _ PluggableTextMorph 
                on: aConnection 
                text: aSymbol 
                accept: (aSymbol, ':') asSymbol.
        textEntry 
                extent: [EMAIL PROTECTED];
                color: (Color r: 1.0 g: 1.0 b: 0.599);
                acceptOnCR: true.
        self addMorph: textEntry frame: ([EMAIL PROTECTED] extent: [EMAIL PROTECTED]).
        textEntryList add: textEntry.

        y _ y + yDelta.
        ! !

!IRCConnectionDialogue methodsFor: 'as yet unclassified' stamp: 'kjs 6/16/2004 02:35'!
for: aConnection
        "open a dialogue for making new connections"
        | connectButton |

        self 
                addEditableFor: aConnection method: #server labeled: 'server';
                addEditableFor: aConnection method: #portAsString labeled: 'port';
                addEditableFor: aConnection method: #nick labeled: 'nick';
                addEditableFor: aConnection method: #userName labeled: 'username';
                addEditableFor: aConnection method: #fullName labeled: 'full name'.

        connectButton _ PluggableButtonMorph on: [
                        textEntryList do: [ :m | m hasUnacceptedEdits ifTrue:[ m 
accept ] ].
                        aConnection connect 
                ] fixTemps getState: nil action: #value.
        connectButton
                hResizing: #spaceFill;
                vResizing: #spaceFill;
                label: 'connect'.
        self addMorph: connectButton frame: ([EMAIL PROTECTED] extent: [EMAIL 
PROTECTED]).

        self setLabel: 'connect to an IRC server'.
! !

!IRCConnectionDialogue methodsFor: 'as yet unclassified' stamp: 'kjs 6/16/2004 01:56'!
initialExtent
        [EMAIL PROTECTED] !

!IRCConnectionDialogue methodsFor: 'as yet unclassified' stamp: 'kjs 6/16/2004 02:38'!
initialize
        y _ 0.
        yDelta _ 0.8 / 5.
        textEntryList _ OrderedCollection new.

        super initialize.! !


Reply via email to