6 * Written by Keith Marshall <keith@users.osdn.me>
7 * Copyright (C) 2012, 2013, 2020, MinGW.org Project
10 * Implementation of the classes and methods required to support the
11 * GUI tabbed view of package information "data sheets".
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.
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.
28 #define _WIN32_IE 0x0300
46 using WTK::StringResource;
47 using WTK::WindowClassMaker;
48 using WTK::ChildWindowMaker;
50 /* Margin settings, controlling the positioning of the
51 * active viewport within the data sheet display pane.
54 #define PARAGRAPH_MARGIN 5
56 #define RIGHT_MARGIN 8
57 #define BOTTOM_MARGIN 5
59 class pkgTroffLayoutEngine: public pkgUTF8Parser
61 /* A privately implemented class, supporting a simplified troff
62 * style layout for a UTF-8 text stream, within a scrolling GUI
66 pkgTroffLayoutEngine( const char *input, long displacement ):
67 pkgUTF8Parser( input ), curr( this ), offset( displacement ){}
68 bool WriteLn( HDC, RECT * );
71 inline bool IsReady();
72 pkgTroffLayoutEngine *curr;
76 inline bool pkgTroffLayoutEngine::IsReady()
78 /* Private helper method, used to position the input stream to
79 * the next parseable token, if any, for processing by WriteLn.
81 while( (curr != NULL) && ((curr->length == 0) || (curr->text == NULL)) )
82 curr = (pkgTroffLayoutEngine *)(curr->next);
83 return (curr != NULL);
86 bool pkgTroffLayoutEngine::WriteLn( HDC canvas, RECT *bounds )
88 /* Method to extract a single line of text from the UTF-8 stream,
89 * (if any is available for processing), format it as appropriate
90 * for display, and write it into the display pane.
94 /* Initialise a buffer, in which to compile the formatted text
95 * record for display; establish and initialise the counters for
96 * controlling the formatting process.
98 wchar_t linebuf[1 + strlen( curr->text )];
99 long curr_width, new_width = 0, max_width = bounds->right - bounds->left;
100 int filled, extent = 0, fold = 0;
102 /* Establish default tracking and justification settings.
104 SetTextCharacterExtra( canvas, 0 );
105 SetTextJustification( canvas, 0, 0 );
107 /* Copy text from the input stream, to fill the transfer buffer
108 * up to the maximum permitted output line length, or until the
109 * input stream has been exhausted.
112 do { if( curr->length > 0 )
113 { /* There is at least one more word of input text to copy,
114 * and there may be sufficient output space to accommodate
115 * it; record the space filled so far, up to the end of the
116 * preceding word, (if any)...
119 curr_width = new_width;
122 /* ...and, when there was a preceding word, add white
123 * space and record a potential line folding point.
125 linebuf[extent++] = L'\x20';
129 /* Append one word, copied from the input stream to the
130 * output line buffer.
132 const char *mark = curr->text;
133 for( int i = 0; i < curr->length; ++i )
134 linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
136 /* Check the effective output line length which would be
137 * required to accommodate the extended output record...
139 if( GetTextExtentPoint32W( canvas, linebuf, extent, &span ) )
141 /* ...and while it still fits within the maximum width
142 * of the display pane...
144 if( max_width >= (new_width = span.cx) )
146 * ...accept the current input word, and move on to
147 * see if we can accommodate another.
149 curr = (pkgTroffLayoutEngine *)(curr->next);
152 /* In the event of any error in evaluating the output
153 * line length, reject any remaining input.
158 /* We found a zero-length entity in the input stream;
159 * ignore it, and move on to the next, if any.
161 curr = (pkgTroffLayoutEngine *)(curr->next);
163 /* Continue the cycle, unless we have exhausted the input
164 * stream, or we have run out of available output space.
166 } while( (curr != NULL) && (max_width > new_width) );
168 /* When we've collected a complete line of output text...
170 if( (bounds->top >= (TOP_MARGIN + offset))
171 && ((bounds->bottom + offset) >= (bounds->top + span.cy)) )
173 /* ...and when it is to be positioned vertically within the
174 * bounds of the active viewport...
176 if( bounds->top < (TOP_MARGIN + offset + span.cy) )
178 * ...when it is the topmost visible line, ensure that it
179 * is vertically aligned flush with the top margin.
181 bounds->top = TOP_MARGIN + offset;
183 /* Check if the output line collection loop, above, ended
184 * on an attempt to over-fill the buffer...
186 if( max_width >= new_width )
188 * ...but when it did not, handle it as a partially filled
189 * line, which is thus exempt from right justification.
193 /* When the output line is over-filled, then we will attempt
194 * to fold it at the last counted fold point, and then insert
195 * padding space at each remaining internal fold point, so as
196 * to achieve flush left/right justification; (note that we
197 * decrement the fold count here, because the point at which
198 * we fold the line has been included in the count, but we
199 * don't want to add padding space at the right margin).
201 else if( --fold > 0 )
203 /* To adjust the output line, we first compute the number
204 * of padding PIXELS required, then...
207 if( (padding = max_width - curr_width) >= filled )
209 /* ...in the event that this is no fewer than the number
210 * of physical GLYPHS to be output, we adjust the tracking
211 * to accommodate as many padding pixels as possible, with
212 * ONE additional inter-glyph tracking pixel per glyph...
214 SetTextCharacterExtra( canvas, 1 );
215 if( GetTextExtentPoint32W( canvas, linebuf, filled, &span ) )
217 * ...and then, we recompute the number of additional
218 * inter-word padding pixels, if any, which are still
221 padding = max_width - span.cx;
223 /* In the event that adjustment of tracking fails, we
224 * must reset it, because the padding count remains as
225 * computed for default tracking.
228 SetTextCharacterExtra( canvas, 0 );
230 /* Now, provided the padding pixels will not increase the
231 * inter-word (fold) spacing to more than 5% of the total
232 * line length at each potential fold point...
234 if( ((padding * 100) / (max_width * fold)) < 5 )
236 * ...distribute the padding pixels among the remaining
237 * inter-word spaces within the output line...
239 SetTextJustification( canvas, padding, fold );
242 /* ...otherwise, we decline to adjust the output line,
243 * and we prefer to also preserve natural tracking.
245 SetTextCharacterExtra( canvas, 0 );
248 { /* If we get to here, then the first item in the output
249 * queue requires more space than the available width of
250 * the display pane, and has no natural fold points; we
251 * MUST handle this, to avoid an infinite loop!
253 * FIXME: The method adopted here simply elides the
254 * portion of the input text, which will not fit into
255 * the available display width, at the right hand end of
256 * the line; we may wish to consider adding horizontal
257 * scrolling, so that such elided text may be viewed.
259 * We begin by loading the content of the first queued
260 * entity into the line transfer buffer.
263 const char *mark = curr->text;
264 for( int i = 0; i < curr->length; ++i )
265 linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
267 /* Reduce the maximum allowable output width sufficiently
268 * to accommodate an ellipsis at the right hand end of the
271 int fit = GetTextExtentPoint32W( canvas, L"...", 3, &span )
272 ? max_width - span.cx : max_width;
274 /* ...then compute the maximum number of characters from
275 * the queued item, which will fit in the remaining space...
277 if( GetTextExtentExPointW
278 ( canvas, linebuf, extent, fit, &filled, NULL, &span )
280 /* ...and then append the ellipsis, in place of any
281 * characters which will not fit, leaving the resultant
282 * line, with elided tail, ready for display.
284 for( int i = 0; i < 3; ++i )
285 linebuf[filled++] = L'.';
287 /* Finally, pop the entity we just processed from the
288 * output queue, before falling through...
290 curr = (pkgTroffLayoutEngine *)(curr->next);
292 /* ...and write the output line at the designated position
293 * within the display viewport.
295 TextOutW( canvas, bounds->left, bounds->top - offset, linebuf, filled );
297 else if( (fold == 0) && (new_width > max_width) )
299 * The output line which we've just processed lies outside
300 * the viewport. We note that it's initial (non-breakable)
301 * "word" would require more display width than the viewport
302 * can accommodate, if it were to be moved into the visible
303 * region; thus, this "word" will continue to be presented
304 * to the formatting engine, as the next input "word" to be
305 * processed. This would result in an infinite loop, so we
306 * MUST discard this "word" from the input queue.
308 curr = (pkgTroffLayoutEngine *)(curr->next);
310 /* Finally, adjust the top boundary of the viewport, to indicate
311 * where the NEXT output line, if any, is to be positioned, and
312 * return TRUE, to indicate that an output line was processed.
314 bounds->top += span.cy;
317 /* If we get to here, then there was nothing in the input stream to
318 * be processed; return FALSE, to indicate this.
323 class DataSheetMaker: public ChildWindowMaker
325 /* Specialised variant of the standard child window class, augmented
326 * to provide the custom methods for formatting and displaying package
327 * data sheet content within the tabbed data display pane.
329 * FIXME: we may eventually need to make this class externally visible,
330 * but for now we implement it as a locally declared class.
333 DataSheetMaker( HINSTANCE inst ): ChildWindowMaker( inst ),
334 PackageRef( NULL ), DataClass( NULL ){}
335 virtual void DisplayData( HWND, HWND );
338 virtual long OnCreate();
339 virtual long OnVerticalScroll( int, int, HWND );
340 virtual long OnPaint();
342 HWND PackageRef, DataClass;
343 static DataSheetMaker *Display;
344 HDC canvas; RECT bounding_box;
345 HFONT NormalFont, BoldFont;
348 long offset; char *desc;
349 void DisplayGeneralData( pkgXmlNode * );
350 static int DisplaySourceURL( const char * );
351 static int DisplayLicenceURL( const char * );
352 static int DisplayPackageURL( const char * );
353 inline void DisplayDescription( pkgXmlNode * );
354 inline void DisplayFilesManifest( pkgXmlNode * );
355 void ComposeDescription( pkgXmlNode *, pkgXmlNode * );
356 int FormatRecord( int, const char *, const char * );
357 inline void FormatText( const char * );
361 /* Don't forget to instantiate the static member variables...
363 int DataSheetMaker::Advance;
364 DataSheetMaker *DataSheetMaker::Display;
367 { /* Tab identifiers for the available data sheet collection.
369 PKG_DATASHEET_GENERAL = 0,
370 PKG_DATASHEET_DESCRIPTION,
371 PKG_DATASHEET_DEPENDENCIES,
372 PKG_DATASHEET_INSTALLED_FILES,
373 PKG_DATASHEET_VERSIONS
376 long DataSheetMaker::OnCreate()
378 /* Method called when creating a data sheet window; initialise font
379 * preferences and line spacing for any instance of a DataSheetMaker
382 * Initially, we match the font properties to the default GUI font...
385 HFONT font = (HFONT)(GetStockObject( DEFAULT_GUI_FONT ));
386 GetObject( BoldFont = NormalFont = font, sizeof( LOGFONT ), &font_info );
388 /* ...then, we substitute the preferred type face.
390 strcpy( (char *)(&(font_info.lfFaceName)), "Verdana" );
391 if( (font = CreateFontIndirect( &font_info )) != NULL )
393 /* On successfully creating the preferred font, we may discard
394 * the original default font object, and assign our preference
395 * as both the normal and bold working font...
397 DeleteObject( NormalFont );
398 BoldFont = NormalFont = font;
400 /* ...before adjusting the weight for the bold variant...
402 font_info.lfWeight = FW_BOLD;
403 if( (font = CreateFontIndirect( &font_info )) != NULL )
405 /* ...and reassigning when successful.
411 /* Finally, we determine the line spacing (in pixels) for a line
412 * of text, in the preferred normal font, within the device context
413 * for the data sheet window.
416 HDC canvas = GetDC( AppWindow );
417 SelectObject( canvas, NormalFont );
418 LineSpacing = GetTextExtentPoint32A( canvas, "Height", 6, &span ) ? span.cy : 13;
419 ReleaseDC( AppWindow, canvas );
424 void DataSheetMaker::DisplayData( HWND tab, HWND package )
426 /* Method to force a refresh of the data sheet display pane.
428 PackageRef = package; DataClass = tab;
429 InvalidateRect( AppWindow, NULL, TRUE );
430 UpdateWindow( AppWindow );
433 inline void DataSheetMaker::FormatText( const char *text )
435 /* Helper method to transfer text to the display device, formatting
436 * it to fill as many lines of the viewing window as may be required,
437 * justifying for flush margins at both left and right.
439 pkgTroffLayoutEngine page( text, offset );
440 while( page.WriteLn( canvas, &bounding_box ) )
444 int DataSheetMaker::FormatRecord( int offset, const char *tag, const char *text )
446 /* Helper method to transfer text to the display device, prefacing
447 * it with a specified record key, before formatting as above.
449 const char *fmt = "%s: %s";
450 int span = snprintf( NULL, 0, fmt, tag, text );
451 char record[ 1 + span ]; snprintf( record, sizeof( record ), fmt, tag, text );
453 bounding_box.top += PARAGRAPH_MARGIN;
454 FormatText( record );
455 return offset + span;
458 void DataSheetMaker::DisplayGeneralData( pkgXmlNode *ref )
460 /* Method to compile the package data, which is to be displayed
461 * on the general information tab; we begin by displaying the
462 * identification records for the selected package, and the
463 * subsystem to which it belongs.
465 FormatRecord( 0, "SubSystem",
466 ref->GetContainerAttribute( subsystem_key, value_unknown )
468 FormatRecord( 1, "Package Name",
469 ref->GetContainerAttribute( name_key, value_unknown )
471 if( ref->IsElementOfType( component_key ) )
472 FormatRecord( 1, "Component Class",
473 ref->GetPropVal( class_key, value_unknown )
476 /* Using a temporary action item, collect information on the
477 * latest available version, and the installed version if any,
478 * of the selected package; print the applicable information,
479 * noting that "none" may be appropriate in the case of the
483 FormatRecord( 0, "Installed Version",
484 ((ref = pkgGetStatus( ref, &avail )) != NULL)
485 ? ref->GetPropVal( tarname_key, value_unknown )
488 FormatRecord( 1, "Repository Version",
489 (ref = avail.Selection())->GetPropVal( tarname_key, value_unknown )
492 /* Finally, report the download URLs for the selected package,
493 * and its associated source and licence archives; (note that we
494 * must save static callback references, so that the PrintURI()
495 * method can access this data sheet context).
497 Display = this; Advance = 0;
498 avail.PrintURI( ref->ArchiveName(), DisplayPackageURL );
499 avail.PrintURI( ref->SourceArchiveName( ACTION_LICENCE ), DisplayLicenceURL );
500 avail.PrintURI( ref->SourceArchiveName( ACTION_SOURCE ), DisplaySourceURL );
503 int DataSheetMaker::DisplayPackageURL( const char *uri )
505 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
506 * to display the package download URL on the active data sheet panel.
508 return Advance = Display->FormatRecord( Advance, "Package URL", uri );
511 int DataSheetMaker::DisplaySourceURL( const char *uri )
513 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
514 * to display the source download URL on the active data sheet panel.
516 return Advance = Display->FormatRecord( Advance, "Source URL", uri );
519 int DataSheetMaker::DisplayLicenceURL( const char *uri )
521 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
522 * to display the licence download URL on the active data sheet panel.
524 return Advance = Display->FormatRecord( Advance, "Licence URL", uri );
527 inline void DataSheetMaker::DisplayDescription( pkgXmlNode *ref )
529 /* A convenience method to invoke the recursive retrieval of any
530 * package description, without requiring a look-up of the address
531 * of the XML document root at every level of recursion.
533 ComposeDescription( ref, ref->GetDocumentRoot() );
536 void DataSheetMaker::ComposeDescription( pkgXmlNode *ref, pkgXmlNode *root )
538 /* Recursive method to compile a package description, from text
539 * fragments retrieved from the XML specification document, and
540 * present it in a data sheet window.
544 /* Recursively walk the XML hierarchy, until we reach the
547 ComposeDescription( ref->GetParent(), root );
549 /* ...then unwind the recursion, selecting "description"
550 * elements, (if any), at each level...
552 if( (root = ref->FindFirstAssociate( description_key )) != NULL )
554 /* ...formatting each, paragraph by paragraph, for display
555 * within the viewport bounding box of the data sheet...
557 do { if( (ref = root->FindFirstAssociate( paragraph_key )) != NULL )
558 do { if( bounding_box.top > (TOP_MARGIN + offset) )
560 * When this is not the top-most visible
561 * paragraph, within the viewport, displace
562 * it downwards by one paragraph margin from
565 bounding_box.top += PARAGRAPH_MARGIN;
567 /* ...before laying out the visible text of
568 * this paragraph, (if any).
570 FormatText( ref->GetText() );
572 /* Cycle, to process any further paragraphs
573 * which are included within the current
574 * description block...
576 } while( (ref = ref->FindNextAssociate( paragraph_key )) != NULL );
578 /* ...and ultimately, for any additional description blocks
579 * which may have been specified within the current XML element,
580 * at the current hierarchical nesting level.
582 } while( (root = root->FindNextAssociate( description_key )) != NULL );
587 void DataSheetMaker::DisplayFilesManifest( pkgXmlNode *ref )
589 /* Helper method to compile the list of files installed by a package,
590 * for display on the "Installed Files" tab of the data sheet panel.
593 if( (ref = pkgGetStatus( ref, &avail )) == NULL )
595 /* This represents a package which is available, but has not been
596 * installed; we simply decline to compile the files list.
598 FormatRecord( 0, "Package",
599 avail.Selection()->GetPropVal( tarname_key, value_unknown )
601 bounding_box.top += PARAGRAPH_MARGIN;
603 "This package has not been installed; "
604 "the list of installed files is not available for packages "
605 "which have not been installed."
609 { /* This represents a package which has been installed; begin
610 * compilation of the list of installed files.
613 FormatRecord( 0, "Package",
614 tarname = ref->GetPropVal( tarname_key, value_unknown )
616 if( match_if_explicit( ref->ArchiveName(), value_none ) )
618 /* This is a meta-package; there are no files to list.
620 bounding_box.top += PARAGRAPH_MARGIN;
622 "This meta-package facilitates the installation of a collection "
623 "of other logically related packages; it provides no files, and "
624 "may be safely removed."
628 { /* This is a real package; retrieve the manifest of installed files,
629 * which was created during the installation process.
632 pkgManifest inventory( package_key, tarname );
633 if( (index = inventory.GetRoot()->FindFirstAssociate( manifest_key )) != NULL )
635 /* We've located a files list within the manifest; process it...
637 FormatRecord( 0, "This package provides the following files", "" );
638 bounding_box.left += LEFT_MARGIN; bounding_box.top += PARAGRAPH_MARGIN;
639 do { if( (ref = index->FindFirstAssociate( filename_key )) != NULL )
641 * We found at least one file name within the list; emit it
642 * to the display, followed by any additional names present.
644 do { FormatText( ref->GetPropVal( pathname_key, value_unknown ) );
645 } while( (ref = ref->FindNextAssociate( filename_key )) != NULL );
647 /* There should be no more than one, but check for, and process
648 * any additional files lists which may be present.
650 } while( (index = index->FindNextAssociate( manifest_key )) != NULL );
653 { /* The manifest appears to be lacking any files list; diagnose.
655 bounding_box.top += PARAGRAPH_MARGIN;
656 FormatText( "This package appears to provide no files." );
662 static pkgXmlNode *pkgListSelection( HWND package_ref, LVITEM *lookup )
664 /* Helper function, to retrieve the active selection from the
665 * package list, as displayed in the upper right data pane.
667 lookup->iSubItem = 0;
668 lookup->mask = LVIF_PARAM;
669 ListView_GetItem( package_ref, lookup );
670 return (pkgXmlNode *)(lookup->lParam);
673 long DataSheetMaker::OnPaint()
675 /* Handler for WM_PAINT message messages, sent to any window
676 * ascribed to the DataSheetMaker class.
679 canvas = BeginPaint( AppWindow, &content );
680 HFONT original_font = (HFONT)(SelectObject( canvas, NormalFont ));
682 /* Establish a viewport, with a suitable margin, within the
683 * bounding rectangle of the window.
685 GetClientRect( AppWindow, &bounding_box );
686 bounding_box.left += LEFT_MARGIN; bounding_box.right -= RIGHT_MARGIN;
687 bounding_box.top += TOP_MARGIN; bounding_box.bottom -= BOTTOM_MARGIN;
689 /* Provide bindings for a vertical scrollbar...
691 SCROLLINFO scrollbar;
694 /* ...and prepare to initialise it, when we redraw
695 * the data sheet from the top.
697 scrollbar.cbSize = sizeof( scrollbar );
698 scrollbar.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
699 scrollbar.nPos = scrollbar.nMin = bounding_box.top;
700 scrollbar.nPage = 1 + bounding_box.bottom - bounding_box.top;
701 scrollbar.nMax = bounding_box.bottom;
704 /* Identify the package, as selected in the package list window,
705 * for which the data sheet is to be compiled.
708 lookup.iItem = (PackageRef != NULL)
709 ? ListView_GetNextItem( PackageRef, (WPARAM)(-1), LVIS_SELECTED )
712 if( lookup.iItem >= 0 )
714 /* There is an active package selection; identify the selected
715 * data sheet tab, if any...
717 int tab = ( DataClass != NULL ) ? TabCtrl_GetCurSel( DataClass )
719 * ...otherwise default to the package description.
721 : PKG_DATASHEET_DESCRIPTION;
723 /* Retrieve the package title from the list view; assign it as
724 * a bold face heading in the data sheet view...
727 SelectObject( canvas, BoldFont );
728 ListView_GetItemText( PackageRef, lookup.iItem, 5, desc, sizeof( desc ) );
732 /* ...adjusting as appropriate, when the heading is scrolled
733 * out of the viewport.
735 if( (offset -= (bounding_box.top - TOP_MARGIN)) < 0 )
737 bounding_box.top = TOP_MARGIN;
740 /* Revert to normal typeface, in preparation for compilation
741 * of the selected data sheet.
743 SelectObject( canvas, NormalFont );
746 case PKG_DATASHEET_GENERAL:
747 /* This comprises package and subsystem identification,
748 * followed by latest version availability, installation
749 * status, and package download URLs.
751 DisplayGeneralData( pkgListSelection( PackageRef, &lookup ) );
754 case PKG_DATASHEET_DESCRIPTION:
755 /* This represents the package description, provided by
756 * the package maintainer, within the XML specification.
758 DisplayDescription( pkgListSelection( PackageRef, &lookup ) );
761 case PKG_DATASHEET_INSTALLED_FILES:
762 /* Available only for packages which have been installed,
763 * this comprises the files list content from the manifest
764 * which was created during the installation process.
766 DisplayFilesManifest( pkgListSelection( PackageRef, &lookup ) );
770 /* Handle requests for data sheets for which we have yet
771 * to provide a compiling routine.
773 bounding_box.top += TOP_MARGIN;
775 "FIXME:data sheet unavailable; a compiler for this "
776 "data category has yet to be implemented."
781 { /* There is no active package selection; advise accordingly.
783 bounding_box.top += TOP_MARGIN << 1;
785 "No package selected."
787 bounding_box.top += PARAGRAPH_MARGIN << 1;
789 "Please select a package from the list above, "
790 "to view related data."
794 /* When redrawing the data sheet window from the top...
798 /* ...adjust the scrolling range to accommodate the full extent
799 * of the data sheet text, and initialise the scrollbar control.
801 if( bounding_box.top > bounding_box.bottom )
802 scrollbar.nMax = bounding_box.top;
803 SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
806 /* Finally, restore the original (default) font assignment
807 * for the data sheet window, complete the redraw action, and
810 SelectObject( canvas, original_font );
811 EndPaint( AppWindow, &content );
815 long DataSheetMaker::OnVerticalScroll( int req, int pos, HWND ctrl )
817 /* Handler for events signalled by the vertical scrollbar control,
818 * (if any), in any window ascribed to the DataSheetMaker class.
820 SCROLLINFO scrollbar;
821 scrollbar.fMask = SIF_ALL;
822 scrollbar.cbSize = sizeof( scrollbar );
823 GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
825 /* Save the original "thumb" position.
827 long origin = scrollbar.nPos;
830 /* Identify, and process the event message.
833 /* User clicked the "scroll-up" button; move the
834 * "thumb" up by a distance equivalent to the height
835 * of a single line of text.
837 scrollbar.nPos -= LineSpacing;
841 /* Similarly, for a click on the "scroll-down" button,
842 * move the "thumb" down by one line height.
844 scrollbar.nPos += LineSpacing;
848 /* User clicked the scrollbar region above the "thumb";
849 * move the "thumb" up by half of the viewport height.
851 scrollbar.nPos -= scrollbar.nPage >> 1;
855 /* Similarly, for a click below the "thumb", move it
856 * down by half of the viewport height.
858 scrollbar.nPos += scrollbar.nPage >> 1;
862 /* User is dragging...
864 case SB_THUMBPOSITION:
865 /* ...or has just finished dragging the "thumb"; move it
866 * by the distance it has been dragged.
868 scrollbar.nPos = scrollbar.nTrackPos;
871 /* Preceding scrollbar event has completed; we do not need
872 * to take any specific action here.
877 /* We received an unexpected scrollbar event message...
879 dmh_notify( DMH_WARNING,
880 "Unhandled scrollbar message: request = %d\n", req
883 /* Update the scrollbar control, to capture any change in
884 * "thumb" position...
886 scrollbar.fMask = SIF_POS;
887 SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
889 /* ...then read it back, since the control hay have adjusted
890 * the actual recorded position.
892 GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
893 if( scrollbar.nPos != origin )
895 /* When the "thumb" has moved, force a redraw of the data
896 * sheet window, to capture any change in the visible text.
898 offset = scrollbar.nPos - scrollbar.nMin;
899 InvalidateRect( AppWindow, NULL, TRUE );
900 UpdateWindow( AppWindow );
902 /* Reset the default starting point, so that any subsequent
903 * redraw will favour a "redraw-from-top"...
907 /* ...and we are done.
912 void AppWindowMaker::InitPackageTabControl()
914 /* Create and initialise a TabControl window, in which to present
915 * miscellaneous package information...
917 WindowClassMaker AppWindowRegistry( AppInstance );
918 StringResource ClassName( AppInstance, ID_SASH_WINDOW_PANE_CLASS );
919 AppWindowRegistry.Register( ClassName );
921 /* Package data sheets will be displayed in a derived child window
922 * which we create as a member of the SASH_WINDOW_PANE_CLASS; it will
923 * ultimately be displayed below the tab bar, within the tab control
924 * region, with content dynamically painted on the basis of package
925 * selection, (in the package list pane), and tab selection.
927 DataSheet = new DataSheetMaker( AppInstance );
928 PackageTabPane = DataSheet->Create( ID_PACKAGE_DATASHEET,
929 AppWindow, ClassName, WS_VSCROLL | WS_BORDER
932 /* The tab control itself is the standard control, selected from
933 * the common controls library.
935 PackageTabControl = CreateWindow( WC_TABCONTROL, NULL,
936 WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 0, 0,
937 AppWindow, (HMENU)(ID_PACKAGE_TABCONTROL),
941 /* Keep the font for tab labels consistent with our preference,
942 * as assigned to the main application window.
944 SendMessage( PackageTabControl, WM_SETFONT, (WPARAM)(DefaultFont), TRUE );
946 /* Create the designated set of tabs, with appropriate labels...
949 tab.mask = TCIF_TEXT;
950 const char *TabLegend[] =
951 { "General", "Description", "Dependencies", "Installed Files", "Versions",
953 /* ...with a NULL sentinel marking the preceding label as
954 * the last in the list.
958 for( int i = 0; TabLegend[i] != NULL; ++i )
960 /* This loop assumes responsibility for actual tab creation...
962 tab.pszText = (char *)(TabLegend[i]);
963 if( TabCtrl_InsertItem( PackageTabControl, i, &tab ) == -1 )
965 /* ...bailing out, and deleting the container window,
966 * in the event of a creation error.
968 TabLegend[i + 1] = NULL;
969 DestroyWindow( PackageTabControl );
970 PackageTabControl = NULL;
973 if( PackageTabControl != NULL )
975 /* When the tab control has been successfully created, we
976 * create one additional basic SASH_WINDOW_PANE_CLASS window;
977 * this serves to draw a border around the tab pane.
979 TabDataPane = new ChildWindowMaker( AppInstance );
980 TabDataPane->Create( ID_PACKAGE_TABPANE, AppWindow, ClassName, WS_BORDER );
982 /* We also assign the package description data sheet as the
983 * initial default tab selection.
985 TabCtrl_SetCurSel( PackageTabControl, PKG_DATASHEET_DESCRIPTION );
989 void AppWindowMaker::UpdatePackageMenuBindings()
990 # define PKGSTATE_FLAG( ID ) (1 << PKGSTATE( ID ))
992 /* Helper method to enable or disable the set of options
993 * which may be chosen from the package menu; (this varies
994 * according to the installation status of the package, if
995 * any, which has been selected in the package list view).
998 if( (menu = GetMenu( AppWindow )) != NULL )
1000 /* We got a valid handle for the menubar; identify the
1001 * list view selection, which controls the available set
1002 * of menu options...
1005 lookup.iItem = (PackageListView != NULL)
1006 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1009 /* ...and identify its state of the associated package,
1010 * as indicated by the assigned icon.
1012 lookup.iSubItem = 0;
1013 lookup.mask = LVIF_IMAGE;
1014 ListView_GetItem( PackageListView, &lookup );
1016 /* Convert the indicated state to a selector bit-flag.
1018 int state = ((lookup.iItem >= 0) && (lookup.iImage <= PKGSTATE( PURGE )))
1019 ? 1 << lookup.iImage : 0;
1021 /* Walk over all state-conditional menu items...
1023 for( int item = IDM_PACKAGE_UNMARK; item <= IDM_PACKAGE_REMOVE; item++ )
1025 /* ...evaluating an independent state flag for each,
1026 * setting it as non-zero for menu items which may be
1027 * made "selectable", or zero otherwise...
1029 int state_flag = state;
1031 { /* ...testing against item specific flag groups, to
1032 * determine which menu items should be enabled for
1033 * the currently selected list view item...
1035 case IDM_PACKAGE_INSTALL:
1036 /* "Mark for Installation" is available for packages
1037 * which exist in the repository, (long-term or new),
1038 * but which are not yet identified as "installed".
1040 state_flag &= PKGSTATE_FLAG( AVAILABLE )
1041 | PKGSTATE_FLAG( AVAILABLE_NEW );
1044 case IDM_PACKAGE_REMOVE:
1045 //case IDM_PACKAGE_REINSTALL: // FIXME: for now, we don't consider this!
1046 /* "Mark for Removal" and "Mark for Reinstallation"
1047 * are viable selections only for packages identified
1048 * as "installed", (current or upgradeable).
1050 state_flag &= PKGSTATE_FLAG( INSTALLED_CURRENT )
1051 | PKGSTATE_FLAG( INSTALLED_OLD );
1054 case IDM_PACKAGE_UPGRADE:
1055 /* "Mark for Upgrade" is viable only for packages
1056 * identified as "installed", and then only when an
1057 * upgrade has been published.
1059 state_flag &= PKGSTATE_FLAG( INSTALLED_OLD );
1062 case IDM_PACKAGE_UNMARK:
1063 /* The "Unmark" facility is available only for packages
1064 * which have been marked, (perhaps inadvertently), for
1065 * any of the preceding actions.
1067 state_flag &= PKGSTATE_FLAG( AVAILABLE_INSTALL )
1068 | PKGSTATE_FLAG( UPGRADE ) | PKGSTATE_FLAG( DOWNGRADE )
1069 | PKGSTATE_FLAG( REMOVE ) | PKGSTATE_FLAG( PURGE )
1070 | PKGSTATE_FLAG( REINSTALL );
1074 /* When none of the preceding is applicable, the menu
1075 * item should not be selectable.
1079 /* ...and set the menu item enabled state accordingly.
1081 EnableMenuItem( menu, item, (state_flag == 0) ? MF_GRAYED : MF_ENABLED );
1084 /* Although it is listed under the "Installation" drop-down menu,
1085 * this is also a convenient point to consider activation of the
1086 * "Apply Changes" capability.
1088 pkgActionItem *pending = pkgData->Schedule();
1089 unsigned long count = (pending != NULL) ? pending->EnumeratePendingActions() : 0UL;
1090 EnableMenuItem( menu, IDM_REPO_APPLY, (count > 0UL) ? MF_ENABLED : MF_GRAYED );
1093 inline void AppWindowMaker::MarkSchedule( pkgActionItem *pending_actions )
1095 /* Helper routine to update the status icons within the package list view,
1096 * reflecting any scheduled action in respect of the package associated with
1097 * each, and updating the menu bindings to match.
1099 if( pending_actions != NULL )
1101 pkgListViewMaker pkglist( PackageListView );
1102 pkglist.MarkScheduledActions( pending_actions );
1104 UpdatePackageMenuBindings();
1107 void AppWindowMaker::Schedule
1108 ( unsigned long action, const char *bounds, const char *pkgname )
1110 /* GUI menu driven interface to the pkgActionItem task scheduler;
1111 * it constructs a pseudo-argument string, emulating the effect of
1112 * parsing a CLI argument, then passes this to the CLI scheduler
1115 if( pkgname == NULL )
1117 /* Initial entry on menu item selection; package name has not
1118 * yet been identified, so find the selected list view item...
1121 lookup.iItem = (PackageListView != NULL)
1122 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1125 /* ...and look up the package name identified within it.
1127 const char *pkg, *fmt = "%s-%s";
1128 pkgXmlNode *ref = pkgListSelection( PackageListView, &lookup );
1129 if( (pkg = ref->GetContainerAttribute( name_key, NULL )) != NULL )
1131 /* We now have a valid package name; check for a
1132 * component package association.
1135 if( (cpt = ref->GetPropVal( class_key, NULL )) == NULL )
1137 /* Current list view selection represents a
1138 * non-component package; encode its name only
1139 * as a string argument, using only the final
1140 * string field of the format specification.
1142 char pkgspec[ 1 + snprintf( NULL, 0, fmt + 3, pkg ) ];
1143 snprintf( pkgspec, sizeof( pkgspec ), fmt + 3, pkg );
1145 /* Recurse, to capture any supplied version bounds
1146 * specification, and ultimately schedule the action.
1148 Schedule( action, bounds, pkgspec );
1151 { /* Current list view selection represents a
1152 * package name qualified by a component name;
1153 * use the full format specification to encode
1154 * the fully qualified package name.
1156 char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkg, cpt ) ];
1157 snprintf( pkgspec, sizeof( pkgspec ), fmt, pkg, cpt );
1159 /* Again, recurse to capture any supplied version
1160 * bounds specification, before ultimately scheduling
1161 * the selected action.
1163 Schedule( action, bounds, pkgspec );
1167 else if( bounds != NULL )
1169 /* Recursive entry, after package name identification,
1170 * but with supplied version bounds specification yet
1171 * to be resolved; append the bounds specification to
1172 * the package name, as it would be in a CLI argument...
1174 const char *fmt = "%s=%s";
1175 char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkgname, bounds ) ];
1176 snprintf( pkgspec, sizeof( pkgspec ), fmt, pkgname, bounds );
1178 * ...then recurse a final time, to schedule the action.
1180 Schedule( action, NULL, pkgspec );
1183 { /* Final recursive entry, with pkgname argument in the
1184 * same form as a CLI package name/bounds specification
1185 * argument; hand it off to the CLI scheduler, capturing
1186 * the resultant schedule of actions, and update the list
1187 * view state icons to reflect the pending actions.
1189 MarkSchedule( pkgData->Schedule( action, pkgname ) );
1193 inline void pkgActionItem::CancelScheduledAction( void )
1195 /* Helper method to mark a scheduled action as "cancelled".
1197 flags &= ~ACTION_MASK;
1200 void AppWindowMaker::UnmarkSelectedPackage( void )
1202 /* Method to clear any request for an action in respect of
1203 * the currently selected package entry in the list view; we
1204 * implement this as a cancellation of any pending scheduled
1205 * action, in respect of the selected package.
1207 * First, obtain a reference for the list view selection...
1210 lookup.iItem = (PackageListView != NULL)
1211 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1214 /* ...and when it represents a valid selection...
1216 if( lookup.iItem >= 0 )
1218 /* ...retrieve its associated XML database package reference...
1220 pkgXmlNode *pkg = pkgListSelection( PackageListView, &lookup );
1222 * ...search the action schedule, for an action associated with
1223 * this package, if any, and cancel it.
1225 pkgActionItem *pkgref = pkgData->Schedule();
1226 if( (pkgref != NULL) && ((pkgref = pkgref->GetReference( pkg )) != NULL) )
1227 pkgref->CancelScheduledAction();
1229 /* The scheduling state for packages shown in the list view
1230 * may have changed, so refresh the icon associations and the
1231 * package menu bindings accordingly.
1233 MarkSchedule( pkgData->Schedule() );
1237 void AppWindowMaker::SelectPackageAction( unsigned mode )
1239 /* Helper method to present the package menu as a floating pop-up.
1242 LVHITTESTINFO whence;
1244 /* Before presenting the menu, ensure that its selection bindings
1245 * are current, as determined for the selected package; note that
1246 * we do this unconditionally, to ensure that the bindings remain
1247 * current, when the user accesses the menu from the menu bar.
1249 UpdatePackageMenuBindings();
1251 /* Locate the cursor position, mapping it into the co-ordinate
1252 * system of the list view client window.
1254 whence.pt.y = GetMessagePos();
1255 whence.pt.x = GET_X_LPARAM( whence.pt.y );
1256 whence.pt.y = GET_Y_LPARAM( whence.pt.y );
1257 ScreenToClient( PackageListView, &whence.pt );
1259 /* Perform a hit-test, to confirm that either the left mouse
1260 * button was clicked on the package status icon, or the right
1261 * button was clicked anywhere on the package list entry; only
1262 * if one of these is detected, do we then proceed to retrieve
1263 * a handle for the pop-up menu itself...
1265 if( (ListView_SubItemHitTest( PackageListView, &whence ) >= 0)
1266 && ((whence.flags & mode) != 0) && ((popup = GetMenu( AppWindow )) != NULL)
1267 && ((popup = GetSubMenu( popup, 1 )) != NULL) )
1269 /* ...and provided it is valid, we remap the cursor position
1270 * back into the screen co-ordinate system, and present the
1271 * menu at the resultant position.
1273 ClientToScreen( PackageListView, &whence.pt );
1274 TrackPopupMenu( popup, 0, whence.pt.x, whence.pt.y, 0, AppWindow, NULL );
1278 void AppWindowMaker::UpdateDataSheet( void )
1280 /* Helper method, called when we wish to update the data sheet
1281 * panel, to match the current list view and tab selection.
1283 DataSheet->DisplayData( PackageTabControl, PackageListView );
1286 long AppWindowMaker::OnNotify( WPARAM client_id, LPARAM data )
1288 /* Handler for notifiable events to be processed in the context
1289 * of the main application window.
1291 * FIXME: this supersedes the stub handler, originally provided
1292 * by pkgview.cpp; it may not yet be substantially complete, and
1293 * may eventually migrate elsewhere.
1297 /* At present, we handle only mouse click events within the
1298 * package list view and data sheet tab control panes...
1300 case ID_PACKAGE_LISTVIEW:
1301 if( ((NMHDR *)(data))->code == NM_RCLICK )
1303 /* A right mouse button click within the package list view
1304 * selects the package under the cursor, refreshing the tab
1305 * pane to display its associated data sheet, and offers a
1306 * pop-up menu of actions which may be performed on it.
1309 SelectPackageAction( LVHT_ONITEMICON | LVHT_ONITEMLABEL );
1312 /* Any other notification from the list view control is handled
1313 * in common with similar notifications from the tab control, so
1314 * we do not break here, but simply fall through.
1316 case ID_PACKAGE_TABCONTROL:
1317 if( ((NMHDR *)(data))->code == NM_CLICK )
1319 /* ...each of which may require the data sheet content
1320 * to be updated, (to reflect a changed selection).
1324 /* Additionally, for a left click on the package status
1325 * icon within the list view, we present a pop-up menu
1326 * offering a selection of available actions.
1328 if( client_id == ID_PACKAGE_LISTVIEW )
1329 SelectPackageAction( LVHT_ONITEMICON );
1333 /* We also need to consider notifications from the tree view...
1335 case ID_PACKAGE_TREEVIEW:
1336 if( ((NMHDR *)(data))->code == TVN_SELCHANGED )
1338 /* ...from which we are interested only in notifications
1339 * that the user has changed the package group selection.
1341 * First, we ensure that any children of the selected
1342 * package group are made visible.
1344 TreeView_Expand( PackageTreeView,
1345 ((NMTREEVIEW *)(data))->itemNew.hItem, TVE_EXPAND
1348 /* We then clear out the previous content of the list view
1349 * pane, and reconstruct it with new content, as determined
1350 * by the new package group selection...
1353 UpdatePackageList();
1355 /* ...and reapply any scheduled action markers, which may
1358 MarkSchedule( pkgData->Schedule() );
1360 /* Finally, provided the previous selection is not an
1361 * ancestor of the current, we may collapse any visible
1362 * subtree descending from the previous.
1364 * FIXME: We may wish to avoid collapsing any subtree
1365 * which was designated as "expanded", in the original
1366 * group hierarchy specification. We may also wish to
1367 * provide a user option, to disable this feature.
1369 bool may_fold = true;
1370 HTREEITEM prev = ((NMTREEVIEW *)(data))->itemNew.hItem;
1371 while( may_fold && (prev != NULL) )
1372 { if( prev == ((NMTREEVIEW *)(data))->itemOld.hItem )
1374 * Previous selection IS an ancestor of current;
1375 * we must not collapse it.
1380 /* Continue tracing ancestry, back to the root.
1382 prev = TreeView_GetParent( PackageTreeView, prev );
1386 * Previous selection may be collapsed; do so.
1388 TreeView_Expand( PackageTreeView,
1389 ((NMTREEVIEW *)(data))->itemOld.hItem, TVE_COLLAPSE
1393 /* Otherwise, this return causes any other notifiable events
1394 * to be simply ignored, (as they were by the original stub).
1396 return EXIT_SUCCESS;
1399 unsigned long pkgActionItem::EnumeratePendingActions( int classified )
1401 /* Helper method to count the pending actions in a
1402 * scheduled action list.
1404 unsigned long count = 0;
1406 /* Regardless of the position of the 'this' pointer,
1407 * within the list of scheduled actions...
1409 pkgActionItem *item = this;
1410 while( item->prev != NULL )
1412 * ...we want to get a reference to the first
1417 /* Now, working through the list...
1419 while( item != NULL )
1421 /* ...note items with any scheduled action...
1424 if( (action = item->flags & ACTION_MASK) != 0 )
1426 /* ...and, when one is found, (noting that ACTION_UPGRADE may
1427 * also be considered as a special case of ACTION_INSTALL)...
1429 if( (action == classified)
1430 || ((action == ACTION_UPGRADE) && (classified == ACTION_INSTALL)) )
1432 /* ...and it matches the classification in which
1433 * we are interested, then we retrieve the tarname
1434 * for the related package...
1436 pkgXmlNode *selected = (classified & ACTION_REMOVE)
1437 ? item->Selection( to_remove )
1438 : item->Selection();
1439 const char *notification = (selected != NULL)
1440 ? selected->GetPropVal( tarname_key, NULL )
1442 if( notification != NULL )
1444 /* ...and, provided it is valid, we append it to
1445 * the DMH driven dialogue in which the enumeration
1446 * is being reported...
1448 dmh_printf( "%s\n", notification );
1450 * ...and include it in the accumulated count...
1455 else if( (classified == 0)
1457 * ...otherwise, when we aren't interested in any particular
1458 * class of action regardless of classification...
1460 || ((classified == ACTION_UNSUCCESSFUL) && ((flags & classified) != 0)) )
1462 * ...or when we are checking for unsuccessful actions, we
1463 * count all those which are found, either unclassified, or
1464 * marked as unsuccessful, respectively.
1468 /* ...then move on, to consider the next entry, if any.
1472 /* Ultimately, return the count of pending actions,
1473 * as noted while processing the above loop.
1478 long AppWindowMaker::OnClose()
1480 /* Intercept application termination requests; check for
1481 * outstanding pending actions, and offer a cancellation
1482 * option for the termination request, so that the user
1483 * has an opportunity to complete such actions.
1485 return ConfirmActionRequest( "quit" ) ? -1 : 0;
1488 /* $RCSfile$: end of file */