6 * Written by Keith Marshall <keithmarshall@users.sourceforge.net>
7 * Copyright (C) 2012, MinGW 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.
41 using WTK::StringResource;
42 using WTK::WindowClassMaker;
43 using WTK::ChildWindowMaker;
45 /* Margin settings, controlling the positioning of the
46 * active viewport within the data sheet display pane.
49 #define PARAGRAPH_MARGIN 5
51 #define RIGHT_MARGIN 8
52 #define BOTTOM_MARGIN 5
54 class pkgTroffLayoutEngine: public pkgUTF8Parser
56 /* A privately implemented class, supporting a simplified troff
57 * style layout for a UTF-8 text stream, within a scrolling GUI
61 pkgTroffLayoutEngine( const char *input, long displacement ):
62 pkgUTF8Parser( input ), curr( this ), offset( displacement ){}
63 bool WriteLn( HDC, RECT * );
66 inline bool IsReady();
67 pkgTroffLayoutEngine *curr;
71 inline bool pkgTroffLayoutEngine::IsReady()
73 /* Private helper method, used to position the input stream to
74 * the next parseable token, if any, for processing by WriteLn.
76 while( (curr != NULL) && ((curr->length == 0) || (curr->text == NULL)) )
77 curr = (pkgTroffLayoutEngine *)(curr->next);
78 return (curr != NULL);
81 bool pkgTroffLayoutEngine::WriteLn( HDC canvas, RECT *bounds )
83 /* Method to extract a single line of text from the UTF-8 stream,
84 * (if any is available for processing), format it as appropriate
85 * for display, and write it into the display pane.
89 /* Initialise a buffer, in which to compile the formatted text
90 * record for display; establish and initialise the counters for
91 * controlling the formatting process.
93 wchar_t linebuf[1 + strlen( curr->text )];
94 long curr_width, new_width = 0, max_width = bounds->right - bounds->left;
95 int filled, extent = 0, fold = 0;
97 /* Establish default tracking and justification settings.
99 SetTextCharacterExtra( canvas, 0 );
100 SetTextJustification( canvas, 0, 0 );
102 /* Copy text from the input stream, to fill the transfer buffer
103 * up to the maximum permitted output line length, or until the
104 * input stream has been exhausted.
107 do { if( curr->length > 0 )
108 { /* There is at least one more word of input text to copy,
109 * and there may be sufficient output space to accommodate
110 * it; record the space filled so far, up to the end of the
111 * preceding word, (if any)...
114 curr_width = new_width;
117 /* ...and, when there was a preceding word, add white
118 * space and record a potential line folding point.
120 linebuf[extent++] = L'\x20';
124 /* Append one word, copied from the input stream to the
125 * output line buffer.
127 const char *mark = curr->text;
128 for( int i = 0; i < curr->length; ++i )
129 linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
131 /* Check the effective output line length which would be
132 * required to accommodate the extended output record...
134 if( GetTextExtentPoint32W( canvas, linebuf, extent, &span ) )
136 /* ...and while it still fits within the maximum width
137 * of the display pane...
139 if( max_width >= (new_width = span.cx) )
141 * ...accept the current input word, and move on to
142 * see if we can accommodate another.
144 curr = (pkgTroffLayoutEngine *)(curr->next);
147 /* In the event of any error in evaluating the output
148 * line length, reject any remaining input.
153 /* We found a zero-length entity in the input stream;
154 * ignore it, and move on to the next, if any.
156 curr = (pkgTroffLayoutEngine *)(curr->next);
158 /* Continue the cycle, unless we have exhausted the input
159 * stream, or we have run out of available output space.
161 } while( (curr != NULL) && (max_width > new_width) );
163 /* When we've collected a complete line of output text...
165 if( (bounds->top >= (TOP_MARGIN + offset))
166 && ((bounds->bottom + offset) >= (bounds->top + span.cy)) )
168 /* ...and when it is to be positioned vertically within the
169 * bounds of the active viewport...
171 if( bounds->top < (TOP_MARGIN + offset + span.cy) )
173 * ...when it is the topmost visible line, ensure that it
174 * is vertically aligned flush with the top margin.
176 bounds->top = TOP_MARGIN + offset;
178 /* Check if the output line collection loop, above, ended
179 * on an attempt to over-fill the buffer...
181 if( max_width >= new_width )
183 * ...but when it did not, handle it as a partially filled
184 * line, which is thus exempt from right justification.
188 /* When the output line is over-filled, then we will attempt
189 * to fold it at the last counted fold point, and then insert
190 * padding space at each remaining internal fold point, so as
191 * to achieve flush left/right justification; (note that we
192 * decrement the fold count here, because the point at which
193 * we fold the line has been included in the count, but we
194 * don't want to add padding space at the right margin).
196 else if( --fold > 0 )
198 /* To adjust the output line, we first compute the number
199 * of padding PIXELS required, then...
202 if( (padding = max_width - curr_width) >= filled )
204 /* ...in the event that this is no fewer than the number
205 * of physical GLYPHS to be output, we adjust the tracking
206 * to accommodate as many padding pixels as possible, with
207 * ONE additional inter-glyph tracking pixel per glyph...
209 SetTextCharacterExtra( canvas, 1 );
210 if( GetTextExtentPoint32W( canvas, linebuf, filled, &span ) )
212 * ...and then, we recompute the number of additional
213 * inter-word padding pixels, if any, which are still
216 padding = max_width - span.cx;
218 /* In the event that adjustment of tracking fails, we
219 * must reset it, because the padding count remains as
220 * computed for default tracking.
223 SetTextCharacterExtra( canvas, 0 );
225 /* Now, provided the padding pixels will not increase the
226 * inter-word (fold) spacing to more than 5% of the total
227 * line length at each potential fold point...
229 if( ((padding * 100) / (max_width * fold)) < 5 )
231 * ...distribute the padding pixels among the remaining
232 * inter-word spaces within the output line...
234 SetTextJustification( canvas, padding, fold );
237 /* ...otherwise, we decline to adjust the output line,
238 * and we prefer to also preserve natural tracking.
240 SetTextCharacterExtra( canvas, 0 );
243 { /* If we get to here, then the first item in the output
244 * queue requires more space than the available width of
245 * the display pane, and has no natural fold points; we
246 * MUST handle this, to avoid an infinite loop!
248 * FIXME: The method adopted here simply elides the
249 * portion of the input text, which will not fit into
250 * the available display width, at the right hand end of
251 * the line; we may wish to consider adding horizontal
252 * scrolling, so that such elided text may be viewed.
254 * We begin by loading the content of the first queued
255 * entity into the line transfer buffer.
258 const char *mark = curr->text;
259 for( int i = 0; i < curr->length; ++i )
260 linebuf[extent++] = GetCodePoint( mark = ScanBuffer( mark ) );
262 /* Reduce the maximum allowable output width sufficiently
263 * to accommodate an ellipsis at the right hand end of the
266 int fit = GetTextExtentPoint32W( canvas, L"...", 3, &span )
267 ? max_width - span.cx : max_width;
269 /* ...then compute the maximum number of characters from
270 * the queued item, which will fit in the remaining space...
272 if( GetTextExtentExPointW
273 ( canvas, linebuf, extent, fit, &filled, NULL, &span )
275 /* ...and then append the ellipsis, in place of any
276 * characters which will not fit, leaving the resultant
277 * line, with elided tail, ready for display.
279 for( int i = 0; i < 3; ++i )
280 linebuf[filled++] = L'.';
282 /* Finally, pop the entity we just processed from the
283 * output queue, before falling through...
285 curr = (pkgTroffLayoutEngine *)(curr->next);
287 /* ...and write the output line at the designated position
288 * within the display viewport.
290 TextOutW( canvas, bounds->left, bounds->top - offset, linebuf, filled );
292 /* Finally, adjust the top boundary of the viewport, to indicate
293 * where the NEXT output line, if any, is to be positioned, and
294 * return TRUE, to indicate that an output line was processed.
296 bounds->top += span.cy;
299 /* If we get to here, then there was nothing in the input stream to
300 * be processed; return FALSE, to indicate this.
305 class DataSheetMaker: public ChildWindowMaker
307 /* Specialised variant of the standard child window class, augmented
308 * to provide the custom methods for formatting and displaying package
309 * data sheet content within the tabbed data display pane.
311 * FIXME: we may eventually need to make this class externally visible,
312 * but for now we implement it as a locally declared class.
315 DataSheetMaker( HINSTANCE inst ): ChildWindowMaker( inst ),
316 PackageRef( NULL ), DataClass( NULL ){}
317 virtual void DisplayData( HWND, HWND );
320 virtual long OnCreate();
321 virtual long OnVerticalScroll( int, int, HWND );
322 virtual long OnPaint();
324 HWND PackageRef, DataClass;
325 static DataSheetMaker *Display;
326 HDC canvas; RECT bounding_box;
327 HFONT NormalFont, BoldFont;
330 long offset; char *desc;
331 void DisplayGeneralData( pkgXmlNode * );
332 static int DisplaySourceURL( const char * );
333 static int DisplayLicenceURL( const char * );
334 static int DisplayPackageURL( const char * );
335 inline void DisplayDescription( pkgXmlNode * );
336 void ComposeDescription( pkgXmlNode *, pkgXmlNode * );
337 int FormatRecord( int, const char *, const char * );
338 inline void FormatText( const char * );
342 /* Don't forget to instantiate the static member variables...
344 int DataSheetMaker::Advance;
345 DataSheetMaker *DataSheetMaker::Display;
348 { /* Tab identifiers for the available data sheet collection.
350 PKG_DATASHEET_GENERAL = 0,
351 PKG_DATASHEET_DESCRIPTION,
352 PKG_DATASHEET_DEPENDENCIES,
353 PKG_DATASHEET_INSTALLED_FILES,
354 PKG_DATASHEET_VERSIONS
357 long DataSheetMaker::OnCreate()
359 /* Method called when creating a data sheet window; initialise font
360 * preferences and line spacing for any instance of a DataSheetMaker
363 * Initially, we match the font properties to the default GUI font...
366 HFONT font = (HFONT)(GetStockObject( DEFAULT_GUI_FONT ));
367 GetObject( BoldFont = NormalFont = font, sizeof( LOGFONT ), &font_info );
369 /* ...then, we substitute the preferred type face.
371 strcpy( (char *)(&(font_info.lfFaceName)), "Verdana" );
372 if( (font = CreateFontIndirect( &font_info )) != NULL )
374 /* On successfully creating the preferred font, we may discard
375 * the original default font object, and assign our preference
376 * as both the normal and bold working font...
378 DeleteObject( NormalFont );
379 BoldFont = NormalFont = font;
381 /* ...before adjusting the weight for the bold variant...
383 font_info.lfWeight = FW_BOLD;
384 if( (font = CreateFontIndirect( &font_info )) != NULL )
386 /* ...and reassigning when successful.
392 /* Finally, we determine the line spacing (in pixels) for a line
393 * of text, in the preferred normal font, within the device context
394 * for the data sheet window.
397 HDC canvas = GetDC( AppWindow );
398 SelectObject( canvas, NormalFont );
399 LineSpacing = GetTextExtentPoint32A( canvas, "Height", 6, &span ) ? span.cy : 13;
400 ReleaseDC( AppWindow, canvas );
405 void DataSheetMaker::DisplayData( HWND tab, HWND package )
407 /* Method to force a refresh of the data sheet display pane.
409 PackageRef = package; DataClass = tab;
410 InvalidateRect( AppWindow, NULL, TRUE );
411 UpdateWindow( AppWindow );
414 inline void DataSheetMaker::FormatText( const char *text )
416 /* Helper method to transfer text to the display device, formatting
417 * it to fill as many lines of the viewing window as may be required,
418 * justifying for flush margins at both left and right.
420 pkgTroffLayoutEngine page( text, offset );
421 while( page.WriteLn( canvas, &bounding_box ) )
425 int DataSheetMaker::FormatRecord( int offset, const char *tag, const char *text )
427 /* Helper method to transfer text to the display device, prefacing
428 * it with a specified record key, before formatting as above.
430 const char *fmt = "%s: %s";
431 int span = snprintf( NULL, 0, fmt, tag, text );
432 char record[ 1 + span ]; snprintf( record, sizeof( record ), fmt, tag, text );
434 bounding_box.top += PARAGRAPH_MARGIN;
435 FormatText( record );
436 return offset + span;
439 void DataSheetMaker::DisplayGeneralData( pkgXmlNode *ref )
441 /* Method to compile the package data, which is to be displayed
442 * on the general information tab; we begin by displaying the
443 * identification records for the selected package, and the
444 * subsystem to which it belongs.
446 FormatRecord( 0, "SubSystem",
447 ref->GetContainerAttribute( subsystem_key, value_unknown )
449 FormatRecord( 1, "Package Name",
450 ref->GetContainerAttribute( name_key, value_unknown )
452 if( ref->IsElementOfType( component_key ) )
453 FormatRecord( 1, "Component Class",
454 ref->GetPropVal( class_key, value_unknown )
457 /* Using a temporary action item, collect information on the
458 * latest available version, and the installed version if any,
459 * of the selected package; print the applicable information,
460 * noting that "none" may be appropriate in the case of the
464 FormatRecord( 0, "Installed Version",
465 ((ref = pkgGetStatus( ref, &avail )) != NULL)
466 ? ref->GetPropVal( tarname_key, value_unknown )
469 FormatRecord( 1, "Repository Version",
470 (ref = avail.Selection())->GetPropVal( tarname_key, value_unknown )
473 /* Finally, report the download URLs for the selected package,
474 * and its associated source and licence archives; (note that we
475 * must save static callback references, so that the PrintURI()
476 * method can access this data sheet context).
478 Display = this; Advance = 0;
479 avail.PrintURI( ref->ArchiveName(), DisplayPackageURL );
480 avail.PrintURI( ref->SourceArchiveName( ACTION_LICENCE ), DisplayLicenceURL );
481 avail.PrintURI( ref->SourceArchiveName( ACTION_SOURCE ), DisplaySourceURL );
484 int DataSheetMaker::DisplayPackageURL( const char *uri )
486 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
487 * to display the package download URL on the active data sheet panel.
489 return Advance = Display->FormatRecord( Advance, "Package URL", uri );
492 int DataSheetMaker::DisplaySourceURL( const char *uri )
494 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
495 * to display the source download URL on the active data sheet panel.
497 return Advance = Display->FormatRecord( Advance, "Source URL", uri );
500 int DataSheetMaker::DisplayLicenceURL( const char *uri )
502 /* Static helper method, which may be passed to pkgActionItem::PrintURI(),
503 * to display the licence download URL on the active data sheet panel.
505 return Advance = Display->FormatRecord( Advance, "Licence URL", uri );
508 inline void DataSheetMaker::DisplayDescription( pkgXmlNode *ref )
510 /* A convenience method to invoke the recursive retrieval of any
511 * package description, without requiring a look-up of the address
512 * of the XML document root at every level of recursion.
514 ComposeDescription( ref, ref->GetDocumentRoot() );
517 void DataSheetMaker::ComposeDescription( pkgXmlNode *ref, pkgXmlNode *root )
519 /* Recursive method to compile a package description, from text
520 * fragments retrieved from the XML specification document, and
521 * present it in a data sheet window.
525 /* Recursively walk the XML hierarchy, until we reach the
528 ComposeDescription( ref->GetParent(), root );
530 /* ...then unwind the recursion, selecting "description"
531 * elements, (if any), at each level...
533 if( (root = ref->FindFirstAssociate( description_key )) != NULL )
535 /* ...formatting each, paragraph by paragraph, for display
536 * within the viewport bounding box of the data sheet...
538 do { if( (ref = root->FindFirstAssociate( paragraph_key )) != NULL )
539 do { if( bounding_box.top > (TOP_MARGIN + offset) )
541 * When this is not the top-most visible
542 * paragraph, within the viewport, displace
543 * it downwards by one paragraph margin from
546 bounding_box.top += PARAGRAPH_MARGIN;
548 /* ...before laying out the visible text of
549 * this paragraph, (if any).
551 FormatText( ref->GetText() );
553 /* Cycle, to process any further paragraphs
554 * which are included within the current
555 * description block...
557 } while( (ref = ref->FindNextAssociate( paragraph_key )) != NULL );
559 /* ...and ultimately, for any additional description blocks
560 * which may have been specified within the current XML element,
561 * at the current hierarchical nesting level.
563 } while( (root = root->FindNextAssociate( description_key )) != NULL );
568 static pkgXmlNode *pkgListSelection( HWND package_ref, LVITEM *lookup )
570 /* Helper function, to retrieve the active selection from the
571 * package list, as displayed in the upper right data pane.
573 lookup->iSubItem = 0;
574 lookup->mask = LVIF_PARAM;
575 ListView_GetItem( package_ref, lookup );
576 return (pkgXmlNode *)(lookup->lParam);
579 long DataSheetMaker::OnPaint()
581 /* Handler for WM_PAINT message messages, sent to any window
582 * ascribed to the DataSheetMaker class.
585 canvas = BeginPaint( AppWindow, &content );
586 HFONT original_font = (HFONT)(SelectObject( canvas, NormalFont ));
588 /* Establish a viewport, with a suitable margin, within the
589 * bounding rectangle of the window.
591 GetClientRect( AppWindow, &bounding_box );
592 bounding_box.left += LEFT_MARGIN; bounding_box.right -= RIGHT_MARGIN;
593 bounding_box.top += TOP_MARGIN; bounding_box.bottom -= BOTTOM_MARGIN;
595 /* Provide bindings for a vertical scrollbar...
597 SCROLLINFO scrollbar;
600 /* ...and prepare to initialise it, when we redraw
601 * the data sheet from the top.
603 scrollbar.cbSize = sizeof( scrollbar );
604 scrollbar.fMask = SIF_POS | SIF_RANGE | SIF_PAGE;
605 scrollbar.nPos = scrollbar.nMin = bounding_box.top;
606 scrollbar.nPage = 1 + bounding_box.bottom - bounding_box.top;
607 scrollbar.nMax = bounding_box.bottom;
610 /* Identify the package, as selected in the package list window,
611 * for which the data sheet is to be compiled.
614 lookup.iItem = (PackageRef != NULL)
615 ? ListView_GetNextItem( PackageRef, (WPARAM)(-1), LVIS_SELECTED )
618 if( lookup.iItem >= 0 )
620 /* There is an active package selection; identify the selected
621 * data sheet tab, if any...
623 int tab = ( DataClass != NULL ) ? TabCtrl_GetCurSel( DataClass )
625 * ...otherwise default to the package description.
627 : PKG_DATASHEET_DESCRIPTION;
629 /* Retrieve the package title from the list view; assign it as
630 * a bold face heading in the data sheet view...
633 SelectObject( canvas, BoldFont );
634 ListView_GetItemText( PackageRef, lookup.iItem, 5, desc, sizeof( desc ) );
638 /* ...adjusting as appropriate, when the heading is scrolled
639 * out of the viewport.
641 if( (offset -= (bounding_box.top - TOP_MARGIN)) < 0 )
643 bounding_box.top = TOP_MARGIN;
646 /* Revert to normal typeface, in preparation for compilation
647 * of the selected data sheet.
649 SelectObject( canvas, NormalFont );
652 case PKG_DATASHEET_GENERAL:
653 /* This comprises package and subsystem identification,
654 * followed by latest version availability, installation
655 * status, and package download URLs.
657 DisplayGeneralData( pkgListSelection( PackageRef, &lookup ) );
660 case PKG_DATASHEET_DESCRIPTION:
661 /* This represents the package description, provided by
662 * the package maintainer, within the XML specification.
664 DisplayDescription( pkgListSelection( PackageRef, &lookup ) );
668 /* Handle requests for data sheets for which we have yet
669 * to provide a compiling routine.
671 bounding_box.top += TOP_MARGIN;
673 "FIXME:data sheet unavailable; a compiler for this "
674 "data category has yet to be implemented."
679 { /* There is no active package selection; advise accordingly.
681 bounding_box.top += TOP_MARGIN << 1;
683 "No package selected."
685 bounding_box.top += PARAGRAPH_MARGIN << 1;
687 "Please select a package from the list above, "
688 "to view related data."
692 /* When redrawing the data sheet window from the top...
696 /* ...adjust the scrolling range to accommodate the full extent
697 * of the data sheet text, and initialise the scrollbar control.
699 if( bounding_box.top > bounding_box.bottom )
700 scrollbar.nMax = bounding_box.top;
701 SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
704 /* Finally, restore the original (default) font assignment
705 * for the data sheet window, complete the redraw action, and
708 SelectObject( canvas, original_font );
709 EndPaint( AppWindow, &content );
713 long DataSheetMaker::OnVerticalScroll( int req, int pos, HWND ctrl )
715 /* Handler for events signalled by the vertical scrollbar control,
716 * (if any), in any window ascribed to the DataSheetMaker class.
718 SCROLLINFO scrollbar;
719 scrollbar.fMask = SIF_ALL;
720 scrollbar.cbSize = sizeof( scrollbar );
721 GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
723 /* Save the original "thumb" position.
725 long origin = scrollbar.nPos;
728 /* Identify, and process the event message.
731 /* User clicked the "scroll-up" button; move the
732 * "thumb" up by a distance equivalent to the height
733 * of a single line of text.
735 scrollbar.nPos -= LineSpacing;
739 /* Similarly, for a click on the "scroll-down" button,
740 * move the "thumb" down by one line height.
742 scrollbar.nPos += LineSpacing;
746 /* User clicked the scrollbar region above the "thumb";
747 * move the "thumb" up by half of the viewport height.
749 scrollbar.nPos -= scrollbar.nPage >> 1;
753 /* Similarly, for a click below the "thumb", move it
754 * down by half of the viewport height.
756 scrollbar.nPos += scrollbar.nPage >> 1;
760 /* User is dragging...
762 case SB_THUMBPOSITION:
763 /* ...or has just finished dragging the "thumb"; move it
764 * by the distance it has been dragged.
766 scrollbar.nPos = scrollbar.nTrackPos;
769 /* Preceding scrollbar event has completed; we do not need
770 * to take any specific action here.
775 /* We received an unexpected scrollbar event message...
777 dmh_notify( DMH_WARNING,
778 "Unhandled scrollbar message: request = %d\n", req
781 /* Update the scrollbar control, to capture any change in
782 * "thumb" position...
784 scrollbar.fMask = SIF_POS;
785 SetScrollInfo( AppWindow, SB_VERT, &scrollbar, TRUE );
787 /* ...then read it back, since the control hay have adjusted
788 * the actual recorded position.
790 GetScrollInfo( AppWindow, SB_VERT, &scrollbar );
791 if( scrollbar.nPos != origin )
793 /* When the "thumb" has moved, force a redraw of the data
794 * sheet window, to capture any change in the visible text.
796 offset = scrollbar.nPos - scrollbar.nMin;
797 InvalidateRect( AppWindow, NULL, TRUE );
798 UpdateWindow( AppWindow );
800 /* Reset the default starting point, so that any subsequent
801 * redraw will favour a "redraw-from-top"...
805 /* ...and we are done.
810 void AppWindowMaker::InitPackageTabControl()
812 /* Create and initialise a TabControl window, in which to present
813 * miscellaneous package information...
815 WindowClassMaker AppWindowRegistry( AppInstance );
816 StringResource ClassName( AppInstance, ID_SASH_WINDOW_PANE_CLASS );
817 AppWindowRegistry.Register( ClassName );
819 /* Package data sheets will be displayed in a derived child window
820 * which we create as a member of the SASH_WINDOW_PANE_CLASS; it will
821 * ultimately be displayed below the tab bar, within the tab control
822 * region, with content dynamically painted on the basis of package
823 * selection, (in the package list pane), and tab selection.
825 DataSheet = new DataSheetMaker( AppInstance );
826 PackageTabPane = DataSheet->Create( ID_PACKAGE_DATASHEET,
827 AppWindow, ClassName, WS_VSCROLL | WS_BORDER
830 /* The tab control itself is the standard control, selected from
831 * the common controls library.
833 PackageTabControl = CreateWindow( WC_TABCONTROL, NULL,
834 WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 0, 0,
835 AppWindow, (HMENU)(ID_PACKAGE_TABCONTROL),
839 /* Keep the font for tab labels consistent with our preference,
840 * as assigned to the main application window.
842 SendMessage( PackageTabControl, WM_SETFONT, (WPARAM)(DefaultFont), TRUE );
844 /* Create the designated set of tabs, with appropriate labels...
847 tab.mask = TCIF_TEXT;
849 { "General", "Description", "Dependencies", "Installed Files", "Versions",
851 /* ...with a NULL sentinel marking the preceding label as
852 * the last in the list.
856 for( int i = 0; TabLegend[i] != NULL; ++i )
858 /* This loop assumes responsibility for actual tab creation...
860 tab.pszText = TabLegend[i];
861 if( TabCtrl_InsertItem( PackageTabControl, i, &tab ) == -1 )
863 /* ...bailing out, and deleting the container window,
864 * in the event of a creation error.
866 TabLegend[i + 1] = NULL;
867 DestroyWindow( PackageTabControl );
868 PackageTabControl = NULL;
871 if( PackageTabControl != NULL )
873 /* When the tab control has been successfully created, we
874 * create one additional basic SASH_WINDOW_PANE_CLASS window;
875 * this serves to draw a border around the tab pane.
877 TabDataPane = new ChildWindowMaker( AppInstance );
878 TabDataPane->Create( ID_PACKAGE_TABPANE, AppWindow, ClassName, WS_BORDER );
880 /* We also assign the package description data sheet as the
881 * initial default tab selection.
883 TabCtrl_SetCurSel( PackageTabControl, PKG_DATASHEET_DESCRIPTION );
887 void AppWindowMaker::UpdatePackageMenuBindings()
888 # define PKGSTATE_FLAG( ID ) (1 << PKGSTATE( ID ))
890 /* Helper method to enable or disable the set of options
891 * which may be chosen from the package menu; (this varies
892 * according to the installation status of the package, if
893 * any, which has been selected in the package list view).
896 if( (menu = GetMenu( AppWindow )) != NULL )
898 /* We got a valid handle for the menubar; identify the
899 * list view selection, which controls the available set
903 lookup.iItem = (PackageListView != NULL)
904 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
907 /* ...and identify its state of the associated package,
908 * as indicated by the assigned icon.
911 lookup.mask = LVIF_IMAGE;
912 ListView_GetItem( PackageListView, &lookup );
914 /* Convert the indicated state to a selector bit-flag.
916 int state = ((lookup.iItem >= 0) && (lookup.iImage <= PKGSTATE( PURGE )))
917 ? 1 << lookup.iImage : 0;
919 /* Walk over all state-conditional menu items...
921 for( int item = IDM_PACKAGE_UNMARK; item <= IDM_PACKAGE_REMOVE; item++ )
923 /* ...evaluating an independent state flag for each,
924 * setting it as non-zero for menu items which may be
925 * made "selectable", or zero otherwise...
927 int state_flag = state;
930 /* ...testing against item specific flag groups, to
931 * determine which menu items should be enabled for
932 * the currently selected list view item...
934 case IDM_PACKAGE_INSTALL:
936 * "Mark for Installation" is available for packages
937 * which exist in the repository, (long-term or new),
938 * but which are not yet identified as "installed".
940 state_flag &= PKGSTATE_FLAG( AVAILABLE )
941 | PKGSTATE_FLAG( AVAILABLE_NEW );
944 case IDM_PACKAGE_REMOVE:
945 case IDM_PACKAGE_REINSTALL:
947 * "Mark for Removal" and "Mark for Reinstallation"
948 * are viable selections only for packages identified
949 * as "installed", (current or upgradeable).
951 state_flag &= PKGSTATE_FLAG( INSTALLED_CURRENT )
952 | PKGSTATE_FLAG( INSTALLED_OLD );
955 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.
961 state_flag &= PKGSTATE_FLAG( INSTALLED_OLD );
964 case IDM_PACKAGE_UNMARK:
966 * The "Unmark" facility is available only for packages
967 * which have been marked, (perhaps inadvertently), for
968 * any of the preceding actions.
970 state_flag &= PKGSTATE_FLAG( AVAILABLE_INSTALL )
971 | PKGSTATE_FLAG( UPGRADE ) | PKGSTATE_FLAG( DOWNGRADE )
972 | PKGSTATE_FLAG( REMOVE ) | PKGSTATE_FLAG( PURGE )
973 | PKGSTATE_FLAG( REINSTALL );
977 /* When none of the preceding is applicable, the menu
978 * item should not be selectable.
982 /* ...and set the menu item enabled state accordingly.
984 EnableMenuItem( menu, item, (state_flag == 0) ? MF_GRAYED : MF_ENABLED );
989 inline void AppWindowMaker::MarkSchedule( pkgActionItem *pending_actions )
991 /* Helper routine to update the status icons within the package list view,
992 * reflecting any scheduled action in respect of the package associated with
993 * each, and updating the menu bindings to match.
995 pkgListViewMaker pkglist( PackageListView );
996 pkglist.MarkScheduledActions( pending_actions );
997 UpdatePackageMenuBindings();
1000 void AppWindowMaker::Schedule
1001 ( unsigned long action, const char *bounds, const char *pkgname )
1003 /* GUI menu driven interface to the pkgActionItem task scheduler;
1004 * it constructs a pseudo-argument string, emulating the effect of
1005 * parsing a CLI argument, then passes this to the CLI scheduler
1008 if( pkgname == NULL )
1010 /* Initial entry on menu item selection; package name has not
1011 * yet been identified, so find the selected list view item...
1014 lookup.iItem = (PackageListView != NULL)
1015 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1018 /* ...and look up the package name identified within it.
1020 const char *pkg, *fmt = "%s-%s";
1021 pkgXmlNode *ref = pkgListSelection( PackageListView, &lookup );
1022 if( (pkg = ref->GetContainerAttribute( name_key, NULL )) != NULL )
1024 /* We now have a valid package name; check for a
1025 * component package association.
1028 if( (cpt = ref->GetPropVal( class_key, NULL )) == NULL )
1030 /* Current list view selection represents a
1031 * non-component package; encode its name only
1032 * as a string argument, using only the final
1033 * string field of the format specification.
1035 char pkgspec[ 1 + snprintf( NULL, 0, fmt + 3, pkg ) ];
1036 snprintf( pkgspec, sizeof( pkgspec ), fmt + 3, pkg );
1038 /* Recurse, to capture any supplied version bounds
1039 * specification, and ultimately schedule the action.
1041 Schedule( action, bounds, pkgspec );
1044 { /* Current list view selection represents a
1045 * package name qualified by a component name;
1046 * use the full format specification to encode
1047 * the fully qualified package name.
1049 char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkg, cpt ) ];
1050 snprintf( pkgspec, sizeof( pkgspec ), fmt, pkg, cpt );
1052 /* Again, recurse to capture any supplied version
1053 * bounds specification, before ultimately scheduling
1054 * the selected action.
1056 Schedule( action, bounds, pkgspec );
1060 else if( bounds != NULL )
1062 /* Recursive entry, after package name identification,
1063 * but with supplied version bounds specification yet
1064 * to be resolved; append the bounds specification to
1065 * the package name, as it would be in a CLI argument...
1067 const char *fmt = "%s=%s";
1068 char pkgspec[ 1 + snprintf( NULL, 0, fmt, pkgname, bounds ) ];
1069 snprintf( pkgspec, sizeof( pkgspec ), fmt, pkgname, bounds );
1071 * ...then recurse a final time, to schedule the action.
1073 Schedule( action, NULL, pkgspec );
1076 { /* Final recursive entry, with pkgname argument in the
1077 * same form as a CLI package name/bounds specification
1078 * argument; hand it off to the CLI scheduler, capturing
1079 * the resultant schedule of actions, and update the list
1080 * view state icons to reflect the pending actions.
1082 MarkSchedule( pkgData->Schedule( action, pkgname ) );
1086 inline unsigned long pkgActionItem::CancelScheduledAction( void )
1088 /* Helper method to mark a scheduled action as "cancelled".
1090 return (this != NULL) ? (flags &= ~ACTION_MASK) : 0UL;
1093 void AppWindowMaker::UnmarkSelectedPackage( void )
1095 /* Method to clear any request for an action in respect of
1096 * the currently selected package entry in the list view; we
1097 * implement this as a cancellation of any pending scheduled
1098 * action, in respect of the selected package.
1100 * First, obtain a reference for the list view selection...
1103 lookup.iItem = (PackageListView != NULL)
1104 ? ListView_GetNextItem( PackageListView, (WPARAM)(-1), LVIS_SELECTED )
1107 /* ...and when it represents a valid selection...
1109 if( lookup.iItem >= 0 )
1111 /* ...retrieve its associated XML database package reference...
1113 pkgXmlNode *pkg = pkgListSelection( PackageListView, &lookup );
1115 * ...search the action schedule, for an action associated with
1116 * this package, if any, and cancel it.
1118 pkgData->Schedule()->GetReference( pkg )->CancelScheduledAction();
1120 /* The scheduling state for packages shown in the list view
1121 * may have changed, so refresh the icon associations and the
1122 * package menu bindings accordingly.
1124 MarkSchedule( pkgData->Schedule() );
1128 long AppWindowMaker::OnNotify( WPARAM client_id, LPARAM data )
1130 /* Handler for notifiable events to be processed in the context
1131 * of the main application window.
1133 * FIXME: this supersedes the stub handler, originally provided
1134 * by pkgview.cpp; it may not yet be substantially complete, and
1135 * may eventually migrate elsewhere.
1139 /* At present, we handle only mouse click events within the
1140 * package list view and data sheet tab control panes...
1142 case ID_PACKAGE_LISTVIEW:
1143 case ID_PACKAGE_TABCONTROL:
1144 if( ((NMHDR *)(data))->code == NM_CLICK )
1146 /* ...each of which may require the data sheet content
1147 * to be updated, (to reflect a changed selection).
1149 DataSheet->DisplayData( PackageTabControl, PackageListView );
1151 if( client_id == ID_PACKAGE_LISTVIEW )
1153 /* A change of package selection, within the list view,
1154 * may also require an update of the available choices in
1155 * the "Package" drop-down menu.
1157 UpdatePackageMenuBindings();
1162 /* Otherwise, this return causes any other notifiable events
1163 * to be simply ignored, (as they were by the original stub).
1165 return EXIT_SUCCESS;
1168 unsigned long pkgActionItem::EnumeratePendingActions( int classified )
1170 /* Helper method to count the pending actions in a
1171 * scheduled action list.
1173 unsigned long count = 0;
1176 /* Regardless of the position of the 'this' pointer,
1177 * within the list of scheduled actions...
1179 pkgActionItem *item = this;
1180 while( item->prev != NULL )
1182 * ...we want to get a reference to the first
1187 /* Now, working through the list...
1189 while( item != NULL )
1191 /* ...note items with any scheduled action...
1194 if( (action = item->flags & ACTION_MASK) != 0 )
1196 /* ...and, when one is found...
1198 if( action == classified )
1200 /* ...and it matches the classification in which
1201 * we are interested, then we retrieve the tarname
1202 * for the related package...
1204 pkgXmlNode *selected = (classified & ACTION_REMOVE)
1205 ? item->Selection( to_remove )
1206 : item->Selection();
1207 const char *notification = (selected != NULL)
1208 ? selected->GetPropVal( tarname_key, NULL )
1210 if( notification != NULL )
1212 /* ...and, provided it is valid, we append it to
1213 * the DMH driven dialogue in which the enumeration
1214 * is being reported...
1216 dmh_printf( "%s\n", notification );
1218 * ...and include it in the accumulated count...
1223 else if( classified == 0 )
1225 * ...otherwise, when we aren't interested in any
1226 * particular class of action, just count all those
1227 * which are found, regardless of classification.
1231 /* ...then move on, to consider the next entry, if any.
1236 /* Ultimately, return the count of pending actions,
1237 * as noted while processing the above loop.
1242 long AppWindowMaker::OnClose()
1244 /* Intercept application termination requests; check for
1245 * outstanding pending actions, and offer a cancellation
1246 * option for the termination request, so that the user
1247 * has an opportunity to complete such actions.
1249 if( (pkgData->Schedule()->EnumeratePendingActions() > 0)
1250 && (MessageBox( AppWindow,
1251 "You have marked changes which have not been applied;\n"
1252 "these will be lost, if you quit without applying them.\n\n"
1253 "Are you sure you want to discard these marked changes?",
1254 "Discard Marked Changes?", MB_YESNO | MB_ICONWARNING
1260 /* $RCSfile$: end of file */