| <chapter> | |
| <title>Windowing system</title> | |
| <sect1> | |
| <title>USER Module</title> | |
| <para> | |
| USER implements windowing and messaging subsystems. It also | |
| contains code for common controls and for other | |
| miscellaneous stuff (rectangles, clipboard, WNet, etc). | |
| Wine USER code is located in <filename>windows/</filename>, | |
| <filename>controls/</filename>, and | |
| <filename>misc/</filename> directories. | |
| </para> | |
| <sect2> | |
| <title>Windowing subsystem</title> | |
| <para><filename>windows/win.c</filename></para> | |
| <para><filename>windows/winpos.c</filename></para> | |
| <para> | |
| Windows are arranged into parent/child hierarchy with one | |
| common ancestor for all windows (desktop window). Each | |
| window structure contains a pointer to the immediate | |
| ancestor (parent window if <constant>WS_CHILD</constant> | |
| style bit is set), a pointer to the sibling (returned by | |
| <function>GetWindow(..., GW_NEXT)</function>), a pointer | |
| to the owner window (set only for popup window if it was | |
| created with valid <varname>hwndParent</varname> | |
| parameter), and a pointer to the first child window | |
| (<function>GetWindow(.., GW_CHILD)</function>). All popup | |
| and non-child windows are therefore placed in the first | |
| level of this hierarchy and their ancestor link | |
| (<varname>wnd->parent</varname>) points to the desktop | |
| window. | |
| </para> | |
| <screen> | |
| Desktop window - root window | |
| | \ `-. | |
| | \ `-. | |
| popup -> wnd1 -> wnd2 - top level windows | |
| | \ `-. `-. | |
| | \ `-. `-. | |
| child1 child2 -> child3 child4 - child windows | |
| </screen> | |
| <para> | |
| Horizontal arrows denote sibling relationship, vertical | |
| lines - ancestor/child. To summarize, all windows with the | |
| same immediate ancestor are sibling windows, all windows | |
| which do not have desktop as their immediate ancestor are | |
| child windows. Popup windows behave as topmost top-level | |
| windows unless they are owned. In this case the only | |
| requirement is that they must precede their owners in the | |
| top-level sibling list (they are not topmost). Child | |
| windows are confined to the client area of their parent | |
| windows (client area is where window gets to do its own | |
| drawing, non-client area consists of caption, menu, | |
| borders, intrinsic scrollbars, and | |
| minimize/maximize/close/help buttons). | |
| </para> | |
| <para> | |
| Another fairly important concept is | |
| <firstterm>z-order</firstterm>. It is derived from the | |
| ancestor/child hierarchy and is used to determine | |
| "above/below" relationship. For instance, in the example | |
| above, z-order is | |
| </para> | |
| <screen> | |
| child1->popup->child2->child3->wnd1->child4->wnd2->desktop. | |
| </screen> | |
| <para> | |
| Current active window ("foreground window" in Win32) is | |
| moved to the front of z-order unless its top-level | |
| ancestor owns popup windows. | |
| </para> | |
| <para> | |
| All these issues are dealt with (or supposed to be) in | |
| <filename>windows/winpos.c</filename> with | |
| <function>SetWindowPos()</function> being the primary | |
| interface to the window manager. | |
| </para> | |
| <para> | |
| Wine specifics: in default and managed mode each top-level | |
| window gets its own X counterpart with desktop window | |
| being basically a fake stub. In desktop mode, however, | |
| only desktop window has an X window associated with it. | |
| Also, <function>SetWindowPos()</function> should | |
| eventually be implemented via | |
| <function>Begin/End/DeferWindowPos()</function> calls and | |
| not the other way around. | |
| </para> | |
| <sect3> | |
| <title>Visible region, clipping region and update region</title> | |
| <para><filename>windows/dce.c</filename></para> | |
| <para><filename>windows/winpos.c</filename></para> | |
| <para><filename>windows/painting.c</filename></para> | |
| <screen> | |
| ________________________ | |
| |_________ | A and B are child windows of C | |
| | A |______ | | |
| | | | | | |
| |---------' | | | |
| | | B | | | |
| | | | | | |
| | `------------' | | |
| | C | | |
| `------------------------' | |
| </screen> | |
| <para> | |
| Visible region determines which part of the window is | |
| not obscured by other windows. If a window has the | |
| <constant>WS_CLIPCHILDREN</constant> style then all | |
| areas below its children are considered invisible. | |
| Similarly, if the <constant>WS_CLIPSIBLINGS</constant> | |
| bit is in effect then all areas obscured by its siblings | |
| are invisible. Child windows are always clipped by the | |
| boundaries of their parent windows. | |
| </para> | |
| <para> | |
| B has a <constant>WS_CLIPSIBLINGS</constant> style: | |
| </para> | |
| <screen> | |
| . ______ | |
| : | | | |
| | ,-----' | | |
| | | B | - visible region of B | |
| | | | | |
| : `------------' | |
| </screen> | |
| <para> | |
| When the program requests a <firstterm>display | |
| context</firstterm> (DC) for a window it can specify | |
| an optional clipping region that further restricts the | |
| area where the graphics output can appear. This area is | |
| calculated as an intersection of the visible region and | |
| a clipping region. | |
| </para> | |
| <para> | |
| Program asked for a DC with a clipping region: | |
| </para> | |
| <screen> | |
| ______ | |
| ,--|--. | . ,--. | |
| ,--+--' | | : _: | | |
| | | B | | => | | | - DC region where the painting will | |
| | | | | | | | be visible | |
| `--|-----|---' : `----' | |
| `-----' | |
| </screen> | |
| <para> | |
| When the window manager detects that some part of the window | |
| became visible it adds this area to the update region of this | |
| window and then generates <constant>WM_ERASEBKGND</constant> and | |
| <constant>WM_PAINT</constant> messages. In addition, | |
| <constant>WM_NCPAINT</constant> message is sent when the | |
| uncovered area intersects a nonclient part of the window. | |
| Application must reply to the <constant>WM_PAINT</constant> | |
| message by calling the | |
| <function>BeginPaint()</function>/<function>EndPaint()</function> | |
| pair of functions. <function>BeginPaint()</function> returns a DC | |
| that uses accumulated update region as a clipping region. This | |
| operation cleans up invalidated area and the window will not | |
| receive another <constant>WM_PAINT</constant> until the window | |
| manager creates a new update region. | |
| </para> | |
| <para> | |
| A was moved to the left: | |
| </para> | |
| <screen> | |
| ________________________ ... / C update region | |
| |______ | : .___ / | |
| | A |_________ | => | ...|___|.. | |
| | | | | | : | | | |
| |------' | | | : '---' | |
| | | B | | | : \ | |
| | | | | : \ | |
| | `------------' | B update region | |
| | C | | |
| `------------------------' | |
| </screen> | |
| <para> | |
| Windows maintains a display context cache consisting of | |
| entries that include the DC itself, the window to which | |
| it belongs, and an optional clipping region (visible | |
| region is stored in the DC itself). When an API call | |
| changes the state of the window tree, window manager has | |
| to go through the DC cache to recalculate visible | |
| regions for entries whose windows were involved in the | |
| operation. DC entries (DCE) can be either private to the | |
| window, or private to the window class, or shared | |
| between all windows (Windows 3.1 limits the number of | |
| shared DCEs to 5). | |
| </para> | |
| </sect3> | |
| </sect2> | |
| <sect2> | |
| <title>Messaging subsystem</title> | |
| <para><filename>windows/queue.c</filename></para> | |
| <para><filename>windows/message.c</filename></para> | |
| <para> | |
| Each Windows task/thread has its own message queue - this | |
| is where it gets messages from. Messages can be: | |
| <orderedlist> | |
| <listitem> | |
| <para> | |
| generated on the fly (<constant>WM_PAINT</constant>, | |
| <constant>WM_NCPAINT</constant>, | |
| <constant>WM_TIMER</constant>) | |
| </para> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| created by the system (hardware messages) | |
| </para> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| posted by other tasks/threads (<function>PostMessage</function>) | |
| </para> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| sent by other tasks/threads (<function>SendMessage</function>) | |
| </para> | |
| </listitem> | |
| </orderedlist> | |
| </para> | |
| <para> | |
| Message priority: | |
| </para> | |
| <para> | |
| First the system looks for sent messages, then for posted | |
| messages, then for hardware messages, then it checks if | |
| the queue has the "dirty window" bit set, and, finally, it | |
| checks for expired timers. See | |
| <filename>windows/message.c</filename>. | |
| </para> | |
| <para> | |
| From all these different types of messages, only posted | |
| messages go directly into the private message queue. | |
| System messages (even in Win95) are first collected in the | |
| system message queue and then they either sit there until | |
| <function>Get/PeekMessage</function> gets to process them | |
| or, as in Win95, if system queue is getting clobbered, a | |
| special thread ("raw input thread") assigns them to the | |
| private queues. Sent messages are queued separately and | |
| the sender sleeps until it gets a reply. Special messages | |
| are generated on the fly depending on the window/queue | |
| state. If the window update region is not empty, the | |
| system sets the <constant>QS_PAINT</constant> bit in the | |
| owning queue and eventually this window receives a | |
| <constant>WM_PAINT</constant> message | |
| (<constant>WM_NCPAINT</constant> too if the update region | |
| intersects with the non-client area). A timer event is | |
| raised when one of the queue timers expire. Depending on | |
| the timer parameters <function>DispatchMessage</function> | |
| either calls the callback function or the window | |
| procedure. If there are no messages pending the | |
| task/thread sleeps until messages appear. | |
| </para> | |
| <para> | |
| There are several tricky moments (open for discussion) - | |
| </para> | |
| <itemizedlist> | |
| <listitem> | |
| <para> | |
| System message order has to be honored and messages | |
| should be processed within correct task/thread | |
| context. Therefore when <function>Get/PeekMessage</function> encounters | |
| unassigned system message and this message appears not | |
| to be for the current task/thread it should either | |
| skip it (or get rid of it by moving it into the | |
| private message queue of the target task/thread - | |
| Win95, AFAIK) and look further or roll back and then | |
| yield until this message gets processed when system | |
| switches to the correct context (Win16). In the first | |
| case we lose correct message ordering, in the second | |
| case we have the infamous synchronous system message | |
| queue. Here is a post to one of the OS/2 newsgroup I | |
| found to be relevant: | |
| </para> | |
| <blockquote> | |
| <attribution>by David Charlap</attribution> | |
| <para> | |
| " Here's the problem in a nutshell, and there is no | |
| good solution. Every possible solution creates a | |
| different problem. | |
| </para> | |
| <para> | |
| With a windowing system, events can go to many | |
| different windows. Most are sent by applications or | |
| by the OS when things relating to that window happen | |
| (like repainting, timers, etc.) | |
| </para> | |
| <para> | |
| Mouse input events go to the window you click on | |
| (unless some window captures the mouse). | |
| </para> | |
| <para> | |
| So far, no problem. Whenever an event happens, you | |
| put a message on the target window's message queue. | |
| Every process has a message queue. If the process | |
| queue fills up, the messages back up onto the system | |
| queue. | |
| </para> | |
| <para> | |
| This is the first cause of apps hanging the GUI. If | |
| an app doesn't handle messages and they back up into | |
| the system queue, other apps can't get any more | |
| messages. The reason is that the next message in | |
| line can't go anywhere, and the system won't skip | |
| over it. | |
| </para> | |
| <para> | |
| This can be fixed by making apps have bigger private | |
| message queues. The SIQ fix does this. PMQSIZE does | |
| this for systems without the SIQ fix. Applications | |
| can also request large queues on their own. | |
| </para> | |
| <para> | |
| Another source of the problem, however, happens when | |
| you include keyboard events. When you press a key, | |
| there's no easy way to know what window the | |
| keystroke message should be delivered to. | |
| </para> | |
| <para> | |
| Most windowing systems use a concept known as | |
| "focus". The window with focus gets all incoming | |
| keyboard messages. Focus can be changed from window | |
| to window by apps or by users clicking on windows. | |
| </para> | |
| <para> | |
| This is the second source of the problem. Suppose | |
| window A has focus. You click on window B and start | |
| typing before the window gets focus. Where should | |
| the keystrokes go? On the one hand, they should go | |
| to A until the focus actually changes to B. On the | |
| other hand, you probably want the keystrokes to go | |
| to B, since you clicked there first. | |
| </para> | |
| <para> | |
| OS/2's solution is that when a focus-changing event | |
| happens (like clicking on a window), OS/2 holds all | |
| messages in the system queue until the focus change | |
| actually happens. This way, subsequent keystrokes | |
| go to the window you clicked on, even if it takes a | |
| while for that window to get focus. | |
| </para> | |
| <para> | |
| The downside is that if the window takes a real long | |
| time to get focus (maybe it's not handling events, | |
| or maybe the window losing focus isn't handling | |
| events), everything backs up in the system queue and | |
| the system appears hung. | |
| </para> | |
| <para> | |
| There are a few solutions to this problem. | |
| </para> | |
| <para> | |
| One is to make focus policy asynchronous. That is, | |
| focus changing has absolutely nothing to do with the | |
| keyboard. If you click on a window and start typing | |
| before the focus actually changes, the keystrokes go | |
| to the first window until focus changes, then they | |
| go to the second. This is what X-windows does. | |
| </para> | |
| <para> | |
| Another is what NT does. When focus changes, | |
| keyboard events are held in the system message | |
| queue, but other events are allowed through. This is | |
| "asynchronous" because the messages in the system | |
| queue are delivered to the application queues in a | |
| different order from that with which they were | |
| posted. If a bad app won't handle the "lose focus" | |
| message, it's of no consequence - the app receiving | |
| focus will get its "gain focus" message, and the | |
| keystrokes will go to it. | |
| </para> | |
| <para> | |
| The NT solution also takes care of the application | |
| queue filling up problem. Since the system delivers | |
| messages asynchronously, messages waiting in the | |
| system queue will just sit there and the rest of the | |
| messages will be delivered to their apps. | |
| </para> | |
| <para> | |
| The OS/2 SIQ solution is this: When a | |
| focus-changing event happens, in addition to | |
| blocking further messages from the application | |
| queues, a timer is started. When the timer goes | |
| off, if the focus change has not yet happened, the | |
| bad app has its focus taken away and all messages | |
| targeted at that window are skipped. When the bad | |
| app finally handles the focus change message, OS/2 | |
| will detect this and stop skipping its messages. | |
| </para> | |
| <para> | |
| As for the pros and cons: | |
| </para> | |
| <para> | |
| The X-windows solution is probably the easiest. The | |
| problem is that users generally don't like having to | |
| wait for the focus to change before they start | |
| typing. On many occasions, you can type and the | |
| characters end up in the wrong window because | |
| something (usually heavy system load) is preventing | |
| the focus change from happening in a timely manner. | |
| </para> | |
| <para> | |
| The NT solution seems pretty nice, but making the | |
| system message queue asynchronous can cause similar | |
| problems to the X-windows problem. Since messages | |
| can be delivered out of order, programs must not | |
| assume that two messages posted in a particular | |
| order will be delivered in that same order. This | |
| can break legacy apps, but since Win32 always had an | |
| asynchronous queue, it is fair to simply tell app | |
| designers "don't do that". It's harder to tell app | |
| designers something like that on OS/2 - they'll | |
| complain "you changed the rules and our apps are | |
| breaking." | |
| </para> | |
| <para> | |
| The OS/2 solution's problem is that nothing happens | |
| until you try to change window focus, and then wait | |
| for the timeout. Until then, the bad app is not | |
| detected and nothing is done." | |
| </para> | |
| </blockquote> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| Intertask/interthread | |
| <function>SendMessage</function>. The system has to | |
| inform the target queue about the forthcoming message, | |
| then it has to carry out the context switch and wait | |
| until the result is available. Win16 stores necessary | |
| parameters in the queue structure and then calls | |
| <function>DirectedYield()</function> function. | |
| However, in Win32 there could be several messages | |
| pending sent by preemptively executing threads, and in | |
| this case <function>SendMessage</function> has to | |
| build some sort of message queue for sent messages. | |
| Another issue is what to do with messages sent to the | |
| sender when it is blocked inside its own | |
| <function>SendMessage</function>. | |
| </para> | |
| </listitem> | |
| </itemizedlist> | |
| </sect2> | |
| <sect2 id="accel-impl"> | |
| <title>Accelerators</title> | |
| <para> | |
| There are <emphasis>three</emphasis> differently sized | |
| accelerator structures exposed to the user: | |
| </para> | |
| <orderedlist> | |
| <listitem> | |
| <para> | |
| Accelerators in NE resources. This is also the internal | |
| layout of the global handle <type>HACCEL</type> (16 and | |
| 32) in Windows 95 and Wine. Exposed to the user as Win16 | |
| global handles <type>HACCEL16</type> and | |
| <type>HACCEL32</type> by the Win16/Win32 API. | |
| These are 5 bytes long, with no padding: | |
| <programlisting> | |
| BYTE fVirt; | |
| WORD key; | |
| WORD cmd; | |
| </programlisting> | |
| </para> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| Accelerators in PE resources. They are exposed to the | |
| user only by direct accessing PE resources. These have a | |
| size of 8 bytes: | |
| </para> | |
| <programlisting> | |
| BYTE fVirt; | |
| BYTE pad0; | |
| WORD key; | |
| WORD cmd; | |
| WORD pad1; | |
| </programlisting> | |
| </listitem> | |
| <listitem> | |
| <para> | |
| Accelerators in the Win32 API. These are exposed to the | |
| user by the <function>CopyAcceleratorTable</function> | |
| and <function>CreateAcceleratorTable</function> functions | |
| in the Win32 API. | |
| These have a size of 6 bytes: | |
| </para> | |
| <programlisting> | |
| BYTE fVirt; | |
| BYTE pad0; | |
| WORD key; | |
| WORD cmd; | |
| </programlisting> | |
| </listitem> | |
| </orderedlist> | |
| <para> | |
| Why two types of accelerators in the Win32 API? We can only | |
| guess, but my best bet is that the Win32 resource compiler | |
| can/does not handle struct packing. Win32 <type>ACCEL</type> | |
| is defined using <function>#pragma(2)</function> for the | |
| compiler but without any packing for RC, so it will assume | |
| <function>#pragma(4)</function>. | |
| </para> | |
| </sect2> | |
| </sect1> | |
| <sect1> | |
| <title>X Windows System interface</title> | |
| <para></para> | |
| <sect2> | |
| <title>Keyboard mapping</title> | |
| <para> | |
| Wine now needs to know about your keyboard layout. This | |
| requirement comes from a need from many apps to have the | |
| correct scancodes available, since they read these directly, | |
| instead of just taking the characters returned by the X | |
| server. This means that Wine now needs to have a mapping | |
| from X keys to the scancodes these programs expect. | |
| </para> | |
| <para> | |
| On startup, Wine will try to recognize the active X layout | |
| by seeing if it matches any of the defined tables. If it | |
| does, everything is alright. If not, you need to define it. | |
| </para> | |
| <para> | |
| To do this, open the file | |
| <filename>dlls/x11drv/keyboard.c</filename> and take a look | |
| at the existing tables. Make a backup copy of it, especially | |
| if you don't use CVS. | |
| </para> | |
| <para> | |
| What you really would need to do, is find out which scancode | |
| each key needs to generate. Find it in the | |
| <function>main_key_scan</function> table, which looks like | |
| this: | |
| </para> | |
| <programlisting> | |
| static const int main_key_scan[MAIN_LEN] = | |
| { | |
| /* this is my (102-key) keyboard layout, sorry if it doesn't quite match yours */ | |
| 0x29,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D, | |
| 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B, | |
| 0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x2B, | |
| 0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0x34,0x35, | |
| 0x56 /* the 102nd key (actually to the right of l-shift) */ | |
| }; | |
| </programlisting> | |
| <para> | |
| Next, assign each scancode the characters imprinted on the | |
| keycaps. This was done (sort of) for the US 101-key keyboard, | |
| which you can find near the top in | |
| <filename>keyboard.c</filename>. It also shows that if there | |
| is no 102nd key, you can skip that. | |
| </para> | |
| <para> | |
| However, for most international 102-key keyboards, we have | |
| done it easy for you. The scancode layout for these already | |
| pretty much matches the physical layout in the | |
| <function>main_key_scan</function>, so all you need to do is | |
| to go through all the keys that generate characters on your | |
| main keyboard (except spacebar), and stuff those into an | |
| appropriate table. The only exception is that the 102nd key, | |
| which is usually to the left of the first key of the last | |
| line (usually <keycap>Z</keycap>), must be placed on a | |
| separate line after the last line. | |
| </para> | |
| <para> | |
| For example, my Norwegian keyboard looks like this | |
| </para> | |
| <screen> | |
| § ! " # ¤ % & / ( ) = ? ` Back- | |
| | 1 2@ 3£ 4$ 5 6 7{ 8[ 9] 0} + \´ space | |
| Tab Q W E R T Y U I O P Å ^ | |
| ¨~ | |
| Enter | |
| Caps A S D F G H J K L Ø Æ * | |
| Lock ' | |
| Sh- > Z X C V B N M ; : _ Shift | |
| ift < , . - | |
| Ctrl Alt Spacebar AltGr Ctrl | |
| </screen> | |
| <para> | |
| Note the 102nd key, which is the <keycap><></keycap> key, to | |
| the left of <keycap>Z</keycap>. The character to the right of | |
| the main character is the character generated by | |
| <keycap>AltGr</keycap>. | |
| </para> | |
| <para> | |
| This keyboard is defined as follows: | |
| </para> | |
| <programlisting> | |
| static const char main_key_NO[MAIN_LEN][4] = | |
| { | |
| "|§","1!","2\"@","3#£","4¤$","5%","6&","7/{","8([","9)]","0=}","+?","\\´", | |
| "qQ","wW","eE","rR","tT","yY","uU","iI","oO","pP","åÅ","¨^~", | |
| "aA","sS","dD","fF","gG","hH","jJ","kK","lL","øØ","æÆ","'*", | |
| "zZ","xX","cC","vV","bB","nN","mM",",;",".:","-_", | |
| "<>" | |
| }; | |
| </programlisting> | |
| <para> | |
| Except that " and \ needs to be quoted with a backslash, and | |
| that the 102nd key is on a separate line, it's pretty | |
| straightforward. | |
| </para> | |
| <para> | |
| After you have written such a table, you need to add it to the | |
| <function>main_key_tab[]</function> layout index table. This | |
| will look like this: | |
| </para> | |
| <programlisting> | |
| static struct { | |
| WORD lang, ansi_codepage, oem_codepage; | |
| const char (*key)[MAIN_LEN][4]; | |
| } main_key_tab[]={ | |
| ... | |
| ... | |
| {MAKELANGID(LANG_NORWEGIAN,SUBLANG_DEFAULT), 1252, 865, &main_key_NO}, | |
| ... | |
| </programlisting> | |
| <para> | |
| After you have added your table, recompile Wine and test that | |
| it works. If it fails to detect your table, try running | |
| </para> | |
| <screen> | |
| WINEDEBUG=+key,+keyboard wine > key.log 2>&1 | |
| </screen> | |
| <para> | |
| and look in the resulting <filename>key.log</filename> file to | |
| find the error messages it gives for your layout. | |
| </para> | |
| <para> | |
| Note that the <constant>LANG_*</constant> and | |
| <constant>SUBLANG_*</constant> definitions are in | |
| <filename>include/winnls.h</filename>, which you might need | |
| to know to find out which numbers your language is assigned, | |
| and find it in the WINEDEBUG output. The numbers will be | |
| <literal>(SUBLANG * 0x400 + LANG)</literal>, so, for example | |
| the combination <literal>LANG_NORWEGIAN (0x14)</literal> and | |
| <literal>SUBLANG_DEFAULT (0x1)</literal> will be (in hex) | |
| <literal>14 + 1*400 = 414</literal>, so since I'm Norwegian, | |
| I could look for <literal>0414</literal> in the WINEDEBUG | |
| output to find out why my keyboard won't detect. | |
| </para> | |
| </sect2> | |
| </sect1> | |
| </chapter> | |
| <!-- Keep this comment at the end of the file | |
| Local variables: | |
| mode: sgml | |
| sgml-parent-document:("wine-devel.sgml" "set" "book" "part" "chapter" "") | |
| End: | |
| --> |