OSDN Git Service

Eliminate invalid comparisons of "this" with nullptr.
[mingw/mingw-get.git] / src / pkgdata.cpp
1 /*
2  * pkgdata.cpp
3  *
4  * $Id$
5  *
6  * Written by Keith Marshall <keith@users.osdn.me>
7  * Copyright (C) 2012, 2013, 2020, MinGW.org Project
8  *
9  *
10  * Implementation of the classes and methods required to support the
11  * GUI tabbed view of package information "data sheets".
12  *
13  *
14  * This is free software.  Permission is granted to copy, modify and
15  * redistribute this software, under the provisions of the GNU General
16  * Public License, Version 3, (or, at your option, any later version),
17  * as published by the Free Software Foundation; see the file COPYING
18  * for licensing details.
19  *
20  * Note, in particular, that this software is provided "as is", in the
21  * hope that it may prove useful, but WITHOUT WARRANTY OF ANY KIND; not
22  * even an implied WARRANTY OF MERCHANTABILITY, nor of FITNESS FOR ANY
23  * PARTICULAR PURPOSE.  Under no circumstances will the author, or the
24  * MinGW Project, accept liability for any damages, however caused,
25  * arising from the use of this software.
26  *
27  */
28 #define _WIN32_IE 0x0300
29
30 #include <stdlib.h>
31 #include <string.h>
32
33 #include "dmh.h"
34
35 #include "guimain.h"
36 #include "pkgbase.h"
37 #include "pkgdata.h"
38 #include "pkgkeys.h"
39 #include "pkginfo.h"
40 #include "pkglist.h"
41 #include "pkgproc.h"
42 #include "pkgtask.h"
43
44 #include <windowsx.h>
45
46 using WTK::StringResource;
47 using WTK::WindowClassMaker;
48 using WTK::ChildWindowMaker;
49
50 /* Margin settings, controlling the positioning of the
51  * active viewport within the data sheet display pane.
52  */
53 #define TOP_MARGIN              5
54 #define PARAGRAPH_MARGIN        5
55 #define LEFT_MARGIN             8
56 #define RIGHT_MARGIN            8
57 #define BOTTOM_MARGIN           5
58
59 class pkgTroffLayoutEngine: public pkgUTF8Parser
60 {
61   /* A privately implemented class, supporting a simplified troff
62    * style layout for a UTF-8 text stream, within a scrolling GUI
63    * display pane.
64    */
65   public:
66     pkgTroffLayoutEngine( const char *input, long displacement ):
67       pkgUTF8Parser( input ), curr( this ), offset( displacement ){}
68     bool WriteLn( HDC, RECT * );
69
70   private:
71     inline bool IsReady();
72     pkgTroffLayoutEngine *curr;
73     long offset;
74 };
75
76 inline bool pkgTroffLayoutEngine::IsReady()
77 {
78   /* Private helper method, used to position the input stream to
79    * the next parseable token, if any, for processing by WriteLn.
80    */
81   while( (curr != NULL) && ((curr->length == 0) || (curr->text == NULL)) )
82     curr = (pkgTroffLayoutEngine *)(curr->next);
83   return (curr != NULL);
84 }
85
86 bool pkgTroffLayoutEngine::WriteLn( HDC canvas, RECT *bounds )
87 {
88   /* Method to extract a single line of text from the UTF-8 stream,
89    * (if any is available for processing), format it as appropriate
90    * for display, and write it into the display pane.
91    */
92   if( IsReady() )
93   {
94     /* Initialise a buffer, in which to compile the formatted text
95      * record for display; establish and initialise the counters for
96      * controlling the formatting process.
97      */
98     wchar_t linebuf[1 + strlen( curr->text )];
99     long curr_width, new_width = 0, max_width = bounds->right - bounds->left;
100     int filled, extent = 0, fold = 0;
101
102     /* Establish default tracking and justification settings.
103      */
104     SetTextCharacterExtra( canvas, 0 );
105     SetTextJustification( canvas, 0, 0 );
106
107     /* Copy text from the input stream, to fill the transfer buffer
108      * up to the maximum permitted output line length, or until the
109      * input stream has been exhausted.
110      */
111     SIZE span;
112     do { if( curr->length > 0 )
113          { /* There is at least one more word of input text to copy,
114             * and there may be sufficient output space to accommodate
115             * it; record the space filled so far, up to the end of the
116             * preceding word, (if any)...
117             */
118            filled = extent;
119            curr_width = new_width;
120            if( extent > 0 )
121            {
122              /* ...and, when there was a preceding word, add white
123               * space and record a potential line folding point.
124               */
125              linebuf[extent++] = L'\x20';
126              ++fold;
127            }
128
129            /* Append one word, copied from the input stream to the
130             * output line buffer.
131             */
132            const char *mark = curr->text;
133            for( int i = 0; i < curr->length; ++i )
134              linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
135
136            /* Check the effective output line length which would be
137             * required to accommodate the extended output record...
138             */
139            if( GetTextExtentPoint32W( canvas, linebuf, extent, &span ) )
140            {
141              /* ...and while it still fits within the maximum width
142               * of the display pane...
143               */
144              if( max_width >= (new_width = span.cx) )
145                /*
146                 * ...accept the current input word, and move on to
147                 * see if we can accommodate another.
148                 */
149                curr = (pkgTroffLayoutEngine *)(curr->next);
150            }
151            else
152              /* In the event of any error in evaluating the output
153               * line length, reject any remaining input.
154               */
155              curr = NULL;
156          }
157          else
158            /* We found a zero-length entity in the input stream;
159             * ignore it, and move on to the next, if any.
160             */
161            curr = (pkgTroffLayoutEngine *)(curr->next);
162
163          /* Continue the cycle, unless we have exhausted the input
164           * stream, or we have run out of available output space.
165           */
166        } while( (curr != NULL) && (max_width > new_width) );
167
168     /* When we've collected a complete line of output text...
169      */
170     if(  (bounds->top >= (TOP_MARGIN + offset))
171     &&  ((bounds->bottom + offset) >= (bounds->top + span.cy))  )
172     {
173       /* ...and when it is to be positioned vertically within the
174        * bounds of the active viewport...
175        */
176       if( bounds->top < (TOP_MARGIN + offset + span.cy) )
177         /*
178          * ...when it is the topmost visible line, ensure that it
179          * is vertically aligned flush with the top margin.
180          */
181         bounds->top = TOP_MARGIN + offset;
182
183       /* Check if the output line collection loop, above, ended
184        * on an attempt to over-fill the buffer...
185        */
186       if( max_width >= new_width )
187         /*
188          * ...but when it did not, handle it as a partially filled
189          * line, which is thus exempt from right justification.
190          */
191         filled = extent;
192
193       /* When the output line is over-filled, then we will attempt
194        * to fold it at the last counted fold point, and then insert
195        * padding space at each remaining internal fold point, so as
196        * to achieve flush left/right justification; (note that we
197        * decrement the fold count here, because the point at which
198        * we fold the line has been included in the count, but we
199        * don't want to add padding space at the right margin).
200        */
201       else if( --fold > 0 )
202       {
203         /* To adjust the output line, we first compute the number
204          * of padding PIXELS required, then...
205          */
206         long padding;
207         if( (padding = max_width - curr_width) >= filled )
208         {
209           /* ...in the event that this is no fewer than the number
210            * of physical GLYPHS to be output, we adjust the tracking
211            * to accommodate as many padding pixels as possible, with
212            * ONE additional inter-glyph tracking pixel per glyph...
213            */
214           SetTextCharacterExtra( canvas, 1 );
215           if( GetTextExtentPoint32W( canvas, linebuf, filled, &span ) )
216             /*
217              * ...and then, we recompute the number of additional
218              * inter-word padding pixels, if any, which are still
219              * required.
220              */
221             padding = max_width - span.cx;
222
223           /* In the event that adjustment of tracking fails, we
224            * must reset it, because the padding count remains as
225            * computed for default tracking.
226            */
227           else
228             SetTextCharacterExtra( canvas, 0 );
229         }
230         /* Now, provided the padding pixels will not increase the
231          * inter-word (fold) spacing to more than 5% of the total
232          * line length at each potential fold point...
233          */
234         if( ((padding * 100) / (max_width * fold)) < 5 )
235           /* 
236            * ...distribute the padding pixels among the remaining
237            * inter-word spaces within the output line...
238            */
239           SetTextJustification( canvas, padding, fold );
240
241         else
242           /* ...otherwise, we decline to adjust the output line,
243            * and we prefer to also preserve natural tracking.
244            */
245           SetTextCharacterExtra( canvas, 0 );
246       }
247       else
248       { /* If we get to here, then the first item in the output
249          * queue requires more space than the available width of
250          * the display pane, and has no natural fold points; we
251          * MUST handle this, to avoid an infinite loop!
252          *
253          * FIXME: The method adopted here simply elides the
254          * portion of the input text, which will not fit into
255          * the available display width, at the right hand end of
256          * the line; we may wish to consider adding horizontal
257          * scrolling, so that such elided text may be viewed.
258          *
259          * We begin by loading the content of the first queued
260          * entity into the line transfer buffer.
261          */
262         extent = 0;
263         const char *mark = curr->text;
264         for( int i = 0; i < curr->length; ++i )
265           linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
266
267         /* Reduce the maximum allowable output width sufficiently
268          * to accommodate an ellipsis at the right hand end of the
269          * output line...
270          */
271         int fit = GetTextExtentPoint32W( canvas, L"...", 3, &span )
272           ? max_width - span.cx : max_width;
273
274         /* ...then compute the maximum number of characters from
275          * the queued item, which will fit in the remaining space...
276          */
277         if( GetTextExtentExPointW
278             ( canvas, linebuf, extent, fit, &filled, NULL, &span )
279           )
280           /* ...and then append the ellipsis, in place of any
281            * characters which will not fit, leaving the resultant
282            * line, with elided tail, ready for display.
283            */
284           for( int i = 0; i < 3; ++i )
285             linebuf[filled++] = L'.';
286         
287         /* Finally, pop the entity we just processed from the
288          * output queue, before falling through...
289          */
290         curr = (pkgTroffLayoutEngine *)(curr->next);
291       }
292       /* ...and write the output line at the designated position
293        * within the display viewport.
294        */
295       TextOutW( canvas, bounds->left, bounds->top - offset, linebuf, filled );
296     }
297     else if( (fold == 0) && (new_width > max_width) )
298       /*
299        * The output line which we've just processed lies outside
300        * the viewport.  We note that it's initial (non-breakable)
301        * "word" would require more display width than the viewport
302        * can accommodate, if it were to be moved into the visible
303        * region; thus, this "word" will continue to be presented
304        * to the formatting engine, as the next input "word" to be
305        * processed.  This would result in an infinite loop, so we
306        * MUST discard this "word" from the input queue.
307        */
308       curr = (pkgTroffLayoutEngine *)(curr->next);
309
310     /* Finally, adjust the top boundary of the viewport, to indicate
311      * where the NEXT output line, if any, is to be positioned, and
312      * return TRUE, to indicate that an output line was processed.
313      */
314     bounds->top += span.cy;
315     return true;
316   }
317   /* If we get to here, then there was nothing in the input stream to
318    * be processed; return FALSE, to indicate this.
319    */
320   return false;
321 }
322
323 class DataSheetMaker: public ChildWindowMaker
324 {
325   /* Specialised variant of the standard child window class, augmented
326    * to provide the custom methods for formatting and displaying package
327    * data sheet content within the tabbed data display pane.
328    *
329    * FIXME: we may eventually need to make this class externally visible,
330    * but for now we implement it as a locally declared class.
331    */
332   public:
333     DataSheetMaker( HINSTANCE inst ): ChildWindowMaker( inst ),
334       PackageRef( NULL ), DataClass( NULL ){}
335     virtual void DisplayData( HWND, HWND );
336
337   private:
338     virtual long OnCreate();
339     virtual long OnVerticalScroll( int, int, HWND );
340     virtual long OnPaint();
341
342     HWND PackageRef, DataClass;
343     static DataSheetMaker *Display;
344     HDC canvas; RECT bounding_box; 
345     HFONT NormalFont, BoldFont;
346
347     static int Advance;
348     long offset; char *desc;
349     void DisplayGeneralData( pkgXmlNode * );
350     static int DisplaySourceURL( const char * );
351     static int DisplayLicenceURL( const char * );
352     static int DisplayPackageURL( const char * );
353     inline void DisplayDescription( pkgXmlNode * );
354     inline void DisplayFilesManifest( pkgXmlNode * );
355     void ComposeDescription( pkgXmlNode *, pkgXmlNode * );
356     int FormatRecord( int, const char *, const char * );
357     inline void FormatText( const char * );
358     long LineSpacing;
359 };
360
361 /* Don't forget to instantiate the static member variables...
362  */
363 int DataSheetMaker::Advance;
364 DataSheetMaker *DataSheetMaker::Display;
365
366 enum
367 { /* Tab identifiers for the available data sheet collection.
368    */
369   PKG_DATASHEET_GENERAL = 0,
370   PKG_DATASHEET_DESCRIPTION,
371   PKG_DATASHEET_DEPENDENCIES,
372   PKG_DATASHEET_INSTALLED_FILES,
373   PKG_DATASHEET_VERSIONS
374 };
375
376 long DataSheetMaker::OnCreate()
377 {
378   /* Method called when creating a data sheet window; initialise font
379    * preferences and line spacing for any instance of a DataSheetMaker
380    * object.
381    *
382    * Initially, we match the font properties to the default GUI font...
383    */
384   LOGFONT font_info;
385   HFONT font = (HFONT)(GetStockObject( DEFAULT_GUI_FONT ));
386   GetObject( BoldFont = NormalFont = font, sizeof( LOGFONT ), &font_info );
387
388   /* ...then, we substitute the preferred type face.
389    */
390   strcpy( (char *)(&(font_info.lfFaceName)), "Verdana" );
391   if( (font = CreateFontIndirect( &font_info )) != NULL )
392   {
393     /* On successfully creating the preferred font, we may discard
394      * the original default font object, and assign our preference
395      * as both the normal and bold working font...
396      */
397     DeleteObject( NormalFont );
398     BoldFont = NormalFont = font;
399
400     /* ...before adjusting the weight for the bold variant...
401      */
402     font_info.lfWeight = FW_BOLD;
403     if( (font = CreateFontIndirect( &font_info )) != NULL )
404     {
405       /* ...and reassigning when successful.
406        */
407       BoldFont = font;
408     }
409   }
410
411   /* Finally, we determine the line spacing (in pixels) for a line
412    * of text, in the preferred normal font, within the device context
413    * for the data sheet window.
414    */
415   SIZE span;
416   HDC canvas = GetDC( AppWindow );
417   SelectObject( canvas, NormalFont );
418   LineSpacing = GetTextExtentPoint32A( canvas, "Height", 6, &span ) ? span.cy : 13;
419   ReleaseDC( AppWindow, canvas );
420   
421   return offset = 0;
422 }
423
424 void DataSheetMaker::DisplayData( HWND tab, HWND package )
425 {
426   /* Method to force a refresh of the data sheet display pane.
427    */
428   PackageRef = package; DataClass = tab;
429   InvalidateRect( AppWindow, NULL, TRUE );
430   UpdateWindow( AppWindow );
431 }
432
433 inline void DataSheetMaker::FormatText( const char *text )
434 {
435   /* Helper method to transfer text to the display device, formatting
436    * it to fill as many lines of the viewing window as may be required,
437    * justifying for flush margins at both left and right.
438    */
439   pkgTroffLayoutEngine page( text, offset );
440   while( page.WriteLn( canvas, &bounding_box ) )
441     ;
442 }
443
444 int DataSheetMaker::FormatRecord( int offset, const char *tag, const char *text )
445 {
446   /* Helper method to transfer text to the display device, prefacing
447    * it with a specified record key, before formatting as above.
448    */
449   const char *fmt = "%s: %s";
450   int span = snprintf( NULL, 0, fmt, tag, text );
451   char record[ 1 + span ]; snprintf( record, sizeof( record ), fmt, tag, text );
452   if( offset == 0 )
453     bounding_box.top += PARAGRAPH_MARGIN;
454   FormatText( record );
455   return offset + span;
456 }
457
458 void DataSheetMaker::DisplayGeneralData( pkgXmlNode *ref )
459 {
460   /* Method to compile the package data, which is to be displayed
461    * on the general information tab; we begin by displaying the
462    * identification records for the selected package, and the
463    * subsystem to which it belongs.
464    */
465   FormatRecord( 0, "SubSystem",
466       ref->GetContainerAttribute( subsystem_key, value_unknown )
467     );
468   FormatRecord( 1, "Package Name",
469       ref->GetContainerAttribute( name_key, value_unknown )
470     );
471   if( ref->IsElementOfType( component_key ) )
472     FormatRecord( 1, "Component Class",
473         ref->GetPropVal( class_key, value_unknown )
474       );
475
476   /* Using a temporary action item, collect information on the
477    * latest available version, and the installed version if any,
478    * of the selected package; print the applicable information,
479    * noting that "none" may be appropriate in the case of the
480    * installed version.
481    */
482   pkgActionItem avail;
483   FormatRecord( 0, "Installed Version",
484       ((ref = pkgGetStatus( ref, &avail )) != NULL)
485         ? ref->GetPropVal( tarname_key, value_unknown )
486         : value_none
487     );
488   FormatRecord( 1, "Repository Version",
489       (ref = avail.Selection())->GetPropVal( tarname_key, value_unknown )
490     );
491
492   /* Finally, report the download URLs for the selected package,
493    * and its associated source and licence archives; (note that we
494    * must save static callback references, so that the PrintURI()
495    * method can access this data sheet context).
496    */
497   Display = this; Advance = 0;
498   avail.PrintURI( ref->ArchiveName(), DisplayPackageURL );
499   avail.PrintURI( ref->SourceArchiveName( ACTION_LICENCE ), DisplayLicenceURL );
500   avail.PrintURI( ref->SourceArchiveName( ACTION_SOURCE ), DisplaySourceURL );
501 }
502
503 int DataSheetMaker::DisplayPackageURL( const char *uri )
504 {
505   /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
506    * to display the package download URL on the active data sheet panel.
507    */
508   return Advance = Display->FormatRecord( Advance, "Package URL", uri );
509 }
510
511 int DataSheetMaker::DisplaySourceURL( const char *uri )
512 {
513   /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
514    * to display the source download URL on the active data sheet panel.
515    */
516   return Advance = Display->FormatRecord( Advance, "Source URL", uri );
517 }
518
519 int DataSheetMaker::DisplayLicenceURL( const char *uri )
520 {
521   /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
522    * to display the licence download URL on the active data sheet panel.
523    */
524   return Advance = Display->FormatRecord( Advance, "Licence URL", uri );
525 }
526
527 inline void DataSheetMaker::DisplayDescription( pkgXmlNode *ref )
528 {
529   /* A convenience method to invoke the recursive retrieval of any
530    * package description, without requiring a look-up of the address
531    * of the XML document root at every level of recursion.
532    */
533   ComposeDescription( ref, ref->GetDocumentRoot() );
534 }
535
536 void DataSheetMaker::ComposeDescription( pkgXmlNode *ref, pkgXmlNode *root )
537 {
538   /* Recursive method to compile a package description, from text
539    * fragments retrieved from the XML specification document, and
540    * present it in a data sheet window.
541    */
542   if( ref != root )
543   {
544     /* Recursively walk the XML hierarchy, until we reach the
545      * document root...
546      */
547     ComposeDescription( ref->GetParent(), root );
548
549     /* ...then unwind the recursion, selecting "description"
550      * elements, (if any), at each level...
551      */
552     if( (root = ref->FindFirstAssociate( description_key )) != NULL )
553     {
554       /* ...formatting each, paragraph by paragraph, for display
555        * within the viewport bounding box of the data sheet...
556        */
557       do { if( (ref = root->FindFirstAssociate( paragraph_key )) != NULL )
558              do { if( bounding_box.top > (TOP_MARGIN + offset) )
559                     /*
560                      * When this is not the top-most visible
561                      * paragraph, within the viewport, displace
562                      * it downwards by one paragraph margin from
563                      * its predecessor...
564                      */
565                     bounding_box.top += PARAGRAPH_MARGIN;
566
567                   /* ...before laying out the visible text of
568                    * this paragraph, (if any).
569                    */
570                   FormatText( ref->GetText() );
571
572                   /* Cycle, to process any further paragraphs
573                    * which are included within the current
574                    * description block...
575                    */
576                 } while( (ref = ref->FindNextAssociate( paragraph_key )) != NULL );
577
578            /* ...and ultimately, for any additional description blocks
579             * which may have been specified within the current XML element,
580             * at the current hierarchical nesting level.
581             */
582          } while( (root = root->FindNextAssociate( description_key )) != NULL );
583     }
584   }
585 }
586
587 void DataSheetMaker::DisplayFilesManifest( pkgXmlNode *ref )
588 {
589   /* Helper method to compile the list of files installed by a package,
590    * for display on the "Installed Files" tab of the data sheet panel.
591    */
592   pkgActionItem avail;
593   if( (ref = pkgGetStatus( ref, &avail )) == NULL )
594   {
595     /* This represents a package which is available, but has not been
596      * installed; we simply decline to compile the files list.
597      */
598     FormatRecord( 0, "Package",
599         avail.Selection()->GetPropVal( tarname_key, value_unknown )
600       );
601     bounding_box.top += PARAGRAPH_MARGIN;
602     FormatText(
603         "This package has not been installed; "
604         "the list of installed files is not available for packages "
605         "which have not been installed."
606       );
607   }
608   else
609   { /* This represents a package which has been installed; begin
610      * compilation of the list of installed files.
611      */
612     const char *tarname;
613     FormatRecord( 0, "Package",
614         tarname = ref->GetPropVal( tarname_key, value_unknown )
615       );
616     if( match_if_explicit( ref->ArchiveName(), value_none ) )
617     {
618       /* This is a meta-package; there are no files to list.
619        */
620       bounding_box.top += PARAGRAPH_MARGIN;
621       FormatText(
622           "This meta-package facilitates the installation of a collection "
623           "of other logically related packages; it provides no files, and "
624           "may be safely removed."
625         );
626     }
627     else
628     { /* This is a real package; retrieve the manifest of installed files,
629        * which was created during the installation process.
630        */
631       pkgXmlNode *index;
632       pkgManifest inventory( package_key, tarname );
633       if( (index = inventory.GetRoot()->FindFirstAssociate( manifest_key )) != NULL )
634       {
635         /* We've located a files list within the manifest; process it...
636          */
637         FormatRecord( 0, "This package provides the following files", "" );
638         bounding_box.left += LEFT_MARGIN; bounding_box.top += PARAGRAPH_MARGIN;
639         do { if( (ref = index->FindFirstAssociate( filename_key )) != NULL )
640                /*
641                 * We found at least one file name within the list; emit it
642                 * to the display, followed by any additional names present.
643                 */
644                do { FormatText( ref->GetPropVal( pathname_key, value_unknown ) );
645                   } while( (ref = ref->FindNextAssociate( filename_key )) != NULL );
646
647              /* There should be no more than one, but check for, and process
648               * any additional files lists which may be present.
649               */
650            } while( (index = index->FindNextAssociate( manifest_key )) != NULL );
651       }
652       else
653       { /* The manifest appears to be lacking any files list; diagnose.
654          */
655         bounding_box.top += PARAGRAPH_MARGIN;
656         FormatText( "This package appears to provide no files." );
657       }
658     }
659   }
660 }
661
662 static pkgXmlNode *pkgListSelection( HWND package_ref, LVITEM *lookup )
663 {
664   /* Helper function, to retrieve the active selection from the
665    * package list, as displayed in the upper right data pane.
666    */
667   lookup->iSubItem = 0;
668   lookup->mask = LVIF_PARAM;
669   ListView_GetItem( package_ref, lookup );
670   return (pkgXmlNode *)(lookup->lParam);
671 }
672
673 long DataSheetMaker::OnPaint()
674 {
675   /* Handler for WM_PAINT message messages, sent to any window
676    * ascribed to the DataSheetMaker class.
677    */
678   PAINTSTRUCT content;
679   canvas = BeginPaint( AppWindow, &content );
680   HFONT original_font = (HFONT)(SelectObject( canvas, NormalFont ));
681
682   /* Establish a viewport, with a suitable margin, within the
683    * bounding rectangle of the window.
684    */
685   GetClientRect( AppWindow, &bounding_box );
686   bounding_box.left += LEFT_MARGIN; bounding_box.right -= RIGHT_MARGIN;
687   bounding_box.top += TOP_MARGIN; bounding_box.bottom -= BOTTOM_MARGIN;
688
689   /* Provide bindings for a vertical scrollbar...
690    */
691   SCROLLINFO scrollbar;
692   if( offset == 0 )
693   {
694     /* ...and prepare to initialise it, when we redraw
695      * the data sheet from the top.
696      */
697     scrollbar.cbSize = sizeof( scrollbar );
698     scrollbar.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
699     scrollbar.nPos = scrollbar.nMin = bounding_box.top;
700     scrollbar.nPage = 1 + bounding_box.bottom - bounding_box.top;
701     scrollbar.nMax = bounding_box.bottom;
702   }
703
704   /* Identify the package, as selected in the package list window,
705    * for which the data sheet is to be compiled.
706    */
707   LVITEM lookup;
708   lookup.iItem = (PackageRef != NULL)
709     ? ListView_GetNextItem( PackageRef, (WPARAM)(-1), LVIS_SELECTED )
710     : -1;
711
712   if( lookup.iItem >= 0 )
713   {
714     /* There is an active package selection; identify the selected
715      * data sheet tab, if any...
716      */
717     int tab = ( DataClass != NULL ) ? TabCtrl_GetCurSel( DataClass )
718       /*
719        * ...otherwise default to the package description.
720        */
721       : PKG_DATASHEET_DESCRIPTION;
722
723     /* Retrieve the package title from the list view; assign it as
724      * a bold face heading in the data sheet view...
725      */
726     char desc[256];
727     SelectObject( canvas, BoldFont );
728     ListView_GetItemText( PackageRef, lookup.iItem, 5, desc, sizeof( desc ) );
729     FormatText( desc );
730     if( offset > 0 )
731     {
732       /* ...adjusting as appropriate, when the heading is scrolled
733        * out of the viewport.
734        */
735       if( (offset -= (bounding_box.top - TOP_MARGIN)) < 0 )
736         offset = 0;
737       bounding_box.top = TOP_MARGIN;
738     }
739
740     /* Revert to normal typeface, in preparation for compilation
741      * of the selected data sheet.
742      */
743     SelectObject( canvas, NormalFont );
744     switch( tab )
745     {
746       case PKG_DATASHEET_GENERAL:
747         /* This comprises package and subsystem identification,
748          * followed by latest version availability, installation
749          * status, and package download URLs.
750          */
751         DisplayGeneralData( pkgListSelection( PackageRef, &lookup ) );
752         break;
753
754       case PKG_DATASHEET_DESCRIPTION:
755         /* This represents the package description, provided by
756          * the package maintainer, within the XML specification.
757          */
758         DisplayDescription( pkgListSelection( PackageRef, &lookup ) );
759         break;
760
761       case PKG_DATASHEET_INSTALLED_FILES:
762         /* Available only for packages which have been installed,
763          * this comprises the files list content from the manifest
764          * which was created during the installation process.
765          */
766         DisplayFilesManifest( pkgListSelection( PackageRef, &lookup ) );
767         break;
768
769       default:
770         /* Handle requests for data sheets for which we have yet
771          * to provide a compiling routine.
772          */
773         bounding_box.top += TOP_MARGIN;
774         FormatText(
775             "FIXME:data sheet unavailable; a compiler for this "
776             "data category has yet to be implemented."
777           );
778     }
779   }
780   else
781   { /* There is no active package selection; advise accordingly.
782      */
783     bounding_box.top += TOP_MARGIN << 1;
784     FormatText(
785         "No package selected."
786       );
787     bounding_box.top += PARAGRAPH_MARGIN << 1;
788     FormatText(
789         "Please select a package from the list above, "
790         "to view related data."
791       );
792   }
793
794   /* When redrawing the data sheet window from the top...
795    */
796   if( offset == 0 )
797   {
798     /* ...adjust the scrolling range to accommodate the full extent
799      * of the data sheet text, and initialise the scrollbar control.
800      */
801     if( bounding_box.top > bounding_box.bottom )
802       scrollbar.nMax = bounding_box.top;
803     SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
804   }
805
806   /* Finally, restore the original (default) font assignment
807    * for the data sheet window, complete the redraw action, and
808    * we are done.
809    */
810   SelectObject( canvas, original_font );
811   EndPaint( AppWindow, &content );
812   return EXIT_SUCCESS;
813 }
814
815 long DataSheetMaker::OnVerticalScroll( int req, int pos, HWND ctrl )
816 {
817   /* Handler for events signalled by the vertical scrollbar control,
818    * (if any), in any window ascribed to the DataSheetMaker class.
819    */
820   SCROLLINFO scrollbar;
821   scrollbar.fMask = SIF_ALL;
822   scrollbar.cbSize = sizeof( scrollbar );
823   GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
824
825   /* Save the original "thumb" position.
826    */
827   long origin = scrollbar.nPos;
828   switch( req )
829   {
830     /* Identify, and process the event message.
831      */
832     case SB_LINEUP:
833       /* User clicked the "scroll-up" button; move the
834        * "thumb" up by a distance equivalent to the height
835        * of a single line of text.
836        */
837       scrollbar.nPos -= LineSpacing;
838       break;
839
840     case SB_LINEDOWN:
841       /* Similarly, for a click on the "scroll-down" button,
842        * move the "thumb" down by one line height.
843        */
844       scrollbar.nPos += LineSpacing;
845       break;
846
847     case SB_PAGEUP:
848       /* User clicked the scrollbar region above the "thumb";
849        * move the "thumb" up by half of the viewport height.
850        */
851       scrollbar.nPos -= scrollbar.nPage >> 1;
852       break;
853
854     case SB_PAGEDOWN:
855       /* Similarly, for a click below the "thumb", move it
856        * down by half of the viewport height.
857        */
858       scrollbar.nPos += scrollbar.nPage >> 1;
859       break;
860
861     case SB_THUMBTRACK:
862       /* User is dragging...
863        */
864     case SB_THUMBPOSITION:
865       /* ...or has just finished dragging the "thumb"; move it
866        * by the distance it has been dragged.
867        */
868       scrollbar.nPos = scrollbar.nTrackPos;
869
870     case SB_ENDSCROLL:
871       /* Preceding scrollbar event has completed; we do not need
872        * to take any specific action here.
873        */
874       break;
875
876     default:
877       /* We received an unexpected scrollbar event message...
878        */
879       dmh_notify( DMH_WARNING,
880           "Unhandled scrollbar message: request = %d\n", req
881         );
882   }
883   /* Update the scrollbar control, to capture any change in
884    * "thumb" position...
885    */
886   scrollbar.fMask = SIF_POS;
887   SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
888
889   /* ...then read it back, since the control hay have adjusted
890    * the actual recorded position.
891    */
892   GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
893   if( scrollbar.nPos != origin )
894   {
895     /* When the "thumb" has moved, force a redraw of the data
896      * sheet window, to capture any change in the visible text.
897      */
898     offset = scrollbar.nPos - scrollbar.nMin;
899     InvalidateRect( AppWindow, NULL, TRUE );
900     UpdateWindow( AppWindow );
901
902     /* Reset the default starting point, so that any subsequent
903      * redraw will favour a "redraw-from-top"...
904      */
905     offset = 0;
906   }
907   /* ...and we are done.
908    */
909   return EXIT_SUCCESS;
910 }
911
912 void AppWindowMaker::InitPackageTabControl()
913 {
914   /* Create and initialise a TabControl window, in which to present
915    * miscellaneous package information...
916    */
917   WindowClassMaker AppWindowRegistry( AppInstance );
918   StringResource ClassName( AppInstance, ID_SASH_WINDOW_PANE_CLASS );
919   AppWindowRegistry.Register( ClassName );
920
921   /* Package data sheets will be displayed in a derived child window
922    * which we create as a member of the SASH_WINDOW_PANE_CLASS; it will
923    * ultimately be displayed below the tab bar, within the tab control
924    * region, with content dynamically painted on the basis of package
925    * selection, (in the package list pane), and tab selection.
926    */
927   DataSheet = new DataSheetMaker( AppInstance );
928   PackageTabPane = DataSheet->Create( ID_PACKAGE_DATASHEET,
929       AppWindow, ClassName, WS_VSCROLL | WS_BORDER
930     );
931
932   /* The tab control itself is the standard control, selected from
933    * the common controls library.
934    */
935   PackageTabControl = CreateWindow( WC_TABCONTROL, NULL,
936       WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 0, 0,
937       AppWindow, (HMENU)(ID_PACKAGE_TABCONTROL),
938       AppInstance, NULL
939     );
940
941   /* Keep the font for tab labels consistent with our preference,
942    * as assigned to the main application window.
943    */
944   SendMessage( PackageTabControl, WM_SETFONT, (WPARAM)(DefaultFont), TRUE );
945
946   /* Create the designated set of tabs, with appropriate labels...
947    */
948   TCITEM tab;
949   tab.mask = TCIF_TEXT;
950   const char *TabLegend[] =
951   { "General", "Description", "Dependencies", "Installed Files", "Versions",
952
953     /* ...with a NULL sentinel marking the preceding label as
954      * the last in the list.
955      */
956     NULL
957   };
958   for( int i = 0; TabLegend[i] != NULL; ++i )
959   {
960     /* This loop assumes responsibility for actual tab creation...
961      */
962     tab.pszText = (char *)(TabLegend[i]);
963     if( TabCtrl_InsertItem( PackageTabControl, i, &tab ) == -1 )
964     {
965       /* ...bailing out, and deleting the container window,
966        * in the event of a creation error.
967        */
968       TabLegend[i + 1] = NULL;
969       DestroyWindow( PackageTabControl );
970       PackageTabControl = NULL;
971     }
972   }
973   if( PackageTabControl != NULL )
974   {
975     /* When the tab control has been successfully created, we
976      * create one additional basic SASH_WINDOW_PANE_CLASS window;
977      * this serves to draw a border around the tab pane.
978      */
979     TabDataPane = new ChildWindowMaker( AppInstance );
980     TabDataPane->Create( ID_PACKAGE_TABPANE, AppWindow, ClassName, WS_BORDER );
981
982     /* We also assign the package description data sheet as the
983      * initial default tab selection.
984      */
985     TabCtrl_SetCurSel( PackageTabControl, PKG_DATASHEET_DESCRIPTION );
986   }
987 }
988
989 void AppWindowMaker::UpdatePackageMenuBindings()
990 # define PKGSTATE_FLAG( ID )  (1 << PKGSTATE( ID ))
991 {
992   /* Helper method to enable or disable the set of options
993    * which may be chosen from the package menu; (this varies
994    * according to the installation status of the package, if
995    * any, which has been selected in the package list view).
996    */
997   HMENU menu;
998   if( (menu = GetMenu( AppWindow )) != NULL )
999   {
1000     /* We got a valid handle for the menubar; identify the
1001      * list view selection, which controls the available set
1002      * of menu options...
1003      */
1004     LVITEM lookup;
1005     lookup.iItem = (PackageListView != NULL)
1006       ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1007       : -1;
1008
1009     /* ...and identify its state of the associated package,
1010      * as indicated by the assigned icon.
1011      */
1012     lookup.iSubItem = 0;
1013     lookup.mask = LVIF_IMAGE;
1014     ListView_GetItem( PackageListView, &lookup );
1015
1016     /* Convert the indicated state to a selector bit-flag.
1017      */
1018     int state = ((lookup.iItem >= 0) && (lookup.iImage <= PKGSTATE( PURGE )))
1019       ? 1 << lookup.iImage : 0;
1020
1021     /* Walk over all state-conditional menu items...
1022      */
1023     for( int item = IDM_PACKAGE_UNMARK; item <= IDM_PACKAGE_REMOVE; item++ )
1024     {
1025       /* ...evaluating an independent state flag for each,
1026        * setting it as non-zero for menu items which may be
1027        * made "selectable", or zero otherwise...
1028        */
1029       int state_flag = state;
1030       switch( item )
1031       { /* ...testing against item specific flag groups, to
1032          * determine which menu items should be enabled for
1033          * the currently selected list view item...
1034          */
1035         case IDM_PACKAGE_INSTALL:
1036           /* "Mark for Installation" is available for packages
1037            * which exist in the repository, (long-term or new),
1038            * but which are not yet identified as "installed".
1039            */
1040           state_flag &= PKGSTATE_FLAG( AVAILABLE )
1041             | PKGSTATE_FLAG( AVAILABLE_NEW );
1042           break;
1043
1044         case IDM_PACKAGE_REMOVE:
1045         //case IDM_PACKAGE_REINSTALL: // FIXME: for now, we don't consider this!
1046           /* "Mark for Removal" and "Mark for Reinstallation"
1047            * are viable selections only for packages identified
1048            * as "installed", (current or upgradeable).
1049            */
1050           state_flag &= PKGSTATE_FLAG( INSTALLED_CURRENT )
1051             | PKGSTATE_FLAG( INSTALLED_OLD );
1052           break;
1053
1054         case IDM_PACKAGE_UPGRADE:
1055           /* "Mark for Upgrade" is viable only for packages
1056            * identified as "installed", and then only when an
1057            * upgrade has been published.
1058            */
1059           state_flag &= PKGSTATE_FLAG( INSTALLED_OLD );
1060           break;
1061
1062         case IDM_PACKAGE_UNMARK:
1063           /* The "Unmark" facility is available only for packages
1064            * which have been marked, (perhaps inadvertently), for
1065            * any of the preceding actions.
1066            */
1067           state_flag &= PKGSTATE_FLAG( AVAILABLE_INSTALL )
1068             | PKGSTATE_FLAG( UPGRADE ) | PKGSTATE_FLAG( DOWNGRADE )
1069             | PKGSTATE_FLAG( REMOVE ) | PKGSTATE_FLAG( PURGE )
1070             | PKGSTATE_FLAG( REINSTALL );
1071           break;
1072
1073         default:
1074           /* When none of the preceding is applicable, the menu
1075            * item should not be selectable.
1076            */
1077           state_flag = 0;
1078       }
1079       /* ...and set the menu item enabled state accordingly.
1080        */
1081       EnableMenuItem( menu, item, (state_flag == 0) ? MF_GRAYED : MF_ENABLED );
1082     }
1083   }
1084   /* Although it is listed under the "Installation" drop-down menu,
1085    * this is also a convenient point to consider activation of the
1086    * "Apply Changes" capability.
1087    */
1088   pkgActionItem *pending = pkgData->Schedule();
1089   unsigned long count = (pending != NULL) ? pending->EnumeratePendingActions() : 0UL;
1090   EnableMenuItem( menu, IDM_REPO_APPLY, (count > 0UL) ? MF_ENABLED : MF_GRAYED );
1091 }
1092
1093 inline void AppWindowMaker::MarkSchedule( pkgActionItem *pending_actions )
1094 {
1095   /* Helper routine to update the status icons within the package list view,
1096    * reflecting any scheduled action in respect of the package associated with
1097    * each, and updating the menu bindings to match.
1098    */
1099   if( pending_actions != NULL )
1100   {
1101     pkgListViewMaker pkglist( PackageListView );
1102     pkglist.MarkScheduledActions( pending_actions );
1103   }
1104   UpdatePackageMenuBindings();
1105 }
1106
1107 void AppWindowMaker::Schedule
1108 ( unsigned long action, const char *bounds, const char *pkgname )
1109 {
1110   /* GUI menu driven interface to the pkgActionItem task scheduler;
1111    * it constructs a pseudo-argument string, emulating the effect of
1112    * parsing a CLI argument, then passes this to the CLI scheduler
1113    * API class method.
1114    */
1115   if( pkgname == NULL )
1116   {
1117     /* Initial entry on menu item selection; package name has not
1118      * yet been identified, so find the selected list view item...
1119      */
1120     LVITEM lookup;
1121     lookup.iItem = (PackageListView != NULL)
1122       ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1123       : -1;
1124
1125     /* ...and look up the package name identified within it.
1126      */
1127     const char *pkg, *fmt = "%s-%s";
1128     pkgXmlNode *ref = pkgListSelection( PackageListView, &lookup );
1129     if( (pkg = ref->GetContainerAttribute( name_key, NULL )) != NULL )
1130     {
1131       /* We now have a valid package name; check for a
1132        * component package association.
1133        */
1134       const char *cpt;
1135       if( (cpt = ref->GetPropVal( class_key, NULL )) == NULL )
1136       {
1137         /* Current list view selection represents a
1138          * non-component package; encode its name only
1139          * as a string argument, using only the final
1140          * string field of the format specification.
1141          */
1142         char pkgspec[ 1 + snprintf( NULL, 0, fmt + 3, pkg ) ];
1143         snprintf( pkgspec, sizeof( pkgspec ), fmt + 3, pkg );
1144
1145         /* Recurse, to capture any supplied version bounds
1146          * specification, and ultimately schedule the action.
1147          */
1148         Schedule( action, bounds, pkgspec );
1149       }
1150       else
1151       { /* Current list view selection represents a
1152          * package name qualified by a component name;
1153          * use the full format specification to encode
1154          * the fully qualified package name.
1155          */
1156         char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkg, cpt ) ];
1157         snprintf( pkgspec, sizeof( pkgspec ), fmt, pkg, cpt );
1158
1159         /* Again, recurse to capture any supplied version
1160          * bounds specification, before ultimately scheduling
1161          * the selected action.
1162          */
1163         Schedule( action, bounds, pkgspec );
1164       }
1165     }
1166   }
1167   else if( bounds != NULL )
1168   {
1169     /* Recursive entry, after package name identification,
1170      * but with supplied version bounds specification yet
1171      * to be resolved; append the bounds specification to
1172      * the package name, as it would be in a CLI argument...
1173      */
1174     const char *fmt = "%s=%s";
1175     char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkgname, bounds ) ];
1176     snprintf( pkgspec, sizeof( pkgspec ), fmt, pkgname, bounds );
1177     /*
1178      * ...then recurse a final time, to schedule the action.
1179      */
1180     Schedule( action, NULL, pkgspec );
1181   }
1182   else
1183   { /* Final recursive entry, with pkgname argument in the
1184      * same form as a CLI package name/bounds specification
1185      * argument; hand it off to the CLI scheduler, capturing
1186      * the resultant schedule of actions, and update the list
1187      * view state icons to reflect the pending actions.
1188      */
1189     MarkSchedule( pkgData->Schedule( action, pkgname ) );
1190   }
1191 }
1192
1193 inline void pkgActionItem::CancelScheduledAction( void )
1194 {
1195   /* Helper method to mark a scheduled action as "cancelled".
1196    */
1197   flags &= ~ACTION_MASK;
1198 }
1199
1200 void AppWindowMaker::UnmarkSelectedPackage( void )
1201 {
1202   /* Method to clear any request for an action in respect of
1203    * the currently selected package entry in the list view; we
1204    * implement this as a cancellation of any pending scheduled
1205    * action, in respect of the selected package.
1206    *
1207    * First, obtain a reference for the list view selection...
1208    */
1209   LVITEM lookup;
1210   lookup.iItem = (PackageListView != NULL)
1211     ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1212     : -1;
1213
1214   /* ...and when it represents a valid selection...
1215    */
1216   if( lookup.iItem >= 0 )
1217   {
1218     /* ...retrieve its associated XML database package reference...
1219      */
1220     pkgXmlNode *pkg = pkgListSelection( PackageListView, &lookup );
1221     /*
1222      * ...search the action schedule, for an action associated with
1223      * this package, if any, and cancel it.
1224      */
1225     pkgActionItem *pkgref = pkgData->Schedule();
1226     if( (pkgref != NULL) && ((pkgref = pkgref->GetReference( pkg )) != NULL) )
1227       pkgref->CancelScheduledAction();
1228
1229     /* The scheduling state for packages shown in the list view
1230      * may have changed, so refresh the icon associations and the
1231      * package menu bindings accordingly.
1232      */
1233     MarkSchedule( pkgData->Schedule() );
1234   }
1235 }
1236
1237 void AppWindowMaker::SelectPackageAction( unsigned mode )
1238 {
1239   /* Helper method to present the package menu as a floating pop-up.
1240    */
1241   HMENU popup;
1242   LVHITTESTINFO whence;
1243
1244   /* Before presenting the menu, ensure that its selection bindings
1245    * are current, as determined for the selected package; note that
1246    * we do this unconditionally, to ensure that the bindings remain
1247    * current, when the user accesses the menu from the menu bar.
1248    */
1249   UpdatePackageMenuBindings();
1250   
1251   /* Locate the cursor position, mapping it into the co-ordinate
1252    * system of the list view client window.
1253    */
1254   whence.pt.y = GetMessagePos();
1255   whence.pt.x = GET_X_LPARAM( whence.pt.y );
1256   whence.pt.y = GET_Y_LPARAM( whence.pt.y );
1257   ScreenToClient( PackageListView, &whence.pt );
1258
1259   /* Perform a hit-test, to confirm that either the left mouse
1260    * button was clicked on the package status icon, or the right
1261    * button was clicked anywhere on the package list entry; only
1262    * if one of these is detected, do we then proceed to retrieve
1263    * a handle for the pop-up menu itself...
1264    */
1265   if(  (ListView_SubItemHitTest( PackageListView, &whence ) >= 0)
1266   &&  ((whence.flags & mode) != 0) && ((popup = GetMenu( AppWindow )) != NULL)
1267   &&  ((popup = GetSubMenu( popup, 1 )) != NULL)   )
1268   {
1269     /* ...and provided it is valid, we remap the cursor position 
1270      * back into the screen co-ordinate system, and present the
1271      * menu at the resultant position.
1272      */
1273     ClientToScreen( PackageListView, &whence.pt );
1274     TrackPopupMenu( popup, 0, whence.pt.x, whence.pt.y, 0, AppWindow, NULL );
1275   }
1276 }
1277
1278 void AppWindowMaker::UpdateDataSheet( void )
1279 {
1280   /* Helper method, called when we wish to update the data sheet
1281    * panel, to match the current list view and tab selection.
1282    */
1283   DataSheet->DisplayData( PackageTabControl, PackageListView );
1284 }
1285
1286 long AppWindowMaker::OnNotify( WPARAM client_id, LPARAM data )
1287 {
1288   /* Handler for notifiable events to be processed in the context
1289    * of the main application window.
1290    *
1291    * FIXME: this supersedes the stub handler, originally provided
1292    * by pkgview.cpp; it may not yet be substantially complete, and
1293    * may eventually migrate elsewhere.
1294    */
1295   switch( client_id )
1296   {
1297     /* At present, we handle only mouse click events within the
1298      * package list view and data sheet tab control panes...
1299      */
1300     case ID_PACKAGE_LISTVIEW:
1301       if( ((NMHDR *)(data))->code == NM_RCLICK )
1302       {
1303         /* A right mouse button click within the package list view
1304          * selects the package under the cursor, refreshing the tab
1305          * pane to display its associated data sheet, and offers a
1306          * pop-up menu of actions which may be performed on it.
1307          */
1308         UpdateDataSheet();
1309         SelectPackageAction( LVHT_ONITEMICON | LVHT_ONITEMLABEL );
1310         break;
1311       }
1312     /* Any other notification from the list view control is handled
1313      * in common with similar notifications from the tab control, so
1314      * we do not break here, but simply fall through.
1315      */
1316     case ID_PACKAGE_TABCONTROL:
1317       if( ((NMHDR *)(data))->code == NM_CLICK )
1318       {
1319         /* ...each of which may require the data sheet content
1320          * to be updated, (to reflect a changed selection).
1321          */
1322         UpdateDataSheet();
1323
1324         /* Additionally, for a left click on the package status
1325          * icon within the list view, we present a pop-up menu
1326          * offering a selection of available actions.
1327          */
1328         if( client_id == ID_PACKAGE_LISTVIEW )
1329           SelectPackageAction( LVHT_ONITEMICON );
1330       }
1331       break;
1332
1333     /* We also need to consider notifications from the tree view...
1334      */
1335     case ID_PACKAGE_TREEVIEW:
1336       if( ((NMHDR *)(data))->code == TVN_SELCHANGED )
1337       {
1338         /* ...from which we are interested only in notifications
1339          * that the user has changed the package group selection.
1340          *
1341          * First, we ensure that any children of the selected
1342          * package group are made visible.
1343          */
1344         TreeView_Expand( PackageTreeView,
1345             ((NMTREEVIEW *)(data))->itemNew.hItem, TVE_EXPAND
1346           );
1347
1348         /* We then clear out the previous content of the list view
1349          * pane, and reconstruct it with new content, as determined
1350          * by the new package group selection...
1351          */
1352         ClearPackageList();
1353         UpdatePackageList();
1354
1355         /* ...and reapply any scheduled action markers, which may
1356          * be applicable.
1357          */
1358         MarkSchedule( pkgData->Schedule() );
1359
1360         /* Finally, provided the previous selection is not an
1361          * ancestor of the current, we may collapse any visible
1362          * subtree descending from the previous.
1363          *
1364          * FIXME: We may wish to avoid collapsing any subtree
1365          * which was designated as "expanded", in the original
1366          * group hierarchy specification.  We may also wish to
1367          * provide a user option, to disable this feature.
1368          */
1369         bool may_fold = true;
1370         HTREEITEM prev = ((NMTREEVIEW *)(data))->itemNew.hItem;
1371         while( may_fold && (prev != NULL) )
1372         { if( prev == ((NMTREEVIEW *)(data))->itemOld.hItem )
1373             /*
1374              * Previous selection IS an ancestor of current;
1375              * we must not collapse it.
1376              */
1377             may_fold = false;
1378
1379           else
1380             /* Continue tracing ancestry, back to the root.
1381              */
1382             prev = TreeView_GetParent( PackageTreeView, prev );
1383         }
1384         if( may_fold )
1385           /*
1386            * Previous selection may be collapsed; do so.
1387            */
1388           TreeView_Expand( PackageTreeView,
1389               ((NMTREEVIEW *)(data))->itemOld.hItem, TVE_COLLAPSE
1390             );
1391       }
1392   }
1393   /* Otherwise, this return causes any other notifiable events
1394    * to be simply ignored, (as they were by the original stub).
1395    */
1396   return EXIT_SUCCESS;
1397 }
1398
1399 unsigned long pkgActionItem::EnumeratePendingActions( int classified )
1400 {
1401   /* Helper method to count the pending actions in a
1402    * scheduled action list.
1403    */
1404   unsigned long count = 0;
1405
1406   /* Regardless of the position of the 'this' pointer,
1407    * within the list of scheduled actions...
1408    */
1409   pkgActionItem *item = this;
1410   while( item->prev != NULL )
1411     /*
1412      * ...we want to get a reference to the first
1413      * item in the list.
1414      */
1415     item = item->prev;
1416
1417   /* Now, working through the list...
1418    */
1419   while( item != NULL )
1420   {
1421     /* ...note items with any scheduled action...
1422      */
1423     int action;
1424     if( (action = item->flags & ACTION_MASK) != 0 )
1425     {
1426       /* ...and, when one is found, (noting that ACTION_UPGRADE may
1427        * also be considered as a special case of ACTION_INSTALL)...
1428        */
1429       if(  (action == classified)
1430       ||  ((action == ACTION_UPGRADE) && (classified == ACTION_INSTALL))  )
1431       {
1432         /* ...and it matches the classification in which
1433          * we are interested, then we retrieve the tarname
1434          * for the related package...
1435          */
1436         pkgXmlNode *selected = (classified & ACTION_REMOVE)
1437           ? item->Selection( to_remove )
1438           : item->Selection();
1439         const char *notification = (selected != NULL)
1440           ? selected->GetPropVal( tarname_key, NULL )
1441           : NULL;
1442         if( notification != NULL )
1443         {
1444           /* ...and, provided it is valid, we append it to
1445            * the DMH driven dialogue in which the enumeration
1446            * is being reported...
1447            */
1448           dmh_printf( "%s\n", notification );
1449           /*
1450            * ...and include it in the accumulated count...
1451            */
1452           ++count;
1453         }
1454       }
1455       else if( (classified == 0)
1456         /*
1457          * ...otherwise, when we aren't interested in any particular
1458          * class of action regardless of classification...
1459          */
1460       || ((classified == ACTION_UNSUCCESSFUL) && ((flags & classified) != 0)) )
1461         /*
1462          * ...or when we are checking for unsuccessful actions, we
1463          * count all those which are found, either unclassified, or
1464          * marked as unsuccessful, respectively.
1465          */
1466         ++count;
1467     }
1468     /* ...then move on, to consider the next entry, if any.
1469      */
1470     item = item->next;
1471   }
1472   /* Ultimately, return the count of pending actions,
1473    * as noted while processing the above loop.
1474    */
1475   return count;
1476 }
1477
1478 long AppWindowMaker::OnClose()
1479 {
1480   /* Intercept application termination requests; check for
1481    * outstanding pending actions, and offer a cancellation
1482    * option for the termination request, so that the user
1483    * has an opportunity to complete such actions.
1484    */
1485   return ConfirmActionRequest( "quit" ) ? -1 : 0;
1486 }
1487
1488 /* $RCSfile$: end of file */