OSDN Git Service

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