OSDN Git Service

Support group affiliation with component package granularity.
[mingw/mingw-get.git] / src / pkglist.cpp
1 /*
2  * pkglist.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 methods for the pkgListViewMaker class, and
11  * its AppWindowMaker client API, to support the display of the package
12  * list in the mingw-get graphical user interface.
13  *
14  *
15  * This is free software.  Permission is granted to copy, modify and
16  * redistribute this software, under the provisions of the GNU General
17  * Public License, Version 3, (or, at your option, any later version),
18  * as published by the Free Software Foundation; see the file COPYING
19  * for licensing details.
20  *
21  * Note, in particular, that this software is provided "as is", in the
22  * hope that it may prove useful, but WITHOUT WARRANTY OF ANY KIND; not
23  * even an implied WARRANTY OF MERCHANTABILITY, nor of FITNESS FOR ANY
24  * PARTICULAR PURPOSE.  Under no circumstances will the author, or the
25  * MinGW Project, accept liability for any damages, however caused,
26  * arising from the use of this software.
27  *
28  */
29 #define _WIN32_IE  0x0300
30
31 #include "guimain.h"
32 #include "pkgbase.h"
33 #include "pkglist.h"
34 #include "pkgtask.h"
35 #include "pkgkeys.h"
36 #include "pkginfo.h"
37
38 #include <wtkexcept.h>
39
40 void AppWindowMaker::InitPackageListView()
41 {
42   /* Create and initialise a ListView window, in which to present
43    * the package list...
44    */
45   PackageListView = CreateWindow( WC_LISTVIEW, NULL,
46       WS_VISIBLE | WS_BORDER | WS_CHILD | WS_EX_CLIENTEDGE |
47       LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS, 0, 0, 0, 0,
48       AppWindow, (HMENU)(ID_PACKAGE_LISTVIEW),
49       AppInstance, NULL
50     );
51   /* ...and set its extended style attributes.
52    */
53   ListView_SetExtendedListViewStyle( PackageListView,
54       LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT | LVS_EX_ONECLICKACTIVATE
55     );
56
57   /* Propagate the default font, as set for the main window itself,
58    * to the list view control.
59    */
60   SendMessage( PackageListView, WM_SETFONT, (WPARAM)(DefaultFont), TRUE );
61
62   /* Assign an image list, to furnish the package status icons.
63    */
64   HIMAGELIST iconlist = ImageList_Create( 16, 16, ILC_COLOR32 | ILC_MASK, 13, 0 );
65   for( int index = 0; index <= PKGSTATE( PURGE ); index++ )
66   {
67     HICON image = LoadIcon( AppInstance, MAKEINTRESOURCE(ID_PKGSTATE(index)) );
68     if( ImageList_AddIcon( iconlist, image ) == -1 )
69       throw WTK::runtime_error( "Error loading package status icons" );
70   }
71   ListView_SetImageList( PackageListView, iconlist, LVSIL_SMALL );
72
73   /* Initialise the table layout, and assign column headings.
74    */
75   LVCOLUMN table;
76   struct { int id; int width; const char *text; } headings[] =
77   {
78     /* Specify the column headings for the package list table.
79      */
80     { ID_PKGLIST_TABLE_HEADINGS,  20, ""  },
81     { ID_PKGNAME_COLUMN_HEADING, 150, "Package" },
82     { ID_PKGTYPE_COLUMN_HEADING,  48, "Class" },
83     { ID_INSTVER_COLUMN_HEADING, 125, "Installed Version" },
84     { ID_REPOVER_COLUMN_HEADING, 125, "Repository Version" },
85     { ID_PKGDESC_COLUMN_HEADING, 400, "Description" },
86     /*
87      * This all-null entry terminates the sequence.
88      */
89     { 0, 0, NULL }
90   };
91   table.fmt = LVCFMT_LEFT;
92   table.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
93   for( int index = 0; headings[index].text != NULL; index++ )
94   {
95     /* Loop over the columns, setting the initial width
96      * (in pixels), and assigning the heading to each.
97      */
98     table.cx = headings[index].width;
99     table.pszText = (char *)(headings[index].text);
100     ListView_InsertColumn( PackageListView, index, &table );
101   }
102   /* "Update" the package list, to initialise the list view.
103    */
104   UpdatePackageList();
105 }
106
107 void AppWindowMaker::UpdatePackageList()
108 {
109   /* Scan the XML package catalogue, adding a ListView item entry
110    * for each package record.
111    */
112   pkgListViewMaker PackageList( PackageListView );
113   pkgDirectory *dir = pkgData->CatalogueAllPackages();
114   dir->InOrder( &PackageList );
115   delete dir;
116
117   /* Force a redraw of the application window, to ensure that the
118    * data pane content remains synchronised.
119    */
120   InvalidateRect( AppWindow, NULL, FALSE );
121   UpdateWindow( AppWindow );
122 }
123
124 inline bool pkgXmlNode::IsVisibleGroupMember()
125 {
126   /* Hook invoked before adding a package reference to the package list,
127    * to ensure that it represents a member of the current package group.
128    */
129   return AppWindowMaker::IsPackageGroupAffiliate( this );
130 }
131
132 /* FIXME: Complementary check for component class visibility requires a
133  * future component class visibility control; for the time being, we may
134  * assume that any component package should be visible if it belongs to
135  * any visible package group, (including a component subset of such a
136  * group, which may comprise only itself).
137  */
138 inline bool pkgXmlNode::IsVisibleClass(){ return IsVisibleGroupMember(); }
139
140 static char *pkgGetTitle( pkgXmlNode *pkg, const pkgXmlNode *xml_root )
141 {
142   /* A private helper method, to retrieve the title attribute
143    * associated with the description for the nominated package.
144    *
145    * Note: this should really return a const char *, but then
146    * we would need to cast it to non-const for mapping into the
147    * ill-formed structure of Microsoft's LVITEM, so we may just
148    * as well return the non-const result anyway.
149    */
150   pkgXmlNode *desc = pkg->FindFirstAssociate( description_key );
151   while( desc != NULL )
152   {
153     /* Handling it internally as the const which it should be...
154      */
155     const char *title;
156     if( (title = desc->GetPropVal( title_key, NULL )) != NULL )
157       /*
158        * As soon as we find a description element with an
159        * assigned title attribute, immediately return it,
160        * (with the required cast to non-const).
161        */
162       return (char *)(title);
163
164     /* If we haven't yet found any title attribute, check for any
165      * further description elements at the current XML nesting level.
166      */
167     desc = desc->FindNextAssociate( description_key );
168   }
169
170   /* If we've exhausted all description elements at the current XML
171    * nesting level, without finding a title attribute, and we haven't
172    * checked all enclosing levels back to the document root...
173    */
174   if( pkg != xml_root )
175     /*
176      * ...then continue the search in the immediately enclosing level.
177      */
178     return pkgGetTitle( pkg->GetParent(), xml_root );
179
180   /* If we get to here, then we've searched all levels back to the
181    * document root, and have failed to find any title attribute; we
182    * have nothing to return.
183    */
184   return NULL;
185 }
186
187 static inline char *pkgGetTitle( pkgXmlNode *pkg )
188 {
189   /* Overload the preceding function, to automatically generate
190    * the required reference to the XML document root.
191    */
192   return pkgGetTitle( pkg->GetParent(), pkg->GetDocumentRoot() );
193 }
194
195 static inline
196 char *version_string_copy( char *buf, const char *text, int fill = 0 )
197 {
198   /* Local helper function to construct a package version string
199    * from individual version specific elements of the tarname.
200    */
201   if( text != NULL )
202   {
203     /* First, if a fill character is specified, copy it as the
204      * first character of the result; (we assume that we are
205      * appending to a previously constructed result, and that
206      * this is the field separator character).
207      */
208     if( fill != 0 ) *buf++ = fill;
209
210     /* Now, append "text" up to, and including its final NUL
211      * terminator; (note that we do NOT guard against buffer
212      * overrun, as we have complete control over the calling
213      * context, where we allocated a result buffer at least
214      * as long as the tarname string from which the composed
215      * version string is extracted, and the composed result
216      * can never exceed the original length of this).
217      */
218     do { if( (*buf = *text) != '\0' ) ++buf; } while( *text++ != '\0' );
219   }
220   /* Finally, we return a pointer to the terminating NUL of
221    * the result, so as to facilitate appending further text.
222    */
223   return buf;
224 }
225
226 static char *pkgVersionString( char *buf, pkgSpecs *pkg )
227 {
228   /* Helper method to construct a fully qualified version string
229    * from the decomposed package tarname form in a pkgSpecs structure.
230    *
231    * We begin with the concatenation of package version and build ID
232    * fields, retrieved from the pkgSpecs representation...
233    */
234   const char *pkg_version_tag;
235   char *update = version_string_copy( buf, pkg->GetPackageVersion() );
236   update = version_string_copy( update, pkg->GetPackageBuild(), '-' );
237   if( (pkg_version_tag = pkg->GetSubSystemVersion()) != NULL )
238   {
239     /* ...then, we append the sub-system ID, if applicable.
240      */
241     update = version_string_copy( update, pkg->GetSubSystemName(), '-' );
242     update = version_string_copy( update, pkg_version_tag, '-' );
243     update = version_string_copy( update, pkg->GetSubSystemBuild(), '-' );
244   }
245   if( (pkg_version_tag = pkg->GetReleaseStatus()) != NULL )
246   {
247     /* When the package name also includes a release classification,
248      * then we also append this to the displayed version number (noting
249      * that an initial '$' is special, and should be suppressed)...
250      */
251     if( *pkg_version_tag == '$' ) ++pkg_version_tag;
252     update = version_string_copy( update, pkg_version_tag, '-' );
253     /*
254      * ...followed by any serialisation index for the release...
255      */
256     update = version_string_copy( update, pkg->GetReleaseIndex(), '-' );
257   }
258   /* ...and finally, we return a pointer to the buffer in which
259    * we constructed the fully qualified version string.
260    */
261   return buf;
262 }
263
264 pkgListViewMaker::pkgListViewMaker( HWND pane ): ListView( pane )
265 {
266   /* Constructor: initialise the invariant parameters within the
267    * embedded W32API ListView control structure.
268    */
269   content.stateMask = 0;
270   content.mask = LVIF_PARAM | LVIF_IMAGE | LVIF_STATE;
271   content.iSubItem = 0;
272   content.iItem = -1;
273 }
274
275 EXTERN_C pkgXmlNode *pkgGetStatus( pkgXmlNode *pkg, pkgActionItem *avail )
276 {
277   /* Helper function to acquire release availability and installation
278    * status attributes for a specified package.
279    */
280   pkg = pkg->FindFirstAssociate( release_key );
281   while( pkg != NULL )
282   {
283     /* Examine each available release specification for the nominated
284      * package; select the one which represents the latest (most recent)
285      * available release.
286      */
287     avail->SelectIfMostRecentFit( pkg );
288
289     /* Also check for the presence of an installation record for each
290      * release; if found, mark it as the currently installed release;
291      * (we assign the "to-remove" attribute, but we don't action it).
292      */
293     if( pkg->GetInstallationRecord( pkg->GetPropVal( tarname_key, NULL )) != NULL )
294       avail->SelectPackage( pkg, to_remove );
295
296     /* Cycle, until all known releases have been examined.
297      */
298     pkg = pkg->FindNextAssociate( release_key );
299   }
300   /* Check the identification of any currently installed release; this
301    * will capture property data for any installed release which may have
302    * been subsequently withdrawn from distribution.
303    */
304   avail->ConfirmInstallationStatus();
305
306   /* Finally, return a pointer to the specification for the installed
307    * release, if any, of the package under consideration.
308    */
309   return avail->Selection( to_remove );
310 }
311
312 void pkgListViewMaker::InsertItem( pkgXmlNode *pkg, char *class_name )
313 {
314   /* Private method to add a single package record, as an individual
315    * row item, to the displayed list view table; begin by filling in
316    * the appropriate fields within the "content" structure...
317    */
318   content.iItem++;
319   content.state = 0;
320   content.lParam = (unsigned long)(pkg);
321
322   /* ...then delegate the actual entry construction to...
323    */
324   UpdateItem( class_name, true );
325 }
326
327 inline bool pkgListViewMaker::GetItem( void )
328 {
329   /* An inline helper method to load the content structure from the
330    * next available item, if any, in the package list view; returns
331    * true when content is successfully loaded, else returns false.
332    */
333   content.iItem++; return ListView_GetItem( ListView, &content );
334 }
335
336 void pkgListViewMaker::UpdateItem( char *class_name, bool new_entry )
337 {
338   /* Assign a temporary action item, through which we may identify
339    * the latest available, and currently installed (if any), version
340    * attributes for the package under consideration.
341    */
342   pkgActionItem avail;
343   pkgXmlNode *package = (pkgXmlNode *)(content.lParam);
344   pkgXmlNode *installed = pkgGetStatus( package, &avail );
345
346   /* Decompose the package tarname identifier for the latest available
347    * release, into its individual package specification properties.
348    */
349   pkgSpecs latest( package = avail.Selection() );
350
351   /* Allocate a temporary working text buffer, in which to format
352    * package property values for display...
353    */
354   size_t len = strlen( package->GetPropVal( tarname_key, value_none ) );
355   if( installed != NULL )
356   {
357     /* ...ensuring that it is at least as large as the longer of the
358      * latest or installed release tarname.
359      */
360     size_t altlen = strlen( installed->GetPropVal( tarname_key, value_none ) );
361     if( altlen > len ) len = altlen;
362   }
363   char buf[1 + len];
364
365   /* Choose a suitable icon for representation of the installation
366    * status of the package under consideration...
367    */
368   if( installed != NULL )
369   {
370     /* ...noting that, when it is already installed...
371      */
372     pkgSpecs current( installed );
373     content.iImage = (latest > current)
374       ? /* ...and, when the latest available is NEWER than the
375          * installed version, then we choose the icon indicating
376          * an installed package with an available update...
377          */
378         PKGSTATE( INSTALLED_OLD )
379
380       : /* ...or, when the latest available is NOT NEWER than
381          * the installed version, then we choose the alternative
382          * icon, simply indicating an installed package...
383          */
384         PKGSTATE( INSTALLED_CURRENT );
385
386     /* ...and also, load the version identification string for
387      * the installed version into the working text buffer.
388      */
389     pkgVersionString( buf, &current );
390   }
391   else
392   { /* Alternatively, for any package which is not recorded as
393      * installed, choose the icon indicating an available, but
394      * not (yet) installed package...
395      */
396     content.iImage = PKGSTATE( AVAILABLE );
397
398     /* ...and mark the list view column entry, for the installed
399      * version, as empty.
400      */
401     *buf = '\0';
402   }
403
404   /* When compiling a new list entry...
405    */
406   if( new_entry )
407   {
408     /* ...add the package identification record, as a new item...
409      */
410     ListView_InsertItem( ListView, &content );
411     /*
412      * ...and fill in the text for the package name, class name,
413      * and package description columns...
414      */
415     ListView_SetItemText( ListView, content.iItem, 1, package_name );
416     ListView_SetItemText( ListView, content.iItem, 2, class_name );
417     ListView_SetItemText( ListView, content.iItem, 5, pkgGetTitle( package ) );
418   }
419   else
420     /* ...otherwise, this is simply a request to update an
421      * existing item, in place; do so.  (Note that, in this
422      * case, we DO NOT touch the package name, class name,
423      * or package description column content).
424      */
425     ListView_SetItem( ListView, &content );
426
427   /* Always fill in the text, as established above, in the
428    * column which identifies the currently installed version,
429    * if any, or clear it if the package is not installed.
430    */
431   ListView_SetItemText( ListView, content.iItem, 3, buf );
432
433   /* Finally, fill in the text of the column which identifies
434    * the latest available version of the package.
435    */
436   ListView_SetItemText( ListView, content.iItem, 4, pkgVersionString( buf, &latest ) );
437 }
438
439 void pkgListViewMaker::Dispatch( pkgXmlNode *package )
440 {
441   /* Implementation of the "dispatcher" method, which is required
442    * by any derivative of the pkgDirectoryViewerEngine class, for
443    * dispatching the content of the directory to the display service,
444    * (which, in this case, populates the list view window pane).
445    */
446   if( package->IsElementOfType( package_key ) )
447   {
448     /* Assemble the package name into the list view record block.
449      */
450     package_name = (char *)(package->GetPropVal( name_key, value_unknown ));
451
452     /* When processing a pkgDirectory entry for a package entity,
453      * generate a sub-directory listing for any contained package
454      * components...
455      */
456     pkgDirectory *dir;
457     if( (dir = EnumerateComponents( package )) != NULL )
458     {
459       /* ...and recurse, to process any which are found...
460        */
461       dir->InOrder( this );
462       delete dir;
463     }
464     else if( package->IsVisibleGroupMember() )
465       /*
466        * ...otherwise, simply insert an unclassified list entry
467        * for the bare package name, omitting the component class.
468        */
469       InsertItem( package, (char *)("") );
470   }
471   else if( package->IsElementOfType( component_key ) && package->IsVisibleClass() )
472   {
473     /* Handle the recursive calls for the component sub-directory,
474      * inheriting the package name entry from the original package
475      * entity, and emit an appropriately classified list view entry
476      * for each identified component package.
477      */
478     InsertItem( package, (char *)(package->GetPropVal( class_key, "" )) );
479   }
480 }
481
482 void pkgListViewMaker::MarkScheduledActions( pkgActionItem *schedule )
483 {
484   /* Method to reassign icons to entries within the package list view,
485    * indicating any which have been marked for installation, upgrade,
486    * or removal of the associated package.
487    */
488   if( schedule != NULL )
489     for( content.iItem = -1; GetItem(); )
490     {
491       /* Visiting every entry in the list...
492        */
493       pkgActionItem *ref;
494       if( (ref = schedule->GetReference( (pkgXmlNode *)(content.lParam) )) != NULL )
495       {
496         /* ...identify those which are associated with a scheduled action...
497          */
498         unsigned long opcode;
499         if( (opcode = ref->HasAttribute( ACTION_MASK )) == ACTION_INSTALL )
500         {
501           /* ...selecting the appropriate icon to mark those packages
502            * which have been scheduled for installation...
503            *
504            * FIXME: we should also consider that such packages
505            * may have been scheduled for reinstallation.
506            */
507           content.iImage = PKGSTATE( AVAILABLE_INSTALL );
508         }
509         else if( opcode == ACTION_UPGRADE )
510         {
511           /* ...those which have been scheduled for upgrade...
512            */
513           content.iImage = PKGSTATE( UPGRADE );
514         }
515         else if( opcode == ACTION_REMOVE )
516         {
517           /* ...and those which have been scheduled for removal.
518            */
519           content.iImage = PKGSTATE( REMOVE );
520         }
521         else
522         { /* Where a scheduled action is any other than those above,
523            * handle as if there was no scheduled action...
524            */
525           opcode = 0UL;
526           /*
527            * ...and ensure that the list view entry reflects the
528            * normal display state for the associated package.
529            */
530           UpdateItem( NULL );
531         }
532         if( opcode != 0UL )
533           /*
534            * Where an action mark is appropriate, ensure that it
535            * is applied to the list view entry.
536            */
537           ListView_SetItem( ListView, &content );
538       }
539     }
540 }
541
542 void pkgListViewMaker::UpdateListView( void )
543 {
544   /* A simple helper method, to refresh the content of the
545    * package list view, resetting each item to its initial
546    * unmarked display state.
547    */
548   for( content.iItem = -1; GetItem(); ) UpdateItem( NULL );
549 }
550
551 /* $RCSfile$: end of file */