OSDN Git Service

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