* $Id$
*
* Written by Keith Marshall <keithmarshall@users.sourceforge.net>
- * Copyright (C) 2012, MinGW Project
+ * Copyright (C) 2012, 2013, MinGW.org Project
*
*
* Implementation of the classes and methods required to support the
* arising from the use of this software.
*
*/
+#define _WIN32_IE 0x0300
+
#include <stdlib.h>
#include <string.h>
#include "pkgbase.h"
#include "pkgdata.h"
#include "pkgkeys.h"
+#include "pkginfo.h"
#include "pkglist.h"
+#include "pkgproc.h"
#include "pkgtask.h"
+#include <windowsx.h>
+
using WTK::StringResource;
using WTK::WindowClassMaker;
using WTK::ChildWindowMaker;
*/
TextOutW( canvas, bounds->left, bounds->top - offset, linebuf, filled );
}
+ else if( (fold == 0) && (new_width > max_width) )
+ /*
+ * The output line which we've just processed lies outside
+ * the viewport. We note that it's initial (non-breakable)
+ * "word" would require more display width than the viewport
+ * can accommodate, if it were to be moved into the visible
+ * region; thus, this "word" will continue to be presented
+ * to the formatting engine, as the next input "word" to be
+ * processed. This would result in an infinite loop, so we
+ * MUST discard this "word" from the input queue.
+ */
+ curr = (pkgTroffLayoutEngine *)(curr->next);
+
/* Finally, adjust the top boundary of the viewport, to indicate
* where the NEXT output line, if any, is to be positioned, and
* return TRUE, to indicate that an output line was processed.
static int DisplayLicenceURL( const char * );
static int DisplayPackageURL( const char * );
inline void DisplayDescription( pkgXmlNode * );
+ inline void DisplayFilesManifest( pkgXmlNode * );
void ComposeDescription( pkgXmlNode *, pkgXmlNode * );
int FormatRecord( int, const char *, const char * );
inline void FormatText( const char * );
);
/* Using a temporary action item, collect information on the
- * available and installed (if any) versions of the selected
- * package.
+ * latest available version, and the installed version if any,
+ * of the selected package; print the applicable information,
+ * noting that "none" may be appropriate in the case of the
+ * installed version.
*/
pkgActionItem avail;
- ref = ref->FindFirstAssociate( release_key );
- while( ref != NULL )
- {
- /* ...to scan all associated release keys, and select the
- * most recent recorded in the database as available...
- */
- avail.SelectIfMostRecentFit( ref );
- if( ref->GetInstallationRecord( ref->GetPropVal( tarname_key, NULL )) != NULL )
- /*
- * ...also noting if any is marked as installed...
- */
- avail.SelectPackage( ref, to_remove );
-
- /* ...until all release keys have been inspected...
- */
- ref = ref->FindNextAssociate( release_key );
- }
- /* ...and performing a final check on installation status.
- */
- avail.ConfirmInstallationStatus();
-
- /* Now print the applicable version information, noting that
- * "none" may be appropriate for the installed version.
- */
FormatRecord( 0, "Installed Version",
- ((ref = avail.Selection( to_remove )) != NULL)
- ? ref->GetPropVal( tarname_key, value_unknown ) : value_none
+ ((ref = pkgGetStatus( ref, &avail )) != NULL)
+ ? ref->GetPropVal( tarname_key, value_unknown )
+ : value_none
);
FormatRecord( 1, "Repository Version",
(ref = avail.Selection())->GetPropVal( tarname_key, value_unknown )
}
}
+void DataSheetMaker::DisplayFilesManifest( pkgXmlNode *ref )
+{
+ /* Helper method to compile the list of files installed by a package,
+ * for display on the "Installed Files" tab of the data sheet panel.
+ */
+ pkgActionItem avail;
+ if( (ref = pkgGetStatus( ref, &avail )) == NULL )
+ {
+ /* This represents a package which is available, but has not been
+ * installed; we simply decline to compile the files list.
+ */
+ FormatRecord( 0, "Package",
+ avail.Selection()->GetPropVal( tarname_key, value_unknown )
+ );
+ bounding_box.top += PARAGRAPH_MARGIN;
+ FormatText(
+ "This package has not been installed; "
+ "the list of installed files is not available for packages "
+ "which have not been installed."
+ );
+ }
+ else
+ { /* This represents a package which has been installed; begin
+ * compilation of the list of installed files.
+ */
+ const char *tarname;
+ FormatRecord( 0, "Package",
+ tarname = ref->GetPropVal( tarname_key, value_unknown )
+ );
+ if( match_if_explicit( ref->ArchiveName(), value_none ) )
+ {
+ /* This is a meta-package; there are no files to list.
+ */
+ bounding_box.top += PARAGRAPH_MARGIN;
+ FormatText(
+ "This meta-package facilitates the installation of a collection "
+ "of other logically related packages; it provides no files, and "
+ "may be safely removed."
+ );
+ }
+ else
+ { /* This is a real package; retrieve the manifest of installed files,
+ * which was created during the installation process.
+ */
+ pkgXmlNode *index;
+ pkgManifest inventory( package_key, tarname );
+ if( (index = inventory.GetRoot()->FindFirstAssociate( manifest_key )) != NULL )
+ {
+ /* We've located a files list within the manifest; process it...
+ */
+ FormatRecord( 0, "This package provides the following files", "" );
+ bounding_box.left += LEFT_MARGIN; bounding_box.top += PARAGRAPH_MARGIN;
+ do { if( (ref = index->FindFirstAssociate( filename_key )) != NULL )
+ /*
+ * We found at least one file name within the list; emit it
+ * to the display, followed by any additional names present.
+ */
+ do { FormatText( ref->GetPropVal( pathname_key, value_unknown ) );
+ } while( (ref = ref->FindNextAssociate( filename_key )) != NULL );
+
+ /* There should be no more than one, but check for, and process
+ * any additional files lists which may be present.
+ */
+ } while( (index = index->FindNextAssociate( manifest_key )) != NULL );
+ }
+ else
+ { /* The manifest appears to be lacking any files list; diagnose.
+ */
+ bounding_box.top += PARAGRAPH_MARGIN;
+ FormatText( "This package appears to provide no files." );
+ }
+ }
+ }
+}
+
static pkgXmlNode *pkgListSelection( HWND package_ref, LVITEM *lookup )
{
/* Helper function, to retrieve the active selection from the
DisplayDescription( pkgListSelection( PackageRef, &lookup ) );
break;
+ case PKG_DATASHEET_INSTALLED_FILES:
+ /* Available only for packages which have been installed,
+ * this comprises the files list content from the manifest
+ * which was created during the installation process.
+ */
+ DisplayFilesManifest( pkgListSelection( PackageRef, &lookup ) );
+ break;
+
default:
/* Handle requests for data sheets for which we have yet
* to provide a compiling routine.
*/
TCITEM tab;
tab.mask = TCIF_TEXT;
- char *TabLegend[] =
+ const char *TabLegend[] =
{ "General", "Description", "Dependencies", "Installed Files", "Versions",
/* ...with a NULL sentinel marking the preceding label as
{
/* This loop assumes responsibility for actual tab creation...
*/
- tab.pszText = TabLegend[i];
+ tab.pszText = (char *)(TabLegend[i]);
if( TabCtrl_InsertItem( PackageTabControl, i, &tab ) == -1 )
{
/* ...bailing out, and deleting the container window,
}
}
+void AppWindowMaker::UpdatePackageMenuBindings()
+# define PKGSTATE_FLAG( ID ) (1 << PKGSTATE( ID ))
+{
+ /* Helper method to enable or disable the set of options
+ * which may be chosen from the package menu; (this varies
+ * according to the installation status of the package, if
+ * any, which has been selected in the package list view).
+ */
+ HMENU menu;
+ if( (menu = GetMenu( AppWindow )) != NULL )
+ {
+ /* We got a valid handle for the menubar; identify the
+ * list view selection, which controls the available set
+ * of menu options...
+ */
+ LVITEM lookup;
+ lookup.iItem = (PackageListView != NULL)
+ ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
+ : -1;
+
+ /* ...and identify its state of the associated package,
+ * as indicated by the assigned icon.
+ */
+ lookup.iSubItem = 0;
+ lookup.mask = LVIF_IMAGE;
+ ListView_GetItem( PackageListView, &lookup );
+
+ /* Convert the indicated state to a selector bit-flag.
+ */
+ int state = ((lookup.iItem >= 0) && (lookup.iImage <= PKGSTATE( PURGE )))
+ ? 1 << lookup.iImage : 0;
+
+ /* Walk over all state-conditional menu items...
+ */
+ for( int item = IDM_PACKAGE_UNMARK; item <= IDM_PACKAGE_REMOVE; item++ )
+ {
+ /* ...evaluating an independent state flag for each,
+ * setting it as non-zero for menu items which may be
+ * made "selectable", or zero otherwise...
+ */
+ int state_flag = state;
+ switch( item )
+ { /* ...testing against item specific flag groups, to
+ * determine which menu items should be enabled for
+ * the currently selected list view item...
+ */
+ case IDM_PACKAGE_INSTALL:
+ /* "Mark for Installation" is available for packages
+ * which exist in the repository, (long-term or new),
+ * but which are not yet identified as "installed".
+ */
+ state_flag &= PKGSTATE_FLAG( AVAILABLE )
+ | PKGSTATE_FLAG( AVAILABLE_NEW );
+ break;
+
+ case IDM_PACKAGE_REMOVE:
+ //case IDM_PACKAGE_REINSTALL: // FIXME: for now, we don't consider this!
+ /* "Mark for Removal" and "Mark for Reinstallation"
+ * are viable selections only for packages identified
+ * as "installed", (current or upgradeable).
+ */
+ state_flag &= PKGSTATE_FLAG( INSTALLED_CURRENT )
+ | PKGSTATE_FLAG( INSTALLED_OLD );
+ break;
+
+ case IDM_PACKAGE_UPGRADE:
+ /* "Mark for Upgrade" is viable only for packages
+ * identified as "installed", and then only when an
+ * upgrade has been published.
+ */
+ state_flag &= PKGSTATE_FLAG( INSTALLED_OLD );
+ break;
+
+ case IDM_PACKAGE_UNMARK:
+ /* The "Unmark" facility is available only for packages
+ * which have been marked, (perhaps inadvertently), for
+ * any of the preceding actions.
+ */
+ state_flag &= PKGSTATE_FLAG( AVAILABLE_INSTALL )
+ | PKGSTATE_FLAG( UPGRADE ) | PKGSTATE_FLAG( DOWNGRADE )
+ | PKGSTATE_FLAG( REMOVE ) | PKGSTATE_FLAG( PURGE )
+ | PKGSTATE_FLAG( REINSTALL );
+ break;
+
+ default:
+ /* When none of the preceding is applicable, the menu
+ * item should not be selectable.
+ */
+ state_flag = 0;
+ }
+ /* ...and set the menu item enabled state accordingly.
+ */
+ EnableMenuItem( menu, item, (state_flag == 0) ? MF_GRAYED : MF_ENABLED );
+ }
+ }
+ /* Although it is listed under the "Installation" drop-down menu,
+ * this is also a convenient point to consider activation of the
+ * "Apply Changes" capability.
+ */
+ EnableMenuItem( menu, IDM_REPO_APPLY,
+ (pkgData->Schedule()->EnumeratePendingActions() > 0) ? MF_ENABLED
+ : MF_GRAYED
+ );
+}
+
+inline void AppWindowMaker::MarkSchedule( pkgActionItem *pending_actions )
+{
+ /* Helper routine to update the status icons within the package list view,
+ * reflecting any scheduled action in respect of the package associated with
+ * each, and updating the menu bindings to match.
+ */
+ if( pending_actions != NULL )
+ {
+ pkgListViewMaker pkglist( PackageListView );
+ pkglist.MarkScheduledActions( pending_actions );
+ }
+ UpdatePackageMenuBindings();
+}
+
+void AppWindowMaker::Schedule
+( unsigned long action, const char *bounds, const char *pkgname )
+{
+ /* GUI menu driven interface to the pkgActionItem task scheduler;
+ * it constructs a pseudo-argument string, emulating the effect of
+ * parsing a CLI argument, then passes this to the CLI scheduler
+ * API class method.
+ */
+ if( pkgname == NULL )
+ {
+ /* Initial entry on menu item selection; package name has not
+ * yet been identified, so find the selected list view item...
+ */
+ LVITEM lookup;
+ lookup.iItem = (PackageListView != NULL)
+ ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
+ : -1;
+
+ /* ...and look up the package name identified within it.
+ */
+ const char *pkg, *fmt = "%s-%s";
+ pkgXmlNode *ref = pkgListSelection( PackageListView, &lookup );
+ if( (pkg = ref->GetContainerAttribute( name_key, NULL )) != NULL )
+ {
+ /* We now have a valid package name; check for a
+ * component package association.
+ */
+ const char *cpt;
+ if( (cpt = ref->GetPropVal( class_key, NULL )) == NULL )
+ {
+ /* Current list view selection represents a
+ * non-component package; encode its name only
+ * as a string argument, using only the final
+ * string field of the format specification.
+ */
+ char pkgspec[ 1 + snprintf( NULL, 0, fmt + 3, pkg ) ];
+ snprintf( pkgspec, sizeof( pkgspec ), fmt + 3, pkg );
+
+ /* Recurse, to capture any supplied version bounds
+ * specification, and ultimately schedule the action.
+ */
+ Schedule( action, bounds, pkgspec );
+ }
+ else
+ { /* Current list view selection represents a
+ * package name qualified by a component name;
+ * use the full format specification to encode
+ * the fully qualified package name.
+ */
+ char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkg, cpt ) ];
+ snprintf( pkgspec, sizeof( pkgspec ), fmt, pkg, cpt );
+
+ /* Again, recurse to capture any supplied version
+ * bounds specification, before ultimately scheduling
+ * the selected action.
+ */
+ Schedule( action, bounds, pkgspec );
+ }
+ }
+ }
+ else if( bounds != NULL )
+ {
+ /* Recursive entry, after package name identification,
+ * but with supplied version bounds specification yet
+ * to be resolved; append the bounds specification to
+ * the package name, as it would be in a CLI argument...
+ */
+ const char *fmt = "%s=%s";
+ char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkgname, bounds ) ];
+ snprintf( pkgspec, sizeof( pkgspec ), fmt, pkgname, bounds );
+ /*
+ * ...then recurse a final time, to schedule the action.
+ */
+ Schedule( action, NULL, pkgspec );
+ }
+ else
+ { /* Final recursive entry, with pkgname argument in the
+ * same form as a CLI package name/bounds specification
+ * argument; hand it off to the CLI scheduler, capturing
+ * the resultant schedule of actions, and update the list
+ * view state icons to reflect the pending actions.
+ */
+ MarkSchedule( pkgData->Schedule( action, pkgname ) );
+ }
+}
+
+inline unsigned long pkgActionItem::CancelScheduledAction( void )
+{
+ /* Helper method to mark a scheduled action as "cancelled".
+ */
+ return (this != NULL) ? (flags &= ~ACTION_MASK) : 0UL;
+}
+
+void AppWindowMaker::UnmarkSelectedPackage( void )
+{
+ /* Method to clear any request for an action in respect of
+ * the currently selected package entry in the list view; we
+ * implement this as a cancellation of any pending scheduled
+ * action, in respect of the selected package.
+ *
+ * First, obtain a reference for the list view selection...
+ */
+ LVITEM lookup;
+ lookup.iItem = (PackageListView != NULL)
+ ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
+ : -1;
+
+ /* ...and when it represents a valid selection...
+ */
+ if( lookup.iItem >= 0 )
+ {
+ /* ...retrieve its associated XML database package reference...
+ */
+ pkgXmlNode *pkg = pkgListSelection( PackageListView, &lookup );
+ /*
+ * ...search the action schedule, for an action associated with
+ * this package, if any, and cancel it.
+ */
+ pkgData->Schedule()->GetReference( pkg )->CancelScheduledAction();
+
+ /* The scheduling state for packages shown in the list view
+ * may have changed, so refresh the icon associations and the
+ * package menu bindings accordingly.
+ */
+ MarkSchedule( pkgData->Schedule() );
+ }
+}
+
+void AppWindowMaker::SelectPackageAction( unsigned mode )
+{
+ /* Helper method to present the package menu as a floating pop-up.
+ */
+ HMENU popup;
+ LVHITTESTINFO whence;
+
+ /* Before presenting the menu, ensure that its selection bindings
+ * are current, as determined for the selected package; note that
+ * we do this unconditionally, to ensure that the bindings remain
+ * current, when the user accesses the menu from the menu bar.
+ */
+ UpdatePackageMenuBindings();
+
+ /* Locate the cursor position, mapping it into the co-ordinate
+ * system of the list view client window.
+ */
+ whence.pt.y = GetMessagePos();
+ whence.pt.x = GET_X_LPARAM( whence.pt.y );
+ whence.pt.y = GET_Y_LPARAM( whence.pt.y );
+ ScreenToClient( PackageListView, &whence.pt );
+
+ /* Perform a hit-test, to confirm that either the left mouse
+ * button was clicked on the package status icon, or the right
+ * button was clicked anywhere on the package list entry; only
+ * if one of these is detected, do we then proceed to retrieve
+ * a handle for the pop-up menu itself...
+ */
+ if( (ListView_SubItemHitTest( PackageListView, &whence ) >= 0)
+ && ((whence.flags & mode) != 0) && ((popup = GetMenu( AppWindow )) != NULL)
+ && ((popup = GetSubMenu( popup, 1 )) != NULL) )
+ {
+ /* ...and provided it is valid, we remap the cursor position
+ * back into the screen co-ordinate system, and present the
+ * menu at the resultant position.
+ */
+ ClientToScreen( PackageListView, &whence.pt );
+ TrackPopupMenu( popup, 0, whence.pt.x, whence.pt.y, 0, AppWindow, NULL );
+ }
+}
+
+void AppWindowMaker::UpdateDataSheet( void )
+{
+ /* Helper method, called when we wish to update the data sheet
+ * panel, to match the current list view and tab selection.
+ */
+ DataSheet->DisplayData( PackageTabControl, PackageListView );
+}
+
long AppWindowMaker::OnNotify( WPARAM client_id, LPARAM data )
{
/* Handler for notifiable events to be processed in the context
* package list view and data sheet tab control panes...
*/
case ID_PACKAGE_LISTVIEW:
+ if( ((NMHDR *)(data))->code == NM_RCLICK )
+ {
+ /* A right mouse button click within the package list view
+ * selects the package under the cursor, refreshing the tab
+ * pane to display its associated data sheet, and offers a
+ * pop-up menu of actions which may be performed on it.
+ */
+ UpdateDataSheet();
+ SelectPackageAction( LVHT_ONITEMICON | LVHT_ONITEMLABEL );
+ break;
+ }
+ /* Any other notification from the list view control is handled
+ * in common with similar notifications from the tab control, so
+ * we do not break here, but simply fall through.
+ */
case ID_PACKAGE_TABCONTROL:
if( ((NMHDR *)(data))->code == NM_CLICK )
- /*
- * ...each of which may require the data sheet content
+ {
+ /* ...each of which may require the data sheet content
* to be updated, (to reflect a changed selection).
*/
- DataSheet->DisplayData( PackageTabControl, PackageListView );
+ UpdateDataSheet();
+
+ /* Additionally, for a left click on the package status
+ * icon within the list view, we present a pop-up menu
+ * offering a selection of available actions.
+ */
+ if( client_id == ID_PACKAGE_LISTVIEW )
+ SelectPackageAction( LVHT_ONITEMICON );
+ }
break;
+
+ /* We also need to consider notifications from the tree view...
+ */
+ case ID_PACKAGE_TREEVIEW:
+ if( ((NMHDR *)(data))->code == TVN_SELCHANGED )
+ {
+ /* ...from which we are interested only in notifications
+ * that the user has changed the package group selection.
+ *
+ * First, we ensure that any children of the selected
+ * package group are made visible.
+ */
+ TreeView_Expand( PackageTreeView,
+ ((NMTREEVIEW *)(data))->itemNew.hItem, TVE_EXPAND
+ );
+
+ /* We then clear out the previous content of the list view
+ * pane, and reconstruct it with new content, as determined
+ * by the new package group selection...
+ */
+ ClearPackageList();
+ UpdatePackageList();
+
+ /* ...and reapply any scheduled action markers, which may
+ * be applicable.
+ */
+ MarkSchedule( pkgData->Schedule() );
+
+ /* Finally, provided the previous selection is not an
+ * ancestor of the current, we may collapse any visible
+ * subtree descending from the previous.
+ *
+ * FIXME: We may wish to avoid collapsing any subtree
+ * which was designated as "expanded", in the original
+ * group hierarchy specification. We may also wish to
+ * provide a user option, to disable this feature.
+ */
+ bool may_fold = true;
+ HTREEITEM prev = ((NMTREEVIEW *)(data))->itemNew.hItem;
+ while( may_fold && (prev != NULL) )
+ { if( prev == ((NMTREEVIEW *)(data))->itemOld.hItem )
+ /*
+ * Previous selection IS an ancestor of current;
+ * we must not collapse it.
+ */
+ may_fold = false;
+
+ else
+ /* Continue tracing ancestry, back to the root.
+ */
+ prev = TreeView_GetParent( PackageTreeView, prev );
+ }
+ if( may_fold )
+ /*
+ * Previous selection may be collapsed; do so.
+ */
+ TreeView_Expand( PackageTreeView,
+ ((NMTREEVIEW *)(data))->itemOld.hItem, TVE_COLLAPSE
+ );
+ }
}
/* Otherwise, this return causes any other notifiable events
* to be simply ignored, (as they were by the original stub).
return EXIT_SUCCESS;
}
+unsigned long pkgActionItem::EnumeratePendingActions( int classified )
+{
+ /* Helper method to count the pending actions in a
+ * scheduled action list.
+ */
+ unsigned long count = 0;
+ if( this != NULL )
+ {
+ /* Regardless of the position of the 'this' pointer,
+ * within the list of scheduled actions...
+ */
+ pkgActionItem *item = this;
+ while( item->prev != NULL )
+ /*
+ * ...we want to get a reference to the first
+ * item in the list.
+ */
+ item = item->prev;
+
+ /* Now, working through the list...
+ */
+ while( item != NULL )
+ {
+ /* ...note items with any scheduled action...
+ */
+ int action;
+ if( (action = item->flags & ACTION_MASK) != 0 )
+ {
+ /* ...and, when one is found, (noting that ACTION_UPGRADE may
+ * also be considered as a special case of ACTION_INSTALL)...
+ */
+ if( (action == classified)
+ || ((action == ACTION_UPGRADE) && (classified == ACTION_INSTALL)) )
+ {
+ /* ...and it matches the classification in which
+ * we are interested, then we retrieve the tarname
+ * for the related package...
+ */
+ pkgXmlNode *selected = (classified & ACTION_REMOVE)
+ ? item->Selection( to_remove )
+ : item->Selection();
+ const char *notification = (selected != NULL)
+ ? selected->GetPropVal( tarname_key, NULL )
+ : NULL;
+ if( notification != NULL )
+ {
+ /* ...and, provided it is valid, we append it to
+ * the DMH driven dialogue in which the enumeration
+ * is being reported...
+ */
+ dmh_printf( "%s\n", notification );
+ /*
+ * ...and include it in the accumulated count...
+ */
+ ++count;
+ }
+ }
+ else if( (classified == 0)
+ /*
+ * ...otherwise, when we aren't interested in any particular
+ * class of action regardless of classification...
+ */
+ || ((classified == ACTION_UNSUCCESSFUL) && ((flags & classified) != 0)) )
+ /*
+ * ...or when we are checking for unsuccessful actions, we
+ * count all those which are found, either unclassified, or
+ * marked as unsuccessful, respectively.
+ */
+ ++count;
+ }
+ /* ...then move on, to consider the next entry, if any.
+ */
+ item = item->next;
+ }
+ }
+ /* Ultimately, return the count of pending actions,
+ * as noted while processing the above loop.
+ */
+ return count;
+}
+
+long AppWindowMaker::OnClose()
+{
+ /* Intercept application termination requests; check for
+ * outstanding pending actions, and offer a cancellation
+ * option for the termination request, so that the user
+ * has an opportunity to complete such actions.
+ */
+ return ConfirmActionRequest( "quit" ) ? -1 : 0;
+}
+
/* $RCSfile$: end of file */